Sveučilište u Zagrebu Fakultet elektrotehnike i računarstva PROGRAMIRANJE U HASKELLU Ak.god. 2011/12. LEKCIJA 9: korisnički tipovi podataka 1 v1.1 (c) 2011 Jan Šnajder ============================================================================== > import Data.List === DATA TYPES =============================================================== > data Tricolor = Red | Green | Blue 'Tricolor' je novi tip. 'Red', 'Green' i 'Blue' su PODATKOVNI KONSTRUKTORI (data constructors). Imena tipova i tipskih konstruktora pišu se velikim početnim slovom. Probajmo: ghci> :t Red Ovakvi tipovi, kod kojih eksplicitno popisujemo sve moguće vrijednosti, nazivaju se ALGEBARSKI TIPOVI PODATAKA. To je drugačije od toga da samo preimenujemo već neki postojeći tip. To se radi ovako: Imena tipova i tipskih konstruktora pišu se velikim početnim slovom. > type Word = [Char] > type Coord = (Int,Int) Algebarski tipovi koje smo do sada viđali: data Bool = True | False data Ordering = LT | EQ | GT Sjetimo se: compare :: Ord a => a -> a -> Ordering Svoj tip možemo naravno koristiti za definirajne novih funkcija. > warmColor :: Tricolor -> Bool > warmColor Red = True > warmColor _ = False > myColor = Red Što se događa ako u ghci-u pokušamo evaluirati 'myColor'? Vrijednost se ne može ispisati jer nije u razredu 'Show'. To je lako riješiti: data Tricolor = Red | Green | Blue deriving Show Na ovaj način tip 'Tricolor' automatski postaje članom tipskog razreda 'Show'. NAPOMENA: Podatkovni konstruktori moraju biti unikatni! Ne smije se isti konstruktor pojavljivati kod više tipova. Ovo nije dobro: data Tricolor = Red | Green | Blue data WarmColors = Red | Orange | Yellow Konstruktori 'Red', 'Green' i 'Blue' su zapravo vrijednosti same za sebe. Takve konstruktore zovemo NULARNI-KONSTRUKTORI (nullary constructors). Tip koji ima samo nularne kosntruktore sličan je ENUMERACIJAMA u drugim prog. jezicima. Konstruktori mogu biti binarni, ternarni itd. Npr. (primjer iz LYAHFGG): > data Shape = > Circle Double Double Double > | Rectangle Double Double Double Double > deriving Show 'Circle' je ternaran konstruktor: uzima tri realna broja (koordinate kružnice i radijus). 'Rectangle je ' kvaternaran konstruktor: uzima četiri broja (dvije koordinate). Konstruktori su zapravo funkcije. Kojeg je tipa konstruktor 'Circle'? Circle :: Float -> Float -> Float -> Shape Provjera je li 'Shape' upravo 'Circle': > isCircle (Circle _ _ _) = True > isCircle _ = False Inače, mogli smo definirati: data Shape = Circle (Float, Float) Float | Rectangle (Float, Float) (Float, Float) Primjer nekih vrijednosti i funkcija: > myCircle = Circle 5 5 10 > myRectangle = Rectangle 10 10 100 200 > unitCircle x y = Circle x y 1 Funkcija za izračun površine: > area :: Shape -> Double > area (Circle _ _ r) = r ^ 2 * pi > area (Rectangle x1 y1 x2 y2) = (abs $ x1 - x2) * (abs $ y1 - y2) Budući da konstuktori 'Circle' i 'Rectangle' daju vrijednosti istog tipa, može ih se kombinirati u listi. > myShapes = [myCircle,myRectangle,unitCircle 0 0,Rectangle 0 0 5 5] > totalArea :: [Shape] -> Double > totalArea = sum . map area Malo bolji pristup definiranju tipa: > data Point = Point Double Double > deriving Show > data Shape2 = Circle2 Point Double | Rectangle2 Point Point > deriving Show Primijetite da 'Point' koristimo i kao tip i kao podatkovni konstruktor. To je OK (i dapače uobičajeno kod tipova sa samo jednim konstruktorom). > myCircle2 = Circle2 (Point 5 5) 10 > myRectangle2 = Rectangle2 (Point 10 10) (Point 10 10) > area2 :: Shape2 -> Double > area2 (Circle2 _ r) = r ^ 2 * pi > area2 (Rectangle2 (Point x1 y1) (Point x2 y2)) = > (abs $ x1 - x2) * (abs $ y1 - y2) === VJEŽBA 1 ================================================================= 1.1. - Definirajte strukturu 'Date' s odgovarajućim poljima. - Napišite funkciju za prikaz datuma u obliku DD.MM.GGGG. (bez vodećih nula). showDate :: Date -> String 1.2. - Napišite funkciju translate :: Point -> Shape2 -> Shape2 koja translatira lik u smjeru vektora (x,y). 1.3. - Napišite funkciju 'inShape' koja provjerava nalazi li se točka unutar (ili na granici) zadanog oblika. inShape :: Shape -> Point -> Bool - Napišite funkciju 'inShapes' koja provjerava nalazi li se točka unutar nekog od oblika iz liste. inShapes :: [Shape] -> Point -> Bool 1.4. - Definirajte svoj tip podataka 'Vehicle' koji može biti 'Car', 'Truck', 'Motorcycle' ili 'Bicycle'. Prva tri imaju ime proizvođača (String) i broj konjskih snaga (Int). - Napišite funkciju 'totalHorsepower' koja zbraja konjske snage. Za bicikl pretpostavite da je broj konjskih snaga 0.3. === ZAPISI (RECORDS) ========================================================= > data Level = Bachelor | Master | PhD deriving (Show,Eq) > data Student2 = Student2 String String String Level Double deriving Show > firstName2 :: Student2 -> String > firstName2 (Student2 f _ _ _ _) = f > lastName2 :: Student2 -> String > lastName2 (Student2 _ l _ _ _) = l > studentId2 :: Student2 -> String > studentId2 (Student2 _ _ i _ _) = i Ovo je malo naporno i nepregledno. Bolje je koristiti zapise: > data Student = Student { > firstName :: String, > lastName :: String, > studentId :: String, > level :: Level, > avgGrade :: Double } deriving Show Ovime automatski dobivamo: firstName :: Student -> String lastName :: Student -> String studentId :: Student -> String level :: Student -> Level avgGrade :: Student -> Double Definicija zapisa: > bestStudent = Student { > studentId = "00364912215", > firstName = "Ivan", lastName = "Ivić", > level = Master, avgGrade = 5.0 } Funkcija daje string za JMBAG, ime i prezime studenta: > showStudent :: Student -> String > showStudent s = studentId s ++ " " ++ firstName s ++ " " ++ lastName s ili: > showStudent2 :: Student -> String > showStudent2 s = intercalate " " [studentId s,firstName s,lastName s] Možemo i ovako: > showStudent3 :: Student -> String > showStudent3 (Student {studentId=id,firstName=f,lastName=l}) = > intercalate " " [id,f,l] Odabir studenata s prosjekom iznad zadanog ili jednakim zadanom: > aboveStudents :: Double -> [Student] -> [Student] > aboveStudents x = filter ((>=x) . avgGrade) Nije obavezno zadati sva polja. Koja ne zadamo su 'undefined'. GHCI će dati upozorenje. > someStudent = Student { firstName = "Marko", avgGrade = 4.3 } Hoće li raditi 'showStudent someStudent' ? Hoće li raditi 'map firstName $ aboveStudents 4.0 [bestStudent, someStudent]' ? Možemo mijenjati polja u zapisu: > bestStudent2 = bestStudent { avgGrade = 4.9 } > someStudent2 = someStudent { lastName = "Markov", studentId = "0036365438" } Ovo je korisno kada želimo definirati podrazumijevane vrijednosti: > bachelorStudent = Student { level = Bachelor } > masterStudent = Student { level = Master } > phdStudent = Student { level = PhD } > newStudent = bachelorStudent { > firstName = "Zoran", lastName = "Zoki", > studentId = "00364532350", avgGrade = 4.5 } Zapise možemo tretirati i ovako: > newStudent2 = Student "Petar" "Perić" "00364542345" Master 3.5 === VJEŽBA 2 ================================================================= 2.1. - Napišite funkciju koja studentu povećava prosjek za 1.0, ali ne na više od 5.0. improveStudent :: Student -> Student 2.2. - Napišite funkciju koja računa prosjek ocjena studenata po razinama studija. avgGradePerLevels :: [Student] -> (Double,Double,Double) 2.3. - Napišite funkciju koja za odabranu razinu studija vraća listu matičnih brojeva studenata sortiranih silazno prema prosjeku. rankedStudents :: Level -> [Students] -> [String] 2.4. - Napišite funkciju addStudent :: Student -> [Student] -> [Student] koja u listu studenata dodaje novog studenta. Ako student s matičnim brojem već postoji u listi, funkcija treba javiti pogrešku. === PARAMETRIZIRANI TIPOVI =================================================== > data OldLevels = Graduate | Doctorate deriving Show > data GeneralStudent a = Student3 String String String a Double deriving Show 'GeneralStudent' ima tipski parametar i daje različite tipove ovisno o tom parametru: > type BolognaStudent = GeneralStudent Level > type FER1Student = GeneralStudent OldLevels Takve tipove koji uzimaju parametra zovemo TIPSKI KONSTRUKTORI. Tipično su parametrizirani tipovi nekakvi kontejneri podataka. Npr. > data MyBox a = InBox a Dakle MyBox je tipski konstruktor s kojim možemo konstruirati različite tipove. Npr. > type StringBox = MyBox String > type IntBox = MyBox Int 'InBox' je podatkovni konstruktor s kojim možemo konstruirati različite vrijednosti. Haskell će automatski odrediti ispravan tip: > b1 = InBox 1.2 > b2 = InBox "Moj string" > b3 = InBox "Haskell" Kojeg su tipa ovi izrazi? Bolji način da se ovo napravi: > data Box a = Box { unbox :: a } deriving Show Parametrizirani tip može imati više parametara: > data MyPair a b = MyPair (a,b) Dakle možemo imati: MyPair 1 1 :: MyPair Int Int MyPair "bla" 1.2 :: MyPair String Double A možemo definirati npr. i: > type IntMyPair = MyPair Int Int > type MyPairType a = MyPair a String Funkcija koja uzima prvi element iz tipa MyPair: > fstMyPair :: MyPair a b -> a > fstMyPair (MyPair (x,_)) = x Jedan važan parametrizirani tip: data Maybe a = Nothing | Just a Dakle možemo imati npr.: Just 5 :: Maybe Int Just "bla" :: Maybe String Just (1,1) :: Maybe (Int,Int) 'Maybe' se koristi za: 1. situacije u kojem je neka vrijednost opcionalna 2. situacije kod kojih može doći do pogreške Opcionalnost: > data Employee = Employee { > name :: String, > salary :: Maybe Double } deriving Show Sad možemo definirati: > showSalary :: Employee -> String > showSalary e = case salary e of > Nothing -> "nepoznato" > Just n -> show n ++ " kn" Funkcija za konkateniranje dva 'Maybe Stringa': > concatMaybeStrings :: Maybe String -> Maybe String -> Maybe String > concatMaybeStrings (Just s1) (Just s2) = Just $ s1 ++ s2 > concatMaybeStrings s@(Just _) Nothing = s > concatMaybeStrings Nothing s@(Just _) = s > concatMaybeStrings _ _ = Nothing Korištenje 'Maybe' u situacijama gdje može nastati pogreška: > safeHead :: [a] -> Maybe a > safeHead [] = Nothing > safeHead (x:_) = Just x Također koristan je tipski konstruktor 'Either': data Either a b = Left a | Right b Obično se koristi kao povratna vrijednost funkcija koje mogu vratiti pogrešku: 'Right b' je vrijednost ako je sve OK, a 'Left a' je tipično poruka o pogrešci. > safeHead2 :: [a] -> Either String a > safeHead2 [] = Left "empty list" > safeHead2 (x:_) = Right x === VJEŽBA 3 ================================================================= 3.1. - Napišite svoj parametriziran tip 'MyTriplet' koji sadržava vrijednosti tri različita tipa. Napravite to pomoću zapisa. - Napišite funkciju toTriplet :: MyTriplet a b c -> (a,b,c) koja vrijednost tipa MyTriplet pretvara u običnu trojku. 3.2. - Definirajte funkciju totalSalaries :: [Employee] -> Double koja zbraja poznate plaće zaposlenika (one koje nisu Nothing). 3.3. - Napišite 'addStudent2' koja radi kao i 'addStudent' iz 2.4, ali umjesto pogreške vraća tip Maybe [Student]. addStudent2 :: Student -> [Student] -> Maybe [Student] - Napišite addStudent3 koji vraća Either. === FMAP ===================================================================== Recimo da imamo strukture: > data Customer = Customer { > customerName :: String, > customerAge :: Int, > customerAddress :: Maybe Address } > deriving (Eq,Show) > data Address = Address { > streetName :: String, > streetNumber :: Int, > zipCode :: String, > city :: String } > deriving (Eq,Show) > c1 = Customer "Ivo" 22 (Just $ Address "Bauerova" 10 "10000" "Zagreb") > c2 = Customer "Ana" 30 Nothing Funkcija koja vraća grad u kojem živi mušterija, ukoliko je adresa poznata: > customerCity :: Customer -> Maybe String > customerCity c = case customerAddress c of > Just a -> Just $ city a > Nothing -> Nothing Funkcija koja vraća ime ulice i kućni broj, ako su poznati: > customerStreet :: Customer -> Maybe (String,Int) > customerStreet c = case customerAddress c of > Just (Address s n _ _) -> Just (s,n) > Nothing -> Nothing U gornje dvije funkcije pojavljuje se obrazac: želimo primijeniti neku funkciju 'f' na podatak zapakiran s 'Just' i vratiti 'Just (f x)', ili vratiti 'Nothing' ako nema podatka. Ako imamo podatak, trebamo ga otpakirati, primijeniti funkciju, a onda opet zapakirati u 'Just'. To radi funkcija 'fmap': fmap :: (a -> b) -> Maybe a -> Maybe b fmap f (Just x) = Just $ f x fmap _ Nothing = Nothing (Funkcija 'fmap' nije baš tako definirana, nego je malo općenitija, ali o tome ćemo kasnije.) Sada možemo definirati: > customerCity2 :: Customer -> Maybe String > customerCity2 = fmap city . customerAddress > customerStreet2 :: Customer -> Maybe (String,Int) > customerStreet2 = fmap (\(Address s n _ _) -> (s,n)) . customerAddress