Właśnie sobie uświadomiłem, że aby opisać to czego się dzisiaj nauczyłem potrzebuje znacznie więcej czasu niż mam go dostępnego :( Dlatego też plan na tydzień jest taki, by znaleźć tyle czasu ile się da by za tydzień nie robić skoku w bok tylko poruszać temat procesów i jak to jest to zrobione w Elixr. Tak myślę i w głowie mi się układa więcej niż jeden post na ten temat. Tyle rzeczy do poruszenia, tyle ciekawej technologii. Będzie się działo :)

Dzisiaj robimy skok w bok, czyli uzupełnimy wiedzę na temat kilku makr, które potem mogą się nam przydać – przy okazji, kilka smaczków języka elixir jest wytłumaczonych, np.: dlaczego 10 < [10] == true :)

case

case to typowy (no prawie) switch. Z tą różnicą iż zamiast porównywać wartość z jakąś stałą to porównywana jest wartość do wzorca:

x = "Jola"
case x do
    "JOla" -> "no"
    "jola" -> "no"
    "Jola" -> "ok"
end

W tym wypadku, wartości w case "JOla", "jola" czy "Jola" to wzorce, do których wartość x jest porównywana. Powyższy kod, zwróci nam "ok". Domyślacie co by się stało gdybyśmy nie mieli pasującej wartości?  Dla przykładu byście sami mogli to przetestować:

case x do
    "JOla" -> "no"
end

Powyższy kod zwróci nam:

** (CaseClauseError) no case clause matching: "Jola"

By uniknąć błędu, wystarczy, że dodamy warunek domyślny, który będzie zawsze spełniony. W c# jest to default, w elixir, jest to wzorzec wypałujący wszystko _:

case x do
    "JOla" -> "no"
    _ -> "match not found"
end

Ze względu na to, że Elixir wykorzystuje wzorce, to kolejność elementów w case ma znaczenie. Pierwszy wzorzec który pasuje do wartości zostanie wykorzystany. Więc najlepiej robić od szczegółu do ogółu – tak jak w MVC i routes. Dodatkowo, tak jak przy pattern matching, istnieje możliwość dodawania strażników (jeżeli strażnik wywala błąd, dany wzorzec zwraca niedopasowanie zamiast propagować błąd) do poszczególnych elementów:

x = 10
case x do
    y when y > 10 -> "no"
    y when y == 10 -> "yes"
    y when y <= 10 -> "no"
    _ -> "whaaaat"
end

Jak widać, możemy też tworzyć lokalne zmienne dla danego wzorca. Dodatkowo, zamiast lokalnych, możemy wykorzystać już istniejące, ale wtedy musimy się do nich odwoływać z wykorzystaniem ^:

x = 10
y = 10
case x do
    ^y -> "yes"
    _ -> "whaaaat"
end

cond

cond to nic innego jak else if w C# którego w elixir nie ma. W elixir jest tylko if…else. Zamiast tego mamy makro cond, które daje nam else if w trochę innej, ale imo przejrzystej formie. I tak jak if w elixir nie umożliwia on dopasowania wzorcowego. To znaczy, możemy robić typowe porównanie wartości i sprawdzać który warunek zostanie spełniony. Jednak nie jesteśmy wstanie tego porównywać do wzorców. Czyli zamiast napisać y when y > 10 musimy:

cond do
    x < 10 -> "no"
    x == 10 -> "yes"
    x > 10 -> "no"
end

Tak jak z case, jeżeli żaden warunek nie zostanie spełniony, zostanie zwrócony wyjątek. Dla przykładu:

x = :atom
cond do
    is_number(x) -> "yes"
end

Powyższy kod zwróci nam:

** (CondClauseError) no cond clause evaluated to a true value

Teraz, dlaczego nie zamieniłem w poprzednim przykładzie x po prostu na string? A sprawdźcie sobie takie wyrażenie w iex:

"test" > 10

Zdziwieni? Pewnie zaraz jakieś WATy pójdą ;) Ale tak nie jest, w elixir elementy mogą być porównywane za pomocą < i > nawet jeżeli wartości są różnego rodzaju. W tym momencie o tym czy < i > zwróci true/false decyduje kolejność typów:

number < atom < reference < fun < port < pid < tuple < map < list < bitstring (binary)

Czyli "test" zawsze będzie większy niż 10, ogólnie wszystko będzie większe niż 10 ;)

To tyle jeżeli chodzi o cond.

if, unless, cond, case

