W zeszłym tygodniu wspomniałem o typespace podczas opisywania statycznej analizy kodu. Warto więc poświęcić temu pięć minut. Może to się nam kiedyś w elixir przydać. Jak wiemy, elixir jest dynamicznym językiem, to znaczy, że sprawdzenie typów następuje dopiero w czasie uruchomienia aplikacji. To może prowadzić do różnych dziwnych problemów. Piszę może gdyż od kiedy bawię się językami dynamicznymi to aż tak często się nie zdarza…. Prawie w ogóle? Albo mam mało z tym styczność (ktoś coś wie? Większe doświadczenie ma?). Dlatego też czasami warto jest mieć narzędzie które umożliwi nam nawet nie tylko podanie typów by analiza przeanalizowała dla nas kod, ale po to by osoba która ma z naszej funkcji korzystać wiedziała jakie parametry akceptujemy. Czy też po to by zrobić pattern matching po to by odpowiednią metodę wykonać.

Dobra ale jak… i gdzie możemy skorzystać z określenia parametrów tak by nasze typy były uwzględnione? Elixir udostępnia nam dyrektywę @spec która powinna się znaleźć nad metodą. Określa ona:

@spec nazwa_funckji(typ_parametru, typ_parametru1) :: typ_zwracany

Gdzie typ_* to nazwa typu który jest dostępny w elixir – tego jest trochę poczynając od podstawowych typów takich jak: any, atom, map, pid, integer, float etc; idąc przez literals: true, false, 1..10, [type], %{key: value} kończąc na typach wbudowanych w elixir: term, arity, keyword, node; typy zdalne określone w jakimś module; oraz nasze własne typy.

Typy podstawowe nie mają swoich odpowiedników, ale już literals i typy wbudowane to nic innego jak połączenie dostępnych opcji łączących podstawowe i literals. Dla przykładu list(type) to to samo co [type], number to albo integer albo float.

Czyli mając metodę, która ma nam zwrócić kwadrat danej wartości moglibyśmy napisać:

@spec pow(integer) :: integer
def pow(num), do: num*num

Ale jak byśmy chcieli by teraz ta nasza metoda działała na float to powinniśmy zmienić specyfikację na:

@spec pow(number) :: number

Znak :: oznacza jedynie, że to co jest po lewej zwraca to co jest po prawej.

To samo co tutaj wykorzystujemy w @spec możemy wykorzystać w @callback (tutaj więcej info).

To co jest fajne w @spec to możemy tworzyć strażników i na przykład zezwalać tylko i wyłącznie na wykonanie metody, kiedy parametry są odpowiednich typów. Robi się to tak samo jak w metodach na przykład:

@spec fun(param) :: [param] when param: integer

Elixir jednak byłby nudny, gdyby nie dał nam możliwości tworzenia własnych typów. W szczególności, że czasami moglibyśmy się nieźle namęczyć pisząc ten sam fragment kodu X set razy. A potem jego zmiana? Good luck :) Dlatego też mamy do dyspozycji metody @type i @typep – tak jak z def i defp jedno jest publiczne drugie jest prywatne. To publiczne umożliwia dostęp do typu z poza danego modułu, prywatne już nie.

Definiujemy typ tak samo jak @spec:

@type nazwa_nasza :: { atom, String.t }

Teraz możemy to wykorzystać w ten sposób:

@spec fun(param) :: [param] when param: nazwa_nasza

Lub

@spec fun(param) :: [param] when param: ModuleName.nazwa_nasza

Prosto i sympatycznie. Aaa dlaczego String.t a nie string? A no dlatego – sekcja notes.

Podsumowanie

I to tyle, mówiłem, że tego dużo nie będzie. Oczywiście jakbym miał wymienić i opisać każdy typ to pewnie bym jeszcze pisał, ale tak to przynajmniej wiemy co to, jak z tego skorzystać i gdzie z tego korzystać. A czy warto? To już zależy od specyfiki waszej pracy. Korzystacie ze statycznej analizy kodu? Jak tak, to się wam to bardzo przyda. Jak nie…. To może się to jedynie przydać programistom korzystającym z waszego modułu.

Niezależnie od tego czy się przyda czy nie, warto wiedzieć, że taka możliwość jest :)

Dzięki i miłego wtorku!