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?

11 KOMENTARZE

  1. 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.

  2. 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ść :)

    • @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ć :)

  3. 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.

Comments are closed.