Elixir: AST и подведение итогов
В Elixir есть функции, упрощающих работу с абстрактным синтаксическим деревом (АСТ) языка. Например:
module = """
defmodule Example do
def sum(a, b) do
a + b
end
end
"""
{:ok, result} = Code.string_to_quoted(module)
result
# => {:defmodule, [line: 1],
# => [
# => {:__aliases__, [line: 1], [:Example]},
# => [
# => do: {:def, [line: 2],
# => [
# => {:sum, [line: 2], [{:a, [line: 2], nil}, {:b, [line: 2], nil}]},
# => [do: {:+, [line: 3], [{:a, [line: 3], nil}, {:b, [line: 3], nil}]}]
# => ]}
# => ]
# => ]}
quoted = quote do: 1 + 2
quoted |> Macro.to_string |> IO.puts
# => 1 + 2
# => :ok
quoted_pipe = quote do: 100 |> div(10) |> div(5)
Macro.unpipe(quoted_pipe)
# => [
# => {100, 0},
# => {{:div, [context: Elixir, imports: [{2, Kernel}]], [10]}, 0},
# => {{:div, [context: Elixir, imports: [{2, Kernel}]], [5]}, 0}
# => ]
Теперь подведем итоги модуля. Мы рассмотрели, как с помощью макросов можно расширять возможности языка и напрямую работать с АСТ.
Однако, в большинстве случаев прибегать к макросам является плохой затеей:
- Макросы сложнее понимать;
- Макросы сложнее отлаживать;
- Макросы притягивают макросы, то есть приходится писать дополнительные обвязки-макросы.
Перед тем как написать, задумайтесь, есть ли возможность решить задачу с помощью функции? Ответ на этот вопрос почти всегда будет - да
.
Хоть Elixir помогает и одновременно ограничивает разработчиков, например:
- Макросы по умолчанию гигиеничны;
- Макросы не дают возможности глобально внедрять произвольный код, то есть необходимо напрямую в конкретном модуле вызвать
require
илиimport
нужного макроса; - Использование макросов в коде явно, поэтому неожиданного поведения, как например, полного переопределения функции, за спиной у разработчика не происходит;
- Использование явных
quote
иunqoute
тоже упрощает понимание макроса, так как сразу видно, что будет выполнено сразу, а что потом.
Но даже эти механизмы не снимают ответственность с разработчика за написанный им код, пишите макросы ответственно и вдумчиво, и только если в этом есть острая необходимость!
Задание
Создайте функцию collect_module_stats
, которая принимает строку, в которой определяется модуль и функции модуля, а затем подсчитывается статистика по функциям, которые определены.
Для начала, изучите функцию string_to_quoted
модуля Code
и функцию prewalk
из модуля Macro
. Формат собираемой статистики представлен в примерах:
new_module = """
defmodule MyModule do
end
"""
Solution.collect_module_stats(new_module)
# => []
new_module = """
defmodule MyModule do
def hello() do
"world"
end
end
"""
Solution.collect_module_stats(new_module)
# => [%{arity: 0, name: :hello}]
new_module = """
defmodule MyModule do
def hello() do
"world"
end
defp test(a, b) do
a + b
end
end
"""
Solution.collect_module_stats(new_module)
# => [%{arity: 2, name: :test}, %{arity: 0, name: :hello}]
new_module = """
defmodule MyModule do
def hello(string) do
[string, "world"]
end
def magic(a, b, c) do
(a + b) * c
end
defp test(a, b) do
a + b
end
end
"""
Solution.collect_module_stats(new_module)
# => [%{arity: 2, name: :test}, %{arity: 3, name: :magic}, %{arity: 1, name: :hello}]
Полезное
Команда проекта находится в телеграм-сообществе. Там можно задать любой вопрос и повлиять на проект
Если вы зашли в тупик, то самое время поговорить с нашим асситентом Тота во вкладке "ИИ-помощник":
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи. В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в обратной связи нашего сообщества
Ваше упражнение проверяется по этим тестам
defmodule Test do
use ExUnit.Case
describe "collect_module_stats work" do
test "when no function defined" do
new_module = """
defmacro MyModule do
require Integer
@some_attr "1"
defmacro no_interference do
quote do: a = 1
end
end
"""
assert Solution.collect_module_stats(new_module) == []
end
test "when few functions defined" do
new_module = """
defmacro MyModule do
def hello() do
"world"
end
def test(a, b) do
a + b
end
end
"""
assert Solution.collect_module_stats(new_module) == [
%{arity: 2, name: :test},
%{arity: 0, name: :hello}
]
end
test "when few functions and protocols defined" do
new_module = """
defmacro MyModule do
def hello(string) do
[string, "world"]
end
def magic(a, b, c) do
(a + b) * c
end
defp test(a, b) do
a + b
end
end
"""
assert Solution.collect_module_stats(new_module) == [
%{arity: 2, name: :test},
%{arity: 3, name: :magic},
%{arity: 1, name: :hello}
]
end
end
end
Решение учителя откроется через:
20:00
