Декорируем функции с помощью макросов
Однажды передо мной стояла задача запустить в массы Telegram-бота для отслеживания времени прибытия автобусов в Дублине (@dublin_bus_bot). Перед этим я задумался, сколько людей будут им пользоваться (спойлер: почти нисколько), и я решил, что было бы неплохо отследить их количество с помощью Google Analytics.
О решении
Протокол передачи статистических данных Google Analytics позволяет отслеживать данные не только веб-сайтов, но и мобильных приложений, программных решений для интернета вещей и др. На данный момент для этого протокола не существует Elixir клиента, а когда он появится, то будет представлять собой ни что иное, как обёртку над API. Мой план заключался в том, чтобы посылать запросы на сервер Google Analytics с помощью HTTPoison, но хотелось бы избежать вызова функции отслеживания после каждой новой команды бота.
Что мне нравится в Elixir, так это макросы, позволяющие генерировать код на этапе компиляции. Мне в голову пришла идея написать макрос, использование которого похоже на объявление функции. Этот макрос определял бы функцию с таким же телом и дополнительным вызовом функции отслеживания. Этот подход показался мне более естественным для функциональной модели Elixir в сравнении со стандартным синтаксисом декораторов в других языках (@decorator
в Python и Javascript).
defmetered sample_function(arg1, arg2) do
IO.inspect([arg1, arg2])
end
# would generate something similar to
def sample_function(arg1, arg2) do
track(:sample_function, [arg1: arg1, arg2: arg2])
IO.inspect([arg1, arg2])
end
Реализация
Описанный выше подход был реализован в пакете meter для использования в созданном мной Telegram-боте.
@doc """
Replace a function definition, automatically tracking every call to the function
on google analytics. It also track exception with the function track_error.
This macro intended use is with a set of uniform functions that can be concettualy
mapped to pageviews (eg: messaging bot commands).
Example:
defmetered function(arg1, arg2), do: IO.inspect({arg1,arg2})
function(1,2)
will call track with this parameters
track(:function, [arg1: 1, arg2: 2])
Additional parameters will be loaded from the configurationd
"""
# A macro definition can use pattern matching to destructure the arguments
defmacro defmetered({function,_,args} = fundef, [do: body]) do
# arguments are defined in 3 elements tuples
# this extract the arguments names in a list
names = Enum.map(args, &elem(&1, 0))
# meter will contain the body of the function that will be defined by the macro
metered = quote do
# quote and unquote allow to switch context,
# simplyfing a lot quoted code will run when the function is called
# unquoted code run at compile time (when the macro is called)
values = unquote(
args
|> Enum.map(fn arg -> quote do
# allow to access a value at runtime knowing the name
# elixir macros are hygienic so it's necessary to mark it
# explicitly
var!(unquote(arg))
end
end)
)
# Match argument names with their own values at call time
map = Enum.zip(unquote(names), values)
# wrap the original function call with a try to track errors too
try do
to_return = unquote(body)
track(unquote(function), map)
to_return
rescue
e ->
track_error(unquote(function), map, e)
raise e
end
end
# define a function with the same name and arguments and with the augmented body
quote do
def(unquote(fundef),unquote([do: metered]))
end
end
Заключение
Макросы Elixir — мощный инструмент для расширения функциональности приложений и создания DSL. Несмотря на то, что их изучение займёт какое-то время, это полностью оправданно, ведь они помогут вам навести порядок в своём коде.