Luku 4 Tietorakenteet funktio-ohjelmoinnissa Koska funktio-ohjelmoinnissa ei käytetä tuhoavaa päivitystä (sijoituslausetta ja sen johdannaisia), eivät läheskään kaikki valtavirtaohjelmoinnista tutut tietorakenteet toimi kuten voisi kuvitella. Chris Okasaki on kirjassaan Purely Functional Data Structures (Cambridge University Press, 1998) käsitellyt tätä ongelmaa, esittänyt tutuille tietorakenteille funktio-ohjelmointiin sopivia määritelmiä sekä uusia rakenteita, jotka ovat mahdollisia vain funktio-ohjelmoinnissa. Tämän luvun tarkoituksena on esitellä joitakin Okasakin tuloksia. 4.1 Äärelliset kuvaukset Sitä, mitä valtavirtaohjelmoijat sanovat assosiaatiotauluksi, funktio-ohjelmoijat kutsuvat äärelliseksi kuvaukseksi (finite map). Kyse on siis tietorakenteesta, johon voidaan tallettaa tietoa niin, että se on avaimella löydettävissä suhteellisen nopeasti. Valtavirtaohjelmoijan suosimia hajautustauluja ei funktioohjelmoinnissa voi käyttää; sen sijasta suosittuja ovat erilaiset hakupuuratkaisut. Äärellisen kuvauksen rajapinta sisältää funktiot tyhjän kuvauksen luomiseen, alkion etsimiseen avaimella ja avain-alkioparin lisäämiseen. Tämän voi toteut- 45
46 LUKU 4. TIETORAKENTEET FUNKTIO-OHJELMOINNISSA taa esimerkiksi binäärisellä hakupuulla: data BinaryTree key elt = BTNode (BinaryTree key elt) key elt (BinaryTree key elt) BTEmpty emptytree :: Ord key BinaryTree key elt emptytree = BTEmpty lookupintree :: Ord key BinaryTree key elt key Maybe elt lookupintree BTEmpty _ = Nothing lookupintree (BTNode lst key elt rst) key key < key = lookupintree lst key key == key = Just elt key > key = lookupintree rst key (Lisäys jätetään harjoitustehtäväksi.) Jos aineisto lisätään puuhun satunnaisessa järjestyksessä, on keskimääräinen haku- ja lisäysaika logaritminen (Θ(log n)). Pahimmassa tapauksessa kuitenkin tällainen naiivi binääripuu surkastuu linkitetyksi listaksi, esimerkiksi jos aineisto lisätään avaimen (<)-järjestyksessä. Parempi puurakenne saadaan, jos tietorakenteen operaatiot pitävät puun tasapainossa. Yksi tällainen tietorakenne on punamustat puut, joissa joka epätyhjällä solmulla on avain alkio-parin lisäksi väri, punainen tai musta. data Color = R B deriving Show data RBTree key elt = E N Color (RBTree key elt) (key, elt) (RBTree key elt) deriving Show Tässä määrittelyssä käytetään lyhyitä koostimien nimiä, koska se helpottaa operaatioiden määritelmien luettavuutta. Punamustalta puulta vaaditaan kaksi invarianttia (ominaisuutta, jotka pätevät rakenteen kaikilta ilmentymiltä): 1. Punaisella solmulla ei ole punaista lapsisolmua. 2. Jokainen polku juuresta tyhjään solmuun sisältää saman määrän mustia solmuja.
4.1. ÄÄRELLISET KUVAUKSET 47 Näiden invarianttien seurauksena puun syvin lehtisolmu on enintään kaksi kertaa niin syvällä kuin puun matalin lehtisolmu. Näin ollen n-solmuisen punamustan puun maksimisyvyys on 2 log(n + 1) eli Θ(log n). Tyhjän puun luontioperaatio emptyrbtree :: Ord key RBTree key elt ja puusta haku lookupinrbtree :: Ord key RBTree key elt key Maybe elt ovat samanlaisia kuin naiivissa binäärihakupuussa; hakuoperaatio jättää värin huomiotta. Koska puun maksimisyvyys on Θ(log n), on puusta haun asymptoottinen aikavaativuus logaritminen. Puuhun lisääminen on hieman mielenkiintoisempi tapaus: addtorbtree :: Ord key key elt RBTree key elt RBTree key elt addtorbtree key elt tree = N B left payload right where N _ left payload right = ins tree ins E = N R E (key, elt) E ins (N color l pl@(k, e) r) key < k = balance color (ins l)plr key > k = balance color rpl (ins r) otherwise = N color l (key, elt) r Lisäyksen tehtävänä on huolehtia siitä, että invariantit säilyvät. Uusi lehtisolmu väritetään punaiseksi, joten toinen invariantti (mustien määrä poluilla) pätee. Koska tämä saattaisi rikkoa ensimmäisen invariantin, täytyy sen ylisolmuja muokata niin, että ykkösinvariantti säilyy. Tämä tapahtuu siten, että tapauksissa key < k ja key > k koostin N korvataan funktiolla balance, jonka tehtävänä on korjata puu sellaiseksi, että ensimmäinenkin invariantti pätee. Huomautus 10 Hahmo x@p, missä x on muuttuja ja P on hahmo, tarkoittaa seuraavaa: Sovita P aivan kuin x@:ää ei olisikaan. Jos se epäonnistuu, x@p:n sovitus epäonnistuu yhtä lailla. Jos se onnistuu, sido P:n muuttujat tavalliseen tapaan ja lisäksi sido x siihen koko arvoon, johon P sopi. Esimerkiksi edellisen määrittelyn hahmo pl@(k, e) sopii mihin tahansa pariin; k tulee sidotuksi parin ensimmäiseen alkioon, e tulee sidotuksi parin jälkimmäiseen alkioon ja pl tulee sidotuksi koko pariin, joten pätee yhtälö pl = (k, e). Funktio balance tutkii puita, joiden juuri on musta ja joilla on kaksi (syvyyssuunnassa) perättäistä punaista alisolmua. Tällaisia epänormaaleita tilanteita on aina vain yhdellä polulla, koska tämä operaatio tehdään joka lisäyksen
48 LUKU 4. TIETORAKENTEET FUNKTIO-OHJELMOINNISSA jälkeen, joten vain viimeisin lisäys voi jättää puun tällä tavalla rikkinäiseen tilaan. Näissä tapauksessa se pyöräyttää puun sellaiseen muotoon, jossa syvyyssuunnassa keskellä ollut punainen solmu nostetaan juureksi ja sen ylä- ja alasolmu väritetään mustiksi ja sen kummankin alipuun juureksi. Muunlaiset puut se jättää rauhaan. balance :: Color RBTree key elt (key, elt) RBTree key elt RBTree key elt balance B (N R (N R a x b) y c) z d = N R (N B a x b) y (N B c z d) balance B (N R a x (N R b y c)) z d = N R (N B a x b) y (N B c z d) balance B a x (N R (N R b y c) z d) = N R (N B a x b) y (N B c z d) balance B a x (N R b y (N R c z d)) = N R (N B a x b) y (N B c z d) balance c a x b = N c a x b Huomaa, kuinka viimeistä tapausta lukuunottamatta tapaukset eroavat toisistaan vain parametriensa hahmoilla: =-merkin jäljessä tuleva määritelmä on kaikissa neljässä muussa tapauksessa sama! Kukin tapaus kannattaa piirtää kuvaksi, jotta balancen ymmärtäisi. Myös puille on mahdollista kehittää fold. Seuraava määritelmä käy listan läpi jälkijärjestyksessä: foldrbtree :: Ord key ((key, elt) β β) β RBTree key elt β foldrbtree _ e E = e foldrbtree f e (N _ lst pl rst) = foldrbtree f (f pl (foldrbtree f e rst)) lst Tämän avulla voidaan määritellä esimerkiksi funktio, joka muuttaa puun listaksi: convertrbtreetolist :: Ord key RBTree key elt [(key, elt)] convertrbtreetolist = foldrbtree (:) [] Myös map on mahdollinen: maprbtree :: Ord key (elt elt) RBTree key elt RBTree key elt maprbtree _ E = E maprbtree f (N c l (k, e) r) = N c (maprbtree f l)(k, f e) (maprbtree f r) 4.2 Abstraktit tietotyypit ja modulit Käytännön ohjelmoijaa harvemmin kiinnostaa se, miten jokin tietorakenne toimii pellin alla. Enemmän häntä kiinnostaa, mitä kaikkea sillä voi tehdä ja kuinka tehokkaasti. Abstrakti tietotyyppi (abstract datatype, ADT) on yleisnimitys tälle käytännön ohjelmoijan näkemykselle tietorakenteista.
4.2. ABSTRAKTIT TIETOTYYPIT JA MODULIT 49 Abstraktin tietotyypin arvot ovat mustia laatikoita. Tyypin yhteydessä määritellään joukko operaatioita, joiden välisestä yhteydestä voidaan sanoa jotain, sekä siitä, kuinka tehokas kukin operaatio on. Abstrakti tietotyyppi piilottaa tietotyypin rakenteen sekä operaatioiden toteutuksen sen käyttäjältä. Haskellissa abstrakti tietotyyppi saadaan aikaiseksi kirjoittamalla siitä moduli, joka julkistaa vain tyyppikoostimen ja joukon funktioita, mutta jättää piiloon tyypin (tieto)koostimet. Tämä tapahtuu kirjoittamalla moduli tiedostoon, jonka nimi alkaa isolla kirjaimella ja päättyy päätteeseen.hs. Tiedoston alkuun kirjoitetaan module Nimi (julkistuslista) where. Nimi on, kuten ennenkin on tullut mainittua, tiedoston nimi ilman.hs-päätettä. Suluissa oleva julkistuslista on luettelo pilkuilla toisistaan erotetuista koostin- ja vakionnimistä, jotka ko. moduli julkistaa. Kaikki muut nimet ovat modulin sisäisiä. Esimerkki 25 Edellä esitetty punamusta puu voidaan kirjoittaa seuraavaksi moduliksi: module RedBlackTree (RBTree, emptyrbtree, lookupinrbtree, addtorbtree, maprbtree, foldrbtree, convertrbtreetolist) where data Color = R B deriving (Show) data RBTree key elt = E N Color (RBTree key elt) (key, elt) (RBTree key elt) deriving (Show) emptyrbtree :: Ord key => RBTree key elt emptyrbtree = E lookupinrbtree :: Ord key => RBTree key elt -> key -> Maybe elt lookupinrbtree E _ = Nothing lookupinrbtree (N _ lst (key, elt) rst) key key < key = lookupinrbtree lst key key == key = Just elt key > key = lookupinrbtree rst key addtorbtree :: Ord key => key -> elt -> RBTree key elt -> RBTree key elt addtorbtree key elt tree = N B left payload right where N _ left payload right = ins tree ins E = N R E (key, elt) E ins (N color l pl@(k, e) r) key < k = balance color (ins l) pl r
50 LUKU 4. TIETORAKENTEET FUNKTIO-OHJELMOINNISSA key > k = balance color r pl (ins r) otherwise = N color l (key, elt) r balance :: Color -> RBTree key elt -> (key, elt) -> RBTree key elt -> RBTree key elt balance B (N R (N R a x b) y c) z d = N R (N B a x b) y (N B c z d) balance B (N R a x (N R b y c)) z d = N R (N B a x b) y (N B c z d) balance B a x (N R (N R b y c) z d) = N R (N B a x b) y (N B c z d) balance B a x (N R b y (N R c z d)) = N R (N B a x b) y (N B c z d) balance c a x b = N c a x b maprbtree :: Ord key => (elt -> elt) -> RBTree key elt -> RBTree key elt maprbtree _ E = E maprbtree f (N c l (k, e) r) = N c (maprbtree f l) (k, f e) (maprbtree f r) foldrbtree :: Ord key => ((key, elt) -> b -> b) -> b -> RBTree key elt -> b foldrbtree _ e E = e foldrbtree f e (N _ lst pl rst) = foldrbtree f (f pl (foldrbtree f e rst)) lst convertrbtreetolist :: Ord key => RBTree key elt -> [(key, elt)] convertrbtreetolist = foldrbtree (:) [] testtree = addtorbtree "auto" 4 $ addtorbtree "liikennemerkki" 14 $ addtorbtree "televisio" 9 $ addtorbtree "yö" 2 $ addtorbtree "ääliö" 5 $ addtorbtree "öljy" 4 $ emptyrbtree Huomautus 11 Operaattori ($) kuuluu varuskirjastoon ja määritellään näin: ($) :: (α β) α β f $ x = f x Sen hyödyllisyys piilee siinä, että sillä on kaikista operaattoreista alhaisin predesenssi ja se assosioi oikealle: näin f $ g $ h x on sama kuin f (g (h x)). Sillä voi siten vähentää sulkeiden määrää lausekkeissa. Modulia päästään käyttämään toisessa modulissa kirjoittamalla tiedoston alkuun module where -rivin jälkeen yksi tai useampi import-rivi, joka on jo-
4.2. ABSTRAKTIT TIETOTYYPIT JA MODULIT 51 ko muotoa import ToinenModuli jolloin tämän modulin nimiavaruuteen lisätään kaikki ToinenModuli-modulin julkistamat nimet, tai muotoa import ToinenModuli (nimi, nomen) jolloin tämän modulin nimiavaruuteen lisätään ToinenModuli-modulin julkistamista nimistä nimet nimi ja nomen, tai muotoa import ToinenModuli hiding (nimi, nomen) jolloin tämän modulin nimiavaruuteen lisätään ToinenModuli-modulin julkistamista nimistä kaikki muut paitsi nimet nimi ja nomen. Jokaisessa näistä muodoista voidaan lisäksi kirjoittaa import-avainsanan jälkeen avainsana qualified, jolloin ne ToinenModuli-modulin nimet, jotka ylipäätään lisätään tämän modulin nimiavaruuteen, lisätään muodossa ToinenModuli.nimi. Huomautus 12 Jos modulissa ei ole eksplisiittistä import-riviä varuskirjastolle Prelude, käsitellään tämä kuin siinä olisi implisiittinen import Prelude. Esimerkki 26 Tyypillinen abstrakti tietotyyppi on esimerkiksi Set. Se voidaan toteuttaa punamustilla puilla seuraavasti: module Set (emptyset, inset, listtoset, settolist, setunion, setcomprehension, setintersection, subset) where import RedBlackTree newtype Set a = MkSet (RBTree a ()) deriving Show emptyset :: Ord a => Set a emptyset = MkSet $ emptyrbtree unitset :: Ord a => a -> Set a
52 LUKU 4. TIETORAKENTEET FUNKTIO-OHJELMOINNISSA unitset a = MkSet $ addtorbtree a () emptyrbtree inset :: Ord a => a -> Set a -> Bool x inset (MkSet s) = case lookupinrbtree s x of Just () -> True Nothing -> False listtoset :: Ord a => [a] -> Set a listtoset l = MkSet $ foldr (uncurry addtorbtree) emptyrbtree $ zip l $ repeat () settolist :: Ord a => Set a -> [a] settolist (MkSet s) = map fst $ convertrbtreetolist s setunion :: Ord a => Set a -> Set a -> Set a (MkSet s) setunion (MkSet t) = MkSet $ foldrbtree (uncurry addtorbtree) s t setcomprehension :: Ord a => (a -> Bool) -> Set a -> Set a setcomprehension f = listtoset. filter f. settolist setintersection :: Ord a => Set a -> Set a -> Set a setintersection s t = setcomprehension ( inset s) t subset :: Ord a => Set a -> Set a -> Bool s subset t = and $ map ( inset t) $ settolist s Tehtävä 1 Määrittele FiniteMap.hs-moduli RedBlackTree.hs-modulin pohjalta. Määrittele sitten Set2.hs-moduli FiniteMap.hs-modulin pohjalta.