Haskell/Dopasowanie wzorców i instrukcje warunkowe
Dopasowanie wzorców
edytujTak naprawdę dopasowanie wzorców pojawiało się już wielokrotnie. Na przykład funkcja map:
map _ [] = [] map f (x:xs)= f x : map f xs
wykorzystuje 4 różne wzorce.
- _ - dzika karta (wild-card)
- [] - pusta lista
- f - dowolne wyrażenie
- (x:xs) - coś co może powstać za pomocą operatora cons (:)
Wzorcem może być dowolne wyrażenie będące konstruktorem określonego typu.
Poniższy przykład wykorzystuje wzorzec, który dopasowuje wyrażenie do określonych wartości:
fun :: ([a], Bool) -> Int -> Int fun ([], True) 1 = 0
Do wzorca dopasowana zostanie tylko krotka o dokładnie takich wartościach jakie zostały określone we wzorcu. Często potrzebujemy dopasować do danego wzorca kilka zestawów parametrów spełniających określone warunki.
Załóżmy, że chcemy napisać funkcję "podziel a b" działającą w następujący sposób:
- jeśli oba parametry będą równe 0 - funkcja zwróci 0
- jeśli drugi parametr będzie równy 1 - funkcja zwróci wartość pierwszego parametru (nie wykonując dzielenia)
- jeśli drugi parametr będzie równy 0 - funkcja zwróci błąd - dzielenie przez 0
- jeśli żaden z powyższych warunków nie będzie spełniony funkcja wykona dzielenie i zwróci wynik
Pierwszy warunek może zostać zrealizowany za pomocą wzorca podobnego do tego z poprzedniego przykładu:
podziel 0 0 = 0
W kolejnym przypadku pierwsza wartość może być dowolną liczbą, wartość drugiego parametru to 1.
podziel a 1 = a
Jeśli drugi z parametrów będzie równy 0 zostanie wywołana funkcja error. Na pierwszy rzut oka warunek ten wygląda podobnie do poprzedniego. W poprzednim przypadku jednak wartość pierwszego parametru była potrzebna do wyznaczenia wartości funkcji. Tutaj pierwszy parametr w żaden sposób nie jest wykorzystywany. Pozwala to na zastosowanie wzorca nazywanego dziką kartą (wild-card). Wzorzec ten oznaczony jest za pomocą symbolu "_".
podziel _ 0 = error "dzielenie przez zero"
Ostatni z warunków zrealizowany zostanie za pomocą wzorca, do którego dopasowane zostaną dowolne dwie liczby, które nie zostały dopasowane do powyższych wzorców.
podziel a b = a/b
Dopasowanie wzorców i listy
edytujMechanizm dopasowania wzorców jest bardzo przydatny podczas pracy z listami.
Jak wiadomo, jako wzorzec może zostać użyte dowolne wyrażenie będące konstruktorem określonego typu. Lista może być utworzona na 3 sposoby:
- poprzez wypisanie wszystkich jej elementów oddzielonych przecinkami np. [1,2,3]
- poprzez symbol listy pustej []
- poprzez operator cons (:), który tworzy listę danego elementu oraz starej listy np. 1:[2,3]
Wszystkie wymienione przypadki można wykorzystać podczas tworzenia wzorców.
Do wzorca [a,b,c] zostanie dopasowana każda lista trójelementowa. Wartości listy zostaną przypisane do odpowiednich identyfikatorów (a,b lub c).
Do wzorca [] zostanie dopasowana tylko i wyłącznie lista pusta.
Do wzorca (x:xs) zostanie dopasowana każda lista, którą da się utworzyć za pomocą operatora cons, a więc taka, która posiada co najmniej jeden element (listę pustą można utworzyć jedynie za pomocą symbolu "[]").
Oczywiście nic nie stoi na przeszkodzie, aby podczas tworzenia wzorców do których dopasowywane są listy używać dzikiej karty. Na przykład definicja funkcji tail zwracającej ogon listy nie musi znać wartości głowy listy.
tail (_:xs) = xs
Definicja funkcji head nie musi natomiast wiedzieć jak wygląda ogon. Ważna jest tylko wartość głowy.
head (x:_) = x
Operator cons może być stosowany wielokrotnie w jednym wzorcu. Definicja funkcji zwracającej trzeci element z listy może wyglądać następująco:
trzeci (_:_:x:_) = x
Uwaga!
|
Wzorzec n+k
edytujJak wcześniej wspomniano, wzorzec to konstruktor dowolnego typu. Od tej reguły jest jednak wyjątek nazywany wzorcem "n+k".
fun (a+1) = a
Taka funkcja zostanie uznana za poprawną, mimo iż (a+1) nie jest konstruktorem żadnego typu. Konstrukcje tego typu są poprawne, ale powinno się ich unikać, gdyż są mało czytelne. Lepszym rozwiązaniem jest zapisanie funkcji w następujący sposób:
fun a = a-1
Wzorzec "as"
edytujCzasami warto nadać pewnej części wzorca (lub całemu wzorcowi) identyfikator, który można wykorzystywać po prawej stronie równania. Na przykład funkcja dublująca pierwszy element listy może wyglądać tak:
fun (x:xs) = x:x:xs
Wzorzec x:xs pojawia sie w identycznej formie po lewej i po prawej stronie równania. Aby zwiększyć czytelność można nadać mu identyfikator za pomocą wzorca "as" (symbol "@"):
fun s@(x:xs) = x:s
Wzorzec "as" nie wpływa na działanie funkcji, a jedynie zwiększa czytelność kodu.
Stosowanie wzorców
edytujW powyższych przykładach wzorce były używane w równaniach definiujących funkcję. Nie jest to jednak jedyne miejsce gdzie znajdują one zastosowanie. Wzorce stosowane są między innymi w:
- funkcjach lambda
- instrukcji case
- wyrażeniach where oraz let
Instrukcje warunkowe
edytujif ... then
edytujif <warunek> then <true-value> else <false-value>
Jeśli spełniony zostanie warunek zwracana jest wartość <true-value> w przeciwnym wypadku <false-value> Warto zauważyć, że <true-value> oraz <false-value> są wartościami zwracanymi przez wyrażenie if, a nie (jak w większości języków) sekwencjami instrukcji do wykonania.
Uwaga!
|
case
edytujcase <wyrazenie> of <wzorzec_1> -> <wartosc_1> <wzorzec_2> -> <wartosc_2> ... <wzorzec_n> -> <wartosc_n>
Instrukcja "case" jest uogólnieniem instrukcji "if". Instrukcja "if" zwraca wartość w zależności od tego jaka jest wartość wyrażenia typu logicznego (warunku). Instrukcja "case" umożliwia zwrócenie różnych wartości w zależności od tego jaką wartość będzie miało wyrażenie dowolnego typu, a dokładniej mówiąc w zależności od tego do jakiego wzorca zostanie dopasowane. Instrukcję "if" można zapisać za pomocą "case" w następujący sposób:
case <warunek> of True -> <true-value> False -> <false-value>
Poniższy przykład wykorzystuje instrukcję case do konwersji liczby na dzień tygodnia:
case i of 1 -> "Poniedzialek" 2 -> "Wtorek" 3 -> "Środa" 4 -> "Czwartek" 5 -> "Piątek" 6 -> "Sobota" 7 -> "Niedziela"
Zmienna "i" dopasowana jest do jednej z liczb. Możliwe jest również dopasowanie do innych typów wzorców. Za pomocą instrukcji "case" możliwe jest zdefiniowanie funkcji "podziel" działającej tak samo jak zdefiniowana wcześniej funkcja. Nowa funkcja "podziel", w przeciwieństwie poprzedniej, przyjmuje parametry jako krotkę:
podziel (a,b) = case (a,b) of (0,0)-> 0 (_,1)-> a (_,0)-> error "dzielenie przez zero" (a,b)-> a/b
Uwaga!
|
Guard - strażnicy
edytuj| <warunek_1> = <wartosc_1> | <warunek_2> = <wartosc_2> ... | <warunek_n> = <wartosc_n>
Instrukcje "if" oraz "case" mają swoje odpowiedniki w większości języków programowania. Strażnicy to instrukcja, którą możemy porównać do instrukcji "if" z dodatkowymi warunkami "if else". Przykładem wykorzystania strażników może być funkcja określająca znak liczby:
sign a | a>0 = "dodatnia" | a==0 = "zero" | a<0 = "ujemna"
Strażnicy to kolejne warunki odpowiadające "if else". Aby określić jaka wartość powinna być zwrócona jeśli żaden z warunków nie zostanie spełniony (odpowiednik "else") definiując ostatniego strażnika zamiast warunku należy wpisać "otherwise". Na przykład:
duzaCzyMala c | c >= 'a' && c <= 'z' = "Mała" | c >= 'A' && c <= 'Z' = "Duża" | otherwise = "to nie litera!"