Pomyśleć by móc napisać o use, napisałem aż dwa dodatkowe posty. Elixir jest pod tym względem bardzo ciekawy. Może nie, nie Elixir, ale nauka języka. Cały czas coś odkrywamy. Przypomina mi się jak podobnie uczyłem się C# – natrafiałem na coś i boom do il code zobaczyć jak to zostało zaprogramowane/zaimplementowane. Więc tutaj by zrozumieć use, trzeba było wiedzieć co to są zachowania i poznać kilka dyrektyw które umożliwiają nam ładowanie, importowanie i aliasowanie modułów. Dziś tą wiedzę wykorzystamy w praktyce i odpowiemy sobie na pytanie dlaczego przy naszym use GenServer mówiliśmy o implementacji zachować a nic nie musieliśmy robić – i nie dlatego, że zachowania były opcjonalne. Bo nie były ;)
Na początku stwórzmy sobie bazowy moduł grupujący zachowania, na bazie którego będziemy tłumaczyć use
:
defmodule Say do @callback hi :: any @callback hi(name::String) :: any @callback bye :: any @callback bye(name::String) :: any @optional_callbacks bye:1 end
Naszym zaś celem będzie stworzenie modułu, który by wyglądał mniej więcej tak (odwołuje się do tej wersji po słowie kluczowym: wersja początkowa):
defmodule SayInEng do use Say end
Tak by przy kompilacji oraz wywoływaniu nie następował żaden problem. Ogólnie chcemy uzyskać GenServer
, ale na prostszym przykładzie :)
use
use
, to nic innego jak z makro zamieniające nasze wywołanie use
na:
require Say Say.__using__()
Gdzie require
wiemy już co robi – ładuje nam dany moduł dając możliwość skorzystania z makr w nim zawartych ale, nie importuje go do naszego modułu. Czyli wciąż musimy się odwoływać do metod/makr poprzez pełną nazwę, w tym wypadku Say.method_name
. Zaś __using__/1
to makro wywoływane od razu po tym jak moduł Say
zostanie załadowany przed require
.
I to wszystko co robi use
. Możemy jeszcze przekazać parametry po use
:
use Say, lang: :eng
To nasze wywołanie __using__/1 będzie wyglądało tak:
Say.__using__(lang: :eng)
__using__/1
__using__/1
to makro, które umożliwia wstrzykiwanie kodu do danego kontekstu – w tym wypadku naszego modułu. Parametrem zaś są opcje, do którym możemy się odwoływać, jednak jakie wartości w tych opcjach powinny być itp. to już decyduje osoba implementująca dany moduł.
Najprostsza implementacja __using__/1
wygląda następująco:
defmodule Say do #callbacks removed for brevity defmacro __using__(_opts) do end end
Śmiało, sprawdzacie sobie w iex
czy wam to śmiga. Działa? Super, to teraz wyjdźcie z iex
i zmodyfikujcie makro na:
defmacro __using__(_opts) do IO.puts "Hello" end
I odpalcie iex
, u mnie wygląda to tak:
$ iex zabawa.exs Eshell V7.2.1 (abort with ^G) Hello Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>
Wyświetliło się Hello
, super ale to jeszcze nic nam nie daje. Pobawmy się tym kodem, zmodyfikujmy __using__/1
tak by nie musieć (w module wykorzystującym nasz kod) wykorzystać przedrostka IO
. Skoro __using__/1
ma służyć jako wstrzykiwanie kodu, to coś takiego powinno zadziałać:
defmacro __using__(_opts) do import IO end
Naszym moduł który robi use na Say
zaktualizujmy do postaci:
defmodule SayInEng do use Say def test do puts "test" end end
(PS, coś słabo mi idzie z wyrabianiem się w czasie, wraz z przykładami, już minęło 20 minut!, ale tym razem się nie dam, skończę temat ;D)
Jak sądzicie, zadziała? Sprawdźcie sami. U mnie wywala:
** (CompileError) zabawa.exs:20: undefined function puts/1 (stdlib) lists.erl:1337: :lists.foreach/2 (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6
U was też? Jak tak to super. By móc użyć import
, to uwaga, musimy zwrócić wewnętrzną reprezentację kodu z makra __using__/1
:
defmacro __using__(_opts) do IO.puts "Say module loaded..." quote do import IO end end
Teraz nasz kod się kompiluje i będziemy mogli z sukcesem wykonać polecenie SayInEng.test
. Pora więc pójść krok dalej i zająć się naszymi behaviours
. Skoro wiemy, by zaimportować musimy to zamknąć w quote do/end
, to tak samo sprawa wygląda z behaviours
:
defmacro __using__(_opts) do IO.puts "Say module loaded..." quote do @behaviour Say def hi, do: "Hi" def bye, do: "Bye" def hi(name), do: "Hi, #{name}" end end
Teraz jak przywrócimy nasz SayInEng
moduł do wersji początkowej, i załadujemy wszystko w iex
, to będziemy mogli wykonać polecenie SayInEng.hi
i nam to zadziała! :)
Dodatkowo, jak już wiemy, to do __using__/1
możemy przekazać parametry, na przykład w jakim języku będziemy działać itp.
defmacro __using__(lang: lang) do IO.puts "Say module loaded... for lang: #{lang}" quote do @behaviour Say def hi, do: "Hi" def bye, do: "Bye" def hi(name), do: "Hi, #{name}" end end
A następnie use
w Say
na:
use Say, lang: "eng"
Teraz przy odpaleniu dostaniemy informacje w jakim języku będziemy mówić hi
. Całość zaś możemy jeszcze trochę udoskonalić dodając do quote
opcje: location: :keep
, dzięki czemu jak się coś wywali to dostaniemy informacje, że wywaliło się to w Say
i w jakiej linijce oraz która linijka z SayInEng
była odpowiedzialna za wywołanie modułu Say
.
Dodatkowo, to, że mamy __using__/1
nie oznacza, ze nie możemy mieć innego kodu w module. Jak już zauważyliście mamy tam behaviours
. Możemy takżw dodać inne metody z których nasz moduł będzie mógł korzystać.
Na przykład dodajcie to do Say
:
def ho_ho_ho do "HO! HO! HO!" end
I potem możecie to wywoływać za pomocą:
Say.ho_ho_ho
Zarówno z poziomy iex
, jak i SayInEng
:
defmodule SayInEng do use Say, lang: "eng" def xmas do Say.ho_ho_ho end end
Efekt końcowy
Na sam koniec pełny kod naszego rozwiązania, który działa zgodnie z naszymi założeniami na początku, ale także może być lekko modyfikowany w zależności od tego co chcemy osiągnąć, możemy na przykład tak zmodyfikować nasze __using__/1
defmodule Say do @callback hi :: any @callback hi(name::String) :: any @callback bye :: any @callback bye(name::String) :: any @optional_callbacks bye: 1 defmacro __using__(lang: lang) do IO.puts "Say module loaded... for lang: #{lang}" quote location: :keep do @behaviour Say def hi, do: "Hi" def bye, do: "Bye" def hi(name), do: "Hi, #{name}" end end def ho_ho_ho do "HO! HO! HO!" end end defmodule SayInEng do use Say, lang: "eng" def xmas do Say.ho_ho_ho end end
Podsumowanie
Chyba po takim opisie wiemy i rozumiemy już na jakiej zasadzie działa use
i dlaczego mieliśmy w use GenServer
dostępne metody które zwaliśmy zrachowaniami, których nie trzeba było implementować. Wszystko z miejsca wygląda na magię ale jak się przypatrzeć jest to dość rozsądne i proste.