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

1 KOMENTARZ

Comments are closed.