Wczoraj opisałem co to jest Pattern Matching. Wstęp był potrzebny, dzięki czemu ten post będzie krótki i treściwy. Zobaczę, może w ten sposób będę robił resztę postów? Wstęp merytryczny opisujący zagadnienie języka funkcjnego i potem jak to jest w elixir rozwiązane? Nie wiem. Szczerze nie mam żadnego planu z tym elixirem. Uczę się dosłownie tego co mnie zaciekawi :) I tak, pattern matching w elixir mnie zaciekawił – dokładnie zdzwiił gdyż prawie wszędzie jest wykorzystywany i to nawet nieświadomie. Dlatego zachęcam do lektury – same, ale to SAME ciekawostki :)
Operator dopasowania
Normalnie w językach programowania kiedy widzimy zapis:
a = 10
Oznacza to, że przypisujemy wartość 10
do zmiennej a
. W elixir zaś znaczy to, że zmienna a
została z boundowana do wartości 10
. Dokładnie mówiąc wartość 10
dopasowuje się do wzorca a
. Czyli operator =
nie jest operatorem przypisania a dopasowania wzorcowego.
Czyli zapis:
0 = 1
Podobnie jak z funkcją w F# nie powiedzie się. Gdyż wzorzec 0
nie pasuje do wartości 1
. Zresztą odpalcie sobie iex i sami zobaczcie co się stanie jak wykonacie powyższe polecenie?
iex(1)> 0 = 1 ** (MatchError) no match of right hand side value: 1
Widzicie error? MatchError
. Nie jakiś AssignmentError
(wymyślony), ale MatchError
.
Podstawowe dopasowania
Kilka prostych przykładów. Będę opisywał przykład komentarzem a następnie pokazywał jego rezultat. Wszystko powinno dać się skopiować/wkleić do iex
.
# dopasowanie tuple do zmiennej dog = {"Eldir", 10} dog # dopasowanie do poszczególnego wzorca (name i age) {name, age} = {"Eldir", 10} name age # olanie jednej z wartości { _, age } = {"Elidr", 10} # rozbicie tuple na pomniejsze {date, time} = :calendar.local_time {year, month, _} = date date year month # złe dopasowanie {date, time} = 10 ** (MatchError) no match of right hand side value: 10
Stałe – trochę bardziej zaawansowane dopasowania
Teraz, dość ciekawa sytuacja jest wtedy kiedy zaczniemy wykorzystywać stałe, tak zwane atomy. Dokładnie jest to dobrą i normalną praktyką w elixir przy zwracaniu operacji które mogły zakończyć się powodzeniem {:ok, contents}
lub błędem {:error, reason}
.
Prosty przykład
dog = {:dog, "Eldir", 10} {:dog, name, age} = dog name age
Ale po co to? Mamy funckję która może zwrócić nam różne wartości dla przykładu, funkcja dog_or_person
może zwrócić {:dog, name, age}
albo {:person, name, age}
. Teraz to co my robimy to podając atom ograniczamy wynik który nas interesuje:
{:dog, name, age} = dog_or_person
Niby proste przykład, ale teraz kiedy mamy czytanie z pliku, próbę nawiązania połączenia do serwera itp., to możemy wykonywać rózne operację w zależności od tego czy mamy :ok
czy :error
– zaraz będą przykłady.
To co warto jeszcze wspomnieć to to, że możemy zagnieżdżać wzorce:
{{year, month, _}, {hour, _, _}} = :calendar.local_time year month hour
Tak samo to działa na listach:
[one, two, three] = [1, 2, 3]
Z tą różnicą iż listy to taki specyficzny twór w Elixir, gdzie mamy head
i tail
. head
to aktualna wartość a tail
to wszystkie pozostałe, a więc:
[head | tail] = [1, 2, 3]
Da nam:
head 1 tail [2,3]
Jak widzicie, dopasowanie wzorców jest prawie wszędzie :)
Przy mapach zaś możemy napisać na przykład:
%{age: age} = ${name: "Eldir", age: 10} age 10
Co daje nam opcjonalne wartości które możemy olać bez ich określenia za pomocą _
. Albo w ogóle pracując z kodem, który może ulec zmianie (i pewnie ulegnie) – wtedy jeżeli tuple by zmienił liczbę parametrów albo ich kolejność, wszystkie dopasowania musiałyby być zmienione. Za pomocą map. nie.
Dopasowanie w funkcjach
To teraz dosłownie to samo co przy opratorze dopasowania =
można zastosować do funkcji i przekazywaniu do nich parametrów. Wróćmy do przykładu dodawania z poprzednich wpisów:
defmodule Euclid do def add({a, b}) do a + b end end
Teraz możemy podać dowolny tuple dwu elementowy do funkcji. To spowoduje dopasowanie wzorca. Czyli podanie na przykład {10}
skończy się wyjątkiem. Ale ok, co mi to daje? Pamiętacie atomy? To teraz patrzcie:
defmodule Euclid do def op({:add, a, b}) do a + b end def op({:pow, a, b}) do :math.pow(a,b) end def op(whaaat) do {:error, "whaaaaaaat"} end end Euclid.op({:add, 10, 10}) Euclid.op({:pow, 2, 3}) Euclid.op({:daa, 2, 3})
WOW. Daje nam to pewne przeciążenie funkcji. Fajne? Dla mnie super. Teraz już wiecie jak można zastosować {:ok, whatever}
, {:error, reason}
?
A zobaczcie jeszcze na to:
op = &Euclid.op/1 op.({:add, 1, 10})
Fajne? No właśnie, zaczyna być z tego super fajna zabawa. &
to operator przypisania (capture, ale po polsku pojmanie jakoś mi nie pasuje) funkcji.
Strażnicy
Czasami proste wzorce nie wystarczają i musimy zrobić coś więcej – przy dzieleniu przez 0
nie dopóścic do operacji na przykład. Można to zrobić za pomocą strażników (guards) których dodajemy do funkcji przez słowo kluczowe when
:
defmodule Euclid do def div(a, b) when b > 0 do a / b end def div(a, b) do {:error, "Pamiętaj cholero nigdy nie dziel przez zero"} end end
Proste? Proste, bardzo fajne i przyjemne. Oczywiście możemy to bardziej skomplikować:
defmodule Euclid do def div(a, b) when b > 0 and a > 10 do a / b end def div(a, b) when is_number(b) do {:error, "Pamiętaj cholero nigdy nie dziel przez zero"} end end
To samo możemy napisać z wykorzsytaniem lambdy. Podstawowa wyglądała by tak:
div = fn(a,b) -> a/b end div.(2,2)
Z ochroniarzami:
div = fn a, b when b > 0 -> a/b a, b -> {:error, "Pamiętaj cholero nigdy nie dziel przez zero"} end
Jaki piękny zapis! :)
Podsumowanie
Nie pokazałem wszystkiego, to miało tylko pokazać wam, że elixir to ogólnie jeden wielki pattern matching :) Już we własnym zakresie proponuje dowiedzieć się o wzorcu ^
, który umożliwia nam oczekiwać pewną wartość w danym miejscu.
Warto jednak wiedzieć jak wygląda dopasowanie wzorcowe w Elixir i kiedy ono zachodzi oraz jakie z tego możemy czerpać korzyści. Jak widzicie, wzorce są nieodłączną częścią Elixir zaczynają od operatora =
kończąc na zaawansowanych wzorcach polimorfizmu funkcji ze strażnikami.
Więc wiedząc już jak działają wzorce i jak można napisać lambdę, małe zadanie :) kto napiszę ciąg Fibonacciego ? :) Czysta frajda co nie? :)
A za tydzień sam nie wiem :) ciągle mnie te moduły po głowie chodzą i operator pipeline |>
. Zobaczymy! :)
[…] 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 […]
Comments are closed.