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 inaczejraise/rescue
– wyłapuje błędy, wyjątki nie przewidziane i te przewidziane.try/catch
albo inaczejthrow/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
toswitch
z pattern matchingcond
toelse if
bez pattern matching, jedynie porównywanie wartości- Makro
defexception
służy do deklarowania własnych błędów try/rescue
–raise/rescue
– wychwytywanie wyjątkówtry/catch
–throw/catch
– do kontrolowanego wyjściaafter
– zawsze pocatch/rescue
– takiefinally
- try jest opcjonalny jeżeli jest
rescue
,catch
czyafter
wystę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.