При создании макросов важно соблюдать их гигиену, так как макросы, которые модифицируют окружение, могут сильно навредить. Негигиеничные макросы еще сложнее понимать и отлаживать, потому что они меняют окружение, в котором исполняется код. Для устранения таких проблем в разных языках добавлены специальные возможности по упрощению создания гигиеничных макросов, Elixir в том числе.
По умолчанию, в Elixir макросы гигиеничны, поэтому можно не переживать за контекст, в котором макрос используется:
defmodule Example do
defmacro no_interference do
quote do: a = 1
end
end
defmodule HygieneTest do
def run() do
require Example
a = 13
Example.no_interference()
a
end
end
HygieneTest.run()
# => 13
В примере выше, макрос не перезаписывает a
, происходит это потому, что Elixir аннотирует переменные контекстом, в котором они объявлены:
defmodule Sample do
def quoted do
quote do: x
end
end
Sample.quoted()
#=> {:x, [line: 3], Sample}
Благодаря аннотации, Elixir понимает к какому контексту переменные принадлежат.
В очень редких случаях, когда иначе никак, макрос можно сделать негигиеничным, например с помощью var!
:
defmodule Example do
defmacro interference do
quote do: var!(a) = 1
end
end
defmodule HygieneTest do
def run() do
require Example
a = 13
Example.interference()
a
end
end
HygieneTest.run()
# => 1
Так делать можно только в исключительных случаях, будьте крайне осторожны.
Иногда, нужно динамически объявить переменную, тогда воспользуемся var
:
defmodule Exercise do
defmacro initialize_to_char_count(variables) do
Enum.map(variables, fn name ->
var = Macro.var(name, nil)
length = name |> Atom.to_string |> String.length
quote do
unquote(var) = unquote(length)
end
end)
end
def run do
initialize_to_char_count [:red, :green, :yellow]
[red, green, yellow]
end
end
Exercise.run()
# => [3, 4, 5]
Обратите внимание, что передается вторым аргументом в var
. Это контекст объявления переменной, благодаря которому пересечения переменных не произойдет, если они были объявлены ранее.
Есть магическая структура __ENV__
, которая хранит всю информацию о скомпилированном окружении, включая модули, файлы, переменные, импорты и так далее:
__ENV__.module
# => nil
__ENV__.file
# => "iex"
__ENV__.requires
# => [Application, Exercise, IEx.Helpers, Kernel, Kernel.Typespec, Solution]
require Integer
__ENV__.requires
# => [Application, Exercise, IEx.Helpers, Integer, Kernel, Kernel.Typespec, Solution]
Большинство функций, которые используются в модуле Macro
, взаимодействуют с этим окружением.
Создайте макрос with_logging
, который принимает функцию, логгирует результат выполнения и возвращает результат. Примеры использования:
defmodule Exercise
require Solution
def run_fn(function) do
Solution.with_logging do
function
end
end
end
Exercise.run_fn(fn -> 1 + 5 end)
# => Started execution...
# => Execution result is: 6
# => 6
Exercise.run_fn(fn -> %{hello: :world} end)
# => Started execution...
# => Execution result is: %{hello: :world}
# => %{hello: :world}
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3
4 import ExUnit.CaptureIO
5
6 defmodule Exercise do
7 require Solution
8
9 def run_fn(function) do
10 Solution.with_logging do
11 function
12 end
13 end
14 end
15
16 test "with_logging work" do
17 assert capture_io(fn -> Exercise.run_fn(fn -> 1 + 5 end) end) ==
18 "Started execution...\nExecution result is: 6\n"
19
20 assert capture_io(fn -> Exercise.run_fn(fn -> %{hello: :world} end) end) ==
21 "Started execution...\nExecution result is: %{hello: :world}\n"
22
23 assert capture_io(fn ->
24 Exercise.run_fn(fn -> "some string" end)
25 end) == "Started execution...\nExecution result is: \"some string\"\n"
26 end
27end
28
Решение учителя откроется через: