Logo
Книга для начинающих
ВходРегистрация
/
Программирование
/
Курс Elixir
/

Гигиена макросов

Elixir: Гигиена макросов

При создании макросов важно соблюдать их гигиену, так как макросы, которые модифицируют окружение, могут сильно навредить. Негигиеничные макросы еще сложнее понимать и отлаживать, потому что они меняют окружение, в котором исполняется код. Для устранения таких проблем в разных языках добавлены специальные возможности по упрощению создания гигиеничных макросов, 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}

Полезное

  • Статья про гигиену макросов

  • Официальная документация

  • Официальная документация про Env

Команда проекта находится в телеграм-сообществе. Там можно задать любой вопрос и повлиять на проект

Если вы зашли в тупик, то самое время поговорить с нашим асситентом Тота во вкладке "ИИ-помощник":

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи. В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в обратной связи нашего сообщества

Нашли ошибку? Есть что добавить? Пулреквесты приветствуются
/
Программирование
/
Курс Elixir
/

Гигиена макросов

Elixir: Гигиена макросов

При создании макросов важно соблюдать их гигиену, так как макросы, которые модифицируют окружение, могут сильно навредить. Негигиеничные макросы еще сложнее понимать и отлаживать, потому что они меняют окружение, в котором исполняется код. Для устранения таких проблем в разных языках добавлены специальные возможности по упрощению создания гигиеничных макросов, 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}

Полезное

  • Статья про гигиену макросов

  • Официальная документация

  • Официальная документация про Env

Команда проекта находится в телеграм-сообществе. Там можно задать любой вопрос и повлиять на проект

Если вы зашли в тупик, то самое время поговорить с нашим асситентом Тота во вкладке "ИИ-помощник":

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи. В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в обратной связи нашего сообщества

Нашли ошибку? Есть что добавить? Пулреквесты приветствуются
← ПредыдущийСледующий →
← ПредыдущийСледующий →
← ПредыдущийСледующий →

Ваше упражнение проверяется по этим тестам

defmodule Test do
  use ExUnit.Case

  import ExUnit.CaptureIO

  defmodule Exercise do
    require Solution

    def run_fn(function) do
      Solution.with_logging do
        function
      end
    end
  end

  test "with_logging work" do
    assert capture_io(fn -> Exercise.run_fn(fn -> 1 + 5 end) end) ==
             "Started execution...\nExecution result is: 6\n"

    assert capture_io(fn -> Exercise.run_fn(fn -> %{hello: :world} end) end) ==
             "Started execution...\nExecution result is: %{hello: :world}\n"

    assert capture_io(fn ->
             Exercise.run_fn(fn -> "some string" end)
           end) == "Started execution...\nExecution result is: \"some string\"\n"
  end
end
← ПредыдущийСледующий →

Решение учителя откроется через:

20:00

waiting_clock
← ПредыдущийСледующий →