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 konstuisati 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
Kako tip () 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.

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
Novo ime, kao i ime svakog tipa, mora početi velikim slovom. Naravno, nije dozvoljeno koristiti već zauzeta imena tipova.
Primer 1.

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
Tip 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.

Primer 2.

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 konstuktor 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).

Primer 3.

Od tipa Bool možemo kreirati novi tip MyBool:

newtype MyBool = MkMyBool Bool
Često se za ime konstruktora uzima ime tipa ispred kojeg se stavlja Mk (skraćeno od Make), jer konstruktor predstavlja jedini način da se "napravi" vrednost tipa 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)
Svaka vrednost tipa 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 glavi Klase tipova. Srećom, u ovom slučaju Haskel može instaciranje automatski da uradi. Dovoljno je samo da nakon definicje tipa dopišemo deriving Show.

newtype MyBool = MkMyBool Bool deriving Show
Za sada nećemo detaljnije objašnjavati 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.

Primer 4.

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 glavi.

Takođe, newtype konstrukcija je specijalan slučaj jedne opštije, i mnogo korisnije konstrukcije, kojoj je posvećena glava 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":

Primer 5.

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 definicja.

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.

Primer 6.

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)
Navedeni apstraktni tip je zasigurno veoma čudan, ali validan. Sada izraz 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 posvatrajmo vrednsti sa kojim smo navikli da radimo. 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.

Primer 7.

Tipovi Bool, Int, Float, [Int], Bool -> Int, (), (Int, [Bool]), itd... su svi vrste *.

Primer 8.

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.

Primer 9.

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 * -> * -> * -> *.