Jedna rzecz na temat if, w ogólne na temat if, unless, cond i case. Jako, że wszystko to są makra to mogą być one wykorzystywane w dziwnych miejscach. Na przykład przy wywoływaniu metody:

is_number(cond do
    x < 10 -> "no"
    x == 10 -> "yes"
    x > 10 -> "no"
end)

Co w C# by nie było możliwe.

Error/raise

W Elixir mamy możliwość tworzenia własnych typów błędów z wykorzystaniem makra defexception:

defmodule DivideByZeroError do
   defexception message: "pamiętaj cholero nigdy nie dziel przez zero!"
end

Stworzy to nam błąd DivideByZeroError z domyślną wiadomością "pamiętaj cholero nigdy nie dziel przez zero!". Taki błąd następnie można wykorzystać w kodzie za pomocą funkcji raise:

raise DivideByZeroError

Oczywiście, raise możemy też wykorzystywać bez podawania typu błędu – wtedy domyślnie zostanie ustawiony RuntimeError:

raise "pamiętaj cholero nigdy nie dziel przez zero"

Możemy też zmieniać wiadomość wyjątku:

raise DivideByZeroError, message: "chodź to cholero!"

try/catch/rescue

To nie jest takie jakieś super proste jak w innych językach – try/catch. Ogólnie nie powinniśmy w ogóle z tego korzystać, ale jak już korzystamy to warto wiedzieć kilka rzeczy:

  • try/rescue albo inaczej raise/rescue – wyłapuje błędy, wyjątki nie przewidziane i te przewidziane.
  • try/catch albo inaczej throw/catch – do kontrolowania flow aplikacji, jak na przykład wyjście z zagnieżdżonej pętli, czy przerwanie/zerwanie transakcji albo jeżeli wiemy, że już nie ma sensu dalej kontynuować kodu itp. Funkcja throw pozwala nam wyjście z wykonywania kodu z określoną wartością.

To wszystko działa w parach, to znaczy, jak wykorzystamy raise to catch nam tego nie złapie.

def throw_err1 do
    try do
        throw 10
    catch
        x -> x + 10
    end
end

def raise_err1 do
    try do
        raise DivideByZeroError
    rescue
        x in DivideByZeroError -> IO.puts x.message
        # tego może być wiecej x in RuntimeRrror -> IO.puts x.message
    end
end

def raise_err2 do
    try do
        raise DivideByZeroError
    rescue
        DivideByZeroError -> "error"
        # jak nie chcemy/nie interesuje nas error
    end
end

Pół biedy, my nawet nie musimy pisać try, gdyż zostanie on automatycznie dodany jeżeli raise, rescue albo after jest określone:

def no_try do
    raise "co się stanie, co się stanie?"
    rescue e in RuntimeError -> IO.puts e.message
end

after

To taki nasz finally, umożliwia wykonanie kodu po catch czy też rescue:

def no_try_but_with_after do
    raise "co się stanie, co się stanie?"
    rescue e in RuntimeError -> IO.puts e.message
    after IO.puts "helloooooooooo"
end

Podsumowanie

Jak widać pewne rzeczy mogłyby się wydawać proste i jasne do zrozumienia a wcale takie nie były ;) kilka punktów zwrotnych jak w dobrym kryminale było. To co należy pamiętać to:

  • Wszystko jest makrem, a więc można to wykorzystywać w miejscu przekazania np. parametru
  • case to switch z pattern matching
  • cond to else if bez pattern matching, jedynie porównywanie wartości
  • Makro defexception służy do deklarowania własnych błędów
  • try/rescueraise/rescue – wychwytywanie wyjątków
  • try/catchthrow/catch – do kontrolowanego wyjścia
  • after – zawsze po catch/rescue – takie finally
  • try jest opcjonalny jeżeli jest rescue, catch czy after występuje
  • Nie zaleca się korzsytania z try/rescue, try/catch, try/after
  • Operatorem < i > można porównywać typy

2 KOMENTARZE

  1. No i powoli zaczynam ogarniać, o co w tym wszystkim chodzi ;)
    Czas zacząć się przymierzać do jakiegoś mini projekciku. Myślałem, żeby zacząć ogarniać phoenixa, bawiles sie juz tym?

    • ;) hehe super, gratki :)

      co do Phoenix’a – nie. mam go w planach ale dopiero po apce która go nie potrzebuje. Mam nadzieję, że się ze wszystkim wyrobię bo jeszcze jeden język chciałbym poznać w ciągu roku :)

Comments are closed.