Programowanie funkcyjne (Haskell Wprowadzenie) Kowalik Adrian
Programowanie funkcyjne Krótka geneza języka Haskell Polecenia i składnia języka Funkcje i wyrażenia Typy i typy klasowe Listy i krotki
Programowanie Funkcyjne Odmiana programowania deklaratywnego, Program tworzy prosta bądź złożona funkcja (w sensie matematycznym), Wykonanie programu polega na obliczaniu wartości funkcji matematycznych, W czystym programowaniu funkcyjnym, raz zdefiniowana funkcja dla tych samych danych wejściowych zawsze zwróci ten sam wynik, Przykład: Fun_kolo r = 2*3.14*r
Podstawą do programowania funkcyjnego był opracowany przez Alonzo Church a Rachunek Lambda z Typami (lata 30-te XX wieku), Pierwszy język funkcyjny: Information Processing Language (IPL) (połowa lat 50-tych), Lisp (1958), Scheme (połowa lat 70-tych) ML (1973), Miranda (początek lat 80-tych), Haskell (początek lat 90-tych). F # (Microsoft) jeden z najnowszych języków funkcyjnych.
Mieszany język funkcyjny, Lisp Jako pierwszy przypominał dzisiejsze języki programowania, Najpopularniejsze dialekty : Common Lisp, Scheme i Clojure. (print "Hello world") (defun factorial (n) (if (<= n 1) 1 (* n (factorial (- n 1)))))
Scheme Powstał z Lisp u miał za zadanie go uprościć Minimalistyczny język (define hello-world (lambda () (begin (write Hello-World) (newline) (hello-world)))) (define fib (lambda (n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (fib (- n 1)) (fib (- n 2)))))))
ML (Meta Language) Rodzina języków o silnym statycznym typowaniu Jako jeden z pierwszych udostępniał typy polimorficzne print "Hello world!\n"; fun ins (n, [ ]) = [n] ins (n, ns as h::t) = if (n<h) then n::ns else h::(ins (n, t)) val insertionsort = List.foldr ins [ ]
Ocaml Wielo-paradagmatowy język programowania Wspiera dobrze programowanie funkcyjne obiektowe jak i programowanie imperatywne. print_string "Hello, world!\n";; (* komentarz *) let rec fib n = if n < 2 then n else fib (n-1) + fib (n-2) ;;
Miranda Pierwszy czysto funkcyjny język programowania Podobny do Haskella fib 0 = 1 fib 1 = 1 fib (n+2) = flist!(n+1) + flist!n where flist = map fib [ 0.. ] 7 Porównanie wielu języków http://hyperpolyglot.org/
Lambda Expressions Nawet języki obiektowe takie jak C# czy Java wykorzystują wstawki programowania funkcyjnego tzw. wyrażenia lambda których celem jest wsparcie programowania wielordzeniowego oraz domknięć.
Haskell Język nazwany na cześć Haskella Curry ego Początkowo intensywnie rozwijany wokół ośrodka University of Glasgow (1998r). Najbardziej popularna dystrybucja : GHC (Glasgow Haskell Compiler). GHC (kompilator) + GHCi (interpreter) Haskell Platform - http://hackage.haskell.org/platform/
Haskell cd. nowoczesny język czysto funkcyjny ogólnego przeznaczenia stworzony po to, aby połączyć wszystkie atuty programowania funkcyjnego w jednym eleganckim, silnym i ogólnie dostępnym języku programowania. Czysto funkcyjny nie występują zmienne ani efekty uboczne: Zmiana zmiennej Wyświetlanie na ekranie Zapis do pliku.
Leniwe wartościowanie wyznacza wartości argumentów funkcji co najwyżej raz i tylko wtedy, kiedy są potrzebne brak wykonywania niepotrzebnych obliczeń wydajność większa
przykład ax 2 + bx + c = 0 pierw(a, b, c) = if d<0 then putstrln pierwiastki urojone else (r1,r2) where r1 = e - sqrt d/ (2 * a) r2 = e + sqrt d/ (2 * a) d = b * b 4 * a * c e = - b / (2 * a)
przykład 2 Funkcje : cycle, repeat, listy nieskończone. Funkcja take wykorzystuje leniwe wartościowanie
Silne typowanie Niemożliwa niejawna konwersja brak błędów automatyczne rozpoznawanie typów
Sesja Haskell pozwala nam na interaktywną prace z poziomu linii poleceń interpretera. Język nie ogranicza nam wielkości liczb jedynie może być ograniczenie sprzętowe.
Plik zapisany w odpowiedniej składni zrozumiałej dla Haskella, zwany jest skryptem. Typowy skrypt zapisuje się w pliku z rozszerzeniem.hs. Przykład: fun1 :: Integer -> Integer fun1 x = x * (x 1) fun2 :: Integer -> Bool fun2 y = if (x>0) Then True else False Definicja składa się z dwóch części: Opisu typu funkcji - funkcja bierze jako argument liczbę całkowitą i zwraca również liczbę całkowitą (opis typu można pominąć, jeśli nie prowadzi to do niejednoznaczności). Sposobu wyliczenia wartości funkcji W wielu sytuacjach składnia Haskella pozwala obejść się bez znaków takich jak średniki czy przecinki - ich rolę spełnia odpowiedni układ tekstu.
Operatory Operatory działające na liczbach oraz operatory listowe
Kolejność wykonania operatorów jest określona przez priorytet operatora Dodatkowa właściwość "fixity" decyduje czy operator wiąże w lewo ("left-associative"), w prawo ("right-associative") czy równorzędnie w obu kierunkach ("non-associative"). Użycie Funkcji ma najwyższy priorytet.
Priorytety operatorów Left-associative Non-associative Right-associative 9!!. 8 ^, ^^, ** 7 *, /, `div`,`mod`, `rem`, `quot` 6 +, - 5 :, ++ 4 ==, /=, <, <=, >, >=, `elem`, `notelem` 3 && 2 1 >>, >>= 0 $, $!, `seq`
Definiowanie operatorów Haskell umożliwia tworzenie własnych operatorów oraz określanie sposobu w jaki mają się zachowywać. Sposób definiowania i używania operatorów dwuargumentowych opiera się na sygnaturze: a -> a -> a gdzie a jest pewnym typem, polimorficznym lub zwykłym. Przykład: Integer -> Integer -> Integer Num a => a -> a -> a
Obowiązuje następująca konwencja notacyjna: Jeśli nazwa funkcji jest zwykła (tzn. składa się z liter, cyfr i ewentualnie znaków podkreślenia), to funkcji tej można używać w zwykły sposób, np. div 7 4 a po ujęciu jej nazwy w odwrócone apostrofy - w notacji infiksowej, np. 7 `div` 4 Jeśli nazwa funkcji składa się z symboli, to w definicji należy ująć ją w nawiasy okrągłe np. (+). Wówczas funkcji (+) używa się w notacji infiksowej, np. 7 + 4 a po ujęciu w nawiasy - w zwykłej, np. (+)7 4
Podobnie jak z operatorami od zastosowanej notacji zależy tez wynik, np.: Prelude> 3 `add` 4 * 2 11 Zastosowanie add jako funkcji da jednak inny wynik: Prelude> add 3 4 * 2 14! Użycie funkcji zawsze ma najwyższy priorytet.
Funkcje wyższego rzędu Funkcje to podstawa Haskella. Funkcje wyższego rzędu przyjmują jako argumenty inne funkcje bądź zwracają inną funkcje. np. map funkcja [lista]
Składanie Funkcji Typy składanych funkcji muszą się odpowiednio zgadzać, tzn. żeby można było złożyć g.f, wynik f musi być zgodny z typem argumentu funkcji g. Złożenia funkcji możemy dokonać za pomocą operatora kropki (. ) W wielu przypadkach nie ma potrzeby jawnego zapisywania składania funkcji, mimo że w języku funkcyjnym to przecież fundamentalna operacja
Składanie Funkcji iter :: (Integer, Integer -> Integer) -> (Integer -> Integer) iter (0, f) x = x iter (n, f) x = f (iter (n - 1, f) x) iter :: (Integer, Integer -> Integer) -> (Integer -> Integer) iter (0, f) = id iter (n, f) = f. iter (n - 1, f)
Where ObliczBmi :: (RealFloat a) => a -> a -> String ObliczBmi wzrost waga wzrost / waga ^ 2 <= 18.5 = " niedowaga!" wzrost / waga ^ 2 <= 25.0 = " OK!" wzrost / waga ^ 2 <= 30.0 = " nadwaga!" otherwise = brak skali!"
ObliczBmi :: (RealFloat a) => a -> a -> String ObliczBmi wzrost waga bmi <= 18.5 = " niedowaga!" bmi <= 25.0 = " OK!" bmi <= 30.0 = " nadwaga!" otherwise = "brak skali! " where bmi = wzrost / waga ^ 2
Guard - strażnicy Odpowiednik instrukcji if else liczba a a>0 = "dodatnia" a==0 = "zero" a<0 = "ujemna duzaczymala c c >= 'a' && c <= 'z' = "Mała" c >= 'A' && c <= 'Z' = "Duża" otherwise = "to nie litera!"
Typy danych Haskell jest językiem silnie typowanym. Podstawowe typy języka Haskell, to: Int - liczby całkowite z ograniczonego zakresu [-2^29.. 2^29-1], Integer - liczby całkowite o dowolnej precyzji, Float - liczby zmiennoprzecinkowe o pojedynczej precyzji, Double - liczby zmiennoprzecinkowe o podwójnej precyzji, Char - typ znakowy (Unicode), Bool - zmienne logiczne.!! Typ każdego wyrażenia w języku Haskell możemy sprawdzić używając komendy :t lub :type.
Typy polimorficzne Sztywne przyporządkowanie typów funkcjom jest często nadmiernym ograniczeniem. Rozważmy przykład funkcji podnoszącej liczbę x do kwadratu: kw :: Integer -> Integer kw x = x * x Jest oczywiste, że chcielibyśmy od tej funkcji aby była polimorficzna (tj. dla każdego typu liczbowego wyglądała tak samo). Można to zrobić używając następującej definicji. kw :: Num a => a -> a kw x = x * x Polimorficzna może być nawet stała: sto :: Num a => a sto = 100 Są funkcje, gdzie ograniczanie zakresu zmiennej typowej w ogóle nie jest potrzebne, gdyż mogą działać dla dowolnego typu. Taka jest np. z funkcją iter zdefiniowaną już wcześniej: iter :: (Integer, a -> a) -> (a -> a) iter (0, f) = id iter (n, f) = f. iter (n - 1, f)
Klasy typów Operując typami polimorficznymi, często potrzebujemy zawęzić klasę typów, które dopuszczamy w danym kontekście. Przy definicji funkcji np. kw, podnoszącą liczbę do kwadratu, korzystaliśmy z definicji polimorficznej, w której typ argumentu i wyniku musiał pochodzić z klasy Num. Sygnatura: kw :: Num a => a -> a dopuszcza w miejscu a dowolny typ liczbowy (tzn. z klasy Num), ale musi to być ten sam typ po obydwu stronach - nie może być np. sytuacji: Float -> Integer Predefiniowane klasy typów do wykorzystania, to: Num - klasa typów liczbowych, Eq - klasa typów, dla których zdefiniowane jest porównywanie (operatory == i /=), Show - klasa typów, których wartości można wypisywać na ekranie, Enum - klasa typów wyliczeniowych (dla których muszą istnieć m.in. operacje brania poprzednika i następnika).
Klasa Num zdefiniowana w standardowej bibliotece Prelude ma postać: class (Eq a, Show a) => Num a where (+), (-), (*) :: a -> a -> a negate :: a -> a abs, signum :: a -> a frominteger :: Integer -> a Definicja ta mówi, że typy z klasy Num muszą spełniać założenia klas Eq i Show (w świetle języka programowania obiektowego powiedzielibyśmy, że Num jest podklasą klas Eq i Show) oraz muszą posiadać wymienione tu operacje. Konkretne instancje typów wprowadza się teraz za pomocą słowa kluczowego instance. Przykład poniżej pokazuje deklarację, która zgłasza typ Int jako instancję klasy Eq, w której do porównania służy wbudowana funkcja primeqint: instance Eq Int where (==) = primeqint
Klasy typów oraz ich wybrane operatory i funkcje
Listy Homogeniczne struktury danych. Lista pozwala na przechowywanie dowolnej liczby elementów tego samego typu. Listy są otoczone nawiasami kwadratowymi, a ich elementy oddzielone przecinkami. Przykład: Prelude> let imiona = [ Zosia, Kasia, Małgosia ] Prelude> let liczby = [3,4,5] Prelude> 2 : liczby [2,3,4,5] Prelude> 0 : 1 : 2 : liczby [0,1,2,3,4,5]
Język Haskell oferuje wiele funkcji operujących na listach. Dwie chyba najważniejsze z nich to head i tail, zwracające odpowiednio głowę (pierwszy element) i ogon listy (to co zostanie po usunięciu głowy). Prelude> head [1,2,3,4] 1 Prelude> tail [1,2,3,4] [2,3,4]
Łączenie i odwracanie list Do łączenia (konkatenacji) list służy operator ++, np. [0, 1] ++ [2, 3, 4] daje [0, 1, 2, 3, 4], [ a, b ] ++ [] ++ [ c ] daje [ a, b, c ] - co interpreter zapewne wyświetli jako abc, ab ++ ++ c Łączone listy muszą być oczywiście tego samego typu. Z kolei definicja prostej funkcji służącej do odwracania dowolnej listy mogłaby mieć postać: odwroc :: [a] -> [a] odwroc [] = [] odwroc (x:xs) = odwroc xs ++ [x]
Aplikacja zbiorowa do list Bardzo przydatną rzeczą jest operator map, który otrzymawszy funkcję i listę argumentów, aplikuje tą funkcję do poszczególnych elementów listy. Jest on zdefiniowany w następujący sposób: map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs Przykładowo, możemy napisać: map (+1) [0, 1, 2] - w wyniku otrzymujemy listę: [1, 2, 3]
Filtrowanie Wybranie z listy tylko tych elementów, które spełniają podane kryterium, i zwrócenie ich jako nowej listy. Do filtrowania służy standardowa funkcja filter i jest ona zdefiniowana w następujący sposób: filter :: (a -> Bool) -> [a] -> [a] filter p [ ] = [ ] filter p (x:xs) = if p x then x : filter p xs else filter p xs Przykładowo: filter (>0) [ 1, 0, 1, 2] - w wyniku otrzymujemy listę: [1, 2] filter even [3, 4, 1, 6, 7, 9, 0] - w wyniku otrzymujemy listę: [4, 6, 0
Tworzenie list z użyciem kwantyfikatorów [ wyrażenie kwalifikator ] Kwalifikator może być generatorem lub strażnikiem (czyli warunkiem). Całość opisuje listę w podobny sposób, jak często opisuje się zbiory w matematyce, np. {x^2 x in {0, 1,, 99}, x-parzyste} w Haskellu zapisać możemy: [x*x x <- [0..99], even x] Generator ma postać x <- y, gdzie x jest zmienną lub n-tką zmiennych, a y jest wyrażeniem listowym. Dozór to po prostu wyrażenie logiczne. Przykłady kilku wyrażeń z kwalifikatorami: [(x, y) x <- [1..4], y <- [1..5]] [(x, y) x <- [1..4], y <- [1..5 x]] Zauważmy, że operację filtrowania moglibyśmy teraz zdefiniować również w następujący sposób: filter p l = [ x x <- l, p x ]
Listy nieskończone Jeżeli ostatni element listy nie zostanie podany, Haskell utworzy listę o "nieskończonej" długości. Jest to możliwe dzięki leniwemu wartościowaniu. Wyznaczony zostanie tylko ten element listy, który będzie w danej chwili potrzebny. Prelude>[1,2..]
Krotki (Tuples) Krotki to heterogeniczne struktury danych. Są wykorzystywane wtedy, kiedy wiadomo dokładnie ile elementów i jakiego typu chcemy przechować. Zmienne wewnątrz jednej krotki nie muszą (w przeciwieństwie do list) być tego samego typu. Krotkę zapisujemy podobnie jak listę, jednak zamiast nawiasów kwadratowych używamy nawiasów okrągłych. Prelude> ( Michał,17) Prelude> ( Jan, Kowalski,1980, Częstochowa, False) Krotki mają ściśle określoną liczbę elementów. Nie możliwe jest więc dołączenie czegokolwiek do krotki. Krotki zawierające dwa elementy są nazywane parami.
Podobnie jak listy, krotki można łączyć w struktury wielowymiarowe. Prelude> (("Jan","Kowalski",35),("Opel","Tigra",1999), 10, 1, 2008) Jest możliwość również takiego połączenia: Prelude> ( "Jaś","Kowalski",[4,4,5],[],[3,2,4],[5,5,5,4],[3,3,2],[2])
Dziękuje za uwagę.