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.