Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add quoting exercise #82

Merged
merged 2 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/20-data-types/30-maps/description.ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ instructions: |

tips:
- |
[Официальная документация](https://hexdocs.pm/elixir/1.12/Map.html)
[Официальная документация](https://hexdocs.pm/elixir/Map.html)
4 changes: 4 additions & 0 deletions modules/60-macros/20-quoting/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test:
@ test.sh

.PHONY: test
11 changes: 11 additions & 0 deletions modules/60-macros/20-quoting/description.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
name: Intoduction to quote and unquote
theory: |

🚧 In development

instructions: |

🚧 In development

tips: []
136 changes: 136 additions & 0 deletions modules/60-macros/20-quoting/description.ru.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
name: Знакомство с quote и unquote
theory: |

Вспомним пример из прошлого упражнения:

```elixir
defmodule Exercise do
defmacro double(x) do
{:*, [], [2, x]}
end
end
```

В этом макросе мы вернули код во `внутреннем представлении` языка Elixir, но работать с таким представлением неудобно, особенно если есть вложенность:

```elixir
# в этом макросе выполняется следующая операция ((x * 4) + 3) * x
defmodule Exercise do
defmacro magic(x) do
{:*, [],
[
{:+, [],
[{:*, [], [x, 4]}, 3]},
x
]
}
end
end

require Exercise

Exercise.magic(1)
# 7, т.к. ((1 * 4) + 3) * 1
Exercise.magic(2)
# 22, т.к. ((2 * 4) + 3) * 2
```

В примере выше мы напрямую управляли структурой AST дерева, однако понимать этот код стало сложнее. Для упрощения работы с AST в макросах Elixir есть функции `quote` и `unquote` прямиком перекочевавшие из Lisp-подобных языков.

Функция `quote` принимает произвольное выражение на языке Elixir и преобразует его во внутренне представление языка:

```elixir
quote do: 1 + 2
# => {:+, [], [1, 2]}

quote do: Integer.to_string((1 + 2) * 3)
# => {{:., [], []}, [],
# => [
# => {:*, [],
# => [{:+, [], [1, 2]}, 3]}
# => ]}
```

Теперь попробуем переписать макрос из начала упражнения:

```elixir
defmodule Exercise do
defmacro double(x) do
quote do: 2 * x
end
end

require Exercise

Exercise.double(2)
# => error: undefined variable "x" (context Exercise)
```

Код не работает из-за того, что `x` при передаче в макрос уже в форме внутреннего представления, поэтому нужно сообщить Elixir, что аргумент не нужно переводить во внутренне представление, для этого используется функция `unquote`:

```elixir
defmodule Exercise do
defmacro double(x) do
quote do
2 * unquote(x)
end
end
end

require Exercise

Exercise.double(2)
# => 4
```

По сути мы сообщили Elixir следующее: `Преврати 2 * x во внутреннее представление, но оставь аргумент x в исходном виде.`

Подведем итоги: `quote` означает `преврати все в блоке do в формат внутреннего представления`, `unquote` означает `не превращай это во внутренний формат`.

Многие, кто пишут макросы, частно допускают ошибку, забывая передать аргументы функции `unquote`. Важно помнить, что *все* аргументы передаются макросам во внутреннем формате.

Так же важно знать, что при передаче в `quote` атома, числа, строки, списка или кортежа с двумя элементами, функция вернет аргумент без изменений, а не кортеж во внутреннем формате.

```elixir
quote do: :a
# => :a
quote do: 2
# => 2
quote do: "hello"
# => "hello
quote do: [1, 2, 3]
# => [1, 2, 3]

quote do: {1, 2}
# => {1, 2}
quote do: {1, 2, 3}
# => {:{}, [], [1, 2, 3]}
quote do: %{a: 2}
# => {:%{}, [], [a: 2]}
```

Происходит это потому, что элементы из примера выше тоже представляют собой часть AST структуры Elixir, поэтому их не нужно дополнительно переводить во внутреннее представление.

instructions: |

Создайте макрос `my_unless`, который повторяет семантику `unless`:

```elixir
require Solution

Solution.my_unless(false, do: 1 + 3)
# => 4
Solution.my_unless(true, do: 1 + 3)
# => nil
Solution.my_unless(2 == 2, "hello")
# => nil
Solution.my_unless(2 == 1, "world")
# => "world"
```

tips:
- |
[Официальная документация](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2)
- |
[Про AST](https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%BE%D0%B5_%D1%81%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE)
10 changes: 10 additions & 0 deletions modules/60-macros/20-quoting/lib/solution.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Solution do
# BEGIN
defmacro my_unless(condition, do: expression) do
quote do
if(!unquote(condition), do: unquote(expression))
end
end

# END
end
11 changes: 11 additions & 0 deletions modules/60-macros/20-quoting/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Exercise.MixProject do
use Mix.Project

def project do
[
app: :exercise,
version: "0.1.0",
]
end
end

12 changes: 12 additions & 0 deletions modules/60-macros/20-quoting/test/solution_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Test do
use ExUnit.Case

require Solution

test "my_unless work" do
assert Solution.my_unless(false, do: 1 + 3) == 4
refute Solution.my_unless(true, do: 2)
refute Solution.my_unless(2 == 2, do: "Hello")
assert Solution.my_unless(1 == 2, do: "world") == "world"
end
end
1 change: 1 addition & 0 deletions modules/60-macros/20-quoting/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()