W zeszłym tygodniu w elixir zajęliśmy się GenServer, przez co wspomniałem o behaviours i o makrze use
. Dziś pora się temu przyjrzeć. Zacznę o behaviours i zobaczymy ile czasu mi na to zejdzie :( święta się zbliżają i z godziny na to mam jedynie 20 minut. Jak się uda to jeszcze dzisiaj będzie o makrze use bo to tak naprawdę działa najlepiej jako całość ;)
Bahaviours to naprawdę interfejs – ciutkę bardziej zaawansowany, ale wciąż interfejs. Niestety przy wykorzystaniu behaviours trzeba przestrzegać pewnych zasad które normalnie możemy olać w trakcie pisania aplikacji w elixir – przez co jest trochę nowości.
Deklaracja
Behaviours deklarujemy za pomocą makra @callback, wygląda to mniej więcej tak:
@callback say_hi() :: any
Gdzie:
@callback
– to dyrektywa wymagana, przed każdą metodą, którą chcemy by była traktowana jako metoda w interfejsie – czyli taka, która trzeba zaimplementować.::
– to dość ciekawy operator (nawet nie wiem jak to nazwać), ogólnie znaczy on “danego typu” ale ma możliwość on być trochę bardziej zaawansowany na przykład może wykorzystywać pattern matching.any
– to dowolny typ danych
Teraz, na przykład możemy zrobić tak by nasza metoda w interfejsie pozwała zwrócić jedynie określony set
@callback say_hi_again() :: { :yes, term :: String} | { :no }
To mówi nam, że metoda say_hi_again może zwrócić dwie różne wartości:
{ :yes, "some_string" } { :no }
Jednak nie ma to znaczenia – jest to bardziej informacja dla kogoś zewnątrz. Elixir tego nie wychwyci jak błąd – nie ma on dobrej walidacji.
Jeżeli chcemy dodać więcej możliwości zwracanych wartości to dodajmy je po operatorze |
– nawet nie wiem jak on się tutaj nazywa. Ale ogólnie wygląda on na takie lub w tym konkretnym przypadku.
Deklaracja typów nie tylko tyczy się tego co zwracamy, ale także tego co przekazujemy jako parametr:
@callback say_hello_to(person :: String) :: any
Gdzie jedynie person musi być ciągiem znaków. Jeżeli nie chcemy podawać typu parametru to wtedy musimy zastosować trick, nasz parametr musi spełnić oczekiwany wzorzec:
@callback say_hello_to(age, person :: String) :: any when age: 10
Na sam koniec, możemy określić które z metod nie muszą być zaimplementowane, możemy to zrobić za pomocą dyrektywy @optional_callbacks
. I tak dla przykładu:
@optional_callbacks say_hello_to: 2
Spowoduje, że nasz przykład say_hello_to(age, person)
będzie opcjonalny do zaimplementowania.
Implementacja
Mając zadeklarowane behaviours pora je zaimplementować. W tym celu wykorzystujemy dyrektywę @behaviour
:
defmodule MyBehaviourImpl do @behaviour Hello end
Teraz musimy zaimplementować wszystkie te metody, które nie były opcjonalne. Nie zaimplementowanie, będzie równe ze zwróceniem warningu w iex
:
warning: undefined behaviour function say_hi_again/0 (for behaviour Hello)
Zaś błędna deklaracja implementacji metody, spowoduje błąd kompilacji:
** (CompileError) test.exs:28: cannot invoke local ::/2 inside match, called as: age :: String (stdlib) lists.erl:1353: :lists.mapfoldl/3
Trzeba go jednak popełnić albo intencjonalnie albo kompletnie przez przypadek.
Podsumowanie
Bechaviours nie są takie trudne jak się wydają. Dość prosto się je piszę i jest to dość zrozumiałe. Prosta konstrukcja, przejrzysty kod. Trochę bardziej explicit niż implicit, ale może nawet i dobrze. Przynajmniej deklarujemy je i implementujemy intencjonalnie a nie przez przypadek ;) To co jednak mi się nie podoba to walidacja tych behaviours. Niby jak interfejsy ale nie aż tak bardzo.
Z chęcią bym zobaczył walidację poprawności zwracanych danych (ciężkie, wiem), ale inaczej nie wiem czemu muszę podawać typ danych.
Nie udało mi się już czasowo poruszyć use
i dlaczego to idzie ładnie w parze z behaviours ale co się odwlecze to nie uciecze :)
[…] To samo co tutaj wykorzystujemy w @spec możemy wykorzystać w @callback (tutaj więcej info). […]
Comments are closed.