HELYESSÉGBIZONYÍTÓ ESZKÖZÖK ALKALMAZÁSA FUNKCIONÁLIS PROGRAMOK VIZSGÁLATÁRA 1 VERIFICATION OF FUNCTIONAL PROGRAMS USING THEOREM PROVERS Tejfel Máté ELTE Informatikai Kar Programozási Nyelvek és Fordítóprogramok Tanszék Összefoglaló Az ELTE programtervező matematikus, illetve az újonnan bevezetett programtervező informatikus képzésében jelentős szerepet kap a funkcionális nyelvek oktatása. A funkcionális nyelven elkészített programok formális helyességvizsgálatával azonban csak az informatikai doktori iskola keretében ismerkedhetnek meg az érdeklődő doktori hallgatók. Elmondható, hogy napjainkban egyre fokozódó igény mutatkozik helyes programok előállítása iránt és a jelenleg széles körben alkalmazott minőségjavító technikák (tesztelés, szimuláció) már viszonylag kisméretű szoftvertermékek esetén sem képesek ezt az igényt maradéktalanul ellátni. Ezért felmerült az igény, hogy a jobb képességű, érdeklődő egyetemi hallgatók számára bevezessünk egy olyan tantárgyat, amely helyes programok előállításának lehetőségeivel (helyességvizsgálat, programszintézis), illetve nagyrészt funkcionális programok helyességbizonyításával és az ehhez használható eszközök rövid bemutatásával foglalkozik. Ennek okán hoztuk létre 2006-ban a Helyességbizonyító eszközök alkalmazása funkcionális programok esetén címet viselő speciálkollégiumot. A tárgy keretében a hallgatók először megismerkednek a programhelyesség fogalmával és a biztosítására szolgáló módszerekkel, majd egyegy kiválasztott eszköz segítségével, példákon keresztül megvizsgálják az adott módszer működését és lehetőségeit. Végezetül pedig részletesen megismerik a Clean nyelvű funkcionális programok helyességbizonyítására szolgáló Sparkle eszközt. Kulcsszavak helyességbizonyítás, funkcionális programozás Abstract The education of functional programming plays an important role in the informatics trainings of ELTE. However courses about verification of functional programming were available only for our PhD students. Since the needs for creating sound programs increase gradually a new special course was created in 2006 for high quality master students with title Verification of functional programs. The course presents methods make possible to increase the program quality, demonstrates examples for tools of these methods and introduces partially the Sparkle theorem prover which is dedicated for the Clean functional language. Keywords verification, functional programming 1 A Deák Ferenc ösztöndíj támogatásával 1
1. Bevezető Az ELTE programtervező matematikus képzésében több éve jelentős szerepet kap a funkcionális nyelvek oktatása [4]. A téma fontosságát jelzi, hogy a funkcionális programozáshoz kötődő tantárgyak beépültek a bolognai folyamat végrehajtása során kialakított programtervező informatikus képzés tananyagába is, illetve néhány, korábban csak az MSc hallgatók számára meghirdetett tárgy átkerült a BSc képzésbe, sőt egyes szakirányokon bevezető jelleggel már az elsőévesek számára is oktatunk funkcionális programozást. A tárgykör oktatása során, a funkcionális nyelvek elemeinek tárgyalásakor több nyelv (Miranda, Haskell, Clean) elemeit mutatjuk be példákon keresztül, ezáltal is kifejezésre jutatva, hogy elsősorban a funkcionális programozási stílus, illetve a modern funkcionális programozási nyelvekben előforduló közös nyelvi elemek megismerése a cél, nem pedig egy konkrét nyelv kimerítő tárgyalása. Első lépésben az a fő célunk, hogy a hallgatók megismerkedjenek a funkcionális programozási stílussal, és gyakorlatot szerezzenek a fontosabb nyelvi elemek használatában. A továbbiakban részletesen tárgyaljuk a funkcionális programozás imperatív nyelvekétől lényegesen eltérő számítási modelljét és bemutatjuk az implementációs technikák részleteit is. A funkcionális program végrehajtása valójában átírási lépések sorozatának végrehajtását jelenti a kezdeti kifejezésből kiindulva. Egy-egy redukciós lépésben egy redukálható részkifejezésben, a redexben szereplő függvényhívás helyettesítődik a függvény megadott kifejezésével, a formális és aktuális paraméterek megfeleltetése mellett. A funkcionális nyelven írt programok végrehajtási modellje minden esetben egy átíró rendszer, melyben az egyes rész-kifejezések átírásának sorrendje a végeredményre nincs hatással, a sorrend legfeljebb azt befolyásolja, hogy az átírási lépéssorozattal eljutunk-e a végeredményig. 2. Az oktatott tantárgy A funkcionális programok helyességvizsgálatával foglalkozó Helyességbizonyító eszközök alkalmazása funkcionális programok esetén címet viselő speciálkollégiumot 2006- ban hoztuk létre elsősorban azon negyed-ötödéves, jobb képességű hallgatók részére, akik tagjai az ELTE Eötvös József Collegiumában működő informatikai műhelynek. A tárgy keretében a hallgatók megismerkednek a programhelyesség fogalmával, a program minőségét javító módszerekkel, mint a tesztelés és a szimuláció, illetve olyan módszerekkel, amelyekkel a program helyessége biztosítható, mint a modell ellenőrzés, a tételbizonyítás és a programszintézis. Ezután egy-egy kiválasztott eszköz segítségével, példákon keresztül megvizsgálják az adott módszer működését, lehetőségeit. Az előadáson a leírt módszerekhez kapcsolódóan a következő eszközöket tekintjük át: modellellenőrzés: NuSMV [2], programszintézis: B method [1], helyességbizonyítás: Isabelle-Hol [7]. Végül a helyességvizsgálati módszerek általános áttekintése után a hallgatók részletesen megismerik a Clean funkcionális nyelvhez készített Sparkle [5, 6] helyességbizonyító eszközt, annak axiómarendszerét és a bizonyítás során alkalmazható levezetési szabályait. Ennek során 2
különös figyelmet fordítunk a Clean nyelv lusta kiértékelési stratégiája miatt bevezetett speciális szabályokra. Megismerkedhetnek az eszköz egy lehetséges kiterjesztésével is, melynek segítségével temporális jellegű tulajdonságok is bizonyíthatók Clean nyelvű programokra. Jellegéből adódóan a tárgyat viszonylag kevés számú hallgató (félévente nagyjából 8-10 fő) veszi fel. A sikeres teljesítéshez a hallgatóknak adott programok tulajdonságait kell bebizonyítaniuk beadandó feladatként. Az eddigi félévek során a tárgyat felvettek mintegy 90% sikeresen elvégezte a tárgyat, jellemzően ötös érdemjeggyel. 3. Programminőség javító módszerek Napjainkban egyre fontosabb problémává válik a programok helyessége, azaz bizonyos felhasználói, tervezői követelményeknek való megfelelése. Ez a megfelelés a program tulajdonságain múlik, azaz a helyességellenőrzés során a program tulajdonságait kell megvizsgálnunk. A jelenleg legelterjedtebb minőségjavító módszerek, a tesztelés és a szimuláció nem alkalmasak a helyesség teljes mértékű biztosítására. A két módszer közötti különbség, hogy a tesztelés magára a programra alkalmazható, míg a szimuláció annak valamely absztrakt modelljére. Mindkét módszer azt vizsgálja, hogy a rendszer adott bemeneti adatok esetén az elvárt válaszokat eredményezi-e, ebből adódóan már kisebb méretű programok esetén sem képesek az összes lehetséges esetet lefedni, viszont költséghatékonyak és sok hibát képesek megtalálni. Ugyancsak a program egy absztrakt modelljén működik a modell ellenőrzés, amely teljes keresést végez az állapottéren, így megvizsgálva, hogy a kérdéses tulajdonság fennáll-e az adott programra, illetve ellenkező esetben ellenpéldát szolgáltat. Működéséből adódóan a módszer csak véges lehetséges állapothalmaz esetén alkalmazható, viszont ekkor pozitív válasz esetén bármilyen bemenő adat mellett garantálja a tulajdonság teljesülését. Ezen módszer alkalmazásakor szakértői tudást csak az absztrakt modell előállítása igényel, maga az ellenőrzési folyamat automatikus. A tételbizonyítás is absztrakt modellen alkalmazható, a módszer során a modellből és a megadott axiómákból kiindulva logikai levezetési szabályok segítségével, emberi közreműködéssel kell bebizonyítani a tulajdonságok fennállását. A módszer előnye, hogy nincs megkötés a vizsgált programra, hátránya, hogy mind a modell, mind a bizonyítás elkészítése magas fokú (matematikai, logikai) szakértelmet igényel, emellett időigényes és csak részben automatizálható. A tételbizonyítás során egy bizonyítási fát építünk fel, melynek gyökere a bizonyítandó állítás, csúcsai állítások, az élei pedig levezetési szabályok. Teljes bizonyításhoz tartozó fa levelei axiómák vagy az absztrakt modellből triviálisan következő állítások. Végezetül egy, a tárgy keretében ismertetett alternatív módszer helyes program előállítására a programszintézis, melynek során az előzőekben ismertetett módszerektől eltérően nem egy elkészített program tulajdonságait vizsgáljuk, hanem a specifikációból kiindulva ellenőrzött finomítási lépések sorozatával állítjuk elő a futtatható kódot. A finomítási lépések mindegyikének ellenőrzése biztosítja, hogy az elkészített kód megfeleljen a kezdeti specifikációnak. 3
4. A Sparkle helyességbizonyító eszköz A Sparkle tételbizonyító eszközt kifejezetten Clean funkcionális nyelven írt programok helyességvizsgálatához készítették. Az eszköz érdekessége, hogy a belső reprezentáció (az absztrakt modell) leírására használt nyelv is a Clean, pontosabban annak egy speciális részhalmaza az úgynevezett Core-Clean. Ez a specialitás lehetővé teszi, hogy Clean nyelvű programok absztrakt modellje automatikusan, az eszköz által felépíthető legyen, azaz a Sparkle eszköz esetén a modell felépítése nem igényel emberi erőforrást, csak a bizonyítás elvégzése. Az eszközben a tulajdonságokat egyenlőségjeles elsőrendű logikai állításokkal írhatjuk le, viszont a predikátumok helyett csak Boole értékű Clean függvények használhatók. Így például a Páros(x) predikátum teljesülésének vizsgálata helyett definiálnunk kell egy Int Boole típusú páros Clean függvényt és a páros x = True egyenlőséget vizsgálhatjuk, azaz valójában az eszköz segítségével Clean függvények egymáshoz képesti viszonya vizsgálható. Az eszköz 42 levezetési lépést tartalmaz, melyek kifejezetten a Clean nyelv figyelembevételével lettek kialakítva. A nyelv alapvetően lusta kiértékelésű, azaz megengedett végtelen adatstruktúrák használata is. Emiatt előfordulhat, hogy a programban bizonyos kifejezések definiálatlanok lesznek (például egy végtelen lista utolsó eleme). Ezért például az indukció és az esetszétválasztás a Sparkle eszközben kibővül egy újabb esettel, a definiálatlan érték esetével. Ez a gyakorlatban azt jelenti, hogy valamely tulajdonság bizonyításakor, ha valamely változó értéke szerinti esetszétválasztás vagy indukció segítségével akarunk bizonyítani, akkor a megadott feltételekből be kell látnunk, hogy az érték nem lehet definiálatlan, vagy ellenkező esetben be kell látnunk, hogy definiálatlan érték esetén is fennáll a tulajdonság. Az esetszétválasztáson és az indukción kívül a legtöbbször használt levezetési szabályok a redukció, a dedukció, a már bizonyított tulajdonságok alkalmazását végző szabály, az új hipotézisek felvételére szolgáló szabály, illetve a definiálatlanságot vizsgáló szabály. A bizonyítások szekciókba szervezhetők, amelyek elmenthetők és újrafelhasználhatók. Az eszköz rendelkezik egy úgynevezett tanácsadó rendszerrel is, amely egy beépített heurisztika alapján megmondja, hogy a bizonyítás egy adott pontján melyik levezetési szabály milyen valószínűséggel használható. Beállítható, hogy az eszköz a legvalószínűbb lépést automatikusan végrehajtsa, ily módon a bizonyítás részben automatizálható. Azért csak részben, mert a rendszer nem mindig képes tanácsot adni, illetve néhány esetben nem a megfelelő lépést javasolja, így a bizonyítás mindenképpen emberi erőforrást igényel. 5. Egy egyszerű példa Nézzünk egy egyszerű példát Sparkle eszközzel elvégzett bizonyításra. Ehhez tekintsük a Clean beépített StdEnv könyvtárában definiált take illetve drop függvényt. take :: Int![a] -> [a] take n [x:xs] n > 0 = [x: take (n-1) xs] = [] take n [] = [] 4
drop :: Int![a] -> [a] drop n [x:xs] n > 0 = drop (n-1) xs = [x:xs] drop n [] = [] Mindkét függvénynek két paramétere van egy egész érték és egy lista (melynek elemtípusára nem teszünk megkötést). A lista paraméterről kikötjük, hogy definiált kell legyen (ezt jelzi a típusleírásnál a! szimbólum), azonban érdemes megjegyezni, hogy ez nem zárja ki a végtelen listákat, mint lehetséges paramétereket. A take függvény a paraméterül adott lista első n elemét adja vissza (illetve a teljes listát, ha annak hossza kisebb n -nél). A függvény a definíció szerint először mintaillesztéssel megállapítja, hogy a paraméterként kapott lista üres-e vagy sem, és ha üres, akkor üres listát ad vissza eredményül. Ha a kapott lista nem üres, akkor megvizsgálja, hogy az ugyancsak paraméterként kapott n érték nagyobb-e nullánál, ha nem, akkor üres lista lesz a visszatérő érték. Ellenkező esetben a függvény leveszi a lista első elemét, és rekurzívan meghívja önmagát eggyel kisebb egész paraméterrel, illetve a lista maradék részével, majd a hívás eredményeként kapott lista elejére hozzáfűzi a levett listaelemet. A drop függvény a fentiekhez nagyon hasonlóan működik, azzal a különbséggel, hogy nem üres lista és nullánál kisebb vagy egyenlő n érték esetén a teljes listát adja vissza eredményként, illetve a rekurzív hívásnál természetesen nem fűzi vissza a lista elejére a levett elemet. Ezen függvényekre megfogalmazhatunk egy olyan állítást, hogy definiált n érték esetén, ha leveszünk n elemet egy listából a take függvénnyel és eldobunk ugyanazon listából n elemet a drop függvénnyel, majd az így kapott két listát összefűzzük, akkor visszakapjuk az eredeti listát. A tulajdonság felírásához felhasználjuk a ++ (összefűzés) függvényt, amely ugyancsak az StdEnv könyvtárában van definiálva. Az állítás formális leírása a következő: (n = ) take n xs ++ drop n xs = xs Az állításban jelöli a definiálatlan értéket. Érdemes megfigyelni, hogy az n definiáltságára vonatkozó feltétel szükséges a bizonyításhoz, mivel definiálatlan n érték esetén take n xs, illetve drop n xs és így természetesen az összefűzésük is definiálatlan, míg xs nyilván lehet definiált. A tulajdonság az xs lista szerinti strukturális indukció, illetve redukció segítségével 17 lépésben bizonyítható a Sparkle eszközzel. A bizonyítás érdekessége, hogy minden lépésben a tanácsadó rendszer által legvalószínűbben alkalmazhatónak tartott levezetési lépést használjuk, azaz a konkrét állítás automatikusan is bizonyítható. 5
6. A Sparkle-T kiterjesztés A bemutatott Sparkle egy kiterjesztése a Sparkle-T [8] eszköz, melynek segítségével temporális jellegű logikai tulajdonságok is vizsgálhatóak Clean nyelvű programok esetén. A temporális logikai állítások különösen jól használhatóak imperatív, párhuzamos vagy reaktív programok helyességvizsgálata esetén. Tisztán funkcionális környezetben azonban általában kevésbé használhatóak, hiszen egy ilyen programban a változók egy adott értékhez vannak kötve, és a program futása során nem változtatják az értéküket. Azonban funkcionális programok estén is előfordul, hogy különböző változók értékeit egymásból számoljuk és a változók valójában ugyanazt az (absztrakt) objektumot jelölik. Például a Clean nyelvnek létezik egy olyan könyvtára (az Object I/O könyvtár), melynek segítségével reaktív, egymással együttműködő, belső állapottal rendelkező folyamatokból álló felhasználói felületek készíthetők, egy ilyen folyamat különböző belső állapotait jelölő változók tekinthetőek egy absztrakt objektum alá tartozónak és a közöttük fennálló összefüggéseket megfogalmazhatjuk temporális logikai állítások segítségével. A Sparkle-T eszköz lehetővé teszi, hogy Clean nyelven írt programokhoz absztrakt objektumokat definiáljunk, bizonyos a programban előforduló változókat az így létrehozott objektumokhoz rendeljük hozzá és meghatározzuk a program azon részeit, melyek végrehajtásakor az objektumok értéke megváltozhat. A kiterjesztés azt is lehetővé teszi, hogy a definiált objektumok felett temporális logikai állításokat fogalmazzunk meg a Unity [3] logikából kölcsönzött invariáns és unless operátorokat felhasználva. A Unity logika használata azért előnyös a hallgatók számára, mert a programtervező informatikus BSc szak szoftverfejlesztő informatikus szakirányos hallgatói az Osztott rendszerek specifikációja és implementációja tárgy keretében részletesen megismerkednek ezen logika operátoraival, az általuk kifejezhető tulajdonságokkal, illetve azok bizonyításának módjával. Természetesen a kiterjesztett eszköz a temporális tulajdonságok bizonyításához új levezetési szabályokat is definiál, melyek segítségével az állítások átalakíthatóak elsőrendű logikai állításokká, amik már bizonyíthatóak a hagyományos (Sparkle-ban definiált) eszközökkel. Irodalomjegyzék [1] Abrial J.-R. (1996) The B-Book. Cambridge University Press. [2] Cavada R., Cimatti A., Jochim C. A., Keighren G. (2006) Olivetti E., Pistore M., Roveri M., Tchaltsev A. NuSMV 2.4 User Manual, online kiadvány http://nusmv.irst.itc.it/nusmv/userman/v24/nusmv.pdf [3] Chandy K. M., Misra J. (1989) Paralell program design: a foundation. Addison-Wesley. [4] Horváth Z., Csörnyei Z., Lövei L., Zsók V. (2005) Funkcionális programozás témakörei a programtervező képzésben. Informatika a felsőoktatásban'05. Debrecen, Pethő A, Herdon M. (szerk): Informatika a felsőoktatásban 2005. 6
[5] Mol M. d., Eekelen M. v., Plasmeijer R. (2001) Theorem Proving for Functional Programmers, Sparkle: A Functional Theorem Prover. Springer, Lecture Notes in Computer Science, 2312. kötet, oldalszám: 55-71. [6] Mol M. d., Eekelen M. v., Plasmeijer R. (2007) The Mathematical Foundation of the Proof Assistant Sparkle. Technical Report ICIS-R07025, Radboud University Nijmegen [7] Nipkow T., Paulson L. C., Wenzel M. (2002) Isabelle/HOL --- A Proof Assistant for Higher-Order Logic. Springer, Lecture Notes in Computer Science, 2283. kötet [8] Tejfel M., Horváth Z., Kozsik T. (2006) Temporal Properties of Clean Programs Proven in Sparkle-T. Central European Functional Programming School, Revised Selected Lectures, Springer, Lecture Notes in Computer Science, 4164. kötet, oldalszám: 168-190. 7