Määritellään esimerkkinä hakupuillemme sellainen samuus x == y joka pätee täsmälleen silloin kun puissa x ja y on samat(avain,tietoalkio)-parit riippumatta siitä minkä muotoisia puut x ja y ovat. Olemme kirjoittamassa koodia funktiolle (==) :: Tree k t -> Tree k t -> Bool koska sellainen vaaditaan jotta oma tyyppimme Tree k t pääsee jäseneksi Luokkaan Eq. Kirjoitamme vain sen ohjelmakoodin emme tätä tyypitystä. Koodissamme haluamme verrata ovatko puiden x ja y contentsof-listat samat. Haluamme siis käyttää niiden välistä samuusvertailua (==) :: [(k,t)] -> [(k,t)] -> Bool eli haluamme että myös Eq [(k,t)]. Haskell päättelee, että Eq [(k,t)] vaatii Eq (k,t) joka puolestaan vaatii että Eq k ja Eq t. Koska nämä k ja t ovat tyyppimuuttujia, niin Haskell ei pysty päättelemään tämän enempää, vaan ohjelmoijan on luvattava ne kirjoittamalla samat faktat. data Tree k t = Empty Node { left :: Tree k t, key :: k, item :: t, right :: Tree k t deriving (Show) instance (Eq k,eq t) => Eq (Tree k t) where x == y = contentsof x == contentsof y additem :: (Ord k) => k -> t -> Tree k t -> Tree k t additem newkey newitem Empty = Node { left = Empty, key = newkey, item = newitem, right = Empty additem newkey newitem this@node{ key = oldkey newkey < oldkey = this { left = additem newkey newitem $ left this newkey > oldkey = this { right = additem newkey newitem $ right this otherwise = this { item = newitem 79
build :: (Ord k) => [(k,t)] -> Tree k t build = foldr (uncurry additem) Empty itemof :: (Ord k) => k -> Tree k t -> Maybe t itemof _ Empty = Nothing itemof newkey this@node { key = oldkey newkey < oldkey = itemof newkey $ left this newkey > oldkey = itemof newkey $ right this otherwise = Just $ item this contentsof :: Tree k t -> [(k,t)] contentsof Empty = [] contentsof this@node{ = contentsof (left this) ++ (key this,item this) : contentsof (right this) contentsof :: Tree k t -> [(k,t)] contentsof = let cont Empty acc = acc cont this@node{ acc = cont (left this) $ (key this,item this) : cont (right this) acc in flip cont [] Intuitiivisesti ohjelmoija on esittänyt säännön: Jos mielivaltaisille tyypeille k on määritelty sen oma samuus (==) :: k -> k -> Bool ja t on määritelty sen oma samuus (==) :: t -> t -> Bool niin minun tyypilleni Tree k t määritellään sen oma samuus (==) :: Tree k t -> Tree k t -> Bool niiden perusteella tällä tavalla. Päätellessään että Eq [(k,t)] Haskell käyttää samanlaisia sääntöjä instance (Eq a) => Eq [a] where... instance (Eq a,eq b) => Eq (a,b) where... ja vastaava metodi (==) :: [(k,t)] -> [(k,t)] -> Bool 80
käyttää niiden where-osien (==)-metodeja. Haskellin tyyppiluokilla voi siis generoida ohjelmakoodia automaattisesti HMtyypinpäättelyn sivutuotteena. Se mahdollistaa monenlaisia uusia tyyppiturvallisia ohjelmointitekniikoita. Haskellin ohjelmointifilosofiassa Tyyppi saa liittyä Luokkaan korkeintaan yhden kerran. Ajattelutapa on (jälleen) samantapainen kuin matematiikassa: tämäntyyppisillä arvoilla on vain tämä yksi oikea ja luonnollinen samuus / järjestys / tulostusasu /... ja niin edelleen. Jos liittymistapoja sallittaisiin useita, niin ei olisi enää selvää, millaista funktiota milloinkin pitäisi käyttää. Entä jos haluammekin tehdä jotenkin toisin vaikkapa vertailla merkkijonoja niin, että emme erottelekaan suuria ja pieniä kirjaimia toisistaan? Haskellin näkökulmasta sellainen on toinen tyyppi kuin tavalliset merkkijonot koska se tekee jotakin toisin. Tällaisiin tilanteisiin newtype soveltuu hyvin. import Data.Char import Data.Ord -- "Case-Insensitive Strings". newtype CIS = IntoCIS{ fromcis :: String deriving (Show) instance Ord CIS where compare = comparing (map tolower. fromcis) instance Eq CIS where (==) = ((EQ ==).). compare 5.5 Uuden tyyppiluokan määritteleminen Uusi tyyppiluokka määritellään päätasolla class (faktat) => Luokka muuttuja where metodi_1 :: Tyyppi_1 metodi_1 = lauseke_1 metodi_2 :: Tyyppi_2 metodi_2 = lauseke_2... muuttuja edustaa tähän Luokkaan kuuluvaa tuntematonta jäsentä eli tyyppiä: Tämä muuttuja kuuluu tähän Luokkaan, jos sillä on nämä metodit. 81
faktat rajoittavat tätä muuttujaa. (Tämä koko rajoite voidaan jättää pois, jos se on tarpeeton.) jokaisen metodin Tyyppi sisältää tämän muuttujan. (Muutenhan se ei kuulu tähän Luokkaan koska se ei käsittele muuttujatyyppisiä arvoja.) metodilla saa (mutta ei tarvitse) olla lauseke joka antaa sen oletustoteutuksen jota käytetään silloin jos jokin instance ei määrittelekään tätä metodia. Otetaan esimerkiksi tyyppiluokan Ord määrittely Preludessa. Sen muuttujana on a eli olemme määrittelemässä mitkä metodit tällä tuntemattomalla tyypillä a pitää olla jotta se kuuluisi tähän tyyppiluokkaan Ord. Sen fakta vaatii että tällä a pitää olla samuus (==). Jos x <= y && y <= x niin silloin x == y joten olisi outoa määritellä sellainen tyyppi a jolla olisi metodi (<=) mutta ei metodi (==). Haskell ei ole spesifiointi- vaan ohjelmointikieli, joten se vaatii vain että nämä metodit (<=) ja (==) on määritelty, mutta ei tarkista toimivatko ne näin. Seuraavaksi annetaan metodit ja niiden tyypit. (Siis where-osan voi järjestää kuten haluaa.) Kommentissa lukee, miten Uusi tyyppi on tarkoitus liittää tähän tyyppiluokkaan Ord: joko instance Ord Uusi where x <= y =... tai instance Ord Uusi where compare x y =... Lopuksi annetaan oletustoteutus jokaiselle metodille. Ideana on, että kun Uusi tyyppi määrittelee instancessaan joko metodinsa tai metodinsa (<=) :: Uusi -> Uusi -> Bool compare :: Uusi -> Uusi -> Ordering niin kaikki muutkin metodit tulevat samalla määritellyiksi oletustoteutustensa kautta. class (Eq a) => Ord a where compare :: a -> a -> Ordering (<), (<=), (>=), (>) :: a -> a -> Bool max, min :: a -> a -> a -- Minimal complete definition: -- (<=) or compare 82
-- Using compare can be more efficient for complex types. compare x y x == y = EQ x <= y = LT otherwise = GT x <= y x < y x >= y x > y = compare x y /= GT = compare x y == LT = compare x y /= LT = compare x y == GT -- note that (min x y, max x y) = (x,y) or (y,x) max x y x <= y = y otherwise = x min x y x <= y = x otherwise = y Luokkamäärittelyn sisäpuolelle pitää kirjoittaa ne metodit jotka sen jäsenet haluavat määritellä itselleen omissa instanceissaan. Ne käyttävät ad hoc -monimuotoisuutta. ulkopuolelle voi kirjoittaa ne funktiot jota määritellään samalla lailla kaikille sen jäsenille kunhan niiden tyypeissä on Luokkarajoite. Ne käyttävät parametrista monimuotoisuutta Luokkarajoitteella täsmennettynä. Luokan Ord määritelmä ei noudata tätä periaatetta tiukasti, koska sen sisällä on myös muut funktiot kuin pelkästään sen kommentissa mainitut metodit (<=) ja compare. Ne muut funktiot voi nostaa määritelmän ulkopuolelle tavallisiksi funktioiksi. class (Eq a) => Ord a where compare :: a -> a -> Ordering compare x y x == y = EQ x <= y = LT otherwise = GT (<=) :: a -> a -> Bool x <= y = compare x y /= GT (<), (>=), (>) :: forall a. (Ord a) => a -> a -> Bool x < y = compare x y == LT x >= y = compare x y /= LT x > y = compare x y == GT max, min :: forall a. (Ord a) => a -> a -> a 83
max x y x <= y = y otherwise = x min x y x <= y = x otherwise = y 5.6 Konstruktoriluokat Edellä kuvailimme tyyppiluokat niin, että niiden jäsenet ovat tyyppejä. Toisin sanoen niin, että class-määritelmässä muuttujan laji on *. Tyyppiluokka voi olla myös (tyyppi)konstruktoriluokka (constructor class) jolloin sen jäsenet ovatkin tyyppikonstruktoreita. Toisin sanoen, sen muuttujan laji voikin olla esimerkiksi * -> *. Konstruktoriluokat ilmaisevat tyyppikonstruktorien yhteisia ominaisuuksia samaan tapaan kuin tavalliset tyyppiluokat ilmaisevat tyyppien yhteisiä ominaisuuksia kuten kaikilla näillä tyypeillä on järjestys (<=). Preludessa on esimerkiksi tyyppiluokka class Functor f where fmap :: (a -> b) -> f a -> f b jonka muuttujan f laji on * -> * koska se ottaa yhden tyyppiparametrin a tai b antaa metodin fmap toisen parametrin tyypiksi f a ja tulostyypiksi f b. Siihen kuuluvat sellaiset tyyppikonstruktorit f joiden sisältämä informaatio (joka on tyyppiä a) voidaan korvata uudella informaatiolla (joka on tyyppiä b) käyttäen annettua muuntofunktiota (joka on siis tyyppiä a -> b). Esimerkiksi tyyppikonstruktori lista kuuluu tähän luokkaan metodinaan fmap :: (a -> b) -> [a] -> [b] fmap = map koska map g soveltaa funktiota g :: a -> b syötelistan jokaiseen alkioon ja palauttaa näin saatujen tulosten listan. Koska tyyppikonstruktorit ovat tyyppien välisiä kuvauksia, niin myös konstruktoriluokalla esitetty niiden yhteinen ominaisuus on funktioiden tasolla. Esimerkiksi tämän konstruktoriluokan Functor jäsenten pitää toteuttaa funktiotason yhtälöt fmap id = id fmap (f. g) = fmap f. fmap g 84
joiden intuitio on, että fmap saa muuttaa tietorakenteen sisältämää informaatiota mutta ei sen muotoa. (Koska Haskell on ohjelmointi- eikä spesifiointikieli, niin se ei tarkista onko näin.) Esimerkiksi listoissa id-säännön perusteella map ei lisää tai poista eikä edes järjestä uudelleen saamansa listan alkioita. Vastaavasti hakupuuesimerkissämme fmap g käsittelee vain lisätietokenttää item. Koska Tree :: * -> * -> * niin Tree k :: * -> * joka sopii Functorin lajiksi. data Tree k t = Empty Node { left :: Tree k t, key :: k, item :: t, right :: Tree k t deriving (Show) instance Functor (Tree k) where fmap _ Empty = Empty fmap g node = node{ left = fmap g $ left node, item = g $ item node, right = fmap g $ right node instance (Eq k,eq t) => Eq (Tree k t) where x == y = contentsof x == contentsof y additem :: (Ord k) => k -> t -> Tree k t -> Tree k t additem newkey newitem Empty = Node { left = Empty, key = newkey, item = newitem, right = Empty additem newkey newitem this@node{ key = oldkey newkey < oldkey = this { left = additem newkey newitem $ left this newkey > oldkey = this { right = additem newkey newitem $ right this otherwise = this { item = newitem build :: (Ord k) => [(k,t)] -> Tree k t build = 85
foldr (uncurry additem) Empty itemof :: (Ord k) => k -> Tree k t -> Maybe t itemof _ Empty = Nothing itemof newkey this@node { key = oldkey newkey < oldkey = itemof newkey $ left this newkey > oldkey = itemof newkey $ right this otherwise = Just $ item this contentsof :: Tree k t -> [(k,t)] contentsof Empty = [] contentsof this@node{ = contentsof (left this) ++ (key this,item this) : contentsof (right this) contentsof :: Tree k t -> [(k,t)] contentsof = let cont Empty acc = acc cont this@node{ acc = cont (left this) $ (key this,item this) : cont (right this) acc in flip cont [] 5.7 Liittyvät tyypit Aiemmissa class-määritelmissä on vain yksi muuttuja eli ne ovat määritelleet yhden tyypin ominaisuuksia. Haskellin class-määritelmässä sallitaan myös monta muuttujaa eli se voi määritellä myös monen tyypin välisiä suhteita: class R t u where... määrittelee 2-paikkaisen suhteen eli relaation R tyyppimuuttujien t ja u välille. Haskell on jo kauan sisältänyt tällaiset moniparametriset tyyppiluokat. Tavallisesti nämä tyypit kuten t ja u eivät kuitenkaan ole tasa-arvoisia keskenään, vaan yleensä yksi niistä on päätyyppi ja toiset siihen liittyviä aputyyppejä. Esimerkiksi Javassa voi määritellä luokan T sisällä siihen liittyviä apuluokkia U kuten iteraattoreita yms. Tällaiset tyyppiluokan sisällä olevat sen varsinaiseen jäseneen liittyvät (associated) tyypit ovat melko uusi ghc-laajennus Haskelliin. Kiselyov et al. (2010) esittelevät sitä. 86
Tähän tarkoitukseen oli jo aiemmin lainattu tietokantateoriasta funktionaaliset riippuvuudet (functional dependencies) mutta se lienevät nyt väistymässä näiden liittyvien tyyppien tieltä. Tämän piirteen mutkikas kehityshistoria johtunee siitä, että sen yhteentoiminta HMtyypinpäättelyn kanssa oli haastavaa. Sellainen tyyppiluokka joka vaatii tyyppimuuttujaltaan, että sillä on tällaisia liittyviä tyyppejä, määritellään seuraavasti: class (faktat) => Luokka muuttuja where type Nimi muuttuja :: laji data Uusi muuttuja :: laji... jossa saa siis olla montakin muuttujaa, mutta keskitymme vain yhteen. Tyyppi pääsee jäseneksi tällaiseen tyyppiluokkaan antamalla vaaditut type-, dataja metodimääritelmät instancensa sisällä. Liittyviä tyyppejä on kahdenlaisia, vastaten Haskellin kahdenlaisia tyypinmäärittelyjä: type vaatii, että tähän Luokkaan päästäkseen tyypin T pitää nimetä jokin aputyyppi U tämännimiseen rooliin mutta sen ei tarvitse olla juuri tätä varten luotu uusi tyyppi. Tätä nimettävää aputyyppiä U merkitään Nimi T. data vaatii, että tähän Luokkaan päästäkseen tyypin T pitääkin luoda itselleen uusi oma aputyyppi V. Tätä luotavaa aputyyppiä V merkitään Uusi T. Matemaattisesti sanotaan että (type ei ole mutta) data on injektiivinen (injective) koska sama V voi olla vain yhden T aputyyppinä Uusi T. Kun aputyyppi V on injektiivinen, niin Haskellin tyypinpäättely pystyy päättelemään sen käyttöyhteydestä sitä vastaavan (eli ainoan mahdollisen) päätyypin T. Luonnostellaan esimerkkinä tyyppiluokka Graph eli tyyppi g on (suunnatun) verkon jokin toteutustyyppi. Tällainen g ei selviä tehtävästään yksin, vaan se tarvitsee avukseen tyypit Vertex g solmuilleen. Määritellään se typenä, koska esimerkiksi Int on luonteva tyyppi solmuille monen eri tyypin g verkoissa, eli sen ei kannata olla injektiivinen. Edge g kaarilleen. Määritellään se datana, jotta saamme ilmaistua että tämä tyyppi on juuri tuon verkkotyypin g kaarityyppi eikä minkään muun eli injektiivisyyden. Label g kaartensa painoille, jos kyseessä on painotettu verkko. Määritelläänkin se vasta aliluokassa, koska on olemassa myös sellaisia verkkoja, joiden kaarilla ei ole painoja. 87
Nyt voidaan tyyppiluokan Graph metodeina vaatia myös metodeja sen aputyypeille Vertex g ja Edge g. Näiden aputyyppimetodien kuten source ja target tyypeissä mainitaan tyyppiluokan Graph muuttuja g vain epäsuorasti osana tyyppejä Vertex g ja Edge g mutta se riittää. Vaadittu ghc-laajennus on nimeltään tyyppiperheet eli TypeFamilies. Collatz on esimerkki verkosta, jossa ei ole kaaripainoja. Kaaripainottomille verkoille voidaan määritellä esimerkiksi leveyssuuntainen läpikäynti. {-# LANGUAGE TypeFamilies,FlexibleContexts #- import qualified Data.Map as Map import qualified Data.Set as Set class Graph g where type Vertex g :: * data Edge g :: * source :: Edge g -> Vertex g target :: Edge g -> Vertex g edgesfrom :: g -> Vertex g -> [Edge g] breadth1st :: (Graph g) => g -> Vertex g -> [[Edge g]] breadth1st = undefined class (Graph g) => LabeledGraph g where type Label g :: * label :: Edge g -> Label g dijkstra :: (LabeledGraph g,num (Label g)) => g -> Vertex g -> [(Vertex g,label g)] dijkstra = undefined data Collatz = Collatz instance Graph Collatz where type Vertex Collatz = Integer data Edge Collatz = CollatzEdge Integer Integer deriving (Show) source (CollatzEdge x _) = x target (CollatzEdge _ y) = y edgesfrom _ n = [CollatzEdge n $ if even n 88
] then n div 2 else 3*n + 1 data DFA s = DFA{ startdfa :: s, tabledfa :: Map.Map s (Bool,Map.Map Char s) exampledfa :: DFA Int exampledfa = DFA{ startdfa = 0, tabledfa = Map.fromList [(0,(False,Map.fromList [( a,1)])),(1,(true, Map.fromList [( b,0)])) ] instance (Ord s) => Graph (DFA s) where type Vertex (DFA s) = s data Edge (DFA s) = TransitionDFA s Char s deriving (Show) source (TransitionDFA x ) = x target (TransitionDFA z) = z edgesfrom dfa state = maybe [] (map (\ (char,state ) -> TransitionDFA state char state ). Map.assocs. snd) $ Map.lookup state $ tabledfa dfa instance (Ord s) => LabeledGraph (DFA s) where type Label (DFA s) = Char label (TransitionDFA _ y _) = y data NFA s = NFA{ startnfa :: s, tablenfa :: Map.Map s (Bool,Set.Set (Char,s)) examplenfa :: NFA String examplenfa = NFA{ startnfa = "start", tablenfa = Map.fromList [("start",(false,set.fromlist [( a,"other")])),("other",(true, Set.fromList [( b,"start")])) ] 89
instance (Ord s) => Graph (NFA s) where type Vertex (NFA s) = s data Edge (NFA s) = TransitionNFA s Char s deriving (Show) source (TransitionNFA x ) = x target (TransitionNFA z) = z edgesfrom nfa state = maybe [] (map (\ (char,state ) -> TransitionNFA state char state ). Set.elems. snd) $ Map.lookup state $ tablenfa nfa instance (Ord s) => LabeledGraph (NFA s) where type Label (NFA s) = Char label (TransitionNFA _ y _) = y Deterministinen (DFA) ja epädeterministinen (NFA) äärellinen automaatti ovat esimerkkejä eri tavoilla toteutetuista verkoista, joissa on kaaripainot tilasiirtymien merkit. Esimerkiksi Dijkstran algoritmi voidaan määritellä sellaisille verkoille, joiden kaarilla on painot, ja ne ovat Numeerista tyyppiä. Rajoitteiden perusmuoto on pelkkä Luokka muuttuja joka ei ole riittävän joustava liittyville tyypeille. ghc-laajennus FlexibleContexts sallii kirjoittaa tämän rajoitteen muodossa Num (Label g) joka ei ole tätä perusmuotoa. Se voitaisiin kirjoittaa melkein perusmuodossa Num h, h Label g jossa käytetään toista ghc-laajennusta joka tarkoittaa, että näiden kahden tyypin pitää olla yksi ja sama tyyppi. 5.7.1 Tyyppiperheet Liittyvien tyyppien määrittely on usein käytetty erikoistapaus yleisestä tyyppiperheen määrittelystä. Tyyppiperhe on tyyppien välinen funktio: Kun kirjoitamme tavallisen funktion, Konstruktori hahmossa tutkii funktion syötteenään saamaa argumenttiarvoa ja haarautuu sen perusteella lausekkeessa tuottaa funktion tulosarvon. Vastaavasti tyyppiperheessä jokainen Tyyppi on Konstruktorin roolissa: Tyyppiperhe haarautuu eri tapauksiin syötteenään saamansa tyypin perusteella 90
tuottaa tuloksenaan kyseistä haaraa vastaavan tyypin. Liittyvät tyypit ovat tällaisen tyyppiperheen määrittelyä tyyppiluokan sisällä. Tyyppiperheitä voi määritellä myös niiden ulkopuolella lähdekooditiedoston päätasolla käyttämällä tarkennetta family. Tyyppiperheen oma laji (eli sen oma tyyppi ) esitellään muodossa jossa type family Nimi muuttuja :: laji data family Nimi muuttuja :: laji Nimi on sille annettu nimi. muuttuja on sen formaali parametri. Niitä saa olla useita, aivan samoin kuin tavallisella funktiolla saa olla useita parametreja, mutta keskitytään yksinkertaisuuden vuoksi vain yhteen. laji on sen tuloksen laji eli tyyppi. Toisin kuin tavallinen funktio, tyyppiperhe pitää esitellä näin: Haskell ei päättele tyyppiperheillä niin perusteellisesti kuin tavallisilla funktioilla, vaan tyytyy tarkistamaan että niiden määrittely ja käyttö vastaa tätä esittelyä. data vaatii, että Nimi on injektiivinen, kun taas type ei vaadi sitä. TämänNimisen tyyppiperheen jokainen haara kirjoitetaan muodossa type instance Nimi Sisään = Ulos data instance Nimi Sisään = Ulos jossa type tai data on sama kuin sen esittelyssä. Tällainen haara toimii Tyyppitasolla samoin kuin tavallisenkin funktion haara: Jos nykyinen Tyyppi sopii Sisään- tyyppihahmoon, niin korvaa se Ulos- tyyppilausekkeella β-reduktion periaatteen mukaan. Näin jatketaan, kunnes korvattavaa ei enää ole. Sisään ja Ulos saavat sisältää tyyppiparametreja ja -muuttujia. Tyyppiperheen määritelmä onkin avoin (toisin kuin tavallisen funktion): Tällaisia haaroja voi lisätä kaikkialla lähdekoodissa, niiden ei siis tarvitse olla peräkkäin eikä edes samassa tiedostossa. Tämä sallii sen, että uutta tyyppiä määriteltäessä määritellään samalla sille oma tyyppiperhehaaransa. Yksinkertaisena esimerkkinä määritellään kokonaislukujen tarkka kertolasku: Kun kaksi b-bittistä kokonaislukua kerrotaan keskenään, niin tulos on yksi 2 b- bittinen kokonaisluku,......mutta (*) :: (Num a) => a -> a -> a 91
leikkaakin tuloksen takaisin samaan b-bittiseen tyyppiin a. Määritellään siis funktio safemult :: (Integral a,integral (Twice a)) => a -> a -> Twice a jossa tyyppiperhe Twice antaa b-bittiselle kokonaislukutyypille a sen 2 b- bittisen vastintyypin. Nyt joudumme käyttämään type-perhettä, joka ei siis ole injektiivinen, koska tarvitsemme Integeria sen tulokseksi useammalle kuin yhdelle sen syötetyypille a. {-# LANGUAGE TypeFamilies,FlexibleContexts #- import Data.Int import Data.Word type family Twice t :: * type instance Twice Int8 = Int16 type instance Twice Int16 = Int32 type instance Twice Int32 = Int64 type instance Twice Int64 = Integer type instance Twice Integer = Integer type instance Twice Word8 = Word16 type instance Twice Word16 = Word32 type instance Twice Word32 = Word64 type instance Twice Word64 = Integer safemult :: (Integral a,integral (Twice a)) => a -> a -> Twice a safemult x y = (fromintegral x) * (fromintegral y) Haskell esittää tyyppiperheillä tyyppien luokilla tyyppien ja funktioiden välisiä suhteita. Ne mahdollistavat tyyppitasolla (type-level) ohjelmoinnin, jossa HM-tyypinpäättely suorittaa käännösaikaista laskentaa tyypeillä. Esimerkkinä tästä niiden yhteispelistä asetetaan seuraava ohjelmointitavoite: Annetaan funktio f :: t -> u. intotable f luo siitä laiskan tietorakenteen taulu joka sisältää kaikki sen arvot siten, että fromtable taulu x antaa vastauksenaan arvon f x jonka se on katsonut tästä taulusta. 92
Arvo f x lasketaan tähän tauluun vain silloin kun sitä ensimmäisen kerran kysytään. Seuraavilla kerroilla se katsotaan tästä taulusta laskematta sitä uudelleen. Toisin sanoen, kehitetään tietorakenne taulu joka muistintaa annetun funktion f. Tämän taulun oma tyyppi riippuu funktion f parametrin tyypistä t. Siten tämä riippuvuus ilmaistaan tyyppiin t liittyvänä tyyppinä eli tyyppiperheenä Table t u jossa u on funktion f tulostyyppi. Tämän taulun omasta tyypistä puolestaan riippuu se, miten sen intotable ja fromtable toteutetaan. Siten tämä riippuvuus ilmaistaan metodeina siinä tyyppiluokassa Memo t jossa myös Table t u määritellään. Lähdemme liikkeelle tietorakenteesta, josta saadaan Table t u sellaisille tyypeille t joita voi käsitellä bitti bitiltä. Erityisesti Table Integer u tarvitsee tällaisen tyypin, jonka arvot ovat laiskasti äärettömiä. Käytämme siihen ääretöntä Braunin puuta (Braun tree). Sen solmut on numeroitu 1, 2, 3,... siten, että juuresta pääsee solmuun s lukemalla sen binääriesitys s = 1d k...d 2 d 1 d 0 pienimmästä toiseksi suurimpaan bittiin eli d 0, d 1, d 2,...,d k ja valitsemalla aina vasen/oikea alipuu seuraavan bitin b i perusteella. Kun Integral-tyyppi on etumerkitön kuten Word niin sen taulun Table Word u voi toteuttaa yhdellä puulla, jonka solmuissa on arvot f 0,f 1,f 2,... etumerkillinen kuten Int niin sen taulun Table Int u voi toteuttaa kahdella puulla, joista yhdessä on arvot f 1,f 2,f 3,... ja toisessa arvot f 0,f -1,f -2,... Kuvassa 3 on tämän puun 3 ylintä tasoa. Puu jatkuu niistä alaspäin laiskasti periaatteessa äärettömyyteen. {-# LANGUAGE TypeFamilies #- import Data.Word import Data.Int import Debug.Trace -- Tämä Braunin puu on ääretön tietorakenne arvoille -- f 1,f 2,f 3,... data Braun t u = Braun { value :: u, zero :: Braun t u, one :: Braun t u 93
1 2 3 4 6 5 7 8 12 10 14 9 13 11 15 Kuva 3: Äärettömän Braunin puun 3 ylintä tasoa. growbraun :: (Integral t) => (t -> u) -> Braun t u growbraun f = let grow present past = let current = present + past future = 2 * present in Braun{ value = f current, zero = grow future past, one = grow future current in grow 1 0 lookbraun :: (Integral t) => Braun t u -> (t -> u) lookbraun b 1 = value b lookbraun b n = lookbraun ((if even n then zero else one) b) (n div 2) -- Tämä Braunin puupari on ääretön tietorakenne arvoille -- positiiviset eli f 1, f 2, f 3,... ja -- negatiiviset eli f 0, f -1, f -2,... data Brauns t u = Brauns{ positive :: Braun t u, negative :: Braun t u growbrauns :: (Integral t) => (t -> u) -> Brauns t u growbrauns f = Brauns{ positive = growbraun f, negative = growbraun $ f. (1 -) lookbrauns :: (Integral t) => Brauns t u -> (t -> u) 94
lookbrauns brauns n = if n>0 then lookbraun (positive brauns) n else lookbraun (negative brauns) $ 1 - n -- Funktion f :: t -> u taulukointi vaatii taulukkotyypin sen -- parametrityypille t. class Memo t where data Table t :: * -> * intotable :: (t -> u) -> Table t u fromtable :: Table t u -> (t -> u) instance Memo Word where newtype Table Word u = TableWord (Braun Word u) intotable = TableWord. growbraun. (. pred) fromtable (TableWord table) = lookbraun table. succ instance Memo Word8 where newtype Table Word8 u = TableWord8 (Braun Word8 u) intotable = TableWord8. growbraun. (. pred) fromtable (TableWord8 table) = lookbraun table. succ instance Memo Int where newtype Table Int u = TableInt (Brauns Int u) intotable = TableInt. growbrauns fromtable (TableInt table) = lookbrauns table instance Memo Int8 where newtype Table Int8 u = TableInt8 (Brauns Int8 u) intotable = TableInt8. growbrauns fromtable (TableInt8 table) = lookbrauns table instance (Memo t1,memo t2) => Memo (t1,t2) where newtype Table (t1,t2) u = TablePair (Table t1 (Table t2 u)) intotable f = 95
TablePair $ intotable $ \ x -> intotable (\ y -> f (x,y)) fromtable (TablePair table) (x,y) = fromtable (fromtable table x) y memoized :: (Memo t) => ((t -> u) -> (t -> u)) -> (t -> u) memoized f = let from = fromtable memo memo = intotable $ f from in from esimerkki :: Int -> Int esimerkki = memoized $ \ fact n -> trace ("lasken " ++ show n) $ if n>1 then n * fact (n-1) else 1 data vaatii, että Table t u on injektiivinen. newtype takaa sen, sehän on tyypinpäättelyssä kuin data. Se estää hyödyntämästä vaikkapa taulujen Table Word u ja Table Word8 u keskinäistä samankaltaisuutta. Memo (t1,t2) on esimerkki tyyppikonstruktorin käsittelystä. Vaaditaan, että Memo t1 ja Memo t2 eli että taulutyypit Table t1 u ja Table t2 v sekä niiden metodit intotable ja fromtable voidaan muodostaa. Silloin Memo (t1,t2) ja sen metodit intotable ja fromtable voidaan muodostaa t1-tauluna joka sisältää t2-tauluja. Nyt voimme automatisoida muistintamisen: Olkoon g :: t -> u g x = e lähdekoodi Haskell-funktiolle, josta haluamme sellaisen muistinnetun version, joka laskeen jokaisen rekursiokutsunsa vain yhden kerran. Olkoon sen parametrin tyyppi t tyyppiluokan Memo jäsen. Silloin h :: t -> u h = memoized $ \ g x -> e on sen muistinnettu versio. 1 Ensin g muuttuu funktion nimestä sen parametriksi. 96
2 Sitten g korvautuu funktiolla from joka kysyy tuloksensa automaattisesti luodusta taulusta memo. Idea on sama kuin kuin kiintopisteoperaattorissa (9) fix mutta tämä operaattori käyttääkin taulua. Muistinnuksen toiminnan voi varmistaa kirjastofunktiolla Debug.Trace.trace :: String -> a -> a joka tulostaa sivuvaikutuksenaan ensimmäisen parametrinsa silloin kun sen toisen parametrin sievennys alkaa. Se on siis debug-seq. esimerkkinä on muistinnettu kertomafunktio (8) fact. Tällainen muistinnus käyttää laiskuutta 2 tavalla: taulu kasvaa laiskasti sitä mukaa kun sen sisältöjä kysellään eri t-arvoilla. Nämä taulun sisällöt viittaavat takaisin tauluun itseensä. 5.7.2 Rajoitelaji Orchard and Schrijvers (2010) havaitsivat, että tyyppiperhelähestymistapa sopii myös tyyppien rajoitteille, eli implikaationuolen => vasemmalla puolella. Nykyinen ghc-laajennus on hieman toinen kuin heidän ehdotuksensa. Ideana on lisätä peruslajin * eli tyyppi rinnalle uusi peruslaji Constraint eli rajoite. Silloin voidaan kirjoittaa tyyppiperheitä joiden lajina on Constraint arvona on implikaationuolen => vasemmalle puolelle kelpaava faktamonikko faktoissa on näiden tyyppiperheiden kutsuja. Nämäkin kutsut korvataan arvoillaan, kuten tyyppiperheiden kutsut. Näin esimerkiksi tyyppiluokkaan jäseneksi liittyvä tyyppi voi lisätä sen metodien rajoitteisiin omia lisärajoitteitaan. Tämä ratkaisisi esimerkiksi sen ongelman, että matemaattisesti käsite joukko on Functor, koska operaatio on luonteva tulkinta operaatiolle fmap g S = {g(x): x S sovella funktiota g joukon S jokaiseen alkioon x ja kokoa nämä tulokset g(x) uudeksi joukoksi eli sovella funktiota g joukon S sisältöön joka on Functorin perusidea. joukkojen Haskell-kirjastototeutus Data.Set.Set ei voikaan kuulua luokkaan Functor 97
koska Setin toteutus olettaa tehokkuussyistä alkiotyypiltään Ordin, joten sovellettavan funktion g tulostyypinkin pitää kuulua Ordiin johon Functorin metodin fmap tyypin määritelmässä ei oltukaan varauduttu. Jos Functoria standardoitaessa olisi jo tunnettu nämä rajoitelajit, niin sen määritelmä olisi voitu lausua muodossa class Functor f where type Inv f e :: Constraint type Inv f e = () fmap :: (Inv f a,inv f b) => (a -> b) -> f a -> f b johon liittyy rajoite nimeltään Inv jonka 1 ensimmäinen tyyppiparametri on tämän luokan muuttuja f 2 toinen tyyppiparametri e edustaa sovellettavan funktion g tulostyyppiä 3 tuloksen laji onkin Constraint eikä *. Lisäksi Inville on annettu oletustoteutus () eli ei rajoitteita. Siten fmapin rajoitteen oletuksena on ((),()) => eli () => eli ei rajoitteita nykyiset instance Functor -määrittelyt säilyisivät ennallaan. 98