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