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 ;)