Tipovi i vrste
U dosadašnjem izlaganju uvideli smo koliko je sistem tipova bitan za Haskel. Razmotrili smo neke osnovne tipove, navikli smo da govorimo o tipu funkcija i tipskim promenljivama, i ukratko smo opisali klase tipova kao kolekcije tipova.
Dva posebna tipa
Najjednostavniji tip sa kojim smo se upoznali je Bool
koji sadrži samo dve vrednosti. Sada ćemo predstaviti tip koji sadrži samo jednu vrednost, kao i tip koji ne sadrži nijednu vrednost.
Tip koji sadrži jednu vrednost označavamo sa ()
i nazivamo jedinični tip1. Jedinu vrednost ovog tipa označavamo takođe sa ()
. Dakle () :: ()
.
Svaka funkcija je u potpunosti određena sa slikama svih vrednosti domena te funkcije. Ako je data funkcija f :: () -> A
, za neki tip A
, tada je ta funkcija u potpunosti određena sa f ()
. Možemo da kažemo da f
"bira" jedinstvenu vrednost tipa A
.
Sa druge strane, ako je data funkcija g :: A -> ()
, tada za svaku vrednost x :: A
mora važiti g x = ()
(jer je ()
jedina vrednost kodomena). Stoga, za svaki tip A
važi da postoji samo jedna funkcija tipa A -> ()
, i ta funkcija je definisana sa \_ -> ()
.
Tip koji ne sadrži nijednu vrednost označava se sa Void
i nazivamo ga prazan tip2.
Posmatrajmo sada funkciju s :: A -> Void
. Za x :: A
, s x
mora biti vrednost tipa Void
. Vrednost tipa Void
ne postoji, iz čega sledi da funkcija s
ne može da postoji ako tip A
nije neprazan. Prema tome, jedina funkcija čiji kodomen je Void
je jedinstvena funkcija tipa Void -> Void
.
Sa druge strane, za svaki tip A
, postoji jedinstvena funkcija3 tipa Void -> A
. Ali kako je nemoguće konstruisati vrednost tipa Void
, takvu funkciju ne možemo nikad "pozvati".
Primetiomo da tip Void
ne odgovara pojmu void
koji se može naći u drugim programskim jezicima popout C-a, C++-a, Jave. U takvim programskim jezicima, void
se koristi za funkcije čija povratna vrednost nas ne interesuje. Prema tome, takav pojam odgovara jediničnom tipu u Haskelu4.
Zadatak 1. Koliko postoji funkcija tipa () -> Bool
?
Postoji onoliko funkcija tipa () -> T
koliko postoji vrednosti tipa T
. Prema tome, postoje dve funkcije tipa () -> Bool
, koje su definisane sa
f1 () = True f2 () = False
()
sadrži samo jednu vrednost, podudaranje oblika je trivijalno. Zapravo, kako su obe navedene funkcije konstantne, mogli smo da koristimo i džoker _
.Iako tipovi ()
i Void
na prvi pogled deluju beskorisno, ti tipovi imaju i teorijsku i praktičnu važnost. Takođe, ova dva tipa će nam poslužiti kao dodatni primeri pri ilustraciju naprednih koncepata.
Zadatak 2. Koliko vrednosti sadrži tip [Void]
?
Zadatak 3. Koliko vrednosti sadrži tip ((), ())
?
Zadatak 4. Koliko vrednosti sadrži tip (Bool, Void)
?
Tipski sinonimi
Do sada smo već upoznali neke načine konstrukcije novih tipova. Na primer, od od nekoliko tipova a1
, a2
, ... an
možemo napraviti odgovarajući tip uređenih ntorki (a1, a2, ... an)
. Pri ovakvim konstrukcijama često se dobijaju tipovi koji nisu baš kratki za pisanje, a što je još bitnije, ni za čitanje5.
Da bismo izbegli navedene probleme, možemo definisati tipski sinonim. Sinonim predstavlja novo imе već postojećeg tipa, i može se koristiti umesto originalnog imena. Sinonim se definiše sa dekleracijom poput naredne:
type Ime = PostojeciTip
Možemo tipu Bool
dodeliti sinonim Logicki
na sledeći način
type Logicki = Bool
Sada na svim mestima na kojima se pojavljuje Bool
, možemo koristiti ime Logicki
:
a :: Logicki a = True || False
Učitavanjem navedog koda u GHC-i, možemo proveriti tipove:
ghci> :t a Logicki ghci> a && False False ghci> :t (a && False) Bool
Logicki
je u potpunosti isti kao tip Bool
. Ali, ako tip neke vrednosti nije eksplicitno definisana kao Logicki
, tada će "prednost imati" originalno ime.Naravno, sinonim za već kratko ime nije mnogo koristan. Pogledajmo zato reprezentativniji primer.
Uređenoj trojci Float
brojeva možemo dodeliti kratko ime Vektor3
:
type Vektor3 = (Float, Float, Float)
Sa ovakvim sinonimom, definicije postaju mnogo preglednije
zbirVektora :: Vektor3 -> Vektor3 -> Vektor3 zbirVektora (a, b, c) (x, y, z) = (a + x, b + y, c + z)
U Haskelovom standardom okruženju Prelude, već je definisan naredni tipski sinonim
type String = [Char]
Novi tipovi
Slično definisanju tipskog sinonima, možemo definisati i potpuno novi tip od postojećeg tipa. Definicija novog tipa ima naredni oblik
newtype NoviTip = Konst PostojeciTip
Gornjim kodom definisan je tip NoviTip
, koji se može shvatiti kao kopija tipa PostojeciTip
. Svaka vrednost u tipu NoviTip
odgovara nekoj vrednosti u tipu PostojeciTip
, ali ova dva tipa nemaju zajedničke vrednosti!
U gore navedenoj definiciji, reč Konst
predstavlja ime konstruktora. Konstruktor je funkcija kojim se konstruišu vrednosti tipa NoviTip
. Svaka vrednost tipa NoviTip
je oblika Konst x
za neko x :: PostojeciTip
. Ime konstruktora i ime novog tipa, ne smeju biti već zauzeti i moraju počinjati velikim slovom. Međutim, dozvoljeno je da tip i konstruktor imaju isto ime.
Veoma bitna razlika između tipova definisanih sa newtype
i tipskih sinonima je ta što sa newtype
dobijamo u potpunosti novi tip. Stoga nije moguće novi tip koristiti sa funkcijama koje su namenjene za postojeći tip (kao što je to bio slučaj sa tipskim sinonimima).
Od tipa Bool
možemo kreirati novi tip MyBool
:
newtype MyBool = MkMyBool Bool
MyBool
.Kreirani tip MyBool
sadrži samo dve vrednosti. Te vrednosti su MkMyBool True
i MkMyBool False
. Ove dve vrednosti nisi isto što i True
i False
! Na primer, možemo se uveriri da vrednosti tipa MyBool
ne mogu se koristiti sa logičkim operatorima:
ghic> a = (MkMyBool True) || (MkMyBool False) <interactive>:1:6: error: • Couldn't match expected type ‘Bool’ with actual type ‘MyBool’ • In the first argument of ‘(||)’, namely ‘(MkMyBool True)’ In the expression: (MkMyBool True) || (MkMyBool False) In an equation for ‘a’: a = (MkMyBool True) || (MkMyBool False) <interactive>:1:25: error: • Couldn't match expected type ‘Bool’ with actual type ‘MyBool’ • In the second argument of ‘(||)’, namely ‘(MkMyBool False)’ In the expression: (MkMyBool True) || (MkMyBool False) In an equation for ‘a’: a = (MkMyBool True) || (MkMyBool False)
Prema tome za vrednosti tipa MyBool
moramo implementirati posebnu funkciju disjunkcije:
ili :: MyBool -> MyBool -> MyBool ili (MkMyBool False) (MkMyBool False) = MkMyBool False ili _ _ = (MkMyBool False)
MyBool
je oblika MkMyBool x
. Haskel dozvoljava da upotrebimo tehniku podudaranja oblika u ovom slučaju. Prva linija definicije odnosi se samo na slučaj kada su obe prosleđene vrednosti baš MkMyBool False
. Svi ostali slučajevi su obuhvaćeni drugom linijom, u kojoj se koriste džokeri.Nakon učitavanja navedenog koda, možemo naći disjunkciju novih logičkih vrednosti
ghci> a = ili (MkMyBool True) (MkMyBool False)
Za sada sve deluje dobro. Kreirali smo novi tip, i napisali smo jednu funkciju čiji je domen i kodomen novi tip. Ali ako interaktivnom okruženju pokušamo da ispišemo vrednost a
, dobićemo grešku:
ghci> a <interactive>:1:1: error: • No instance for (Show MyBool) arising from a use of ‘print’ • In a stmt of an interactive GHCi command: print it
Razlog nastanka greške je taj što program GHCi ne zna kako da ispiše vrednost tipa MyBool
. Za ispisivanje svih vrednosti, GHCi koristi show
funkciju koja je deklarisana u Show
klasi tipova. Kako tip MyBool
nije instanca ove klase, GHCi ne može da iskoristi funkciju show
sa vrednostima tipa MyBool
.
Navedeni problem se rešava uvođenjem tipa MyBool
u klasu Show
, što će biti detaljno objašnjeno u lekciji Klase tipova. Srećom, u ovom slučaju Haskel može instaciranje automatski da uradi. Dovoljno je nakon definicije tipa dopisati deriving Show
. Automatsko instanciranje poput navedenog nazivamo izvođenje.
newtype MyBool = MkMyBool Bool deriving Show
deriving Show
.Ponovnim učitavanjem koda, možemo bez problema da ispisujemo vrednosti novog tipa
ghci> a = ili (MkMyBool True) (MkMyBool False) ghci> a MkMyBool True
Zašto je newtype
konstrukcija korisna ako već imamo tipske sinonime? Deluje da sa newtype
samo komplikujemo kod. Funkcije koje smo ranije koristili, moramo reimplementirati za novi tip. Dobar razlog korišćenja newtype
-a je taj, što ponekad namerno želimo da budemo ograničeni sa sistemom tipova, što prikazuje primer koji sledi.
Zamislimo da radimo na bankarskom programu, koji se mora osposobiti za rad sa svotama u evrima i dolarima. U kodu možemo definisati dva tipa, čije vrednosti predstavljaju svote novca:
newtype Eur = MkEur Double newtype Usd = MkUsd Double
Vrednosti oba definisana tipa sadrže suštinski iste podatke (jedan double broj). Kako su u pitanju različiti tipovi, kompajler neće dozvoliti prozvoljno mešanje ovih tipova (kao što bi bio slučaj sa tipskim sinonimima). Na taj način, na primer, možemo napisati funkciju koju je moguće isključvo primeniti na svote novca u evrima. Pokušaj primene takve funkcije na svote u dolarima dovešće do tipske greške i program neće biti iskompajliran.
Ograničenja poput opisanih ne deluju mnogo korisna kada se posmatraju mali programi. Ali sa programima s milionskim brojem linija ovakva ograničenja značajno "umanjuju brigu" pri menjanju koda. Na primer, promena funkcija koje koriste tip Eur
neće uopšte uticati na funkcije koje koriste tip Usd
.
Naravno, razdvajanje valuta ima smisla samo na onom delu programa koji različito obrađuje svote u evrima i dolarima (u ovom slučaju to može biti obračunavanje poreza koji je specifičan za valutu, i sl.). Deo logike koji je isti za obe valute, ne treba duplirati. Izbegavanje dupliranja koda se može postići sa tipskim klasama, što će biti prikazano u narednoj lekciji.
Takođe, newtype
konstrukcija je specijalan slučaj jedne opštije, i mnogo korisnije konstrukcije, kojoj je posvećena lekcija Algebarski tipovi podataka.
Apstraktni tipovi
Ponekad je slučaj da se u kodu konstruiše nekoliko sličnih tipova, koji se samo razlikuju u jednom "delu":
Za predstavljanje trodimenzionalnih vektora možemo konstrisati tipove:
newtype VecInt = MkVecInt (Int, Int, Int) newtype VecFloat = MkVecFloat (Float , Float, Float) newtype VecDouble = MkVecDouble (Double, Double, Double)
Za ovako definisane tipove nije teško konstruisati odgovarajuće funkcije sabiranja, skaliranja, vektorskog i skalarnog množenja, itd..
sumVecInt :: VecInt -> VecInt -> VecInt sumVecInt (MkVecInt (a,b,c)) (MkVecInt (x,y,z)) = MkVecInt (a+x, b+y, c+z) sumVecFloat :: VecFloat -> VecFloat -> VecFloat sumVecFlaot (MkVecFloat (a,b,c)) (MkVecFloat (x,y,z)) = MkVecFloat (a+x, b+y, c+z) sumVecDouble :: VecDouble -> VecDouble -> VecDouble sumVecDouble (MkVecDouble (a,b,c)) (MkVecDouble (x,y,z)) = MkVecDouble (a+x, b+y, c+z)
Kao što vidimo, svaki numerički tip (Int
, Float
, Double
) zahteva posebnu definiciju odgovarajućeg tipa vektora, što dovodi do toga da imamo mnogo međusobno sličnih definicija.
Da bismo pri definiciji tipova izbegli nepotrebno dupliranje koda, možemo koristiti apstraktne tipove. Apstraktni tip je tip koji zavisi od nekog drugog tipa. U nekom smilsu, apstraktni tipovi predstavljaju funkciju čiji su argumenti tipovi! Apstraktni tip se definiše tako što se nakon imena tipa navede tipska promenljiva, a nakon konstruktora tip koji potencijalno zavisi od te tipske promeljive6.
Za jednobraznu konstrukciju trodimenzionalnih vektora, možemo koristiti naredni apstraktni tip
newtype Vec a = MkVec (a, a, a)
Na opisani način konstruisana je jedna funkcija na nivou tipova. Ova funkcija proizvoljan tip preslikava u tip (a, a, a)
. Na primer, Vec Char
predstavlja tip (Char, Char,Char)
.
Tipovi iz prethodnog primera nam nisu više neophodni, i sada možemo da koristimo Vec Int
, Vec Float
i Vec Double
:
sumVecInt :: Vec Int -> Vec Int -> Vec Int sumVecInt (MkVec (a,b,c)) (MkVec (x,y,z)) = MkVec (a+x, b+y, c+z) sumVecFloat :: Vec Float -> Vec Float -> Vec Float sumVecFlaot (MkVec (a,b,c)) (MkVec (x,y,z)) = MkVec (a+x, b+y, c+z) sumVecDouble :: Vec Double -> Vec Double -> Vec Double sumVecDouble (MkVec (a,b,c)) (MkVec (x,y,z)) = MkVec (a+x, b+y, c+z)
I dalje se nismo umanjili broj definicija funkcija, ali možemo primetiti da je karakteristično za tipove Int
, Float
i Double
to što se vrednosti ovih tipova mogu sabirati. Sva tri navedena tipa pripadaju Num
klasi, pa možemo tri definicije zameniti sa jednom:
sumVec :: Num a -> Vec a -> Vec a -> Vec a sumVec (MkVec (a,b,c)) (MkVec (x,y,z)) = MkVec (a+x, b+y, c+z)
Nije nužno da apstraktan tip zavisi samo od jednog parametra, ili da ne sadrži konstantne tipove u sebi. Na primer, sledeći apstraktni tip je u potpunosti validan
data A a b c = MkA (a, [b], Int -> c)
A Char Bool Float
predstavlja tip (Char, [Bool], Int -> Float)
.Apstraktni tipovi, nazivaju se još i konstruktori tipa, jer kao što ime kaže, oni konstruišu tipovi. U tom smilsu, konstruktori koji konstruišu vrednosti se nazivaju konstruktori vrednosti. U gornjem bloku koda A
je konstruktor tipa, a MkA
konstruktor vrednosti.
Vrste
Apstraktni tipovi jesu funkcije koje tipove preslikavaju u tipove. Ali kao i sa svim preslikavanjima, potrebno je biti pažljiv pri primeni funkcija na vrednost.
Vratimo se za tenutak korak u nazad, i posmatrajmo vrednosti. Sistem tipova nam govori nema smisla primeniti funkciju tipa Int -> Int
na vrednost tipa Char
. Još manje smisla ima primena vrednosti tipa Bool
na vrednost tipa Int
, jer prva vrednost nije čak ni funkcija.
Iz istih razloga potrebno je klasifikovati tipove i apstraktne tipove na sličan način kao što su vrednosti klasifikovane u tipove. Drugim rečima, potrebno je uvesti pojam "tipa" za tipove7. Ovakav "tip tipa" nazivamo vrsta8.
Ako neki tip ili apstraktni tip T
poseduje vrstu v
, tada to označavamo sa T :: v
, baš kao i tip vrednosti.
U Haskelu, vrsta koja obuhvata sve tipove naziva se Hask i označava se sa *
. Za svaki tip (koji nije apstraktan) T
kažemo da je vrste *
, odnosno pišemo T :: *
. Za ovakve tipove kažemo još da su konkretni tipovi (ili monotipovi), da bismo napravili distinkciju od apstraktnih tipova.
Sa druge strane, apstraktni tipovi poseduju vrstu oblika P -> Q
gde su P
i Q
takođe neke vrste. Vrsta oblika P -> Q
sugeriše da su apstraktni tipovi funkcije koje tipove preslikavaju tipove.
Tipovi Bool
, Int
, Float
, [Int]
, Bool -> Int
, ()
, (Int, [Bool])
, itd... su svi vrste *
.
Neka je apstraktni tip AbsType
definisan sa
newtype AbsType a = MkAbsType a
.
Tip AbsType
uzima jedan konkretan tip i konstruiše konkretan tip, pa je AbsType
vrste * -> *
. Prema tome, izraz AbsType AbsType
nema smisla, ali izraz AbsType Int
ima smisla i predstavlja tip.
Uzmimo primer apstraktnog tipa iz prošle sekcije
data A a b c = MkA (a, [b], Int -> c)
Navedeni apstraktni tip ima vrstu * -> (* -> (* -> *))
koja se naravno jednostavno zapisuje kao * -> * -> * -> *
.