Rekurencja - Haskell Bartosz Pawlak Sebastian Żółtowski Adam Stegenda Krystian Sobótka Tomasz Gołębiewski
Haskell – czysto funkcyjny język programowania Języki czysto funkcyjne: Wynik działania funkcji jest uzależniony od przekazywanych jej argumentów i tylko od nich Nie ma efektów ubocznych1 Programy funkcyjne nie zawierają przypisań, więc wartości zmiennych raz ustalone, nigdy nie mogą zostać zmienione. 1.Efekt Uboczny - dowolny efekt wyrażenia, lub wywołania funkcji, który wykracza poza zwrócenie wartości np. wyrażenie 2 + 3 nie ma skutków ubocznych, wyrażenie a = 2 + 3 ma oczywisty skutek uboczny na zmiennej a.
Haskell – czysto funkcyjny język programowania
Co to jest rekurencja? Rekurencja zwana również 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. Najpopularniejsze przykłady zastosowania: ciąg Fibonacciego silnia NWD za pomocą algorytmu Euklidesa
Rekurencja - Haskell Chyba każdy program (poza "Hello world") napisany w języku imperatywnym wykorzystuje jakiegoś rodzaju pętle. Działanie pętli kończy się, gdy zostanie spełniony (lub nie spełniony) pewien warunek. Wiąże się to ze zmianą wartości zmiennej będącej licznikiem pętli. Jak wiadomo, zmiana wartości zmiennej w Haskellu nie jest możliwa, a więc zastosowanie pętli staje się również niemożliwe. Wszystkie zadania, które w językach imperatywnych są wykonywane w pętli, w Haskellu można zrealizować wykorzystując rekurencję. W językach imperatywnych rekurencja jest stosowana rzadko, między innymi ze względu na słabą wydajność. Kompilatory języków funkcyjnych posiadają bardzo dobre mechanizmy optymalizacji dla rekurencji Język imperatywny - język w którym program jest pojmowany jako ciąg wykonywanych po sobie instrukcji. Przykładami języków imperatywnych są Pascal i C.
Rekurencja - Haskell W Haskellu rekurencja odgrywa bardzo ważną rolę, gdyż w przeciwieństwie do języków imperatywnych (np. Java czy Python) w Haskellu wykonujemy obliczenia deklarując co chcemy uzyskać a nie jak to chcemy uzyskać. Z tego powodu język ten nie posiada pętli for i while a żeby otrzymać wynik często musimy użyć właśnie rekursji.
Silnia Jak wiemy silnią liczby naturalnej n nazywamy iloczyn wszystkich dodatnich liczb naturalnych nie większych niż n. Symbolicznie oznaczamy za pomocą wykrzyknika n! i czytamy n silnia. Na przykład silnia z liczby 5 to 5 * 4 * 3 * 2 * 1 = 120. Składnia silni w Haskellu: silnia 0 = 1 silnia n = n * silnia (n-1)
Silnia Bez warunku końca (silnia 0 = 1) otrzymamy błąd (Exception: stack overflow) co oznacza przepełnienie stosu. Jest to spowodowane tym, że program nie wie kiedy ma skończyć funkcję i bierze pod uwagę także liczby ujemne.
Silnia Silnia z użyciem Guards
Rekurencyjne mnożenie dwóch liczb Z pewnością każdy programista, zarówno piszący w językach imperatywnych jak i funkcyjnych, aby pomnożyć dwie liczby a i b napisze po prostu a * b. Mnożenie można jednak zapisać w sposób rekurencyjny. Wykorzystamy do tego definicję podawaną dzieciom w szkole podstawowej. Aby pomnożyć liczbę a razy liczbę b, weź liczbę a i dodaj do siebie b razy. Na przykład 6*4 = 6+6+6+6. Podobnie jak w poprzednim przykładzie, zapiszmy dwa przykłady mnożenia.
Rekurencyjne mnożenie dwóch liczb Tak samo jak w przypadku silni zapis ten możemy uogólnić: a * b = a + a * (b-1) Pozostaje jeszcze zdefiniowanie przypadku bazowego. Będzie to mnożenie przez liczbę 0. Dowolna liczba pomnożona przez 0 daje 0. a * 0 = 0 Funkcja pomnoz zapisana w Haskellu będzie więc wyglądać następująco:
Ciąg Fibonacciego Jest to ciąg liczb naturalnych określony rekurencyjnie w sposób następujący: Pierwszy wyraz jest równy 0, drugi jest równy 1, każdy następny jest sumą dwóch poprzednich. Ciąg Fibonacciego w Haskellu: fib :: Integer -> Integer fib 0 = 1 fib 1 = 1 fib n = fib (n-1) + fib (n-2)
Ciąg Fibonacciego Podany algorytm dla większych liczb liczy bardzo długo, poniższy algorytm rozwiązuje ten problem. fibs:: [Integer] fibs= 0 : 1 : zipWith (+) fibs(tail fibs) fib :: Int -> Integer fib n = fibs!! (n+1)
Potęgowanie rekurencyjnie Potęgę o wykładniku naturalnym również można zdefiniować rekurencyjnie: a0 = 1 an = a * an−1 , dla n ≠ 0 W jaki sposób zapisać to w Haskellu?
Maximum Funkcja maximum zwraca największy element z zadanego zbioru, dlatego też musi być zabezpieczona na wypadek gdyby zbiór był pusty, lub zawierał tylko jeden element.
Maximum Pierwszy warunek końcowy zwraca błąd jeśli nasza lista jest pusta. Maximum [] = error „Lista jest pusta” W wypadku jeśli jest to lista jednoelementowa - wynikiem będzie ten jeden element. Maximum [x] = x 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. Maximum (x: xs) | x> maxTail = x | otherwise = maxTail where maxTail = maximum xs
Maximum
Maximum Łatwiejszy zapis z użyciem funkcji max
Funkcja elem - rekurencyjnie Funkcja elem sprawdza, czy x jest elementem listy Funkcja elem rekurencyjnie
Funkcja replicate - rekurencyjnie Funkcja replicate pobiera dwa argumenty z czego pierwszy to int który mówi o tym ile razy ma zostać powtórzony drugi argument funkcji. Wynikiem działania tej funkcji jest lista powtarzających się elementów. Funkcja replicate rekurencyjnie
Funkcja take - rekurencyjnie Funkcja take jest to funkcja, która pobiera zadaną liczbę elementów z listy. Funkcja take rekurencyjnie
Funkcja reverse - rekurencyjnie Funkcja reverse jest to funkcja, która odwraca listę. Warunkiem końca jest uzyskanie pustej listy. Jeżeli podzielimy listę na head i tail, to odwrócona lista jest odpowiednikiem odwróconego tail (ogona) i head (głowy) na samym końcu.
Funkcja repeat - rekurencyjnie Funkcja repeat jest to funkcja, która zwraca nieskończoną listę, której elementem jest podany argument. W funkcji tej brakuje warunku końca, gdyż ma ona nam uświadomić, że Haskel umożliwia nam tworzenie nieskończonych struktur danych w tym przypadku nieskończonych list.
Funkcja repeat - rekurencyjnie Jednakże zastosowanie funkcji repeat dobrze sprawdza się w zestawieniu z innymi funkcjami np. w przypadku gdy chcemy stworzyć listę x-elementową która wypełniona będzie takimi samymi wartościami. (Skorzystamy wtedy z funkcji take)
Sortowanie (Quick Sort) Załóżmy, że mamy listę elementów typu Ord do posortowania. Najefektywniej można to osiągnąć za pomocą popularnego algorytmu QuickSort. O ile jednak w językach imperatywnych taki algorytm zajmować może nawet kilkanaście linijek kodu, to jego implementacja w Haskellu jest dużo krótsza i bardziej przejrzysta.
Sortowanie (Quick Sort) Zasada działania szybkiego sortowania w Haskellu: Pierwsza linijka pokazuje, że będziemy korzystać z typu Ord, który działa dla uporządkowanych danych. quicksort :: (Ord a) => [a] -> [a] W drugiej linijce mamy warunek dla pustej listy quicksort [] = [] Działanie głównej funkcji można opisać tak: Bierzemy pierwszą liczbę z listy a potem tworzymy listę liczb mniejszych bądź równych tej liczbie oraz listę liczb większych. Oczywiście obie te listy są zdefiniowane rekurencyjnie, więc wewnątrz tych list dzieje się to samo co w liście głównej. Po policzeniu obu list pozostaje nam tylko skleić obie listy oraz wstawić pomiędzy nie pierwszą liczbę głównej listy, którą pobraliśmy na początku quicksort (x:xs) = let smallerSorted = quicksort [a | a <- xs , a <= x] biggerSorted = quicksort [a | a <- xs, a > x] in smallerSorted ++ [x] ++ biggerSorted
Sortowanie (Quick Sort) Algorytm sortowania szybkiego jest uważany za najszybszy algorytm dla danych losowych. Zasada jego działania opiera się o metodę dziel i zwyciężaj. Zbiór danych zostaje podzielony na dwa podzbiory i każdy z nich jest sortowany niezależnie od drugiego.
Dziękujemy za uwagę