Skip to content

Commit

Permalink
Merge pull request #82 from solar05/add-quoting
Browse files Browse the repository at this point in the history
Add quoting exercise
  • Loading branch information
fey committed Jul 25, 2023
2 parents 756cb04 + 26e053a commit 7aed20e
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 1 deletion.
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()

0 comments on commit 7aed20e

Please sign in to comment.