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! :)

1 KOMENTARZ

Comments are closed.