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.

ZOSTAW KOMENTARZ

Please enter your comment!
Please enter your name here