HASKELL 2 R O Z D Z I A Ł 4 SKŁADNIA W FUNKCJACH R O Z D Z I A Ł 5 REKURENCJA Learn You a Haskell for Great Good! Miran Lipovac
ROZDZIAŁ 4 ODNAJDYWANIE WZORCA (P ATTERN MATCHING)
CZYM JEST PATTERN MATCHING? Odnajdywanie wzorca, czy też bardziej popularna nazwa pattern matching polega na sprecyzowaniu wzorców, dla których niektóre dane powinny być zgodne, sprawdzeniu wzorców oraz rozkładzie danych zgodnie z wzorcami. Możliwe jest definiowanie oddzielnych funkcji dla różnych wzorców. Umożliwia to przekierowywanie z poszczególnych typów danych do innych, np. z int'a do list, znaków itp.
Przykładowa funkcja przedstawiająca przypisanie ciągów do liczb: test:: ( Integral a) => a -> String test 0 = " To zdanie jest fałszywe" test 1 = " To zdanie jest prawdziwe" test x = " To nie jest zdanie logiczne"
Gdy zostanie wywołana funkcja test wzorzec zostanie sprawdzony od góry do dołu. W przypadku, gdy odnajdzie on pasujący wzorzec zostanie użyta odpowiednia funkcja (w tym przypadku wyświetli jeden z napisów). Jeśli liczba testowana wynosi 0, sprawdzanie patternu zatrzyma się już na pierwszej funkcji odpowiadającej tej liczbie (mimo iż 0 zawiera się również w "x") i wyświetli napis " To zdanie jest fałszywe. Jeśli jednak spróbujemy odnaleźć wzorzec dla jakiejkolwiek liczby spoza systemu binarnego, pierwszą odpowiadającą funkcją będzie dopiero ta w 4 linijce - " To nie jest zdanie logiczne".
Oczywiście funkcja tego rodzaju mogłaby w bardzo prosty sposób być zastąpiona funkcja IF, jednakże użycie wzorców jest idealnym rozwiązaniem, gdy mamy więcej funkcji. Oto przykładowy wzorzec przypisujący nazwę dnia tygodnia do numeru dnia tygodnia: week:: ( Integral a) => a -> String week 1 = " Poniedzialek " week 2 = " Wtorek " week 3 = " Sroda " week 4 = " Czwartek " week 5 = " Piatek " week 5 = " Sobota " week 5 = " Niedziela " week x = " To nie jest liczba z przedzialu 1-7"
Bardzo podobne do wzorców jest funkcja switch w programowaniu proceduralnym, oto jej przykład zastosowania w jeżyku c++ switch (week) { case "1": cout << "Poniedzialek"; break; case "2": cout << "Wtorek "; break; case "3": cout << "Sroda "; break; case "4": cout << "Czwartek "; break; case "5": cout << "Piatek"; break; case "6": cout << "Sobota"; break; case "7": cout << "Niedziela"; break; default: cout<< " To nie jest liczba z przedzialu 1-7"; break; }
To, co odróżnia je od siebie to kolejność czytania, w switchu wszystkie przypadki (case) są równoważne, natomiast gdybyśmy w wzorcu zamienili linijkę 2 i 9 napis " To nie jest liczba z przedziału 1-7" byłby wyświetlany dla każdej liczby. week:: ( Integral a) => a -> String week x = " To nie jest liczba z przedzialu 1-7" week 2 = " Wtorek " week 3 = " Sroda " week 4 = " Czwartek " week 5 = " Piatek " week 5 = " Sobota " week 5 = " Niedziela " week 1 = " Poniedzialek "
Wzorce umożliwiają również pisanie funkcji rekurencyjnych, jak np. silnia silnia:: ( Integral a ) => a -> a silnia 0 = 1 silnia n = n * silnia ( n - 1) Dzięki temu ze najpierw zdefiniowaliśmy silnie dla 1, wzorzec nie będzie próbował liczyć silni -1, bo zatrzyma się na silnia 0. Gdyby nie to, nasza rekurencja była by nieskończona.
Oczywiście, jeśli poszukujemy wzorca, który nie został przez nas wcześniej zdefiniowany otrzymamy błąd. imie:: Char -> String imie'a ' = " Rafal " imie'b ' = " Lukasz " imie'c ' = Michal
ghci > charname 'a ' Rafal ghci > charname 'b ' Lukasz ghci > charname 'h ' *** Exception: tut. hs :(53,0) -(55,21): Non - exhaustive patterns in function charname Program informuje nas o tym ze nie znalazł żadnego wyjścia przypisanego do wejścia, które mu podaliśmy. Dlatego należy dodawać na końcu wzorca jakiś odpowiednik "default a" z switcha, czyli np.: imie x = "Nie ma takiego wpisu". Unikniemy w ten sposób błędów oraz niespodziewanych efektów.
Odnajdywanie wzorców może być również stosowane na krotkach. Oto przykład funkcji, do której wpisujemy dwa wektory opisane na dwu wymiarowej przestrzeni. Jej zadaniem jest obliczenie sumy wektorowej. Należałoby zsumować pierwsze liczby obu wektorów i drugie liczby wektorów oddzielnie. Nie używając wzorców, można to zrobić w następujący sposób: suma :: ( Num a ) = > (x, x) -> (x, x) -> (x, x) suma x y = ( fst x + fst y, snd x + snd y)
Sposób jak najbardziej działający, ale niezbyt optymalny, szczególnie przy bardziej złożonych krotkach. Ta sama funkcja zbudowana za pomocą odnajdywania wzorców wyglądałaby następująco: suma :: ( Num a) = > (a, a) -> (a, a) -> (a, a) suma (x1, y1 ) (x2, y2 ) = ( x1 + x2, y1 + y2 )
Metoda ta jest o wiele bardziej optymalna, gdyż równie łatwo można ja zastosować do większych krotek. Tak wyglądałaby funkcja tego typu dla trzy liczbowego wektora: frst:: (a, b, c) -> a frst(x, _, _ ) = x scnd:: (a, b, c) -> b scnd(_, y, _) = y thrd:: (a, b, c) -> c thrd(_, _, z ) = z
WZORCE UMOŻLIWIAJĄ TEZ ZDEFINIOWANIE LISTY ARGUMENTÓW, Z KTÓREJ KORZYSTA FUNKCJA ghci > let xs = [(1, 3), (4, 3), (2, 4), (5, 3), (5, 6), (3,1)] ghci > [a+ b (a, b) <- xs ] [4,7,6,8,11,4] Definiujemy xs jako listę wektorów 2 stopnia oraz przypisujemy ta listę do dwóch zmiennych. W wypadku, gdy program nie odnajdzie odpowiedniego wzorca, przemieszcza się do kolejnego elementu.
Listy same w sobie mogą być użyte do stworzenia wzorca, np. x:xs przypisze head listy do x'a, natomiast cala resztę listy do xs, nawet jeśli lista jest jednoelementowa, czyli xs stanie się lista pusta. Rozwiązanie to znajduje swoje zastosowanie w tworzeniu funkcji rekurencyjnych. Jeśli np. chcemy przypisać trzy pierwsze elementy listy do trzech różnych zmiennych i resztę listy do czwartej zmiennej, wystarczy użyć x:y:z:zs, ważne zęby pamiętać ze listy porównywane z tym wzorcem musza mieć co najmniej 3 elementy.
Znając te rozwiązania możemy stworzyć nasza własna funkcje "head : head ' :: [a] -> a head ' [] = error " Pierwszy element pustej listy? To raczej nie zadziała..." head ' (x: _) = x A oto nasza własna funkcja lenght: length ' :: ( Num b) => [a] -> b length ' [] = 0 length ' ( _: xs ) = 1 + length ' xs
Oczywiście dla pustej listy lenght będzie wynosił 0. Jak widać jest to funkcja rekurencyjna, która usuwa pierwszy element listy i zwiększa lenght o 1, aż lista będzie pusta? Spójrzmy jak wygląda implementacja funkcji sum: sum ' :: ( Num a) => [a] -> a sum ' [] = 0 sum ' (x: xs ) = x + sum ' xs Oczywiście dla pustej listy suma będzie wynosić 0. Wiemy również, ze suma całej listy to suma head oraz reszty listy, wiec znów zapętlamy ten wzorzec rekurencyjnie.
Jeśli chcemy wyświetlić listę, którą podzieliliśmy na np. (x:y:ys) to możemy ją przypisać do zmiennej przy pomocy "@" w następujący sposób: all@(x:y:ys) Dzięki temu, gdy wywołamy all wyświetli nam się cała lista i nie musimy wypisywać oddzielnie x : y : ys. Oto przykład zastosowania tego triku: capital :: String -> String capital "" = " Yup... it's empty..." capital all@ (x : xs ) = " The first letter of " ++ all ++ " is " ++ [ x]
ROZDZIAŁ 4 STRAŻNICY (G UARDS)
KIM (CZYM) SĄ STRAŻNICY? Strażnicy działają podobnie do funkcji IF - zwracają wartość 0 lub 1 dla (odpowiednio) fałszu oraz prawdy. Od IF'a różnią się przede wszystkim przejrzystością, czytelnością, ale także umożliwiają sprawdzanie więcej niż jednego zdania logicznego.
Oto przykład przedstawiający jego zastosowanie: bmitell :: ( RealFloat a) => a -> String bmitell bmi bmi <= 18.5 = " Waga piorkowa!" bmi <= 25.0 = " jest dobrze!" bmi <= 30.0 = " prawie tyle co maly fiat!" otherwise = " wiecej niz maly fiat?! o.0"
Podczas sprawdzania strażnika program będzie przypisywał wartości logiczne kolejnym linijkom. Jeśli wartość ta będzie równa 0, to program przejdzie do następnej linijki. Natomiast, jeśli wartość ta będzie wynosiła 1, ta linijka zostanie wyświetlona. Czyli gdybyśmy przyjęli bmi = 24.3 program sprawdzi czy 24.3 jest mniejsze/równe 18.5. Jako iż nie jest to prawda przejdzie do następnej linijki i sprawdzi czy 24.3 jest mniejsze/równe 25. Jest to prawda, wiec wyświetli zdanie " jest dobrze!"
Jest to porównywalne z wielkim drzewem IF ów i ELSE'ów w programowaniu imperatywnym, jednak jest dużo bardziej przejrzyste oraz lepiej zoptymalizowane. W wielu przypadkach ostatnim strażnikiem jest otherwise. Jest on odpowiednikiem defaultu z slajdu 7 i zapobiega on wszelkiego rodzaju błędom oraz niespodziewanym wynikom.
Innym przykładem jest nasza własna implementacja funkcji max: max ' :: (Ord a) => a -> a -> a max ' a b a > b = a otherwise = b
Kolejnym przykładem zastosowania strażników będzie nasza implementacja funkcji compare: mycompare :: ( Ord a) => a -> a -> Ordering a ` mycompare ` b a > b = GT a == b = EQ otherwise = LT
ROZDZIAŁ 4 WHERE?
ABY UNIKNĄĆ WIELOKROTNEGO OBLICZANIA TEGO SAMEGO, MOŻNA PRZYPISAĆ TO OBLICZENIE DO ZMIENNEJ W NASTĘPUJĄCY SPOSÓB: bmitell :: ( RealFloat a) => a -> a -> String bmitell weight height bmi <= 18.5 = " Waga piorkowa!" bmi <= 25.0 = " jest dobrze!" bmi <= 30.0 = " prawie tyle co maly fiat!" o.0"" otherwise = " wiecej niz maly fiat?! where bmi = weight / height ^ 2
Dzięki umieszczeniu where na końcu cały strażnik jest dużo bardziej czytelny i możemy zmienić sposób obliczania bmi robiąc to tylko raz. Where pozwala na dodanie więcej niż jednego wiązania: bmitell :: ( RealFloat a) => a -> a -> String bmitell weight height bmi <= 18.5 = " Waga piorkowa!" bmi <= 25.0 = " jest dobrze!" bmi <= 30.0 = " prawie tyle co maly fiat!" otherwise = " wiecej niz maly fiat?! o.0"" where bmi = weight / height ^ 2 skinny = 18.5 normal = 25.0 fat = 30.0
Dzięki temu wszystkie zmienne opisane są w jednym miejscu, co sprawia że strażnik jest jeszcze bardziej przejrzysty. Ważne jest, aby wszystkie te nazwy były w tej samej kolumnie, w innym przypadku Haskell nie wiedziałby, że wszystkie te zmienne są częścią tego samego bloku. Wiązania where nie są wspólne dla wielu strażników, jeśli potrzebujemy jednego wiązania dla kilku strażników należy zdefiniować globalnego where.
Wiązania where można też użyć do odnajdywania wzorców: bmitell :: ( RealFloat a) => a -> a -> String bmitell weight height bmi <= 18.5 = " Waga piorkowa!" bmi <= 25.0 = " jest dobrze!" bmi <= 30.0 = " prawie tyle co maly fiat!" otherwise = " wiecej niz maly fiat?! o.0"" where bmi = weight / height ^ 2 ( skinny, normal, fat ) = (18.5, 25.0, 30.0)
Kolejnym przykładem będzie funkcja oddająca inicjały po wprowadzeniu imienia oraz nazwiska: inicjaly :: String -> String -> String inicjaly imie nazwisko= [f ] ++ ". " ++ [l] ++ ". " where (f :_) = imie (l: _) = nazwisko
W wiązaniach where można tez umieszczać definicje funkcji: calcbmis :: ( RealFloat a ) = > [(a, a )] -> [a] calcbmis xs = [ bmi w h (w, h) <- xs ] where bmi weight height = weight / height ^ 2
ROZDZIAŁ 4 LET I T B E
Wiązania let są podobne do wiązań where, ale nie rozprzestrzeniają się na całego guarda, czy wzór, są bardzo lokalne. Wykorzystuje się je głownie do przypisywania wartości do zmiennych. Oto przykładowa funkcja obliczająca powierzchnie stożka: cylinder :: ( RealFloat a) =& gt ; a -& gt ; a -& gt ; a cylinder r h = let sidearea = 2 * pi * r * h toparea = pi * r ^2 in sidearea + 2 * toparea
KONSTRUKCJA LET Let <wiązania> in <wyrażenie> Wyrażenia definiowane w let są dostępne po "in". Znów ważne jest to, aby wszystkie definiowane zmienne były w jednej kolumnie. Wiec jaka jest różnica pomiędzy let a where? Przede wszystkim w let najpierw definiujemy wiązania, a potem ich używamy w wyrażeniu, natomiast w where jest odwrotnie. Zmieniają się też wyrażenia same w sobie, w where mogliśmy wstawiać tylko konstrukcje składniowe, natomiast w let mogą być całymi wyrażeniami.
ghci > 4 * ( let a = 9 in a + 1) + 2 42 Wewnątrz let zawiera się cala funkcja rekurencyjna. Jeśli chcemy zdefiniować wiele zmiennych naraz, a nie możemy tego zrobić w kolumnie, możemy je też oddzielać średnikiem: ghci > ( let a = 100; b = 200; c = 300 in a *b*c, let foo =" Hey "; bar = " there!" in foo ++ bar ) (6000000, " Hey there!")
Można tez przeszukiwać wzorce przy pomocy let ghci > ( let (a,b,c ) = (1,2,3) in a + b + c) * 100 600 Let może tez być stosowany przy tworzeniu list: calcbmis :: ( RealFloat a) = > [(a, a )] -> [a] calcbmis xs = [ bmi (w, h) <- xs, let bmi = w / h ^ 2] Używamy let'a w orzeczeniu, ale zamiast filtrować nasza listę, let przypisuje wagę oraz wzrost do danego bmi.
Gdybyśmy chcieli jeszcze filtrować taka listę, należałoby dopisać warunek filtrowania po przecinku: calcbmis :: ( RealFloat a ) = > [(a, a )] -> [a] calcbmis xs = [ bmi (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0] W tworzeniu listy nie używamy in, ponieważ to, do której funkcji używane są zmienne definiowane w let jest predefiniowane przez listę.
Jeśli pominiemy in gdziekolwiek indziej, definicje z let'a zostaną na stale: ghci > let zoot x y z = x * y + z ghci > zoot 3 9 2 29 ghci > let boot x y z = x * y + z in boot 3 4 2 14 ghci > boot < interactive >:1:0: Not in scope : ` boot '
ROZDZIAŁ 4 C ASE E X P RESSIONS
CZYM JEST CASE EXPRESSIONS? Case expressions to wyrażenia najbardziej podobne do czasów z programowania imperatywnego, o których mówiliśmy na początku wykładu:
switch (week) { case "1": cout << "Poniedzialek"; break; case "2": cout << "Wtorek "; break; case "3": cout << "Sroda "; break; case "4": cout << "Czwartek "; break; case "5": cout << "Piatek"; break; case "6": cout << "Sobota"; break; case "7": cout << "Niedziela"; break; default: cout<< " To nie jest liczba z przedzialu 1-7"; break; }
Haskell posuwa ten koncept o krok na przód, case expressions pozwalają nam na wykorzystanie wszystkiego tego, czego nauczyliśmy się na tym wykładzie naraz. Składnia case expressions jest bardzo prosta: case wyrażenie of wzór-> rezultat wzór-> rezultat wzór-> rezultat...
Gdzie wyrażenie jest porównywane z wzorem i oczywiście egzekwowanie odpowiedniego rezultatu np. head ' :: [a] -> a head ' xs = case xs of [] -> error " pusta lista!" (x: _) -> x
Case sprawdza czy lista jest pusta, jeśli nie to wypisuje jej pierwszy element. Case expressions są bardzo elastyczne i można ich użyć praktycznie w dowolnym miejscu. describelist :: [a ] -> String describelist xs = " The list is " ++ case xs of [] -> " empty." [x] -> "a singleton list. " xs -> "a longer list."
Można to też zdefiniować w następujący sposób: describelist :: [a ] -> String describelist xs = " The list is " ++ what xs where what [] = " empty." what [x] = " a singleton list." what xs = "a longer list."
ROZDZIAŁ 5 REKURENCJA
CZYM JEST REKURENCJA? Rekurencja zwana rekursją, polega na wywołaniu przez funkcję samej siebie. Algorytmy rekurencyjne zastępują w pewnym sensie iteracje. Zazwyczaj zadania rozwiązywane tą techniką są wolniejsze od iteracyjnego odpowiednika, natomiast rozwiązanie niektórych problemów jest znacznie wygodniejsze. Rekurencja jest często stosowana w matematyce.
Przykładem rekurencji może być ciąg Fibonacci'ego, gdzie każdy kolejny wyraz ciągu jest sumą dwóch poprzednich. Zauważmy że dwa pierwsze elementy ciągu musimy wyznaczyć bez rekurencji. F0=0 F1=1 Fn=Fn-1+Fn-2, dla n 2 Początkowe wartości tego ciągu to: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,...
Rekurencja jest bardzo ważna dla Haskella, gdyż (w przeciwieństwie do języków imperatywnych) w tym języku nie jest możliwa zmiana wartości zmiennej. W związku z powyższym - nie możemy stosować pętli i jesteśmy zmuszeni do korzystania z rekurencji.
ROZDZIAŁ 5 FUNKCJA MAXIMUM
FUNKCJA MAXIMUM Funkcja maximum przyjmuje listę rzeczy, które mogą być zestawione ze sobą w celu wybrania największej z nich.
W TAKIM RAZIE JAKBYŚMY ZAIMPLEMENTOWALI FUNKCJĘ MAXIMUM W JĘZYKU IMPERATYWNYM? Prawdopodobnie stworzylibyśmy zmienną, która przechowywałaby największe wartości oraz pętlę, która przy wykonywaniu sprawdzałaby elementy listy. Jeśli sprawdzany aktualnie element listy jest większy od aktualnej maksymalnej wartości, wtedy zapisujemy ten element do naszej zmiennej. Kiedy pętla przestanie się wykonywać ostatni wynik jest naszym rozwiązaniem!
SPRÓBUJMY TERAZ ROZWIĄZAĆ TEN PROBLEM REKURENCYJNIE. Najpierw ustalamy nasz warunek końcowy, czyli wartość maksymalna z listy Singleton jest równa jedynemu elementowi w tej liście. Następnie mówimy, że maximum naszej listy jest głową, jeżeli głowa ta jest większa od maximum ogona. Jeśli jednak maximum ogona jest większe od głowy, wtedy sprawdzamy jeszcze raz maximum ogona.
PORA TO ZAIMPLEMENTOWAĆ W HASKELLU! maximum ' :: ( Ord a) => [a] -> a maximum ' [] = error " maximum of empty list " maximum ' [x] = x maximum ' (x: xs ) x > maxtail = x otherwise = maxtail where maxtail = maximum ' xs
Pierwszy warunek końcowy zwraca błąd jeśli nasza lista jest pusta. W wypadku jeśli jest to lista Singleton - wynikiem będzie ten jeden element. W trzecim warunku końcowym rozłączamy naszą listę na głowę i ogon. Używamy funkcji where w celu zdefiniowania funkcji maxtail, jako maximum reszty listy, a następnie sprawdzamy warunek (if) czy głowa jest większa od reszty listy. Jeśli tak- zwracamy głowę jako wynik. W innym wypadku zwracamy maximum reszty listy.
ŁATWIEJSZY ZAPIS Z UŻYCIEM FUNKCJI MAX maximum ' :: ( Ord a) => [a] -> a maximum ' [] = error " maximum of empty list " maximum ' [x] = x maximum ' (x: xs ) = max x ( maximum ' xs )
ROZDZIAŁ 5 K I L K A D O D A T K O W Y C H F U N K C J I O P A R T Y C H N A R E K U R E N C J I
REPLIKACJA Funkcja replikacja pobiera dwie wartości, z czego pierwsza to int i zwraca listę powtarzających się elementów. Przykładowo wywołując funkcję: replicate 3 5 zwraca nam listę [5,5,5]
PRZYKŁAD REPLIKACJI replicate ' :: ( Num i, Ord i ) => i -> a -> [a] replicate ' n x n <= 0 = [] warunek końcowy naszej rekurencji otherwise = x: replicate ' (n -1) x Zwracamy listę zaczynającą się kolejno od x, a potem powtarzamy czynność aż do osiągnięcia warunku końcowego.
FUNKCJA TAKE Funkcja pobiera daną liczbę elementów listy. Przykładowo wywołując funkcję: take 3 [5,4,3,2,1] Otrzymamy listę [5,4,3]
PRZYKŁAD FUNKCJI TAKE take ' :: ( Num i, Ord i ) => i -> [a] -> [a] take ' n _ używamy symbolu _ ponieważ nie interesuje nas co znajduje się w liście, sprawdzamy tylko czy n<=0 n <= 0 = [] take ' _ [] = [] pusta lista na wejściu, czyli zwracamy pustą listę take ' n (x: xs ) = x : take ' (n -1) xs W tym przypadku wykorzystujemy n oraz od razu rozbijamy listę na znany nam już ogon i głowę. Bierzemy pierwszy element listy i łączymy go poprzez rekurencję (n-1) razy z pozostałymi elementami.
REWERSJA Dzięki tej funkcji mamy możliwość tworzenia nieskończonych list. Zaletą list jest możliwość cięcia ich w dowolnym miejscu. Przykładowo wywołując funkcję: reverse ' :: [a] -> [a] reverse ' [] = [] reverse ' (x: xs ) = reverse ' xs ++ [ x] zamieniamy miejscami głowę i ogon.
STWÓRZMY LISTĘ NIESKOŃCZONĄ! repeat ' :: a -> [a] repeat ' x = x: repeat ' x
FUNKCJA ZIP Funkcja ZIP ma za zadanie połączyć w pary kolejne elementy list.
zip ' :: [a] -> [b] -> [( a,b )] zip ' _ [] = [] zip ' [] _ = [] jesli któraś z list jest pusta to zwracamy pustą listę zip ' (x: xs ) (y: ys ) = (x,y ): zip ' xs ys W ostatniej linijce występuje łączenie list. Zauważmy że jeśli mamy listy wejściowe o różnych wielkościach to lista większa będzie obcięta do wielkości mniejszej listy.
ROZDZIAŁ 5 SZYBKIE SORTOWANIE
QUICKSORT Quicksort, zwany także szybkim sortowaniem jest algorytmem, dzięki któremu możemy uszeregować rzeczy w danej liście. Jest on pewnego rodzaju znakiem rozpoznawczym Haskella.
STRUKTURA QUICKSORT quicksort :: (Ord a) => [a] -> [a] Posortowaną listą nazywamy taką listę, której wszystkie elementy są mniejsze bądź równe największemu elementowi. Następnie zaczynając od pierwszej liczby sprawdzamy czy każda następna jest większa od poprzedniej.
LISTA SKŁADANA quicksort :: ( Ord a) => [a] -> [a] quicksort [] = [] quicksort (x:xs) = let smallersorted = quicksort [a a <- xs, a <= x] biggersorted = quicksort [a a <- xs, a > x] in smallersorted ++ [x] ++ biggersorted
PRZYKŁADY ghci > quicksort [10,2,5,3,1,6,7,4,2,3,4,8,9] [1,2,2,3,3,4,4,5,6,7,8,9,10] ghci > quicksort " the quick brown fox jumps over the lazy dog " " abcdeeefghhijklmnoooopqrrsttuuvwxyz "
ROZDZIAŁ 5 MYŚLENIE REKURENCYJNE
Wykonaliśmy już trochę działań za pomocą rekurencji i powinniśmy zauważać już pewną zależność. Zazwyczaj definiujemy warunek końcowy, po czym definiujemy funkcję, która wykonuje operacje pomiędzy niektórymi elementami względem reszty. Nie ma znaczenia czy to jest drzewo, lista, czy inna struktura danych. Oczywiście mamy warunki końcowe. Zazwyczaj warunkiem końcowym jest punkt, gdzie rekurencja traci sens (otrzymujemy ten sam wynik cały czas). W przypadku list, najczęściej warunkiem końcowym jest pusta lista. W przypadku drzew - jest to punkt bez żadnych dzieci.
W szybkim sortowaniu warunkiem końcowym jest pusta lista, którą rozpoznajemy po tym, że jeżeli dodamy ją do innej listy to otrzymamy ponownie oryginalną listę. Żeby myśleć rekurencyjne za warunek końcowy musimy uznać moment, w którym rekurencja nie zostaje użyta. Musimy także pamiętać o danych, z których będziemy korzystać, rozłożeniu parametrów funkcji oraz momencie, w którym użyjemy rekurencji.
KONIEC Bibliografia: Learn You a Haskell for Great Good! Miran Lipovac Autorzy prezentacji: Przemek Cyliński Piotr Podgórski Michał Witkowski Natalia Zmysłowska