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.














