Z-skeemojen animointi Haskellilla Antti-Juhani Kaijanaho 6. toukokuuta 2001 Sisältö 1 Johdanto 2 2 Joitakin funktionaalisen ohjelmoinnin käsitteitä 3 2.1 Imperatiivinen funktionaalinen ohjelmointi 3 2.2 Hindley-Milner-tyyppijärjestelmä 3 3 Joukot ja logiikka 4 4 Funktiot 5 5 Skeemat 5 5.1 Tilaskeemat 5 5.2 Operaatioskeemat 6 5.3 Käyttöliittymä 7 6 Pääohjelma 8 7 Jatkoa 9 Formaalit menetelmät -kurssin seminaarityö 1
1 Johdanto Formaaleilla menetelmillä pyritään luomaan täsmällinen ja yksikäsitteinen ohjelman toiminnallisuuden määritelmä (specifications). Kun ohjelmasta on sitten tehty toteutus, on suhteellisen helppo varmistaa, että ohjelma täyttää määritelmän. Mutta kuinka voidaan olla varmoja, että määritelmä vastaa ohjelmalle asetettuja vaatimuksia (requirements), kuinka se voidaan validoida? Spesifikaation validointi voidaan tehdä esimerkiksi johtamalla siitä yleisiä lauseita, joita verrataan vaatimuksiin. Jos vaatimukset ja lauseet eivät käy yksiin, spesifikaatio on rikki. On esitetty, että tämän tulisi olla ainoa validoinnin keino [5]. Toinen tapa on animoida spesifikaatio. Tällöin spesifikaatio on joko sellaisenaan tai suhteellisen pienin muutoksin ajettavissa (executable) joko tulkin tai kääntäjän avulla. Tätä lähestymistapaa on puolustettu mm. artikkelissa [3]. Animoinnin tuottama ajattava ohjelma on vain harvoin ohjelman käypä implementaatio. Animointi on yleensä varsin suoraviivainen tai vähintäänkin mekaaninen prosessi, jonka on tarkoitus olla mahdollisimman nopea. Sen sijaan tuotettu ohjelma on usein hyvin tehoton jopa käyttökelvottomuuteen asti. Animoinnissa epätriviaalit ongelmat lakaistaan yleensä maton alle, jolloin animoinnin tulos saattaa sisältää jopa eksponentiaalisia algoritmeja. Tämä ei haittaa, koska animoinnin tulosta ei olekaan tarkoitus käyttää oikeasti, vaan sillä tarkastetaan, että spesifikaation mukainen ohjelma toimii niinkuin vaatimuksissa on määrätty. 1 Z-notaation [15] animointia on tutkittu varsin paljon. Yleensä esitetyt animointimenetelmät muuntavat spesifikaation mekaanisesti jollakin kielellä kirjoitetuksi ohjelmaksi, joka voidaan sitten ajaa tulkin avulla tai kääntää ajettavaksi, konekieliseksi ohjelmaksi. Esimerkiksi Mirandaa [1], Haskellia [4, 13, 14] ja LSL:ää [3] on käytetty Z-notaation animointiin. Tarkastelen tässä kirjoitelmassa Z:lla [15] kirjoitettujen spesifikaatioiden animointia Haskellilla [10, 11]. Oletan lukijan hallitsevan sekä Z:aa että Haskellia jonkin verran. Haskellia taitamattomat voivat tutustua seuraaviin materiaaleihin: [2, 6, 7, 12]. Tässä kirjoitelmassa käytetään esimerkkinä BlockHandler-spesifikaatiota, joka on kokonaisuudessaan luentomonisteessa [8, luku 12]. Sen animaation listaus on liitteenä. Pääosan kirjoitelmasta muodostaa sellaisen epämuodollisen ajattelutavan esittely, jota soveltamalla Z-notaatio kääntyy suhteellisen helposti Haskelliksi. Metodi on suurimmaksi osaksi täysin mekaaninen, ja uskon, että lisätutkimuksella se voidaan saattaa täysin mekaaniseksi. Metodi on kokonaisuudessaan itse muodostamani, mutta osa sen ideoista on peräsin Abdallahin et al. [1] ja Goodmanin [4] metodeista. Kumpaakaan metodia ei olisi voinut käyttää sellaisenaan: Abdallahin et al. metodin kohdekieli on Miranda, joka joissakin kohdin eroaa Haskellista, ja Goodmanin metodista, jonka kohdekieli oli varsin ratkaisevasti vanhentunut Haskellin versio, tiedossani oli vain yleinen esittely. Olen koonnut Haskell-modulin Z, joka sisältää joitakin usein tarvittavia määrittelyjä. Sen listaus on liitteenä. 1. Ehkä se on jopa hyvä asia. Onhan tunnettua, että liian hyvältä näyttävän prototyypin näyttäminen tilaajalle saattaa katkaista rahahanat, kun tilaaja kuvittelee ohjelman olevan valmis. 2
2 Joitakin funktionaalisen ohjelmoinnin käsitteitä Funktionaalisen ohjelmoinnin perusidea on ajatella ohjelmaa funktiona eikä käskynä: sen sijaan, että ohjelma muuttaa maailman tilaa, se ottaa maailman tilan parametrinaan ja palauttaa uuden maailman tilan. Tämän vuoksi funktionaalinen ohjelmointi soveltuu varsin hyvin Z-skeemojen animointiin, sillä niissä myös luodaan uusi tila eikä muuteta vanhaa. 2.1 Imperatiivinen funktionaalinen ohjelmointi Haskell noudattaa uudempaa imperatiivisen funktionaalisen ohjelmoinnin paradigmaa suhteessaan ulkomaailman kanssa operointiin. Tässä mallissa puhtaasti funktonaalinen ohjelma tuottaa tuloksenaan arvon, joka esittää potentiaalista tekoa. Tämän teon kuvaama teko tehdään, kun ohjelma päättyy. Näin imperatiivisesti funktionaalinen ohjelma ikäänkuin päättyy ennen kuin se on alkanutkaan. Imperatiivisen funktionaalisen ohjelmoinnin keskeinen käsite on monadi. Monadikäsite on peräisin maailman hyödyttömimmäksi kutsutusta matematiikan osa-alueesta eli kategoriateoriasta. Matemaattisesti ottaen monadi on mikä tahansa joukko Y X, jolle on määritelty operaatiot return : X Y X ja >>= : Y X (X Y X ) Y X siten, että tietyt lait [16, luku 2.5] pätevät. Ohjelmoijan kannalta monadi on geneerisesti polymorfinen tyyppi, jonka arvot esittävät potentiaalisia tekoja. Monadioperaatio return ruiskuttaa puhtaan arvon monadiin, jolloin tuloksena on potentiaalinen teko, joka aktualisoituessaan ei tee mitään muuta kuin tuottaa tuloksekseen kyseisen arvon. Operaatio >>= taas peräkkäistää kaksi tekoa: se ottaa kaksi parametria (potentiaalisen teon ja funktion, joka tuottaa potentiaalisen teon) ja tuottaa potentiaalisen teon, joka aktualisoituessaan ensin aktualisoi ensimmäisen argumentin, antaa sitten tuon teon tuloksen toiselle argumentille argumenttina ja aktualisoi lopuksi tuon argumentin palauttaman potentiaalisen teon. Haskellissa on kaksi hyödyllistä ja käytännönläheistä esimerkkiä monadista. Toinen on IOmonadi ja toinen Maybe-monadi. IO-monadi on Haskellin tapa olla yhteydessä ulkomaailmaan. Siihen kuuluu monadioperaatioiden lisäksi primitiivifunktioita kuten getline. Maybe-monadi mallittaa epävarmuutta: sen arvot ovat joko Just jotakin tai Nothing. 2.2 Hindley-Milner-tyyppijärjestelmä Haskellin edustamassa funktionaalisen ohjelmoinnin koulukunnassa käytetään Hindley-Milner-tyyppisysteemin [9] muunnelmia. Hindley-Milner-tyypitystä käyttävät kielet ovat erittäin vahvasti tyypitettyjä 2. Haskellin käyttämässä Hindley-Milner-tyypityksessä on olemassa mm. seuraavat tavat muodostaa tyyppejä: tyyppivakiot (esimerkiksi Bool), algebralliset tyypit (olennaisesti karteesisten tulojen erillisiä yhdisteitä) ja funktiotyypit. Lisäksi Haskell sisältää Hindley-Milner-tyypityksen laajennoksen, tyyppiluokat [17], jotka mahdollistavat olio-ohjelmoinnista tutun rajapinnan perinnän (mutta ei toteutuksen perintää). Näiden avulla ohjelmoija voi ilmaista minkä tahansa lausekkeen tyypin tai olla ilmaisematta, sillä kääntäjä osaa useimmissa tapauksissa päätellä ne. 2. Näihin verrattuna esimerkiksi C on heikosti tyypitetty. 3
Z-notaatio P tyyppi joukko1 joukko2 joukko1 joukko2 joukko1 \ joukko2 joukko1 joukko2 joukko1 = joukko2 alkio joukko Haskell-notaatio P tyyppi emptyp joukko1 $\/ joukko2 joukko1 $/\ joukko2 joukko1 $\ joukko2 joukko1 $ = joukko2 joukko1 = joukko2 alkio memberof joukko listtoset lista settolist joukko 3 Joukot ja logiikka Taulukko 1: Z-modulin joukko-operaatioita Z:n joukko voidaan esittää Haskellissa listana. Z-modulissa joukko käsitetään listaksi, joka on kapseloitu omaksi parametrisoiduksi tietotyypikseen P a. Joukoille on Z-modulissa määritelty operaatioita, jotka on esitetty taulukossa 1. Logiikka voidaan kirjoittaa Haskellilla pääosin sellaisenaan. Z-moduli määrittelee myös kvanttorit forall :: P a -> (a -> Bool) -> Bool ja exists :: P a -> (a -> Bool) -> Bool. Z-lauseke x : joukko predikaatti kirjoitetaan Haskellilla forall joukko $ \x -> predikaatti ja vastaavasti Z-lauseke x : joukko predikaatti kirjoitetaan exists joukko $ \x -> predikaatti Kvanttoreita käytettäessä on huomattava, että joukko, jonka yli kvantifioidaan, on valittava tarkasti. Aina ei voida valita samoin kuin Z-notaatiossa. Yleisesti ottaen ei voida kvantifioida yli jonkin tyypin, vaan mielummin kvantifioidaan yli jonkin konkreettisen joukon. Vaikka äärettömien joukkojen käyttö onnistuukin Haskellissa varsin hyvin, ei sellaisen yli kvantifiointi ole mahdollista kuin triviaaleissa tapauksissa. Jos tätä tarvitaan jonkin muuttujan arvon määrittelemiseen, joudutaan määritelmä kirjoittamaan hieman konstruktiivisempaan muotoon, mikä usein onnistuu helposti Haskellin standardifunktioiden kuten filter luovalla käytöllä. Tästä olkoon esimerkkinä joukkojen leikkauksen Haskell-määritelmä Z-modulista: xset $/\ yset = listtoset $ (flip filter) xs $ \x -> exists ys $ \y -> x == y where xs = settolist xset ys = settolist yset 4
Z-notaatio tyyppi1 tyyppi2 f x dom f ran f joukko f f joukko Haskell-notaatio F tyyppi1 tyyppi2 f @@ x dom f ran f joukko < f f > joukko 4 Funktiot Taulukko 2: Z-modulin funktio-operaatioita Z:n funktiot ovat yleensä tietorakenteita eivätkä proseduureja, joten niiden esittämiseen eivät sovi Haskellin funktiot. Sen sijaan Z-modulissa määritellään abstrakti, parametrisoitu tietotyyppi F a b. Se noudattaa osittaisfunktion invarianttia, joten kaikilla määrittelytyypin alkioilla ei ole välttämättä kuvaa. Tällaisia funktioita pääsee rakentamaan ja muuttamaan Haskell-funktioiden makef :: (Eq a, Eq b) => P (a, b) -> F a b (joka konstruoi funktion joukosta, joka on karteesisen tulon osajoukko) ja f2p :: F a b -> P (a, b) (joka toimii toisin päin) avulla. Taulukossa 2 esitellään funktioiden operaatiot, joista osan epäilemättä saisi suhteellisen helposti yleistettyä relaatioillekin. Haskellin funktioita on toki hyvä käyttää silloin, kun funktion määritelmä ei muutu ohjelman aikana. 5 Skeemat Voi olla hyvä idea kirjoittaa jokaisen skeeman käännös omaksi Haskell-modulikseen, mutta se ei ole ehdottaman välttämätöntä. 5.1 Tilaskeemat Yksinkertaiset tilaskeemat, jotka viittaavat vain omiin tilamuuttujiinsa, voidaan toteuttaa Haskellissa algebrallisena tietotyyppinä. Tällöin kutakin tilamuuttujaa varten varataan tyypissä yksi paikka. Muuttujien nimiä ei voi järkevästi kirjata tietotyyppiin standardi-haskellissa, mutta se ei käytännössä juurikaan haittaa. Tilaskeemaa vastaava algebrallinen tietotyyppi on hyvä parametrisoida ja varata tyypin viimeinen (tai ensimmäinen) paikka parametrityyppiseksi. Tällöin operaatioskeemojen syötteet ja tulosteet on helppo kuljettaa tilan mukana. Esimerkiksi BlockHandler-skeema toteutetaan tietotyyppimäärittelynä seuraavasti: data BlockHandler a = BlockHandler (P BlockType) [P BlockType] a Tilan invariantit saadaan mallinnettua niin, että konstruktorin sijaan käytetään aina erillistä luontifunktiota tila-arvon luontiin. Tällainen luontifunktio sitten tarkistaa invariantin ennen kuin luo arvon. Esimerkiksi: 5
makeblockhandler used blockseq more forall (listtoset blockseq) ($ = used) && (forall blockinx $ \i -> forall blockinx $ \j -> i == j (blockseq!! i) $/\ (blockseq!! j) == emptyp) = (BlockHandler used blockseq more) otherwise = error "unsatisfied invariant for BlockHandler" where blockinx = (listtoset [0.. length blockseq - 1]) Tilaskeemoja, jotka viittaavat oman tilansa ulkopuolelle, ei voi toteuttaa itsenäisinä, vaan ne on yhdistettävä johonkin toiseen tilaskeemaan. Kahden tilaskeeman yhdistämisen tapauksessa luodaan kolmen skeeman (kaksi yhdistettävää ja yhdiste) sijasta kaksi: toinen on perusskeema ja toinen on sen laajennos. Laajennoksen tilan yhdeksi komponentiksi otetaan perusskeeman tila. Esimerkiksi: data UserBlockHandler a = UserBlockHandler (BlockHandler a) (P UserType) (F BlockType UserType) a 5.2 Operaatioskeemat Operaatioskeemat mallinnetaan Haskellin funktioina, jotka ottavat tilan parametrinaan ja palauttavat sen Maybe-monadissa. Skeeman syötteet ja tulosteet välitetään tilan tyyppiparametrisoidussa paikassa. Tyypillisen operaatioskeeman tyyppimerkintä on siis Tilatyyppi Syötetyyppi -> Maybe (Tilatyyppi Tulostyyppi) Osittaiseksi operaatioskeemaksi sanotaan operaatioskeemaa, jonka esiehdot saattavat hylätä joitakin tila-syötekombinaatioita. Tällaisen skeeman animaation runko on tavallisesti seuraava: skeema (tilan-pattern-match) = precond (esiehto) (paluutilan-konstruointi) where jälkitilamuuttujien-määrittelyt Esimerkiksi: addblocks :: BlockHandler (P BlockType) -> Maybe (BlockHandler ReportType) addblocks (BlockHandler used blockseq ablocks) = precond (ablocks $ = used) (makeblockhandler used blockseq "") where used = used blockseq = blockseq ++ [ablocks] Tässä käytetty precond on Z-modulissa määritelty Haskell-funktio. Huomaa, että samaksi operaatioskeemaksi yhdistettävien osittaisten operaatioskeemojen tyyppitunnisteiden on oltava samat, joten niillä on oltava täsmälleen samat syötteet ja tulosteet osa voi 6
toki jäädä joiltakin skeemoilta käyttämättä, kuten edellisessä esimerkissä. Osittaiset operaatioskeemat, jotka käyttävät laajennettua tilaa, ovat hitusen monimutkaisempia sisäkkäisten tilojen vuoksi. Jokseenkin hankalia ovat tilanteet, joissa operaatioskeema laajentaa olemassaolevaa skeemaa. Tästä annan vain esimerkin: removeuserblocks :: UserBlockHandler () -> Maybe (UserBlockHandler ReportType) removeuserblocks (UserBlockHandler bh@(blockhandler used blockseq _) users bl_use _) = precond ((ran hrs $ = users) && (isjust bh )) $ makeuserblockhandler ((\(Just x) -> x) bh ) users bl_use "" where hrs = head blockseq < bl_use bh = removeblocks bh users = users bl_use = makef ((f2p bl_use) $\ (f2p hrs)) Osittaisista operaatioskeemoista kootaan toisia operaatioskeemoja Z-modulissa määriteltyä \/- operaattoria käyttäen: doaddblocks :: BlockHandler (P BlockType) -> Maybe (BlockHandler ReportType) doaddblocks = addblocks \/ freeblocktoqueue 5.3 Käyttöliittymä Käyttöliittymäskeemojen animointi on kinkkistä puuhaa. Käyttämäni tapa on luultavasti epäoptimaalinen, mutta yksinkertaisen tavan löytäminen ei ole ihan helppoa. Perusidea on se, että kuten edellä komentoskeemat animoidaan erikseen. Aiemmasta poiketen kuitenkin nämä on tarkoitus yhdistää varsinaisiin operaatioskeemoihin konjunktiivisesti eikä disjunktiivisesti, joten komentoskeemojen animointistrategiakin on erilainen. Komentoskeeman animaation tyyppitunniste on seuraavanlainen: Tilatyyppi Komentotyyppi -> IO (Maybe (Tilatyyppi Operaaation-syötetyyppi)) Toisin sanoen komentoskeeman tulee komennon tarkistettuaan lukea käyttäjältä tarvittavat syötteet. Tämä tekee komentoskeeman animaatiokaavasta sotkuisen: skeema (tilan-pattern-match) = if onko-komento-oikea then return Nothing else do kysy-syötteet return $ Just $ konstruoi-paluutila Esimerkki: doblocksofusercommand :: UserBlockHandler (Command) -> IO (Maybe (UserBlockHandler (UserType))) 7
doblocksofusercommand (UserBlockHandler (BlockHandler used blockseq _) users bl_use cmd) = if cmd /= "bou" then return Nothing else do putstr "user> " user <- getline return $ Just $ (UserBlockHandler (BlockHandler used blockseq user) users bl_use user) Suoritusskeeman animaation tehtävänä on yhdistää komentoskeema ja vastaava operaatioskeema konjunktiivisesti. Tässä käytetään hyväksi Maybe-tyypin monadisuutta: yhdistäminen tehdään monadioperaatiolla»=. Animaatiokaava on kuitenkin hieman monimutkaisempi, koska myös IOmonadi on kuvassa mukana: Esimerkki: suoritusskeema a = do res <- komentoskeema a return $ res»= operaatioskeema codoremoveuserblocks a = do res <- doremoveuserblockscommand a return $ res >>= doremoveuserblocks Jäljelle jää enää kokonaisuus, josta näytän aluksi animaatiokaavan skeema u = try [ suoritusskeema1, suoritusskeema2,... suoritusskeeman ] u»= putstrln. show ja sitten esimerkin handleuserblocks u = try [ codoaddblocks, codoremoveuserblocks, codoblocksofuser, unknowncommand ] u >>= putstrln. show Haskell-funktio try määritellään Z-modulissa. 6 Pääohjelma Toimiakseen animoitu ohjelma tarvitsee pääohjelman. Sen animointikaava on seuraavanlainen: main :: IO () main= main initialisoi-tila where 8
main (tilan-pattern-match) = do putstr ": " cmd <- getline if cmd == "" then return () else do tilan-pattern-match <- luo-tila-jossa-cmd main luo-komennoton-tila Esimerkiksi: main :: IO () main = main inituserblockhandler where main (UserBlockHandler (BlockHandler a b _) c d _) = do putstr ": " cmd <- getline if cmd == "" then return () else do (UserBlockHandler (BlockHandler a b _) c d _) <- handleuserblocks (makeuserblockhandler (makeblockhandler a b cmd) c d cmd) main (makeuserblockhandler (makeblockhandler a b ()) c d ()) 7 Jatkoa Esittelemäni metodi tulisi formalisoida täysin mekaaniseksi proseduuriksi ja se tulisi todistaa oikeaksi. Viitteet [1] Ali E. Abdallah, Alexandra Barros, José B. Barros, ja Jonathan P. Bowen. Deriving correct prototypes from formal Z specifications. Tekninen raportti SBU-CISM-00-27, South Bank University School of Computing, Information Systems and Mathematics, 2000. [2] Richard Bird. Introduction to Functional Programming using Haskell. Prentice Hall, toinen laitos, 1998. [3] Norbert E. Fuchs. Specifications are (preferably) executable. IEE/BCS Software Engineering Journal, 7(5):323 334, 1992. 9
[4] Howard S. Goodman. The Z-into-Haskell tool-kit. Tekninen raportti, University of Birmingham School of Computer Science, huhtikuu, 1995. [5] I. J. Hayes ja C. B. Jones. Specifications are not (necessarily) executable. Tekninen raportti, Key Centre for Software Technology, Department of Computer Science, University of Queensland, St. Lucia, Queensland, Australia 4072, tammikuu 1990. [6] Paul Hudak, John Peterson, ja Joseph H. Fasel. A gentle introduction to Haskell 98. http://www.haskell.org/tutorial/, kesäkuu 2000. [7] Antti-Juhani Kaijanaho. Haskell 98: Puhdasta funktionalismia nonstriktissä paketissa. http://www.iki.fi/gaia/tekstit/ohjsem99/, 1999. [8] Tommi Kärkkäinen. Formaalit menetelmät. Luentomoniste, Jyväskylän yliopisto, tietotekniikan laitos, 2001. [9] Robin Milner. A theory of type polymorphism in programming. Journal of Computer and System Sciences, 7(3):348 375, 1978. [10] Simon Peyton Jones, John Hughes, et al. Report on the programming language Haskell 98 a non-strict, purely functional language. http://www.haskell.org/definition/, helmikuu 1999. [11] Simon Peyton Jones, John Hughes, et al. Standard libraries for the Haskell 98 programming language. http://www.haskell.org/definition/, helmikuu 1999. [12] Fethi Rabhi ja Guy Lapalme. Algorithms A Functional Programming Approach. Addison- Wesley, toinen laitos, 1999. [13] L. B. Sherrell ja D. L. Carver. Z meets Haskell: A case study. Kirjassa 17th Annual International Computer Software and Applications Conference, ss. 320 326, 1993. [14] L. B. Sherrell ja D. L. Carver. Experiences in translating Z designs to Haskell implementations. Software Practice and Experience, 24(12), 1994. [15] J. M. Spivey. The Z Notation: A Reference Manual. J. M. Spivey, Oriel College, Oxford, OX1 4EW, England, 1998. [16] Philip Wadler. How to declare an imperative. ACM Computing Surveys, 29(3), syyskuu 1997. [17] Philip Wadler ja Stephen Blott. How to make ad-hoc polymorphism less ad hoc. Kirjassa 16th ACM Symposium on Principles of Programming Languages, Austin, Texas, tammikuu 1989. 10