Ale ten czas leci, ledwo co udało mi się wyskrobać te ekstra 2 godziny by poznać coś nowego w elixir i to opisać. Na szczęście się udało :) Tym razem w końcu pobrudziłem sobie rękawy i liznąłem trochę kodu… wnioski? Na końcu posta :) Zapraszam do podróży :)
Setup
Długo myślałem co bym chciał napisać i im dłużej myślałem tym większa dziurę w głowie miałem. Więc zrobiłem to samo co zwykle robię w takiej sytuacji. Zdecydowałem, że napiszę banalny, powtarzam BANALNY kalkulator. Głównym celem kalkulatora będzie:
- Zdefiniowanie paru metod publicznych
- Może jakąś metodę prywatną
- Coś na kształt interfejsu?
- Wyjątki, wyjątki, wyjątki
- Unit test bym wiedział, że wszystko śmiga ;)
Wykorzystałem wiedzę swoją z ostatniego posta i dla tego celu stworzyłem prosty projekt z wykorzystaniem mix
. Na tym projekcie następnie bazowałem całą swoją zabawę:
mix new euclid
Konstrukcja metody/testów w Elixir
Wychodzi na to, że wszystko tutaj jest dość proste pod tym względem. Piszemy słowo kluczowe
a następnie nazwę
i potem do/end
:
# własny DSL test "nazwa testu" do end # podobne do public void|object privateMethod() {} def publicMethod() do end # podobne do private void|object privateMethod() {} defp privateMethod() do end
Tam jest więcej opcji, ale ja osobiście na razie potrzebowałem tylko tych. Wszystkie te medoty trzeba dodać do modułu który jest tworzony przez słowo klucz defmodule
i znów do/end
:
# coś na kształt class Nazwa {} defmodule Nazwa do end
Na razie to wygląda całkiem spoko i prosto. Pewnie zastosowałem tutaj niesamowite skróty myślowe ale… na końcu all się wyjaśni.
Testy w Elixir
Wszystko zacząłem pisać jak przystało na programistę 21 wieku od testów. Na pierwszy rzut poszły testy do dodawania, w pliku /test/euclid_test.exs
usunąłem domyślny test i napisałem swój pierwszy ;)
defmodule EuclidTest do use ExUnit.Case doctest Euclid test "adding 1 to 1 should give 2" do assert Euclid.add(1,1) == 2 end end
Ha :) jaki banalny. To co jest ciekawe, nie musiałem tutaj tworzyć Euclid, on tak jakby jest już stworzony (więcej o tym na końcu postu). Odpalenie testów też było proste:
> mix test Compiling 1 file (.ex) warning: function check_number/1 is unused lib/euclid.ex:4 1) test adding 1 to 1 should give 2 (EuclidTest) test/euclid_test.exs:5 ** (UndefinedFunctionError) function Euclid.add/2 is undefined or private stacktrace: (euclid) Euclid.add(1, 1) test/euclid_test.exs:6: (test) Finished in 0.03 seconds 1 test, 1 failure Randomized with seed 795733
No jasne ;) metody nie ma. Trzeba ją dodać. Do pliku /lib/euclid.ex
więc dodałem:
defmodule Euclid do def add(a, b) do a + b end end
I odpaliłem testy:
> mix test Compiling 1 file (.ex) . Finished in 0.03 seconds 1 test, 0 failures Randomized with seed 690171
Świetnie, wszystko na razie działa tak jak chciałem. Więc dopisałem szybko pozostałem testy do odejmowania, mnożenia i dzielenia:
defmodule EuclidTest do use ExUnit.Case doctest Euclid test "adding 1 to 1 should give 2" do assert Euclid.add(1,1) == 2 end test "dividing 5 by 5 should give 1" do assert Euclid.divide(5,5) == 1 end test "subtracting 1 from 2 should give 1" do assert Euclid.sub(2,1) == 1 end test "multipling 1 by 2 should give 2" do assert Euclid.multiple(1,2) == 2 end end
Do każdego z testów dodałem kod i było wszystko tak jakby ok ;)
Wywoływanie błędówów
Pora przyszła na przetestowania zachowania co się stanie kiedy wywołam błąd z poziomu kodu. W elixir błędy wywołuje się poprzez słowo kluczowe raise
i opis błędu. W moim przypadku zmodyfikowałem metodę implementującą dzielenie tak by wyrzucała wyjątek przy dzieleniu przez zero:
def divide(a,b) do if b == 0 do raise "Pamiętaj cholero, nigdy nie dziel przez zero!" else a / b end end
Teraz trzeba było to przetestować. Prosty test:
test "we should not be able to divide by zero" do assert Euclid.divide(1,0) end
już nie zadziałał:
> mix test ..... 1) test we should not be able to divide by zero (EuclidTest) test/euclid_test.exs:5 ** (RuntimeError) Pamiętaj cholero, nigdy nie dziel przez zero! stacktrace: (euclid) lib/euclid.ex:27: Euclid.divide/2 test/euclid_test.exs:7: (test) Finished in 0.06 seconds 6 tests, 1 failure Randomized with seed 235372
Okazuje się, że w elixir istnieje specyficzny assert
do przechwytywania błędów: assert_raise
. Jego konstrukcja jest następująca:
assert_raise TypeOfError, "Message to check", annonymous funcion that should fail
Anonimowe funkcje zaś tworzy się tak samo prawie jak w C#:
fn -> #do things end
Czyli kod testu powinien wyglądać następująco:
test "we should not be able to divide by zero" do assert_raise RuntimeError, "Pamiętaj cholero, nigdy nie dziel przez zero!", fn -> Euclid.divide(1,0) end end
Idąc za ciosem zrobiłem też szybki test na dodawaniu czy aby na pewno dodaje liczby:
defp check_number(a) do if !is_number(a) do raise "#{a} is not a number" end end def add(a, b) do check_number(a); check_number(b); a + b end
Tutaj widać opcję interpolacji ciągu znaków – #{a}
podstawi to co jest pod zmienną a
, zaś is_number
to wbudowana funkcja sprawdzająca czy przekazany parametr to liczba. Do przypadku oczywiście powstał test, najpierw, nie potem ;)
test "adding 1 to HELLO should throw error" do assert_raise RuntimeError, "HELLO is not a number", fn -> Euclid.add(1,"HELLO") end end
Prawie jak Interfejsy
Z tym miałem problem, głównie przypadkiem na to natrafiłem jak szukałem/czytałem informacje o def
i defp
. Mianowicie, każdy moduł może posiadać wiele atrybutów. Jednym z atrybytów jest @callback
który umożliwia deklarowanie zachowania (behaviours). Te zachowania następnie mogą być nakładane na inne moduły. @callback
to nic innego jak deklaracja metody bez jej implementacji, ale z informacją na temat parametrów:
defmodule Euclid.Contract do @callback sub(a :: any, b :: any) :: integer end
Gdzie ::
określa typ danego parametry, zwracanej wartości. Taki moduł tworzymy i zapisujemy jako osobny plik w w /lib
. Ja go nazwałem euclid.contract.ex
.
Następnie w module w którym chcemy wykorzystać dane zachowanie (w naszym przypadku Euclid) wykorzystujemy atrybut @behaviour
:
defmodule Euclid do @behaviour Euclid.Contract # ... end
Nie jest to tak, że jak nie zaimplementujemy tego zachowania to się nam niebo na głowę zwali. Nie, jedynie dostaniemy mały warning przy kompilacji
> mix test Compiling 1 file (.ex) warning: undefined behaviour function sub/2 (for behaviour Euclid.Contract) lib/euclid.ex:1 ...... Finished in 0.06 seconds 6 tests, 0 failures Randomized with seed 76359
To co jest fajne, to elixir posiada opcje opcjonalnych zachowań, to znaczy takich, których nie trzeba implementować ;) wszystko dzięki atrybytowi @optional_callbacks
który przyjmuje listę metod i liczbę ich implementacji (?):
defmodule Euclid.Contract do # To jest prawie jak interfejs, prawie robi rónicę @callback sub(a :: any,b :: any) :: integer @callback whatever() :: any # to nie bedzie potrzebne @optional_callbacks whatever: 0 end
Ciekawa rzecz jest taka, że jeżeli zamiast 0
wstawimy 1
to dostajemy błąd kompilacji a nie tak jak przy niezaimplementowaniu sub – warning.
> mix test Compiling 2 files (.ex) == Compilation error on file lib/euclid.contract.ex == ** (CompileError) lib/euclid.contract.ex:8: callback whatever/1 is undefined (stdlib) lists.erl:1338: :lists.foreach/2 (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
To tyle lekcji na dzisiaj. Pora na dwa trzy ostatnie punkty.
Bonus
Pamiętacie z poprzedniej lekcji polecenie iex -S mix
? Pora je sobie przypomnieć ;)
Pełny kod z “lekcji”
Poniżej pełny kod z trzech plików. Zaraz pod nim podsumowanie.
# /lib/euclid.contract.ex defmodule Euclid.Contract do # To jest prawie jak interfejs, prawie robi rónicę @callback sub(a :: any,b :: any) :: integer @callback whatever() :: any # to nie bedzie potrzebne @optional_callbacks whatever: 0 end # /lib/euclid.ex defmodule Euclid do @behaviour Euclid.Contract defp check_number(a) do if !is_number(a) do raise "#{a} is not a number" end end def add(a, b) do check_number(a); check_number(b); a + b end def sub(a, b) do a - b end def multiple(a,b) do a * b end def divide(a,b) do if b == 0 do raise "Pamiętaj cholero, nigdy nie dziel przez zero!" else a / b end end end # /test/euclid_test.exs defmodule EuclidTest do use ExUnit.Case doctest Euclid test "we should not be able to divide by zero" do assert_raise RuntimeError, "Pamiętaj cholero, nigdy nie dziel przez zero!", fn -> Euclid.divide(1,0) end end test "dividing 5 by 5 should give 1" do assert Euclid.divide(5,5) == 1 end test "adding 1 to 1 should give 2" do assert Euclid.add(1,1) == 2 end test "subtracting 1 from 2 should give 1" do assert Euclid.sub(2,1) == 1 end test "multipling 1 by 2 should give 2" do assert Euclid.multiple(1,2) == 2 end test "adding 1 to HELLO should throw error" do assert_raise RuntimeError, "HELLO is not a number", fn -> Euclid.add(1,"HELLO") end end end
Podsumowanie
Mam wrażenie, że robię coś źle. Pomijam jakąś istotną część całego elixira. I chyba ten tydzień poświęcę na to by to odkryć i już kolejną lekcję bym chciał zrobić bez uczucia, że coś źle robię. Pewnie to będzie oznaczało powrotu do definicji i teorii, ale zobaczymy. Na przykład chciałbym móc wytłumaczyć dlaczego Euclid jest dostępny bez inicjalizacji albo dokładniej mówiąc on jest tworzony, ale kiedy? Gdzie? I przez co? Czy może to jest statyczne coś? Nie wiem, mogę pisać głupoty tutaj. Dlatego chyba będę musiał coś zmienić w tej nauce ;)
Ktoś z was też się uczy elixira? Może macie pomysł co powinienem zrobić? Albo może wam czegoś brakuje tak samo jak mi tutaj?
Tez sie go ucze. Tylko ja najpierw Ruby zglebialem, a elixir jest dla mnie jakby jego funkcyjna wersja. Niestety przez chwile nie mam czasu :)
Ja to rozumiem tak, a pewnie sie myle, ze mix euclid tworzy modul euclid, ktory zawiera metody. Co wazne dla mnie modul to nie klasa a klasa statyczna. Bo to nie jest jak w f# ze masz obiekty, tu rozumiem sa tylko funkcje.
Ale zaznaczam ze pewnie sie myle, bo kestem jakos na tym samym poziomie nauki… Za miesiac pewnie rusze.
@Pawelek
Dzięki, ma to ręce i nogi :)
I ja również. Na moją wiedzę o Elixirze, wygląda to tak, że moduł należy traktować jako coś, co gromadzi funkcje. I tylko tyle. Określenie go mianem klasy statycznej wydaje mi się dobrym skrótem myślowym @Pawełek. Można powiedzieć też namespace.
Nie szukałbym w Elixirze żadnych obiektów (czegokolwiek co posiada swój stan; a przynajmniej nie w tym momencie), tu raczej posługujemy się transformacją danych wejściowych. Funkcje Twojego Euclida właśnie takie są, przyjmują dwa parametry i przekształcają je na (chociażby) ich sumę i to jest dobrze. Teraz możesz zobaczyć jak kolejne funkcje tego modułu można ładnie “pipeować” operatorem “|>” w coś większego :D
Moim zdaniem, najlepszym co możesz (możecie) zrobić dla siebie, chcąc nauczyć się Elixira, jest wykonanie kilku zadań zamieszczonych tutaj: http://exercism.io/languages/elixir/. Absolutnie przyjazne miejsce do nauki :D
@macborowy
Dzięki za linka, na pewno skorzystam. O przekształcaniu danych wejściowych i pipeline właśnie chciałbym napisać więcej, ale muszę to lepiej opanować.
Dzięki za info o module. Muszę po prostu to lepiej merytorycznie chyba opanować. W sensie podstawy podstawy i podstawy ;) jakoś z nimi lepiej zrozumieć całość :)
Jako programista .Netowy, nie zastanawiałeś się nad wyborem F#? Elixir jest lepszy? Co Cię do niego skłoniło :)?
@Jurek
Pisałem w F#, język mi nie leży. jest dla mnie nie czytelny. Natrafiałem na problemy których nie mogłem obejść. Ma on ekstra funkcje i możliwości. Ale też jest to inny język funkcyjny niż Elixir. Elixr ma w sobie Actor model, dzięki czemu jest idealnym rozwiązaniem dla aplikacji rozproszonych, wieloserwerowych, wielowątkowych, wydajnych, z naciskiem na stabilnych aplikacji. Te języki, mimo, że funkcyjne spełniają zupełnie inne założenia.
Do tego… Elixir jest piękny a jak masz coś brzydkiego w nim to możesz to upiększyć :)
@Gutek
Dzięki, to chyba mu się przyjrzę bliżej w “wolnej chwili”;)
[…] z poprzedniego tygodnia add i divide powinny się nazywaś add! i divide!. Zobaczę jak mi pójdzie dostosowanie się do […]
[…] Jeżeli zaś byśmy załadowali projekt nasz z Elixir #03: […]
Wybacz że piszę tutaj, możliwe że już dalej to rozwiązałeś ale byłem na Twojej prezentacji wczoraj i czytam od 1 posta :) niestety Elixir i ify to nie jest najlepsze rozwiązanie :D
Jako że lecimy od góry pliku to funkcja divide(a,b) powinna raczej wygladać tak:
def divide(a,0), do: raise “Pamiętaj cholero, nigdy nie dziel przez zero!”
def divide(a,b), do: a/b
Pattern match is strong with this one. Powinny testy przejść przy tym. Jeśli kiedyś wpadniesz również na jakieś tego typu problemy to guardy ratują wszystko.
Albo nawet raczej
def(_, 0), do: raise…
Comments are closed.