9 marca 2017
Spis treści 1 2
Wprowadzenie Każda wartość jak i funkcja ma w haskellu ściśle określony typ. Jawne definiowanie typów nie jest konieczne, ponieważ Haskell sam rozpoznaje typ wartości. Warto jednak jawnie określać typ wyrażenia, ponieważ bardzo ułatwia to analizę i dokumentację kodu oraz usuwanie błędów. Najprostszym sposobem na poznanie typów jest użycie polecenia interpretera, dzięki któremu możemy sprawdzić typ dowolnego wyrażenia. Poleceniem tym jest :type, lub krócej :t. ( :: jest odczytywany jako, jest typu ) 1. Prelude>:t "witaj" "witaj"::[char] 2. Prelude>:t e e ::Char 3. Prelude>let i=5 Prelude>:t i i::integer 4. Prelude>lez z=5.6 Prelude>:t z z::double
Wprowadzenie Podstawowe typy w haskell: Int - Liczby całkowite z zakresu [-2^29.. 2^29-1] Integer - Wartością Integer może być dowolna liczba całkowita (zarówno ujemna jak i dodatnia) Float - Liczba zmiennoprzecinkowa pojedynczej precyzji Double - Liczba zmiennoprzecinkowa podwójnej precyzji Bool - Podobnie jak w innych językach, do reprezentowania zmiennych logicznych Char Typ znakowy Dodatkowo mamy również typ wyliczeniowy posiadający trzy wartości: Np. LT - mniejszy niż EQ równy GT większy niż Prelude> compare 1 2 LT
Wprowadzenie Nie tylko zmienne, ale również funkcje mają w Haskellu swój typ. Na typ funkcji składają się typy przyjmowanych przez nią parametrów oraz typ wartości zwracanej przez funkcję. Typy te podajemy w następujący sposób: Nazwa_funkcji :: TypParametru1 -> TypParamentru2 ->... -> TypParametru_n -> TypWartosciZwracanej Np. Dodawanie :: Double -> Double -> Double
Klasy typów Dla typów polimorficznych często zachodzi potrzeba ograniczenia klasy typów w zależności od kontekstu zastosowań. Predefiniowane klasy typów: Num - klasa której członkowie mogą zachowywać się jak liczby. ghci> :t 20 20 :: (Num t) => t ghci> 20 :: Int 20 ghci> 20 :: Integer 20 ghci> 20 :: Float 20.0 ghci> 20 :: Double 20.0
Klasy typów Eq - klasa typów, dla których zdefiniowane jest porównywanie (operatory == i /=) ghci> :t (==) (==) :: (Eq a) => a -> a -> Bool Tak zdeklarowany typ funkcji == oznacza, że funkcja porównania pobiera na wejściu dwie wartości tego samego typu i zwraca wartość typu Bool, gdzie obie wartości wejściowe muszą należeć do klasy typów Eq. => Symbol ten oznacza, że wszystko przed nim nazywamy ograniczeniem klasy ghci> 5 /= 5 False ghci> a == a True ghci> "Ho Ho" == "Ho Ho" True ghci> 3.432 == 3.432 True
Klasy typów Show - klasa typów, których wartości można wypisać na ekranie ghci> show 3 "3" ghci> show 5.334 "5.334" ghci> show True "True" Enum - klasa obiektów do którego należą typy, które mogą być wyliczane. W typie tym dostęp do poprzednika udostępnia funkcja pred, a do następnika funkcja succ. Typy które należą do tej klasy to: (), Bool, Char, Ordering, Int, Integer, Float i Double. ghci> [ a.. e ] "abcde" ghci> [LT.. GT] [LT,EQ,GT] ghci> [3.. 5] [3,4,5] ghci> succ B C
Klasy typów Ord jest dla typów które mają "kolejność". Funkcje porównujące >, =, <= pobierają dwa argumenty tego samego typu należące do klasy Ord i zwracają obiekt klasy Ordering tzn. GT, LT lub EQ. Aby typ należał do klasy Ord, musi należeć do klasy Eq. ghci> :t (>) (>) :: (Ord a) => a -> a -> Bool ghci> "Abrakadabra" < "Zebra" True ghci> "Abrakadabra" compare "Zebra" LT ghci> 5 >= 2 True ghci> 5 compate 3 GT
Klasy typów Read - jest pewnego rodzaju przeciwieństwem typu klasy Show. Funkcja Read pobiera string i zwraca typ który jest częścią funkcji Read. ghci> read "True" False True ghci> read "8.2" + 3.8 12.0 ghci> read "5" - 2 3 ghci> read "[1,2,3,4]" ++ [3] [1,2,3,4,3] Wykorzystanie Read do rzutowania wyniku na dany typ: ghci> read "5" :: Int 5 ghci> read "5" :: Float 5.0 ghci> (read "5" :: Float) * 4 20.0 ghci> read "[1,2,3,4]" :: [Int] [1,2,3,4] ghci> read "(3, a )" :: (Int, Char) (3, a )
Dopasowywanie wzorców (pattern matching) W języku Haskell możliwe jest zdefiniowanie kilku różnych ciał funkcji, których wywołanie będzie zależne od konkretnych parametrów odpowiadających zadanym wzorcom. lucky :: ( Integral a) => a -> String lucky 7 = " LUCKY NUMBER SEVEN! " lucky x = " Sorry, you re out of luck, pal!" W takim wypadku wszystkie możliwości są sprawdzane od góry do dołu, i kiedy podany na wejściu parametr odpowiada jednemu z wzorców, wtedy zostaje wywołane odpowiadające mu ciało funkcji. Widzimy w przykładzie, że w przypadku wywołania funkcji lucky z parametrem 7, funkcja zwróci nam stringa LUCKY NUMBER SEVEN!. Ważna jest możliwość obsługiwania wszystkich możliwości, dlatego więc dla dowolnej innej niż 7 liczby wprowadzonej na wejściu zostanie wypisane " Sorry, you re out of luck, pal!" Ponieważ Haskell sprawdza możliwości zaczynając od góry, w przypadku użycia takiego wzorca musimy umieścić go na końcu kodu. W innym wypadku wzorce znajdujące się po nim nie zostaną w ogóle sprawdzone
Dopasowywanie wzorców (pattern matching) tarcza : : ( Integral a ) => a - > String tarcza x = Nie trafiles. tarcza 1 = Trafiles 1 punkt. tarcza 2 = Trafiles 2 punkt. tarcza 3 = Trafiles 3 punkt. tarcza 4 = Trafiles 4 punkt. tarcza 5 = Trafiles 5 punkt. W tym przypadku bez znaczenia jaki wzorzec wywołamy, z każdym razem otrzymamy komunikat Nie trafiles ponieważ wzorzec z parametrem x jest na samym początku. Przy dopasowaniu wzorców możemy oczywiście używać rekursji. Oto przykład: silnia :: ( Integral a ) => a -> a silnia 0 = 1 silnia n = n * silnia ( n - 1) Na początku ustalamy, że dla parametru 0 silnia wynosi 1. Następnie dodajemy wzorzec dla dowolnej innej liczby zgodnie z definicją silni.
Dopasowywanie wzorców (pattern matching) Jeżeli nie przewidzimy możliwości występowania dowolnego parametru. W przypadku wywołania innego wzorca niż podane program zwróci błąd. Rozważmy następujący przykład: charname :: Char -> String charname a = " Albert " charname b = " Broseph charname c = " Cecil " Teraz gdy wywołamy nieznany parametr (h), zostanie zwrócony błąd. ghci > charname a " Albert " ghci > charname b " Broseph " ghci > charname h " *** Exception : tut. hs :(53,0) -(55,21): Non - exhaustive patterns in function charname
Dopasowywanie wzorców (pattern matching) Wzorce można także stosować na krotkach. Poniższy przykład ilustruje funkcję, która dodaje do siebie wektory. addvectors :: ( Num a) = > (a, a) -> (a, a) -> (a, a) addvectors (x1, y1 ) (x2, y2 ) = ( x1 + x2, y1 + y2 ) Tak skonstruowana funkcja przyjmuje na wejście dowolne zmienne i zwraca wynik w postaci dwóch par liczb (wektorów). Jest również możliwe stosowanie wzorców na listach. W tym przykładzie wyszukiwane i sumowane są elementy listy odpowiadające zadanemu wzorcowi (a, b). 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] W przypadku kiedy dopasowanie wzorca jest niemożliwe, funkcja przechodzi do następnego elementu listy.
Dopasowywanie wzorców (pattern matching) Dopuszczalne jest także użycie listy pustej w określaniu wzorca. Tu na przykładzie zmodyfikowanej funkcji head: head :: [a] -> a head [] = error " Can t call head on an empty list, dummy!" head (x: _) = x I działa to następująco ghci > head [4,5,6] 4 ghci > head " Hello " H Możemy też zastosować rekurencje, w zmodyfikowanej wersji funkcji sum, która dodaje do siebie wszystkie elementy listy: sum :: ( Num a) => [a] -> a sum [] = 0 sum (x: xs ) = x + sum xs
Dopasowywanie wzorców (pattern matching) Można również za pomocą wzorca odwołać się do całości dowolnej listy, bez potrzeby przepisywania w każdym miejscu jej nazwy lub definicji. W tym celu stosuje się znak "@" wstawiany bezpośrednio przed wzorcem capital :: String -> String capital "" = " Empty string, whoops!" capital all@ (x : xs ) = " The first letter of " ++ all ++ " is " ++ [ x]
Osłony (guards) Osłony spełniają taką samą funkcję jak wyrażenie if służą do stwierdzenia czy postawiony warunek został spełniony. W odróżnieniu jednak od składni if, osłony są bardziej czytelne przy dużej liczbie warunków dla jednego wyrażenia oraz dobrze współgrają z haskellowymi wzorcami. bmitell :: ( RealFloat a) => a -> String bmitell bmi bmi <= 18.5 = " You re underweight, you emo, you!" bmi <= 25.0 = " You re supposedly normal. Pffft, I bet you re ugly!" bmi <= 30.0 = " You re fat! Lose some weight, fatty!" otherwise = " You re a whale, congratulations!" Osłony oznacza się znakiem " ", każda osłona jest wyrażeniem typu boolean przyjmującym wartość True lub False. Znajdujące się na końcu wyrażenie otherwise jest odpowiednikiem else znanego z konstrukcji if-else. Podany powyżej przykład w zależności od podanego przez nas BMI wyświetla odpowiedni tekst. Oczywiście do poprawnego działania, konieczne jest aby wzorce były ustawione w kolejności rosnącej.
Osłony (guards) Możemy też łatwo zmodyfikować naszą funkcję i stworzyć kalkulator BMI. Wystarczy, że zamiast wartości BMI na wejściu będziemy podawać nasz wzrost i wagę a reszta obliczy się sama. Wygląda to następująco: bmitell :: ( RealFloat a) => a -> a -> String bmitell weight height weight / height ^ 2 <= 18.5 = " You re underweight, you emo, you!" weight / height ^ 2 <= 25.0 = " You re supposedly normal. Pffft, I bet you re ugly!" weight / height ^ 2 <= 30.0 = " You re fat! Lose some weight, fatty!" otherwise = " You re a whale, congratulations!" Inny przykład zastosowania: max :: ( Ord a) => a -> a -> a max a b a > b = a otherwise = b W tym przypadku osłony wykorzystujemy do znajdowania większej liczby. Jak widzimy jest to tą metodą bardzo proste.
Klauzula where Klauzula where jest to miejsce, w którym wyrażeniom lub wartościom przypisuje się nazwę, tworząc stałe. Umieszcza się ją po osłonach. Jest przydatna, gdy funkcja zawiera obliczenia, które muszą być kilkakrotnie powtarzane. Aby zrozumieć kiedy ją używać weźmy nasz kalkulator BMI pokazany wcześniej. bmitell :: ( RealFloat a) => a -> a -> String bmitell weight height weight / height ^ 2 <= 18.5 = " You re underweight, you emo, you!" weight / height ^ 2 <= 25.0 = " You re supposedly normal. Pffft, I bet you re ugly!" weight / height ^ 2 <= 30.0 = " You re fat! Lose some weight, fatty!" otherwise = " You re a whale, congratulations!" Widzimy tutaj, że przy każdej osłonie musimy na nowo liczyć BMI i pisać weight / height ^ 2. I tutaj właśnie z pomocą przychodzi nam klauzula where, która znacząco uprości nam cały zapis. Po zmianie nasza funkcja wygląda tak: bmitell :: ( RealFloat a) => a -> a -> String bmitell weight height bmi <= 18.5 = " You re underweight, you emo, you!" bmi <= 25.0 = " You re supposedly normal. Pffft, I bet you re ugly!" bmi <= 30.0 = " You re fat! Lose some weight, fatty!" otherwise = " You re a whale, congratulations!" where bmi = weight / height ^ 2 Wszystko co musieliśmy zrobić to tylko na końcu dodać linijkę where bmi = weight / height ^ 2.
Klauzula where Możemy pójść o krok dalej i przekształcić naszą funkcję w następujący sposób: bmitell :: ( RealFloat a) => a -> a -> String bmitell weight height bmi <= skinny = " You re underweight, you emo, you!" bmi <= normal = " You re supposedly normal. Pffft, I bet you re ugly! " bmi <= fat = " You re fat! Lose some weight, fatty! " otherwise = " You re a whale, congratulations!" where bmi = weight / height ^ 2 skinny = 18.5 normal = 25.0 fat = 30.0 Wszystkie wartości zdefiniowane w where są widoczne tylko w obrębie danej funkcji. Sekcja where pozwala również na dopasowywanie wzorców. Musimy wtedy przepisać sekcję where naszej poprzedniej funkcji w następujący sposób:... where bmi = weight / height ^ 2 ( skinny, normal, fat ) = (18.5, 25.0, 30.0) Lepiej jednak robić to, definiując ciała funkcji, ale ten przykład pokazuje, że takie coś jest możliwe.
Klauzula where Tak jak wcześniej definiowaliśmy za pomocą where stałe, istnieje również możliwość definiowania funkcji. Stwórzmy funkcję, która przyjmuje listę par (waga, wzrost) i dla każdej takiej pary zwraca BMI. calcbmis :: ( RealFloat a ) = > [(a, a )] -> [a] calcbmis xs = [ bmi w h (w, h) <- xs ] where bmi weight height = weight / height ^ 2
Klauzula let Jest podobna do klauzuli where. Jest jednak między nimi istotna różnica. Otóż where jest tylko częścią składni i nie może istnieć samodzielnie, natomiast klauzula let jest wyrażeniem, więc możemy używać jej gdziekolwiek zechcemy. Poniżej przykład: 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 Podana funkcja policzy nam pole powierzchni walca. Let możemy także użyć aby wstawić funkcję do listy: ghci > [ let square x = x * x in ( square 5, square 3, square 2)] lub rozbić krotkę na komponenty i przypisanie im nazw: ghci > ( let (a,b,c ) = (1,2,3) in a+b +c) * 100 Dzięki let unikniemy tworzenia funkcji pomocniczych: calcbmis :: ( RealFloat a) = > [(a, a )] -> [a] calcbmis xs = [ bmi (w, h) <- xs, let bmi = w / h ^ 2] Jak widać let zostało użyte od razu przy tworzeniu listy.
Klauzula let Kolejne zastosowanie to tworzenie w konsoli zmiennych i funkcji, do których mamy dostęp do momentu zamknięcia konsoli: ghci > let zoot x y z = x * y + z ghci > zoot 3 9 2
Wyrażenie warunkowe case Jest oparte na dopasowywaniu do wzorca i rozszerza je, bo dopasowywanie do wzorca mogło być użyte tylko w definicji funkcji, a wyrażeń warunkowych możemy użyć tam, gdzie to potrzebne. head :: [a] -> a head xs = case xs of [] -> error " No head for empty lists!" (x: _) -> x Case nie jest skomplikowany, używamy go następująco: case expression of pattern -> result pattern -> result pattern -> result...
Wyrażenie warunkowe case Tutaj kolejny prosty przykład wykorzystania case describelist :: [a ] -> String describelist xs = " The list is " ++ case xs of [] -> " empty."