Haskell Nazwa Haskell Brooks Curry (1900 1982) Czysty język funkcyjny Bez efektów ubocznych Ułatwia wnioskowanie o programach Leniwy Wyrażenia nie sa obliczane wcześniej niz potrzeba Umożliwia programowanie z (potencjalnie) nieskończonymi strukturami Zaprojektowany w 1990 jako standard leniwego języka funkcyjnego primes :: [Integer] primes = sieve [2..] where sieve (p:xs) = p : sieve[x x<-xs, x mod p /=0] Rachunek lambda Alonzo Church 1932 (i inni) Najmniejszy uniwersalny język programowania Mozna zdefiniować każda funkcję (Turing-complete) Tylko trzy konstrukcje... M x λx.m M 1 M 2... i jedna reguła obliczenia: (λx.m)n M[N/x] Leży u podstaw programowania funkcyjnego. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 5 / 41 Po pierwsze: wartości Wykonanie programu polega na obliczaniu wartości wyrażeń dla uzyskania ich wartości. W czystym języku funkcyjnym: let x=2 in x+x 4 nie ma przypisania ani w ogóle zmiennych, których wartość może się zmieniać, zmienne sa lokalnymi nazwami dla wartości obliczenia nie maja efektów ubocznych globalne sa tylko stałe (nazwy funkcji, typów, etc) podstawowym mechanizmem jest zastosowanie funkcji do argumentów (wywołanie funkcji). Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 6 / 41 Odkrywamy Haskell Haskell zasadniczo jest językiem kompilowanym, ale też interpreter: ghci ben@mewa:~/zajecia/jpp/haskell$ ghci GHCi, version 6.12.1: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim... linking... done. Loading package integer... linking... done. Loading package base... linking... done. Prelude> 2 ^ 100 1267650600228229401496703205376 Prelude> (^) 2 100 1267650600228229401496703205376 Prelude> mod 99 7 1 Listy Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 7 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 8 / 41 Napisy [ ] lista pusta x : xs lista o głowie x i ogonie xs Prelude> [1,2,3] [1,2,3] Prelude> [4] ++ [5,6] [4,5,6] Prelude> 1:[] [1] Prelude> [0..9] [0,1,2,3,4,5,6,7,8,9] Prelude> [1,3..10] [1,3,5,7,9] Prelude> take 4 [0..9] [0,1,2,3] Napisy sa listami znaków Prelude> "napis" "napis" Prelude> [ H, a, s, k, e, l, l ] "Haskell" Prelude> unwords ["hello","world"] "hello world" Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 9 / 41 Wycinanki listowe W matematyce często tworzymy zbiory przy pomocy aksjomatu wycinania: {3x x {1,...,10}, x mod 2 = 1} Podobnie możemy tworzyć listy w Haskellu: > [3*x x <- [1..10], mod x 2 == 1] [3,9,15,21,27] Konstrukcjęx<-[1..10] nazywamy generatorem amod x 2 == 1 filtrem. Możemy tworzyć wycinanki o większej liczbie generatorów i filtrów, np. > [(x,y) x<-[1..5],y<-[1,2,3],x+y == 5,x*y == 6] [(2,3),(3,2)] Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 10 / 41 Po drugie: przejrzystość Zasada przejrzystości 1 Zastapienie dowolnego wyrażenia innym wyrażeniem o tej samej wartości daje równoważny program. 2 Wartość funkcji zależy tylko od wartości jej argumentów. W Ocamlu(print_string "foo") ma tę sama wartość co (print_string "bar"), ale łatwo widzieć, ze nie sa równoważne (powód: efekty uboczne). W C funkcjaint f(int x) {return (a+=x);} nie jest przejrzysta (powód: zmienne globalne i przypisanie). Przejrzystość ułatwia modularyzację, testowanie i wnioskowanie o programach Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 11 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 12 / 41
Po trzecie: funkcje Funkcje sa pełnoprawnymi obywatelami : wartości moga być funkcjami funkcje moga być argumentami funkcji funkcje moga być wynikami funkcji Funkcje moga być anonimowe: \x -> x + 1 oznacza funkcję, która dla argumentu x daje wartość x + 1 f. g = \x -> f(g(x)) -- złożenie funkcji f i isdivby x y = (x mod y) == 0 notdivby = not. isdivby sieve (p:xs) = p : sieve [x x<-xs, notdivby x p] Definiowanie funkcji Stwórzmy plike01.hs mul x y = x * y square x = mul x x area r = pi * square r teraz ben@sowa:~/zajecia/jpp/haskell$ ghci e01.hs [...] Ok, modules loaded: Main. *Main> area 1 3.141592653589793 *Main> area 2 12.566370614359172 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 13 / 41 Program obowiazkowy: silnia Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 14 / 41 Definicje przez przypadki (dopasowanie) fact1 n = if(n <= 0) then 1 else n*fact1(n-1) W Haskellu mozemy to zapisać także tak: fact2 n n <= 1 = 1 otherwise = n*fact2(n-1) Silnia na N sposobów http://www.willamette.edu/~fruehr/haskell/evolution.ht Jak obliczyć długość listy? 1 o lista pusta ([ ]) 0 2 o lista niepusta (h:t) 1+długość t len [] = 0 len (h:t) = 1 + len t Takie definicje rekurencyjne spotykamy często zarówno w matematyce i w Haskellu. Także silnię możemy zdefiniować w ten sposób: fact3 0 = 1 fact3 n = n * fact3 (n-1) Typy Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 15 / 41 Kazda wartość ma swój typ (intuicyjnie, możemy mysleć o typach jako o zbiorach wartości), np: True :: Bool 5 :: Int a :: Char [1, 4, 9] ::[Int] -- lista "hello" :: String -- (String = [Char]) ("luty",2011) :: (String, Int) -- para Funkcje oczywiście też maja typy: isdivby :: Int -> Int -> Bool isdivby x y = (mod x y) == 0 W większości wypadków Haskell potrafi sprawdzić poprawność typów bez żadnych deklaracji (rekonstruuje typy) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 16 / 41 Podstawowe typy Int maszynowe liczby całkowite Integer liczby całkowite dowolnej precyzji Char znaki (Unicode) String napisy (Unicode) Maybe: jeśli a jest typem, tomaybe a jest typem Nothing::Maybe a jeślix::a to(just x)::maybe a Listy: [a] jest lista elementów typua []::[a] jeślix::a orazxs::[a], to(x:xs)::[a] Krotki: jeśli a 1,...,a n sa typami, to (a 1,...,a n ) jest ich produktem kartezjańskim. W szczególności() jest typem zawierajacym jeden element: ()::() Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 17 / 41 Schoenfinkelizacja i częściowe aplikacje Dlaczego piszemyf x y =... a nief(x,y) =...? Pierwsza forma pozwala zastosować f do jednego argumentu (f x) jest funkcja; f x y (f x) y Takie zastosowanie f nazywamy częściowa aplikacja. Z koleig(x,y)=... definiuje funkcję jednoargumentowa (oczekujac a argumentu w postaci pary). Izomorfizm tych postaci wyznaczaja operacje (nazywane curry/uncurry, ale wprowadzone przez Schoenfinkela): curry g x y = g(x,y) uncurry f (x,y) = f x y Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 19 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 18 / 41 Polimorfizm Funkcja ma typ co oznacza: fst (x,y) = x (a,b) -> a Dla dowolnych typów a i b, funkcja dajaca dla argumentu typu (a, b) wynik typu a. Na przykładfst ("marzec", 2011) "marzec":: String fst (isdivby, f) isdivby:: Int->Int->Bool Dla tych, którzy się nudza, szybka zagadka: jaki typ ma następujaca funkcja? length [] = 0 length (x:xs) = 1 + length xs Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 20 / 41
Polimorfizm Jakie typy maja funkcje curry/uncurry? Dla dowolnych typów a, b, c oraz funkcji g :: (a, b) c, wartościa wyrażenia curry g jest funkcja typu a b c curry :: ((a, b) -> c) -> a -> b -> c uncurry ::(a -> b -> c) -> (a, b) -> c Ten rodzaj polimorfizmu czasem nazywany jest parametrycznym. Funkcja polimorficzna działa tak samo niezależnie od typu argumentu. Dlatego często z typu funkcji możemy się domyslić jej działania, np funkcja typu (a, b) a jeśli daje jakiś wynik, to musi to być pierwszy element pary (czyli ekstensjonalnie musi być funkcjafst). Wyrażenia Podstawowe rodzaje wyrażeń w Haskellu to: literały, np. 42,"Foo"; zmienne, np. x (musza być z małej litery); zastosowanie funkcji do argumentu, np. succ 9; zastosowanie operatora infiksowego, np. x + 1; wyrażenieif e then e else e ; wyrażeniecase (analiza przypadków); wyrażenielet (lokalna deklaracja); wyrażenie λ (funkcja anonimowa). Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 21 / 41 Wyrażenie case Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 22 / 41 Układ czyli wcięcia maja znaczenie Wyrażenie if jest lukrem syntaktycznym dla case: if e then e else e jest tłumaczone do case e of { True -> e ; False -> e } W ogólności case ma postać case e of { wzorzec1 -> e1;...; wzorzec_n -> e_n }... i oznacza: przeanalizuj wyrażenie e i w zalezności od tego, do którego z wzorców pasuje, daj odpowiedni wynik Nawiasy klamrowe i średniki możemy pominać, jeśli użyjemy odpowiednich wcięć w programie: case e of True -> wyrazenie ew. dalszy ciąg wyrażenia bardziej wcięty False -> nastepny przypadek tak samo wcięty Używanie układu nie jest obowiazkowe jeśli użyjemy nawiasów i średników, możemy wcinać dowolnie. Układ jest dopuszczalny także w innych konstrukcjach, takich jak let, do, where, itp. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 23 / 41 Definicje Program (a ściślej moduł) w Haskellu jest zbiorem definicji funkcji, typów i klas. Kolejność tych definicji nie ma znaczenia. Definicja funkcji składa się z (opcjonalnej) sygnatury typu i ciagu równań (dla różnych przypadków tu kolejność ma znaczenie). Równania moga być rekurencyjne, moga też używać innych funkcji, także tych zdefiniowanych niżej. f :: Int -> Int f 0 = 1 f n = n * g(n-1) g n = n * f(n-1) g 0 = 1 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 25 / 41 Dopasowywanie wzorców Wzorce moga być bardziej złożone, np third :: [a] -> a third (x:y:z:_) = z (podkreślenie oznacza cokolwiek ) Równanie moze też wymagać dopasowania więcej niż jednego argumentu: myzip :: [a] -> [b] -> [(a,b)] myzip (x:xs) (y:ys) = (x,y):myzip xs ys myzip = [] Dopasowanie wzorców działa w równaniach podobnie jak w case (i innych miejscach), poza tym, że w case oczywiście dopasowujemy tylko jedno wyrażenie. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 24 / 41 Kolejność równań f :: Int -> Int f 0 = 1 f n = n * f(n-1) g n = n * g(n-1) g 0 = 1 kolejnoscrownan.hs:5:0: Warning: Pattern match(es) are overlapped In the definition of g : g 0 =... Ok, modules loaded: Main. *Main> f 5 120 *Main> g 5 *** Exception: stack overflow *Main> Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 26 / 41 Wyrażenie let Wyrażenie let pozwala nam na użycie lokalnych definicji pomocniczych dla obliczenia jakiegoś wyrazenia: let { definicja1;...; definicja_n } in e Tak jak przy case możemy uzyć wcięć zamiast nawiasów i średników, np. let x = 1 y = 2 in x + y Definicje w let moga być wzajemnie zależne (tak jak na najwyższym poziomie). Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 27 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 28 / 41
Wyrażenie let Wyrażenie λ W wyrażeniu let możemy definiować funkcje, używać rekurencji i dopasowań: f xs = let len [] = 0 len (x:xs) = 1 + len xs in len xs Uwaga: reguły wcinania wymagaja aby w tym wypadku: len było bardziej wcięte niż linia, w której zaczyna się let (czy f) oba równania dla len były tak samo wcięte in było wcięte mniej niż len, ale bardziej niż f (oczywiście jeśli uzywamy{ ; } mozemy wcinać dowolnie). Wyrażenie λ pozwala na skonstruowanie i użycie w wyrażeniu funkcji anonimowej (jak w rachunku lambda). Tekstowo lambdę zapisujemy jako backslash (\x -> x + 1) 9 10 Możemy używać dopasowania wzorca (ale tylko jeden przypadek): pierwszy = \(x,y) -> x Może być więcej niż jeden argument: (\x y -> x) 1 2 to to samo co (\x -> \y -> x) 1 2 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 29 / 41 Podprzypadki Zadanie: podzielić listę na dwie: elementy n oraz > n splitby :: Int -> [Int] -> ([Int],[Int]) splitby n [] = ([],[]) splitby n (x:xs) = let (ys,zs) = splitby n xs in if x<= n then (x:ys,zs) else (ys,x:zs) Drugi przypadek naturalnie dzieli sie na dwa podprzypadki; nie możemy tego zapisać przez wzorce, ale możemy tak: splitby n (x:xs) x<=n = let (ys,zs)=splitby n xs in (x:ys,zs) x>n = let (ys,zs)=splitby n xs in (ys,x:zs) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 31 / 41 Operatory infiksowe Nazwy złożone z symboli domyslnie sa używane w składni infiksowej: xs ++ ys to to samo co(++) xs ys. Zauważmy, że ujęcie operatora w nawiasy odbiera mu infiksowość. Podobnie ujęcie nazwy prefiksowej (z liter i cyfr) w odwrócone apostrofy nadaje jej infiksowość : x mod 2 Operatory nie sa magiczne, możemy definiować własne: infixl 6 +++ (+++) :: Int -> Int -> Int x +++ y = (x+y)*(x+y+1) div 2 Deklaracjainfixl... oznacza, że+++ wiaże w lewo i ma priorytet 6 (taki sam jak+) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 30 / 41 Klauzula where splitby n (x:xs) x<=n = let (ys,zs)=splitby n xs in (x:ys,zs) x>n = let (ys,zs)=splitby n xs in (ys,x:zs) W obu przypadkach powtarza się ta sama definicja, możemy to krócej zapisać: splitby n (x:xs) x<=n = (x:ys,zs) otherwise = (ys,x:zs) where (ys,zs) = splitby n xs where jest poniekad podobne do let, ale let jest wyrażeniem, where jest częścia definicji Zasięgiem definicji w let jest wyrażenie po in; zasięgiem definicji w where całe równanie Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 32 / 41 Jeszcze o priorytetach Minusa unarnego nie należy mieszać z operatorami infiksowymi: Prelude> 2 * -3 Precedence parsing error cannot mix * [infixl 7] and prefix -... Prelude> 2 * (-3) -6 Prefiksowa aplikacja funkcji ma wyższy priorytet niż operatory infiksowe Z kolei($) jest operatorem aplikacji funkcji o priorytecie 0, co pozwala oszczędzić nawiasów i napisać np. length $ show $ foldl (*) 1 [1..1000] zamiast length ( show ( foldl (*) 1 [1..1000] )) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 33 / 41 Przekroje Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 34 / 41 Rekursja ogonowa Operatory infiksowe sa z natury swej dwuargumentowe. Podajac operatorowi jeden z argumentów mozemy uzyskać funkcję jednoargumentowa. Konstrukcja taka nazywa się przekrojem (section) operatora. Prelude> (+1) 2 3 Prelude> (1+) 3 4 Prelude> (0-) 4-4 Przekrojów używamy przeważnie, gdy chcemy taka funkcję przekazać do innej funkcji. Rozważmy dwie funkcje liczace długość listy: length1 [] = 0 length1 (x:xs) = 1 + length1 xs length2 xs = len xs 0 where len :: [a] -> Int -> Int len [] a = a len (_:xs) a = len xs (a+1) Która z nich jest lepsza? Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 35 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 36 / 41
Rekursja ogonowa *Main> :set +s *Main> length1 [1..5*10^6] 5000000 (116.94 secs, 795706272 bytes) *Main> length2 [1..5*10^6] 5000000 (7.10 secs, 792654956 bytes) length2 ma lepsze parametry, gdyż rekurencja wlen jest ogonowa: wywołanie rekurencyjne jest ostatnia czynnościa do wykonania. NB można jeszcze lepiej: *Main> length3 [1..5*10^6] 5000000 (3.13 secs, 764169472 bytes) Leniwe obliczenia Mówimy że Haskell jest językiem leniwym: wyrażenia nie sa obliczane wcześniej niz potrzeba, programowanie z (potencjalnie) nieskończonymi strukturami from n = n:from(n+1) -- const x y = x Prelude> const 0 (from 1) 0 Prelude> take 5 (from 1) [1,2,3,4,5] from buduje (potencjalnie) nieskończona listę. Obliczenie wartościconst 0 (from 1) nie wymaga obliczenia from 1 (nie jest więc ono obliczane). Obliczenietake 5 xs wymaga 5 pierwszych elementówxs. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 37 / 41 Kiedy wykonywane sa obliczenia? W językach gorliwych (C, Java, Ocaml) wartości argumentów sa obliczane przed wywołaniem funkcji (wywołanie przez wartość itp). W językach leniwych nie przekazujemy wartości, ale cuś (ang. thunk), co pozwoli ja obliczyć. Funkcja wywoływana oblicza wartość jeśli i kiedy tego potrzebuje (ale tylko raz!) W praktyce obliczenie wartości jest niezbędne przy dopasowaniu do wzorca (ale tylko na tyle, na ile wzorzec tego potrzebuje nie potrzeba obliczać całej listy, żeby ustalić, czy jest ona pusta). Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 38 / 41 Wymuszanie obliczeń Operator($!) oznacza podobnie jak podobnie jak$aplikację funkcji, ale aplikację gorliwa wymuszajac a uprzednie obliczenie wartości argumentu. length3 xs = len xs 0 where len [] n = n len (_:xs) n = len xs $! (n+1) Jest on zdefiniowany przy pomocy wbudowanej funkcji seq :: a -> t -> t Która oblicza swój pierwszy argument i daje w wyniku drugi. ($!) :: (a -> b) -> a -> b f $! x = x seq f x Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 39 / 41 Własności strategii obliczeń Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 40 / 41 Funkcje wyższego rzędu Funkcjami wyższego rzędu nazywamy funkcje operujace na funkcjach, np złożenie (w Prelude operator (.)) Twierdzenie Churcha-Rossera: jeśli dwie różne strategie (kolejności) obliczeń dadza wynik, to będzie to ten sam wynik. Jeśli jakaś strategia obliczeń prowadzi do wyniku to strategia leniwa też. Strategia gorliwa nie ma tej własności. o :: (b->c) -> (a->b) -> (a->c) (f o g) x = f(g x) *Main> (+1) o (*2) $ 3 7 *Main> (+1). (*2) $ 3 7 albo flip, które zamienia kolejność argumentów funkcji: flip :: (a -> b -> c) -> (b -> a ->c) flip f x y = f y x Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 14 lutego 2011 41 / 41 Funkcje map i filter map zamienia funkcję na elementach w funkcję na listach: *Main> :info map map :: (a -> b) -> [a] -> [b] *Main> map (*2) [1,2,3] [2,4,6] map f xs daje listę złożona z zastosowania funkcji f do każdego elementu xs *Main> :info filter filter :: (a -> Bool) -> [a] -> [a] *Main> filter (>5) [1..8] [6,7,8] filter p xs daje liste złożona z tych elementów xs, które spełniaja predykat p Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 3 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 2 / 41 Quicksort i partition qs [] = [] qs (x:xs) = (qs (filter (<=x) xs)) ++ [x] ++ (qs (filter (>x) xs)) Ale można inaczej: partition :: (a->bool) -> [a] -> ([a],[a]) partition p [] = ([],[]) partition p (x:xs) p x = (x:ys,zs) otherwise = (ys,x:zs) where (ys,zs) = partition p xs qs [] = [] qs (x:xs) = (qs ys) ++ [x] ++ (qs zs) where (ys,zs) = partition (<x) xs Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 4 / 41
Funkcja foldr Policzmy sumę i iloczyn listy suma [] = 0 suma (x:xs) = x + suma xs prod [] = 1 prod (x:xs) = x * prod xs Mozemy uogólnić foldr :: (a -> b -> b) -> b -> [a] -> b foldr op z [] = z foldr op z (y:ys) = y op foldr op z ys suma = foldr (+) 0 prod = foldr (*) 1 Funkcje foldr i foldl W ogólności foldr redukuję listę prawostronnie foldr z[x 1, x 2,...,x n ] = x 1 (x 2 (x n z) ) Możemy rozważyć, poniekad dualna, operację foldl: foldl z[x 1, x 2,...,x n ] = ( ((z x 1 ) x 2 ) ) x n foldl :: (a -> b -> a) -> a -> [b] -> a foldl f z0 xs0 = lgo z0 xs0 where lgo z [] = z lgo z (x:xs) = lgo (f z x) xs Dla operatorów łacznych wynik foldr i foldl powinien być ten sam (ale foldl powinno być szybsze jako ogonowe). Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 5 / 41 Przykłady Łatwo zauważyć, że Ale co robia poniższe funkcje? a xs ys = foldr (:) ys xs s n = foldl (*) 1 [1..n] foldr (:) []xs == xs r xs = foldl (flip(:)) [] xs NB wersja gorliwa foldl : foldl f z [] = z foldl f z (x:xs) = let z = z f x in seq z $ foldl f z xs Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 6 / 41 Typy algebraiczne Możemy definiować własne typy, np: data TakNie = Tak Nie... i używać ich we wzorcach nie :: TakNie -> TakNie nie Tak = Nie nie Nie = Tak Tak i Nie nazywamy konstruktorami (wartości). Zachowuja się one jak funkcje (w tym wypadku zeroargumentowe). Dla wbudowanego typu listowego [] jest konstruktorem 0-argumentowym, a (:) 2-argumentowym. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 7 / 41 Typy algebraiczne Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 8 / 41 Moduły data Tree a = Leaf a Branch (Tree a) (Tree a) maptree :: (a->b) -> Tree a -> Tree b maptree f (Leaf a) = Leaf (f a) maptree f (Branch l r) = Branch (m l) (m r) where m = maptree f Leaf jest 1-argumentowym konstruktorem, Branch 2-argumentowym. Per analogiam mówimy, że Tree jest jednoargumentowym konstruktorem typu: jeśli x jest wartościa, to Leaf x jest wartościa; jesli a jest typem, to Tree a jest typem. Program w Haskellu jest kolekcja modułów module Lub where import TakNie lub :: TakNie -> TakNie -> TakNie Tak lub coś = coś Nie lub coś = Nie w tym przykładzie widzimy moduł Lub importujacy moduł TakNie. Moduł powinien być umieszczony w pliku o odpowiedniej nazwie (np. moduł Lub w pliku Lub.hs) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 9 / 41 Moduły hierarchiczne Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 10 / 41 Selektywny eksport i import Moduły moga być (i często sa) organizowane hierarchicznie module Utils.Char where import Data.Char decdigit :: Char -> Int... Moduł Utils.Char powinien być w pliku Utils/Char.hs (względem korzenia drzewa naszego programu). Domyślnie moduł eksportuje (a import importuje) wszystkie definicje modułu. Możemy to oczywiście zmienić: module Utils.Char2(decDigit,hexDigit,isDigit) where import Data.Char(isDigit,ord,toLower) decdigit :: Char -> Int... W tym przykładzie z Data.Char importujemy definicje isdigit,ord,tolower, eksportujemy własne funkcje decdigit, hexdigit. Reeksportujemy zaimportowana definicję isdigit Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 11 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 12 / 41
Import kwalifikowany i ukrywanie Do nazw możemy się też odwoływać, kwalifikujac je nazwa modułu Prelude.map (Prelude.+1) [1..9] Jeśli chcemy, żeby zaimportowane nazwy nie mieszały się z lokalnymi, możemy użyć import qualified import qualified Data.Vector as V import qualified Data.List as L L.map $ tolist $ V.map (+1) (V.enumFromTo 1 10) Z kolei jeśli chcemy zaimportować wszystkie nazwy oprócz kilku, możemy użyć hiding: import Prelude hiding(map,filter) map =... filter =... Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 13 / 41 Kompilacja programu wielomodułowego Wśród modułów programu jeden musi być główny: module Main(main) where... main :: IO () Najprościej zbudować program przez ghc -o nazwa --make main.hs Moduł Main stanowi wyjatek od reguły nazywania plików, może się nazywać jakkolwiek (zwykle tak jak cały program) ghc --make nazwa.hs Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 15 / 41 Synonimy Interakcja i kompilator ghc Wejściem/wyjściem zajmiemy się na późniejszych wykładach, na razie wystarczy nasze programy budować tak, aby główna funkcja była typu String String, np. module Main where cat :: String -> String cat xs = xs main = interact cat Program możemy potem skompilować przy użyciu ghc: $ ghc -o cat --make cat.hs [1 of 1] Compiling Main ( cat.hs, cat.o ) Linking cat... $ echo "Hello world"./cat Hello world Program zawierajacy funkcję main możemy też uruchomić runhaskell cat.hs Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 14 / 41 Typy Maybe i Either Dwa przydatne typy (predefiniowane w Prelude): data Maybe a = Nothing Just a data Either a b = Left a Right b -- Prelude> head [] -- *** Exception: Prelude.head: empty list safehead :: [a] -> Maybe a safehead [] = Nothing safehead (x:_) = Just x -- *Main> safehead [] -- Nothing safehead2 :: [a] -> Either String a safehead2 [] = Left "Empty list" safehead2 (x:xs) = Right x Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 16 / 41 Etykiety pól Czasem przydatne jest wprowadzenie własnej nazwy (synonimu) dla jakiegoś typu. type Name = String type Possibly = Either Name safehead3 :: [a] -> Possibly a safehead3 [] = Left "Empty list" safehead3 (x:xs) = Right x Synonim nie jest konstruktorem typu; jest identyczny z nazywanym typem. Spójrzmy na definicje data Point = Pt Float Float pointx :: Point -> Float pointx (Pt x _) = x pointy... Definicja pointx jest oczywista ; możemy krócej: data Point = Pt {pointx, pointy :: Float} W jednej linii definiujemy typ Point, konstruktor Pt oraz funkcje pointx i pointy. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 17 / 41 Opakowywanie typów: newtype Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 18 / 41 Klasy motywacja Jeśli chcemy opakować istniejacy typ w nowy konstruktor typu, mozemy uzyć konstrukcji newtype: newtype Identity a = Identity { runidentity :: a } deriving (Eq, Show) *Newtype> Identity "Ala" Identity {runidentity = "Ala"} *Newtype> runidentity it "Ala" newtype działa niemal identycznie jak data z jednym konstruktorem (ale efektywniej; pakowanie/odpakowywanie odbywa się w cczasie kompilacji a nie wykonania). Rozważmy funkcję sprawdzajac a, czy element należy do listy: el x [] = False el x (y:ys) = (x==y) el x ys Będzie ona miała typ a [a] Bool dla każdego typu a, dla którego jest zdefiniowana równość. W ML jest specjalna notacja dla takich typów: a -> a list -> bool ale dotyczy tylko równości. W Haskellu el ma typ (Eq a) a [a] Bool, przy czym fragment (Eq a) nazywamy ograniczeniem typu; jest on częścia szerszego mechanizmu: klas Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 19 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 20 / 41
Klasy Pojęcie klasy nie jest tym samym co w językach obiektowych. Określa raczej protokół, który musi wypełnić typ, by traktować go jako instancję danej klasy. Mechanizm klas można kojarzyć z polimorfizmem w Smalltalku, czy duck typing w językach skryptowych (ale to odległe skojarzenie). Bliższym skojarzeniem jest mechanizm przeciażania operatorów, ale klasy pozwalaja na o wiele więcej... Funkcja typu (C a) t jest przeciażona zwn a: działa dla typów a, które sa instancjami C; Może działać w różny sposób dla różnych typów a (w odróznieniu od funkcji polimorficznych); sposób działania wynika z definicji instancji C Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 21 / 41 Klasy a synonimy Instancje klas nie powinny sie odnosic do synonimów: type Baby = Maybe instance (Por a) => Por (Baby a) where Nothing === Nothing = Tak Baby.hs:7:0: Illegal instance declaration for Por (Baby a) (All instance types must be of the form (T t1... tn) where T is not a synonym. Use -XTypeSynonymInstances if you want to disable this.) In the instance declaration for Por (Baby a) Oczywiście nie przeszkadza to w zdefiniowaniu instance (Por a) => Por (Maybe a) where... Definicja klasy Zdefiniujmy własna klasę typów Porównywalnych: class Por a where (===) :: a -> a -> TakNie (=/=) :: a -> a -> TakNie teraz możemy powiedzieć, że elementy typu TakNie sa Porównywalne (pamiętajac o wcięciach!): instance Por TakNie where Tak === Tak = Tak Nie === Nie = Tak _ === _ = Nie a =/= b = nie $ a === b Zauważmy, że to ostatnie równanie będzie się pojawiało w każdej prawie definicji instancji, możemy więc je przenieść do definicji klasy jako domyślna definicję (=/=) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 22 / 41 Klasy Eq i Ord class Eq a where (==), (/=) :: a -> a -> Bool x /= y = not (x == y) x == y = not (x /= y) Ord: tylko sygnatury, bez domyślnych definicji class (Eq a) => Ord a where compare :: a -> a -> Ordering (<), (<=), (>=), (>) :: a -> a -> Bool max, min :: a -> a -> a Ta druga notacja oznacza typ jest instancja Ord jeśli jest instancja Eq, a ponadto sa zdefiniowane funkcje... Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 23 / 41 Klasa Enum Typy wyliczeniowe (niekoniecznie skończone) class Enum a where succ, pred :: a -> a toenum :: Int -> a fromenum :: a -> Int enumfrom :: a -> [a] -- [n..] enumfromthen :: a -> a -> [a] -- [n,n..] enumfromto :: a -> a -> [a] -- [n..m] enumfromthento :: a -> a -> a -> [a] -- [n,n..m] Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 24 / 41 Klasa Show Klasa Show zawiera funkcje dajace tekstowa reprezentację wartości. W skrócie class Show a where show :: a -> String... ghci potrafi wydrukować tylko elementy typów, które sa instancjami klasy Show: > Tak === Nie No instance for (Show TakNie) arising from a use of print at... Possible fix: add an instance declaration for (Show TakNie) powinniśmy więc dodać definicję, która powie w jaki sposób typ TakNie może spełnić protokół Show, ale jest prostszy sposób... Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 25 / 41 Automatyczne instancje Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 26 / 41 Automatyczne instancje Instancje dla klas typu Eq czy Show sa przeważnie nudne i przewidywalne, kompilator mógłby je sam wygenerować... Instancje dla klas typu Eq czy Show sa przeważnie nudne i przewidywalne, kompilator mógłby je sam wygenerować...... i potrafi, możemy go poinstruować w tym kierunku przy pomocy dyrektywy deriving, np. data TakNie = Tak Nie deriving (Eq, Show) Ten mechanizm ma zastosowanie tylko do klas standardowych (istnieja bogatsze mechanizmy, ale nie będziemy się tu nimi zajmować). Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 27 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 27 / 41
Wejście-wyjście Skad się bierze IO Funkcje w Haskellu co do zasady nie maja efektów ubocznych Przeważnie jednak chcemy, aby nasz program miał jakiś efekt Dlatego funkcja main jest (poniekad) wyjatkiem: może mieć efekty, co jest zaznaczone w jej typie: IO () oznacza, że funkcja nie daje interesujacego wyniku, za to daje efekt wejścia-wyjścia. Więcej na kolejnych wykładach, na razie dwie funkcje IO Poznaliśmy juz funkcję interact :: (String -> String) -> IO () bierze ona jako argument funkcję operujac a na strumieniach znaków i zamienia ja w interakcję. Zauważmy, że dzięki leniwości możemy zaczać produkować wyjście zanim wczytamy całe wejście. Inna przydatna funkcja jest print print :: (Show a) => a -> IO () potrafi ona wydrukować wszystkie instancje klasy Show (wszystko co da się zamienić na String). Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 28 / 41 Klasa Num class (Eq a, Show a) => Num a where (+) :: a -> a -> a (*) :: a -> a -> a (-) :: a -> a -> a negate :: a -> a abs :: a -> a signum :: a -> a frominteger :: Integer -> a instance Num Int instance Num Integer instance Num Double instance Num Float Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 29 / 41 Klasa Read Klasa Read jest poniekad dualna do klasy Show: pozwala na odczytanie wartości z jej reprezentacji napisowej. Jest to na ogół trudniejsze niż show, na razie wystarczy nam znać funkcję read :: (Read a) => String -> a Prelude> (+) (read "2") (read "3") 5 Prelude> :t 0 0 :: (Num t) => t Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 30 / 41 Wskazywanie typu W przypadku funkcji przeciażonych nie zawsze można rozstrzygnać, o jaki typ nam chodzi, np Prelude> :t read read :: (Read a) => String -> a Prelude> read "1" <interactive>:1:0: Ambiguous type variable a in the constraint: Read a arising from a use of read at <interactive Probable fix: add a type signature that fixes these type variable(s) Prelude> (read "1")::Int 1 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 31 / 41 Klasy konstruktorowe Typy polimorficzne jak [a] czy Tree a moga być instancjami klas (przeważnie pod warunkiem, ze a jest tez instancja odpowiedniej klasy)... data Tree a = Leaf a Branch (Tree a) (Tree a) deriving Show instance Eq a => Eq (Tree a) where Leaf x == Leaf y = x == y Branch l r == Branch l r = (l==l )&&(r==r ) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 32 / 41 Klasy konstruktorowe Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 33 / 41 Klasy konstruktorowe... ale sa też klasy, których instancjami sa nie typy, a konstruktory typów. -- Klasa Functor jest zdefiniowana w Prelude -- class Functor t where -- fmap :: (a -> b) -> t a -> t b instance Functor Tree where fmap f (Leaf a) = Leaf $ f a fmap f (Branch l r) = Branch (fmap f l)(fmap f r) Typy takie jak Tree czy listy sa pojemnikami przechowujacymi obiekty Instancja Eq(Tree a) mówi o własnościach pojemnika z zawartościa Instancja Functor Tree mówi o własnościach samego pojemnika, niezależnie od zawartości *Tree> fmap (+1) $ Leaf 6 Leaf 7 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 34 / 41 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 35 / 41
Klasy konstruktorowe Klasy wieloparametrowe import Prelude hiding(functor(..)) class Functor f where fmap :: (a -> b) -> (f a -> f b) -- [] poniżej oznacza konstruktor *typu* dla list instance Functor [] where fmap = map instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing Powiedzmy, ze chcemy zdefiniować klasę kolekcji class Collection c where insert :: e -> c -> c member :: e -> c -> Bool instance Eq a => Collection [a] where insert x xs = x:xs member = elem to się niestety nie skompiluje (co to jest e w Collection?) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 36 / 41 Klasy wieloparametrowe Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 37 / 41 Zależności funkcyjne (functional dependencies) {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} class Collection c e where insert :: e -> c -> c member :: e -> c -> Bool instance Eq a => Collection [a] a where insert = (:) member = elem NB musimy uzyć rozszerzeń wykraczajacych poza standard Hasdkell98, stad pragmy. Powiedzmy, że piszemy bibliotekę dla algebry liniowej data Vector = Vector Int Int deriving (Eq, Show) data Matrix = Matrix Vector Vector deriving (Eq, Show)... i między innymi chcemy zdefiniować mnożenie: macierzy i macierzy przez wektor i skalar ( *) :: Matrix -> Matrix -> Matrix ( *) :: Matrix -> Vector -> Vector ( *) :: Matrix -> Int -> Matrix ( *) :: Int -> Matrix -> Matrix Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 38 / 41 Zależności funkcyjne (functional dependencies) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 39 / 41 Zależności funkcyjne (functional dependencies) Próbujemy zdefiniować odpowiedna klasę: {-# LANGUAGE MultiParamTypeClasses #-} class Mult a b c where ( *) :: a -> b -> c instance Mult Matrix Matrix Matrix where {-... -} instance Mult Matrix Vector Vector where {-... -} Niestety: (m1 * m2) * m3 nie da się jednoznacznie otypować Chcielibyśmy wyrazić fakt, że w definicji Mult, parametry a i b jednoznacznie wyznaczaja c: {-# LANGUAGE FunctionalDependencies #-} class Mult a b c a b -> c where ( *) :: a -> b -> c Teraz jeśli spróbujemy zdefiniować dodatkowo instance Mult Matrix Matrix String where {- whatever -}... system na to nie pozwoli: mnożenie macierzy musi dawać jednoznaczny wynik. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 40 / 41 Modele obliczeń Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 21 lutego 2011 41 / 41 Paradygmaty programowania Funkcje rekurencyjne (1931) Rachunek lambda (1935 36); Maszyna Turinga (1936 37) Maszyny rejestrowe (1961 ) Maszyny RAM (1961 ) Logika predykatów, rachunek relacji (1968 70) Przekazywanie komunikatów CSP, rachunek π,... programowanie imperatywne (TM, RAM) programowanie funkcyjne (λ, funkcje rekurencyjne) programowanie w logice i języki zapytań (FOL, rachunek relacji) programowanie obiektowe (przekazywanie komunikatów) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 2 / 27 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 3 / 27
Programowanie imperatywne Istotę programowania imperatywnego stanowia przede wszystkim: obliczenia z róznego rodzaju efektami ubocznymi globalny stan i jego modyfikacja szeroko rozumiane I/O wyjatki... sekwencjonowanie obliczeń: zrób to, a potem zrób tamto. Obliczenia funkcyjne Zasada przejrzystości zapewnia, że obliczenie wyrażenia da zawsze ten sam wynik, mozemy zatem fragmenty programu wykonywać w dowolnej kolejności, a nawet równolegle. Jak w takim modelu mieszcza się obliczenia imperatywne, sekwencyjne? Możemy zdefiniować typ, którego wartościami będa obliczenia, z operacjami: Obliczenie czyste (daje jakaś wartość, nie ma efektów ubocznych) Sekwencjonowanie obliczeń: obliczenie -> (wynik -> obliczenie) -> obliczenie Operacje pierwotne (np. wczytaj, wypisz, etc.) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 4 / 27 Klasa Monad Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 5 / 27 Klasa Monad class Monad obliczenie where return :: a -> obliczenie a (>>=) :: obliczenie a -> (a -> obliczenie b) -> obliczenie b Klasa Monad jest klasa konstruktorowa (jak Functor). Monad a jest typem obliczeń o wyniku typu a. return x jest czystym obliczeniem dajacym wynik x Operator(>>=) (zwany bind ) sekwencjonuje obliczenia. Jeżeli kolejne obliczenie nie korzysta z wyniku (a tylko z efektu) poprzedniego, możemy użyć operatora(>>) o1 >> o2 = o1 >>= \_ -> o2 Ponadto czasami wygodniej jest zapisywać złożenie obliczeń w kolejności analogicznej do złożenia funkcji: f =<< o = o >>= f Monads: just a fancy name for scripting your semicolons (via @TacticalGrace, inspired by @donsbot) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 6 / 27 Najprostszy efekt: brak efektu Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 7 / 27 Trzy prawa monadyki Każda monada musi spełniać następujace prawa: Najprostsza monada jest Identity (moduł Control.Monad.Identity w bibliotece standardowej) newtype Identity a = Identity { runidentity :: a } instance Monad Identity where return a = Identity a -- return = id (Identity x) >>= f = f x -- x >>= f = f x 1. (return x) >>= f == f x 2. m >>= return == m 3. (m >>= f) >>= g == m >>= (\x -> (f x >>= g)) Pierwsze dwa prawa mówia, że return nie ma efektów. Trzecie prawo mówi, że sekwencjonowanie obliczeń jest łaczne, czyli w pewnym sensie, że (o1;o2);o3 === o1;(o2;o3)... i możemy je traktować jako sekwencjęo1;o2;o3 Podobnie jak zapis a+b + c jest jednoznaczny dzięki łaczności dodawania. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 8 / 27 Prosty efekt: obliczenia zawodne Cel: chcemy modelować obliczenia, które czasem daja wynik, a czasem zawodza i nie daja wyniku. Środek: monada Maybe instance Monad Maybe where return x = Just x Nothing >>= k = Nothing Just x >>= k = k x Obliczenie, które nie daje wyniku: Nothing Obliczenie, które daje wynik x: Just x Jeśli pierwsze obliczenie sekwencji zawodzi, to cała sekwencja zawodzi Prelude> do { x <- Nothing ; return 1} Nothing Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 9 / 27 Korzyści z monady Maybe Możemy oczywiście korzystać z Maybe bez mechanizmu monad: case obliczenie1 of Nothing -> Nothing Just x -> case obliczenie2 of Nothing -> Nothing Just y -> obliczenie3 Monada pozwala nam to zapisać zgrabniej: obliczenie1 >>= (\x -> obliczenie2 >>= (\y -> obliczenie3)) A nawet jak się przekonamy za chwilę, jeszcze zgrabniej... Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 10 / 27 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 11 / 27
Notacja do W obliczeniach monadycznych czesto pojawia się kod typu obliczenie1 >>= (\x -> obliczenie2 >>= \y -> obliczenie3)) Dla usprawnienia zapisu oraz dla podkreślenia potencjalnej imperatywności mozemy zapisywać je przy pomocy notacji do: do { x <- obliczenie1; y <- obliczenie2; obliczenie3 } Uwaga do jest tylko równoważna notacja, nie powoduje wykonania efektu. Layout i do do możemy zapisywać też przy pomocy wcięć, czyli zamiast do { x <- obliczenie1; y <- obliczenie2; obliczenie3 } możemy napisać do x <- obliczenie1 y <- obliczenie2 obliczenie3 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 12 / 27 do i let Konstrukcja do { obliczenie1; let x=e; obliczenie2 } jest równoważna do { fragment1; let x = e in do { fragment2 }} Przykład: runsim sim = do g <- getgen let s = startstate g let (result, s ) = run sim s setgen s return result Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 13 / 27 do i dopasowanie wzorca Podobnie jak możemy napisaćlet (x:xs) = foo in... możemy w bloku do napisać(x:xs) <- foo do (x:xs) <- foo ys <- bar xs return (x:ys) Co się stanie jeśli wynik foo jest lista pusta? Otóż klasa Monad definiuje jeszcze jedna metodę: fail :: (Monad m) => String -> m a... w przypadku gdy dopasowanie wzorca zwiedzie jest ona wywoływana ze stosownym komunikatem. Zachowanie metody fail zależy oczywiście od konkretnej monady. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 14 / 27 Obsługa błędów Przeważnie w wypadku błedu chcemy mieć więcej informacji niż tylko, że obliczenie zawiodło: komunikat o błedzie. Możemy do tego wykorzystać typ Either: instance Monad (Either error) where return = Right (Left e) >>= _ = Left e (Right x) >>= k = k x ghci> Left "error" >> return 3 Left "error" ghci> do { x <- return 2; return(x+40) } :: Either Str Right 42 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 15 / 27 Obsługa błędów: MonadError Możemy też abstrakcyjnie zdefiniować protokół obsługi błędów: class Error a where nomsg :: a strmsg :: String -> a class (Monad m) => MonadError e m m -> e where throwerror :: e -> m a catcherror :: m a -> (e -> m a) -> m a instance (Error e) => MonadError e (Either e)... Wskazówka Klasy Error i MonadError sa zdefiniowane w module Control.Monad.Error Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 16 / 27 MonadError przykład data ParseError = Err {location::int, reason::string} instance Error ParseError... type ParseMonad = Either ParseError parsehexdigit :: Char -> Int -> ParseMonad Integer parsehex :: String -> ParseMonad Integer tostring :: Integer -> ParseMonad String -- convert zamienia napis z liczba szesnastkowa -- na napis z liczba dziesietna convert :: String -> String convert s = str where (Right str) = tryparse s catcherror printerror tryparse s = do {n <- parsehex s; tostring n} printerror (Err loc msg) = return $ concat ["At index ",show loc,":",msg] Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 17 / 27 Monada IO Typ IO jest wbudowanym typem reprezentujacym obliczenia, które moga mieć (szeroko rozumiane) efekty wejścia-wyjścia. Na przykładgetline::io String jest obliczeniem, które pobiera linię ze standardowego wejścia, a jego wynikiem jest pobrana linia. Funkcja main powinna być typu IO (). Wykonanie funkcji main powoduje realizację zwiazanych z nia efektów, np. main = print 42 spowoduje wypisanie na stdout (reprezentacji tekstowej) liczby 42. Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 18 / 27 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 19 / 27
Ważniejsze funkcje IO print :: Show a => a -> IO () putstrln :: String -> IO () putchar :: Char -> IO () putstr :: String -> IO () getchar :: IO Char getline :: IO String getcontents :: IO String getargs :: IO [String] -- System.Environment Funkcja getcontents daje cała zawartość wejścia jako leniwa listę. Funkcja interact może być zdefiniowana właśnie przy użyciu getcontents: interact :: (String -> String) -> IO () interact f = do s <- getcontents putstr (f s) IO prosty dialog main = do putstrln "Hej, co powiesz?" input <- getline putstrln $ "Powiedziałeś: " ++ input putstrln "Do widzenia" Przykład działania: ben@marcin:~/proby/haskell$ runhaskell dialog0.hs Hej, co powiesz? nic nie powiem Powiedziałeś: nic nie powiem Do widzenia Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 20 / 27 Pułapka buforowanie Uwaga: jeśli w poprzednim przykładzie użyjemy putstr zamiast putstrln, przewaznie efekt będzie inny niż oczekiwany. Nie ma to zwiazku z Haskellem, a tylko ze standardowym buforowaniem terminala. Możemy ten problem rozwiazać np. tak: import System.IO promptline :: String -> IO String promptline prompt = do putstr prompt hflush stdout getline main = do input <- promptline "Prompt> " putstrln $ "Powiedziałeś: " ++ input Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 21 / 27 Buforowanie Innym rozwiazaniem jest wyłaczenie buforowania: import System.IO promptline :: String -> IO String promptline prompt = do putstr prompt getline main = do hsetbuffering stdout NoBuffering input <- promptline "Prompt> " putstrln $ "Powiedziałeś: " ++ input Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 22 / 27 Dialog w pętli doesquit :: String -> Bool promptline :: String ->IO String main = mainloop mainloop :: IO() mainloop = do input <- promptline "> " more <- processinput input if more then mainloop else return () processinput :: String -> IO Bool processinput input = if doesquit input then return False else do putstrln $ "Powiedziałeś: " ++ input return True Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 24 / 27 Operacje na plikach i deskryptorach withfile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r openfile :: FilePath -> IOMode -> IO Handle data IOMode = ReadMode WriteMode AppendMode ReadWriteMode hsetbuffering :: Handle -> BufferMode -> IO () data BufferMode = NoBuffering LineBuffering BlockBuffering (Maybe Int) Więcej patrz dokumentacja modułu System.IO Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 23 / 27 Operacje na plikach i deskryptorach type FilePath = String readfile :: FilePath -> IO String writefile :: FilePath -> String -> IO () appendfile :: FilePath -> String -> IO () data Handle =... stdin, stdout, stderr :: Handle hclose :: Handle -> IO () hflush :: Handle -> IO () hgetchar :: Handle -> IO Char hgetcontents :: Handle -> IO String hputstr :: Handle -> String -> IO () Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 25 / 27 Użyteczne kombinatory monadyczne... w większości zdefiniowane w module Control.Monad mapm :: Monad m => (a -> m b) -> [a] -> m [b] mapm_ :: Monad m => (a -> m b) -> [a] -> m () form :: Monad m => [a] -> (a -> m b) -> m [b] form_ :: Monad m => [a] -> (a -> m b) -> m () sequence :: Monad m => [m a] -> m [a] sequence_ :: Monad m => [m a] -> m () liftm :: Monad m => (a1->r) -> m a1 -> m r -- fmap liftm2 :: Monad m => (a1->a2->r) -> m a1 -> m a2 -> m r... Na przykład form [1..7] print >> return () form_ [ 1.. 7 ] putchar >> putstrln "" sequence $ map print [1..7] Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 26 / 27 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 28 lutego 2011 27 / 27
Protokół MonadReader class (Monad m) => MonadReader r m m -> r where ask :: m r local :: (r -> r) -> m a -> m a -- Defined in Control.Monad.Reader.Class asks :: (MonadReader r m) => (r -> a) -> m a Protokół MonadReader jest przydatny w sytuacji, gdy mamy grupę funkcji korzystajacych ze wspólnych danych i chcemy uniknać jawnego przekazywania ich z funkcji do funkcji Protokół MonadReader przykład -- instance MonadReader r ((->) r) -- Defined in Control.Monad.Reader data Env = Env { var_a :: Int, var_b :: Int } type R a = Env -> a example :: R Int example = do a <- asks var_a b <- asks var_b return $ a+b example2 = liftm2 (+) (asks var_a) (asks var_b) run :: R a -> a run r = r Env { var_a = 5, var_b = 10 } ghci> run example 15 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 7 marca 2011 2 / 25 MonadReader local Stan Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 7 marca 2011 3 / 25 Metoda local pozwala na lokalne zmiany środowiska inc_a :: Env -> Env inc_a env = env { var_a = var_a env +1 } ghci> :{ form_ [run $ local inc_a example, run example] print :} 16 15 :{ oraz:} sa komendami ghci pozwalajace na wprowadzanie wyrażenia w kilku liniach Najbardziej chyba typowym efektem imperatywnym jest możliwość korzystania z globalnego stanu i jego modyfikacji. Obliczenie ze stanem możemy traktować jako funkcję type Obliczenie a = (Stan -> (a, Stan)) return :: a -> Obliczenie a return x s = (x,s) (>>=) :: Obliczenie a -> (a -> Obliczenie b) -> Obliczenie b (o >>= k) s = let (x,s ) = o s in (k x) s Stan Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 7 marca 2011 4 / 25 Teraz mozemy zdefiniować np -- Odczyt stanu czytaj :: Obliczenie Stan czytaj s = (s,s) -- Zapis nowego stanu zapisz :: Stan -> Obliczenie () zapisz s = \_ -> ((),s) -- Funkcja zmiany stanu jako obliczenie transformator :: (Stan->Stan) -> Obliczenie () transformator t = czytaj >>= \stan -> zapisz $ t stan zwiekszlicznik :: Obliczenie () zwiekszlicznik = transformator (+1) Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 7 marca 2011 5 / 25 Monada State Poniewaz synonimy zasadniczo nie moga być instancjami, uzywamy newtype: newtype State s a = State{runState :: (s -> (a,s))} instance Monad (State s) where return a = State $ \s -> (a,s) (State x) >>= f = State $ \s -> let (v,s ) = x s in runstate (f v) s evalstate :: State s a -> s -> a execstate :: State s a -> s -> s Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 7 marca 2011 6 / 25 Protokół MonadState Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 7 marca 2011 7 / 25 Funkcje pomocnicze gets i modify class MonadState m s m -> s where get :: m s put :: s -> m () instance MonadState (State s) s where get = State $ \s -> (s,s) put s = State $ \_ -> ((),s) get odczytuje stan put modyfikuje stan (zapisuje nowy) Przy programowaniu ze stanem często pojawiaja się operacje typu do {...; s <- get; let x = foo s;... } do {...; s <- get; put (f s);... } Dlatego Control.Monad.State definiuje funkcje gets :: (MonadState s m) => (s -> a) -> m a modify :: (MonadState s m) => (s -> s) -> m () które pozwalaja nam napisać zwięźlej i czytelniej do {...; x <- gets foo;... } do {...; modify f;... } Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 7 marca 2011 8 / 25 Marcin Benke (MIM UW) Języki i Paradygmaty Programowania 7 marca 2011 9 / 25