W zeszłym tygodniu dowiedziałem się, że Elixir pisze się sobą samym. I by zrozumieć jak to jest możliwe musiałem trochę doczytać oraz pobawić się kodem. Okazuje się, że Elixir posiada wewnętrzną reprezentację każdego kodu który napiszemy. Reprezentacja ta, jest przed nami ukryta. Jednak istnieje możliwość dobrania się do niej i to właśnie umożliwia i wspomaga działania makr.
A więc by lepiej zrozumieć makra musiałem przysiąść i poznać quote
i unquote
.
Quote
quote
umożliwia nam uzyskanie wewnętrznej reprezentacji danego wyrażenia. To znaczy, jak piszemy 1+2
to, nasze wyrażenie jest odpowiednio konwertowane do zapisu rozumianego przez Elixir. By sprawdzić co daje nam 1+2
to wystarczy, że napiszemy:
iex(10)> quote do: 1 + 2 {:+, [context: Elixir, import: Kernel], [1, 2]}
To co nam elixir zwaraca to 3 wartośćiowy tuple:
- Pierwszy to jest atom albo inny tuple.
- Drugi to lista słów kluczowych zawierająca informacje o kontekście, metadanych itp.. Coś co jest wymagane by zadziałało dane wyrażenie.
- Trzecia wartość to lista argumentów albo atom. Jeżeli trzeci parametr to jest atom to znaczy, że cały
quote
dotyczy zmiennej.
Side note: użyłem słowa atom
. Atom
to stała, której nazwa to jej własna wartość. Coś takiego jak symbole w innych językach. Atom
jest przeważnie reprezentowany przez :NAZWA
. Na przykład atom :gutek
ma wartość gutek
. Nazwy modułów zaś to nic innego jak atom alias. Jaki jest ich sens? Pewnie taki sam jak posiadania enum. Przydaje się.
Dla przykładu quote dla x
:
iex(12)> quote do: x {:x, [], Elixir}
Nie rozumiem tylko zbytnio czemu x jest traktowany tutaj jako atom. Elixir zaś na końcu to jest atom. Można to sprawdzić (przy okazji orientując się co to jest atom alias):
iex(31)> is_atom Elixir true iex(32)> quote do: Elixir {:__aliases__, [counter: 0], [Elixir]}
Jeżeli zaś byśmy załadowali projekt nasz z Elixir #03:
defmodule Euclid do def add(a, b) do a + b end end
To możemy wykonać:
iex(33)> quote do: Euclid.add 1, 2 {{:., [], [{:__aliases__, [alias: false], [:Euclid]}, :add]}, [], [1, 2]}
Tutaj sytuacja jest już dużo ciekawsza. Rozbijmy to na czynniki pierwsze.
Pierwszy touple odnosi się do metody add
:
{:., [], [{:__aliases__, [alias: false], [:Euclid]}, :add]}
W którym, atomem jest kropka (Euclid ->.
<- add), listą słów kluczowych nic. Zaś parametrem jest tablica touple i atomu. Ta tablica składa się z argumentów które są do wykorzystania przy wywoływaniu metody. Nie wiem jeszcze dokładnie jak jest metoda wywoływana w tym momencie, ale z tego co rozumiem to, pierwszym elementem argumentów jest tutaj toupe który jest quote
aliasu Euclid.
{:__aliases__, [alias: false], [:Euclid]}
Jak to sprwadzić? A no jest metoda Macro.to_string ()
:
iex(35)> Macro.to_string {:__aliases__, [alias: false], [:Euclid]} "Euclid"
Czyli pozostaje nam ostatni argument który jest atomem :add
.
Łącząc to wszystko, dochodzimy do tego, że linijka to zapis:
Euclid.add
Jak połączymy to z trzecim parametrem do oryginalnego zapisy to otrzymamy:
Euclid.add 1, 2
Takie coś normanie nazywane jest Abstract Syntax Tree (AST). W Elixir quoted expressions. Ładniejszy zapis tego pierwszego touple to na przykład:
{ { :., [], [{ :__aliases__, [alias: false], [:Euclid] }, :add] }, [], [1, 2] }
Ogólnie fajnie się można quote
pobawić by zobaczyć, ze pewny kod tak samo jest zapisywany – czy nawiasy mają znaczenie czy też nie. Nie wiem czemu ale mi osobiście informacja o reprezentacji tego trochę pomogła. Jestem wstanie sobie ciutkę lepiej wszystko w głowie poukładać.
Unquote
quote
reprezentuje wewnętrzny zapis danych w Elixir. Który zwraca nam dosłownie to co żeśmy napisali, tak jak żeśmy napisali. Czyli mając sytuację następującą:
quote do: 1 + num
Po przekonwertowaniu tego za pomocą Macro.to_string
:
Macro.to_string quote do: 1 + num
Otrzymamy zapis początkowy:
1 + num
A co jakbyśmy chcieli wstawić pod num liczbę którą ta zmienna reprezentuje? Tutaj właśnie unquote się przydaje. Czyli by wstawić liczbę musimy napisać:
iex(44)> num = 10 10 iex(45)> quote do: 1 + unqoute(num) {:+, [context: Elixir, import: Kernel], [1, {:unqoute, [], [{:num, [], Elixir}]}]}
Teraz jak byśmy nasz qouted tekst zamienili na string to trzymamy:
iex(50)> Macro.to_string(quote do: 1 + unquote(num)) "1 + 10"
To co chcemy tak jak chcemy.
To się bardzo przydaje w makrach, gdzie makro dostaje quoted tekst i musi taki zwrócić. Ale jak chcemy coś zmienić, podstawić to już musimy wykorzystać unquoted.
Podsumowanie
Dla mnie bomba. Reprezentacja kodu która jest trochę lepiej czytalna niż IL
, nie wymaga tyle kropek co ExpressionBody
. Szczerze, to jest coś co sami jesteśmy wstanie napisać. Serio:
{:gutek, [], Elixir} {:-, [context: Elixir, import: Kernel], [1,2]}
Proste? Proste. I jakie fajne zarazem :) Nawet wewnątrz elixir jest piękny ;)