Ostatnią opcją debugowania (jeżeli tak ją można nazwać) wbudowaną w elixir albo dokładnie mówiąc w erlang to statyczna analiza kodu. Coś co programiści C# mają by default, już programiści erlanga i elixira nie – wszystko ze względu na dynamiczną naturę tych języków. Poprzednie opcje debugowania to observer, debugger w VS Code i debugger Erlanga, IEx.pry/0 oraz break! – trochę tego jest! :)

Więc co takiego może pomóc statyczna analiza kodu? Nie tylko może przeanalizować kod pod względem standardowych najlepszych praktyk, ale za pomocą Typespec dostępnego w elixir, może sprawdzić też poprawność typów danych na wejściu i wyjściu.

Oczywiście typespec trzeba określić i podać w trakcie pisania kodu. Ja chyba kompletnie pominąłem ten temat a widać trzeba będzie do niego powrócić i go opisać. Ogólnie chodzi o to, że możemy podać w prosty sposób, że nasza funkcja zwraca na przykład integer:

@spec my_fun :: integer
def my_fun do
    1
end

I teraz, taki analizator sprawdzi nam czy aby na pewno mamy zwracamy liczbę całkowitą. Jeżeli robimy jakieś obliczenia to dość łatwo jest zwrócić liczbę zmienno-przecinkową. Więc taka statyczna analiza jest naszym backup planem na wypadek jak o czymś zapomnimy. Plusem statycznej analizy jest to, że możemy, ale nie musimy z niej skorzystać i by to zrobić nic zmieniać nie musimy. Po prostu korzystamy albo nie :)

Statycznym analizatorem kodu w eralngu (a dokładniej mówiąc analizatorem beam czyli każdego języka który kompiluje się do beam bytecode) jest Dialyzer i jest to zwykła aplikacja które po analizie zwraca nam wyniki w postaci:

lib/bencode.ex:37: The pattern 'nil' can never match the type binary() | [any()] | integer() | #{binary() | [any()] | integer() | map()=>binary() | [any()] | integer() | map()}

done (warnings were emitted)

Jednak wykorzystanie erlangowego dialyzera byłoby ciężkie (bardziej skomplikowane niż potrzeba) w elixir. Dlatego też powstała paczka, która umila nam korzystanie z dialyzera pod elixirem. Paczka ma bardzo nietypową nazwę  Dialyxir ;) i możemy ją zainstalować zarówno z poziomu hex jak i globalnie (opcje pozostawiam wam do zbadania).

Jeżeli mowa o hex to znaczy, że jest to zależność naszego projektu mix którą definiujemy w pliku mix.exs w metodzie dep:

{:dialyxir, "~> 0.5.1" }

Tak zdefiniowaną paczkę, możemy zainstalować:

mix do deps.get, deps.compile

Jeżeli się zdarzyło, że nie mamy hex, to zostaniemy poproszeni o jego zainstalowanie:

Gutek-Mac:dbg_test gutek$ mix do deps.get, deps.compile

Could not find Hex, which is needed to build dependency :dialyxir
Shall I install Hex? (if running non-interactively, use "mix local.hex --force") [Yn] Y
* creating /Users/gutek/.mix/archives/hex-0.16.1
Running dependency resolution...
Dependency resolution completed:
  dialyxir 0.5.1
* Getting dialyxir (Hex package)
  Checking package (https://repo.hex.pm/tarballs/dialyxir-0.5.1.tar)
  Fetched package
==> dialyxir
Compiling 5 files (.ex)
Generated dialyxir app

Po takim zabiegu, możemy użyć polecenia poniższego polecenia w celu odpalenia statycznej analizy kodu:

mix dialyzer

Pierwsze uruchomienie zabierze nam trochę czasu bo budowany jest PLT (Persistent Lookup Table) – taki plik który wie gdzie co się znajduje, takie z cachowane wyniki. Taki PLT jest budowany dla podstawowych modułów jak kernel itp. Dzięki czemu jak wykorzystujemy jakąś funkcję z tych modułów to nie musimy za każdym razem czekać aż dialyzer przeanalizuje dane wywołanie. Wszystko ceną około 5-6 minut (tyle to u mnie trwało).

Teraz do czasu zaktualizowania elixira lub erlanga uruchomienie testów dialyzerem będzie bardzo szybkie – no powiedzmy, ale nie w minutach ;)

Gutek-Mac:dbg_test gutek$ mix dialyzer

Checking PLT...
[:compiler, :elixir, :kernel, :logger, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [check_plt: false,
 init_plt: '/Users/gutek/projects/dbg_test/_build/dev/dialyxir_erlang-20.0_elixir-1.5.0_deps-dev.plt',
 files_rec: ['/Users/gutek/projects/dbg_test/_build/dev/lib/dbg_test/ebin'],
 warnings: [:unknown]]
done in 0m1.49s
lib/bencode.ex:37: The pattern 'nil' can never match the type binary() | [any()] | integer() | #{binary() | [any()] | integer() | map()=>binary() | [any()] | integer() | map()}

done (warnings were emitted)

Podumowanie

To tyle z opcji domyślnie dostępnych.  Są jeszcze dwa projekty na które chcę zwrócić uwagę, ale są to osobne biblioteki, co one robią? Sam jeszcze dokładnie nie wiem :) jak się dowiem to o nich z chęcią napiszę.

A tym czasem, pora zabrać się za typespec :)