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/rescuealbo inaczej- raise/rescue– wyłapuje błędy, wyjątki nie przewidziane i te przewidziane.
- try/catchalbo 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
- caseto- switchz pattern matching
- condto- else ifbez pattern matching, jedynie porównywanie wartości
- Makro defexceptionsłuży do deklarowania własnych błędów
- try/rescue–- raise/rescue– wychwytywanie wyjątków
- try/catch–- throw/catch– do kontrolowanego wyjścia
- after– zawsze po- catch/rescue– takie- finally
- try jest opcjonalny jeżeli jest rescue,catchczyafterwystępuje
- Nie zaleca się korzsytania z try/rescue,try/catch,try/after
- Operatorem <i>można porównywać typy











 
        



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.