BUDAPESTI MŰSZAKI EGYETEM VILLAMOSMÉRNÖKI ÉS INFORMATIKAI KAR DEKLARATÍV PROGRAMOZÁS OKTATÁSI SEGÉDLET Bevezetés a funkcionális programozásba Ötödik, bővített kiadás Hanák D. Péter Irányítástechnika és Informatika Tanszék Budapest, 2005. március
Tartalomjegyzék 1. Bevezetés 7 1.1. A programozási paradigmákról................................ 7 1.2. A monolitikus és a strukturált programozásról........................ 7 1.3. Az imperatív programozási paradigma............................ 8 1.4. A deklaratív programozási paradigma............................ 9 1.4.1. A logikai programozási paradigma.......................... 9 1.4.2. A funkcionális programozási paradigma....................... 9 1.4.2.1. A read-eval-print ciklus.......................... 10 1.4.2.2. Hivatkozási átlátszóság.......................... 10 1.4.2.3. A funkcionális program.......................... 10 1.5. SML-értelmezők és fordítók................................. 10 1.6. Információforrások...................................... 12 1.7. Változások az előző kiadáshoz képest............................ 13 1.8. Köszönetnyilvánítás...................................... 13 1.9. Hibajelentés.......................................... 13 2. Egyszerű példák SML-ben 14 2.1. Egész szám négyzete..................................... 14 2.2. Legnagyobb közös osztó................................... 14 2.3. Intervallumösszeg....................................... 15 2.4. Pénzváltás........................................... 17 3. Nevek, függvények, egyszerű típusok 19 3.1. Értékdeklaráció........................................ 19 3.1.1. Névadás állandónak................................. 19 3.1.2. Névadás függvénynek................................ 20 3.1.3. Nevek újradefiniálása................................. 21 3.1.3.1. Nevek képzése............................... 21 3.2. Egész, valós, füzér, karakter és más egyszerű típusok.................... 22 3.2.1. Egészek és valósak.................................. 22 3.2.1.1. A real, floor, ceil, abs, round és trunc függvény....... 22 3.2.1.2. Alapműveletek előjeles egész számokkal................. 23 3.2.1.3. Alapműveletek valós számokkal..................... 24 3.2.1.4. Alapműveletek előjel nélküli egészekkel................. 25 3.2.2. Típusmegkötés.................................... 25 3.2.3. Füzérek........................................ 26 3.2.3.1. Escape-szekvenciák............................ 26 3.2.3.2. Gyakori műveletek füzérekkel....................... 27 3.2.4. Karakterek...................................... 27 3.2.4.1. Gyakori műveletek karakterekkel..................... 27 2
TARTALOMJEGYZÉK 3 3.2.5. Igazságértékek, logikai kifejezések, feltételes kifejezések.............. 27 3.2.5.1. Feltételes operátor............................. 28 3.2.5.2. Logikai operátorok............................ 28 3.2.5.3. Tesztelő függvények............................ 29 3.3. Infix operátorok........................................ 29 3.3.1. Infix operátorok precedenciája............................ 29 3.3.2. Felhasználói infix operátor.............................. 30 3.3.3. Infix operátor kötése................................. 31 4. Ennesek, rekordok, polimorf típusok 33 4.1. Ennes............................................. 33 4.1.1. Típuskifejezés.................................... 33 4.1.2. Példa: vektorok.................................... 34 4.1.3. Függvény több argumentummal és eredménnyel.................. 34 4.1.4. Ennes elemeinek kiválasztása mintaillesztéssel................... 34 4.1.5. A nullas és a unit típus............................... 35 4.1.5.1. A print, a use és a load üggvény................... 35 4.2. Rekord............................................. 35 4.2.1. Rekordminta..................................... 36 4.2.2. Gyakorló feladatok.................................. 37 4.3. Polimorfizmus......................................... 37 4.3.1. Polimorf típusellenőrzés............................... 37 4.3.2. Egyenlőségvizsgálat polimorf függvényekben.................... 38 5. Kiértékelés, deklaráció 39 5.1. Kifejezések kiértékelése az SML-ben............................ 39 5.1.1. Mohó kiértékelés................................... 40 5.1.1.1. Mohó kiértékelés rekurzív függvények esetén.............. 40 5.1.1.2. Iteratív függvények............................ 40 5.1.1.3. Feltételes kifejezések speciális kiértékelése................ 41 5.1.2. Lusta kiértékelés................................... 42 5.1.3. A mohó és a lusta kiértékelés összevetése...................... 42 5.2. Lokális érvényű és egyidejű deklaráció............................ 43 5.2.1. Kifejezés lokális érvényű deklarációval....................... 43 5.2.2. Deklaráció lokális érvényű deklarációval...................... 44 5.2.3. Egyidejű deklaráció................................. 44 5.3. Gyakorló feladat....................................... 44 6. Számítások rekurzív függvényekkel 45 6.1. Egész kitevőjű hatványozás.................................. 45 6.2. Fibonacci-számok....................................... 46 6.3. Egész négyzetgyök közelítéssel................................ 48 6.4. Valós szám négyzetgyöke Newton-Raphson módszerrel................... 49 6.5. π/4 közelítő értéke kölcsönös rekurzióval.......................... 50 7. Listák 52 7.1. Listajelölések......................................... 52 7.1.1. Típuskifejezés.................................... 52 7.2. Lista létrehozása....................................... 53 7.3. Egyszerű műveletek listákkal................................. 53 7.3.1. Egyesével növekvő számtani sorozat......................... 53 7.3.2. Lista elemeinek szorzata és összege......................... 53
4 TARTALOMJEGYZÉK 7.3.3. Lista legnagyobb eleme............................... 54 7.3.4. Karakter, füzér és lista................................ 55 7.4. Listák vizsgálata és darabokra szedése............................ 55 7.5. Listák és egész számok.................................... 56 7.6. Listák összefűzése és megfordítása.............................. 58 7.7. Listákból álló lista, párokból álló lista............................ 59 7.8. Listák és halmazok...................................... 60 8. Adattípusdeklaráció 62 8.1. Felsorolásos típus adatkonstruktorállandókkal........................ 62 8.2. Felsorolásos típus adatkonstruktorfüggvényekkel...................... 63 8.3. Polimorf adattípusok..................................... 65 8.4. A case-kifejezés....................................... 66 9. Magasabbrendű függvények 67 9.1. Az fn jelölés......................................... 67 9.1.1. Függvény definiálása fun, val és val rec kulcsszóval.............. 68 9.2. Részlegesen alkalmazható függvények............................ 68 9.3. Magasabbrendű függvények................................. 69 9.3.1. secl és secr.................................... 70 9.3.2. Két függvény kompozíciója............................. 70 9.3.3. curry és uncurry................................. 71 9.3.4. map és filter................................... 71 9.3.4.1. Gyakorló feladat.............................. 72 9.3.5. takewhile és dropwhile............................ 72 9.3.6. exists és forall................................. 73 9.3.7. foldl és foldr.................................. 74 9.3.8. repeat....................................... 75 9.3.9. map újradefiniálása foldr-rel........................... 76 10. Kivételkezelés 77 10.1. Kivétel deklarálása az exception kulcsszóval....................... 77 10.2. Kivétel jelzése a raise kulcsszóval............................. 77 10.2.1. Belső kivételek.................................... 78 10.3. Kivétel feldolgozása a handle kulcsszóval......................... 78 10.4. Néhány példa a kivételkezelésre............................... 78 11. Bináris fák 81 11.1. Egyszerű műveletek bináris fákon.............................. 82 11.2. Lista előállítása bináris fa elemeiből............................. 84 11.3. Bináris fa előállítása lista elemeiből............................. 85 11.4. Elem törlése bináris fából................................... 86 11.5. Bináris keresőfák....................................... 87 12. Listák rendezése 89 12.1. Beszúró rendezés....................................... 89 12.1.1. Generikus megoldások................................ 89 12.1.2. Beszúró rendezés foldr-rel és foldl-lel..................... 91 12.1.3. A futási idők összehasonlítása............................ 91 12.2. Gyorsrendezés......................................... 93 12.3. Összefésülő rendezés..................................... 95 12.3.1. Fölülről lefelé haladó összefésülő rendezés..................... 95 12.3.2. Alulról fölfelé haladó összefésülő rendezés..................... 96
TARTALOMJEGYZÉK 5 12.4. Simarendezés......................................... 98 13. Lusta kifejezések 100 13.1. Lusta kifejezés és függvény létrehozása........................... 100 13.2. Lusta lista........................................... 101 13.3. Elemi feldolgozási műveletek lusta listákon......................... 103 13.4. Magasabbrendű függvények lusta listákra.......................... 104 13.5. Három összetett példa lusta listával.............................. 105 13.5.1. Álvéletlenszámok................................... 105 13.5.2. Prímszámok..................................... 105 13.5.3. Gyökvonás...................................... 106 13.6. Lusta listák listája és egymásba ékelése........................... 107 13.6.1. Keresztszorzatokból álló lista............................ 107 13.6.2. Keresztszorzatokból álló lusta lista.......................... 108 14. Példaprogramok: füzérek és listák 110 14.1. Füzér adott tulajdonságú elemei (mezok).......................... 110 14.2. Füzér adott tulajdonságú elemei (basename)........................ 112 14.3. Füzér adott tulajdonságú elemei (rootname)........................ 112 14.4. Füzér egyes elemeinek azonosítása (parpairs)...................... 113 14.5. Lista adott tulajdonságú részlistái (szomsor)........................ 114 14.6. Bináris számok inkrementálása (binc)........................... 115 14.7. Mátrix transzponáltja (trans)................................ 116 15. Példaprogramok: fák 118 15.1. Fa adott tulajdonságának ellenőrzése (ugyanannyi).................... 118 15.2. Fa adott tulajdonságú részfáinak száma (bea)........................ 120 15.3. Fa adott tulajdonságú részfáinak száma (testvere).................... 121 15.4. Fa adott elemeinek összegzése (szintossz)........................ 122 15.5. Kifejezésfa egyszerűsítése (egyszerusit)........................ 124 15.6. Kifejezésfa egyszerűsítése (coeff)............................. 125 16. Egy egyszerű fordítóprogram SML-ben 127 16.1. A forrásnyelv......................................... 127 16.2. A forrásnyelv konkrét szintaxisa............................... 127 16.3. A célnyelv........................................... 128 16.4. A fordítás folyamata..................................... 130 16.5. A forrásnyelv absztrakt szintaxisa.............................. 130 16.6. A fordítóprogram építőkockái................................ 130 16.7. A fordító forráskódja SML-nyelven............................. 132 16.7.1. Symtab szignatúrája és struktúrája.......................... 133 16.7.2. Lexical szignatúrája és struktúrája.......................... 137 16.7.3. Parsefun szignatúrája és struktúrája......................... 139 16.7.4. Parse szignatúrája és struktúrája........................... 145 16.7.5. Encode szignatúrája és struktúrája.......................... 148 16.7.6. Assemble szignatúrája és struktúrája......................... 152 16.7.7. Compile szignatúrája és struktúrája......................... 155
6 TARTALOMJEGYZÉK A. Az SML alapnyelv szintaxisa 158 A.1. Fogalmak és jelölések..................................... 158 A.1.1. Nevek......................................... 158 A.1.2. Infix operátorok.................................... 159 A.1.3. Jelölések....................................... 159 A.2. Az SML alapnyelv szintaxisa................................ 160 A.2.1. Kifejezések és klózsorozatok............................. 160 A.2.2. Deklarációk és kötések................................ 161 A.2.3. Típuskifejezések................................... 162 A.2.4. Minták........................................ 162 A.2.5. Szintaktikai korlátozások............................... 163 B. Válogatás az SML Alapkönyvtárából 164 B.1. Structure Binarymap..................................... 166 B.2. Structure Binaryset...................................... 167 B.3. Structure Bool......................................... 169 B.4. Structure Char......................................... 169 B.5. Structure General....................................... 173 B.6. Structure Int.......................................... 178 B.7. Structure List......................................... 180 B.8. Structure ListPair....................................... 182 B.9. Structure Listsort....................................... 184 B.10. Structure Math........................................ 184 B.11. Structure Meta........................................ 185 B.12. Structure Option........................................ 189 B.13. Structure Random....................................... 190 B.14. Structure Real......................................... 191 B.15. Structure Regex........................................ 193 B.16. Structure Splaymap...................................... 198 B.17. Structure Splayset....................................... 199 B.18. Structure String........................................ 201 B.19. Structure StringCvt...................................... 203 B.20. Structure TextIO....................................... 204 B.21. Structure Time........................................ 208 B.22. Structure Timer........................................ 210 B.23. Structure Word........................................ 211 B.24. Structure Word8........................................ 214
1. fejezet Bevezetés Ez a jegyzet oktatási segédletként a Deklaratív programozás c. tárgy funkcionális programozással foglalkozó részéhez készült. 1.1. A programozási paradigmákról A programozási paradigma 1 viszonylag újkeletű szakkifejezés (terminus technicus). A paradigma az Idegen szavak és kifejezések szótára 2 szerint görög-latin eredetű, és két jelentése is van: bizonyításra vagy összehasonlításra alkalmazott példa; (nyelvtani) ragozási minta. Az Akadémiai Kislexikon 3 a fentieken túl még egy, a mi szempontunkból fontos jelentését említi: valamely tudományterület sarkalatos megállapítása. Programozási paradigmának nevezzük: 1. azt a módot, ahogyan a programozási alapfogalmakat felhasználják valamely programozási nyelv létrehozására; ill. 2. azt a programozási stílust, amelyet valamely programozási nyelv sugall. A programozási paradigmáknak két alaptípusa van: imperatív és deklaratív. 1.2. A monolitikus és a strukturált programozásról Minden programnak, legyen szó bármilyen stílusról, van valamilyen szerkezete. E tekintetben különbséget kell tennünk a monolitikus és a strukturált (vagy moduláris) programozás között. A monolitikus program lineáris szerkezetű, azaz a programszövegben egymás után álló programelemeket rendszerint az adott sorrendben kell végrehajtani vagy kiértékelni, és nincsenek benne bonyolultabb adatszerkezetek; egyetlen fordítási egység, azaz változás esetén a teljes programszöveget újra kell fordítani. 1 1999-ig a most Deklaratív programozásnak nevezett tantárgynak Programozási paradigmák volt a neve. 2 Akadémiai Kiadó, Budapest 1989 3 Akadémiai Kiadó, Budapest 1990 7
8 1. FEJEZET. BEVEZETÉS Nyilvánvaló, hogy ilyen stílusban nem lehet nagyméretű programokat készíteni. A hatvanas évek közepén mozgalom indult a strukturált programozási elvek elfogadtatására, a megfelelő programozási nyelvek és fordítóprogramok kidolgozására és elterjesztésére, a szükséges elméleti és módszertani háttér kimunkálására. A strukturált, más néven moduláris programot elsősorban eljárások, függvények, biztonságos vezérlési szerkezetek, elemi és összetett adattípusok, absztrakt adattípusok, osztályok, objektumok, valamint önálló fordítási egységek, kapcsolatleírások, generikus programrészek megjelenése, használata jellemzi. A több évtizedes tapasztalatok és kutatások megváltoztatták a programozás mibenlétéről kialakult képet: egyre nagyobb jelentőséget tulajdonítunk a követelmények elemzésének, a (formális) specifikációnak, a módszeres és szabványos tervezésnek és dokumentálásnak, a programhelyességnek, a karbantartásnak, a módosíthatóságnak, a változáskövetésnek, a hordozhatóságnak, a minőségnek, és egyre kisebbet magának a kódolásnak. E tekintetben itt elsősorban a specifikáció és a megvalósítás, a mit és a hogyan szétválasztásának fontosságát emeljük ki. 1.3. Az imperatív programozási paradigma Az imperatív 4 (más néven procedurális) programozási paradigma a legelterjedtebb, a legrégibb; erősen kötődik a Neumann-féle számítógép-architektúrához. Két fő jellemzője a parancs és az állapot. A program állapottere az a sokdimenziós tér, amelyet a program változóinak értelmezési tartománya határoz meg; a program pillanatnyi állapotát változóinak pillanatnyi tartalma írja le. A program állapotát értékadással azaz a változók frissítésével változtathatjuk meg. Állapotváltozás nélkül körülményes modellezni az időt, a valós világ jelenségeit, a ki- és beviteli műveleteket. 5 Az imperatív paradigmán belül sajátos stílus jellemzi többek között a szekvenciális, a valós (azonos, ill. kötött) idejű, a párhuzamos és elosztott, valamint az objektum-orientált programozást. A szekvenciális programozás mindennek az alapja, hiszen pl. bármely párhuzamos program szekvenciális programrészekből áll. A parancsokból, mint jól tudjuk, vezérlési szerkezetek felsorolás, választás, ismétlés felhasználásával összetett parancsokat, absztrakcióval pedig eljárásokat hozhatunk létre; ezért szoktunk az imperatív programozásról mint procedurális programozásról beszélni. A valós idejű programozás erősen kötődik a párhuzamos és elosztott programozáshoz, ui. valós idejű rendszerekben egyes programrészeket rendszerint egyidejűleg, egymással párhuzamosan kell végrehajtani. Párhuzamos végrehajtásra ugyanakkor más esetekben, pl. numerikus számítások elvégzéséhez, aritmetikai kifejezések kiértékelésekor is szükség lehet. A ma oly divatos objektum-orientált programozás is az imperatív programozási paradigma egyik válfaja, szoros rokonságban az absztrakt adattípusokra épülő programozással. Imperatív stílusú programozás esetén a programozónak tudatosan törekednie kell a mit és a hogyan módszeres szétválasztására. A ma legelterjedtebb programozási nyelvek közül a FORTRAN, a COBOL és az eredeti Pascal alig, a Turbo és a Borland Pascal, a Delphi és a C inkább, az Ada, a Modula, a C++ és a Java még inkább támogatja e szétválasztást. Azonban sikerüljön bármilyen jól a szétválasztás, a feladatot megoldó algoritmusok megírása a programozó dolga marad. 4 Latin szó, jelentése: parancsoló (vö. imperativus, imperátor). 5 Egyes tiszta funkcionális programozási nyelvek, pl. a haskell és a clean speciális nyelvi elemekkel oldják meg az állapotváltozás nélküli programozást.
1.4. A DEKLARATÍV PROGRAMOZÁSI PARADIGMA 9 1.4. A deklaratív programozási paradigma Az imperatív stílussal ellentétben a deklaratív 6 stílusban programozónak elvileg csak azt kell megmondania, hogy mit akarunk, az algoritmust az értelmező- vagy fordítóprogram állítja elő. A deklaratív programozás két válfaját szokás megkülönböztetni: a logikai és a funkcionális programozást. 7 1.4.1. A logikai programozási paradigma A programozási paradigmák közül, amint a neve is mutatja, ez a paradigma kötődik a legerősebben a matematikai logikához. Jellemzői: a tények, a szabályok, és a következtetőrendszer. A legelterjedtebb logikai programozási nyelv a Prolog. Professzionális, gyakorlati feladatok megoldására alkalmas megvalósításai a deklaratív nyelvi elemek mellett imperatív elemeket is tartalmaznak. Természetesen más logikai programozási nyelvek is vannak, pl. az OPS5 vagy a Mercury. (Az utóbbi a Prologtól átvett logikai programozási elemeket a típusfogalommal és a funkcionális programozást támogató nyelvi elemekkel egészíti ki.) 1.4.2. A funkcionális programozási paradigma A funkcionális programozás két fő jellemzője az érték és a függvényalkalmazás. A funkcionális programozás nevét a függvények kitüntetett szerepének köszönheti. A tisztán funkcionális programozási nyelvek a matematikában megszokott függvényfogalmat valósítják meg: a függvény egyértelmű leképzés a függvény argumentuma és eredménye között, a függvény alkalmazásának nincs semmilyen más hatása. Tisztán funkcionális programozás esetén tehát nincs állapot, nincs (mellék)hatás, nincs értékadás. Funkcionális program pl. az e e1 kifejezés, ahol az e-nek függvényértéket 8 eredményező kifejezésnek kell lennie, az e1 pedig tetszőleges kifejezés lehet. A matematikában megszokott módon azt mondjuk, hogy az e függvényt (vagy kissé körülményesebben: az e függvényértéket adó kifejezést) alkalmazzuk az e1 argumentumra. Függvények alkalmazásáról lévén szó, funkcionális helyett szinonimaként gyakran applikatív 9 programozásról beszélünk. Az applikatív programozás elmélete a λ-kalkulus (lambda-kalkulus), az a függvényelmélet, amelyet Alonzo Church az 1930-as években dolgozott ki, majd Moses Schönfinkel és Haskell Curry fejlesztett tovább. A λ-kalkuluson alapuló első funkcionális programozási nyelvet, a LISP-et (LISt Programming) John McCarthy dolgozta ki az 1950-es évek közepén, az 1960-as évek elején. A sokféle változat közül a professzionális célokra alkalmazható Common LISP a legismertebb. A LISP-dialektusok és modernebb utódjuk, a Scheme is típus nélküli nyelvek. Az első típusos funkcionális nyelv az ML (Meta Language) egyik korai változata volt a 70-es évek közepén, amelyben Robin Milner megvalósította típuselméleti eredményeit. Eredetileg logikai állítások igazolására, tételbizonyításra tervezték, erre utal a nem túl ötletes Meta Language elnevezés is. A HOPE-pal 6 Ugyancsak latin szó, jelentése: kijelentő, kinyilatkoztató (vö. deklaráció). 7 Vannak, akik a deklaratív programozást a logikaival azonosítják, és a funkcionális programozást nem tekintik deklaratívnak. 8 A függvényérték olyan érték, amely függvényként más értékre alkalmazható. 9 Szintén latin szó, jelentése: alkalmazó (vö. applikáció).
10 1. FEJEZET. BEVEZETÉS és más funkcionális nyelvekkel szerzett tapasztalatok alapján dolgozták ki a Standard ML (SML) nyelvet a 80-as évek közepétől kezdve. Számos megvalósítása készült el különféle számítógépekre, és természetesen megjelentek különféle dialektusai is, pl. a Caml. A SML-családba tartozó nyelvek, kevés kivétellel, ún. mohó kiértékelést, azaz érték szerinti paraméterátadást alkalmaznak. Ez azt jelenti, hogy amikor egy függvénykifejezést alkalmazunk egy argumentumra, akkor az SML-értelmező először az argumentumot értékeli ki, és csak ezután lát hozzá a függvénykifejezés kiértékeléséhez. A Miranda, az 1990-ben megjelent Haskell, és a még újabb Clean nyelv ezzel szemben lusta kiértékelést használ. A lusta kiértékelés az 1960-as években az Algol nyelvben alkalmazott név szerinti paraméterátadás modern leszármazottja; nem tévesztendő össze a Pascalban, a C-ben és más nyelvekben használt cím szerinti paraméterátadással. Megjegyzendő, hogy az SML egyik legújabb kiterjesztése, az Alice lusta kiértékelésű értékek deklarálását is lehetővé teszi. Az SML akárcsak a körülményes szintaxisú, típus nélküli Common LISP gyakorlati programozási feladatok megoldására készült, ezért nemcsak a tisztán funkcionális, hanem az imperatív stílusú programozáshoz szükséges nyelvi elemek is megtalálhatók benne: frissíthető változók, tömbök, mellékhatással járó függvények stb., továbbá a nagybani programozást segítő fejlett modulrendszere van. 1.4.2.1. A read-eval-print ciklus Az SML-t, más deklaratív nyelvekhez hasonlóan, rendszerint értelmezőprogrammal (interpreterrel) valósítják meg: az értelmezőprogram a kifejezéseket beolvassa és kiértékeli, majd kiírja az eredményt, és azután ismét a beolvasással folytatja (ezt nevezik read-eval-print ciklusnak). 1.4.2.2. Hivatkozási átlátszóság Az ún. hivatkozási átlátszóság (referential transparency) megléte vagy hiánya fontos jellemzője a programozási nyelveknek. Ha egy programnyelv, mint pl. az SML, rendelkezik ezzel a tulajdonsággal, akkor ez azt jelenti, hogy egyenlők helyettesíthetők egyenlőkkel, pl. egy kifejezés az értékével, az E 1 + E 2 kifejezés az E 2 + E 1 kifejezéssel (ahol a + jel a kommutatív aritmetikai összeadást jelenti). A hivatkozási átlátszóság megléte esetén egy kifejezés értelme, jelentése egyszerűen a kiértékelésének az eredménye, és ezért egyes részkifejezéseit egymástól függetlenül lehet kiértékelni. Ezzel szemben egy parancs végrehajtása azt jelenti, hogy a program állapota megváltozik, vagyis a parancs megértéséhez meg kell érteni a parancs hatását a program teljes állapotterére. 1.4.2.3. A funkcionális program A funkcionális program: mennyiségek közötti kapcsolatokat leíró egyenletek halmaza. Pl. a square(x) = x * x megfelelő alakú egyenlet, ún. kiszámítási szabály. Ezzel szemben az sqrt(x) * sqrt(x) = x alakú egyenlet csak deklarálja a kívánt tulajdonságokat, kiszámításra nem, csupán ellenőrzésre alkalmas. 1.5. SML-értelmezők és fordítók Az SML-nyelvnek két szintje van. A nyelv magját az alapnyelv (Core Language) képezi, a nagyobb programok írását a modulnyelv (Module Language) támogatja. A nyelvbe beépített elemeket gazdag és egyre bővülő Alapkönyvtár (Basis Library) egészíti ki. A SML-nyelv és az Alapkönyvtár definícióját legutóbb 1997-ben vizsgálták fölül. Az első ML-értelmezőt 1977-ben írták az edinborough-i egyetemen. Az évek során számos értelmező és fordítóprogram készült el, egy részük licencköteles, más részük szabadon használható. Az utóbbiak körül négyet ajánlunk az olvasó figyelmébe. Mind a négy használható egyebek mellett linux, WinNT, Win2k és WinXP alatt is.
1.5. SML-ÉRTELMEZŐK ÉS FORDÍTÓK 11 A kis erőforrásigényű mosml (Moscow SML) legújabb, 2.x változata az alapnyelvet és a modulnyelvet teljes egészében megvalósítja, sőt az utóbbit új elemekkel egészíti ki. Alapkönyvtára is folyamatosan bővül, a már elkészült modulok kielégítik az 1997-es definíciót. A viszonylag erőforrásigényes smlnj (SML of New Jersey) a teljes SML-nyelvet, azaz a szabványos alapnyelv mellett az ugyancsak szabványos modulnyelvet, valamint az 1997-es definíciónak megfelelő alapkönyvtárat valósítja meg. A kis erőforrásigényű Poly/ML ugyancsak a teljes SML-nyelvet, azaz a szabványos alapnyelv mellett az ugyancsak szabványos modulnyelvet, valamint az 1997-es definíciónak megfelelő alapkönyvtárat valósítja meg. Előnye a többi SML-értelmezővel szemben, hogy a programhibák megtalálását nyomkövető (trace) és hibakereső (debugger) funkcióval segíti. A viszonylag erőforrásigényes Alice a teljes SML-nyelvet, azaz a szabványos alapnyelv mellett az ugyancsak szabványos modulnyelvet, valamint az 1997-es definíciónak megfelelő alapkönyvtárat megvalósítja, és ezeken felül számtalan új elemmel egészíti ki mindhármat. Többek között lehetővé teszi a lusta kiértékelést, a párhuzamos és elosztott programozást, a korlát-alapú programozást, valamint a dinamikus kötést. Az SML-nyelvvel most ismerkedők igényeinek a kis erőforrásigényű mosml mindenben megfelel. Az SML-értelmezőknek van egy nagy hátránya: a kezelői felületük írógépszerű, alig van mód az elütések javítására, és nincs lehetőség a korábban leírt sorok előhívására. Ezen az emacs szövegszerkesztő SML-progamozást támogató környezete segít, nevezetesen az SML-mód. (A Prolog-értelmezők kényelmes használatához ugyancsak az emacs-ra, mégpedig az emacs Prolog-módjára van szükség.) Windows alatt vannak más olyan programok is, amelyek az értelmezők kényelmesebb kezelését teszik lehetővé. A tárgy előadói az emacs használatát preferálják. A funkcionális nyelvek általában interaktívak, megvalósításukra értelmezőprogramot (interpretert) írnak. Az SML-értelmezők és a programozók többek között a készenléti jel, a folytatójel, a kiértékelőjel és a válaszjel révén társalognak egymással. Az alábbi táblázatban a bal oldali oszlopban az mosml, ill. az smlnj által használt jeleket adjuk meg: - a sor elején álló készenléti jel (prompt): az SML új kifejezés begépelésére vár, ; a bevitelt záró kiértékelőjel: hatására megkezdődik a kiértékelés, = a sor elején álló folytatójel: az SML a megkezdett kifejezés folytatására vagy lezárására (a kiértékelőjelre) vár (az smlnj-ben; az mosml-ben a folytatósort nem jelzi külön jel), > a sor elején álló válaszjel: az SML válaszát jelöli (az mosml-ben; az smlnj-ben a válaszsort nem jelzi külön jel). Az mosml, az smlnj, a Poly/ML és az Alice válaszai és főleg hibaüzenetei különböznek egymástól. Ebben a jegyzetben rendszerint az mosml 2.01 verziójának válaszait és hibaüzeneteit adjuk meg, és általában utalunk rá, ha ettől valamilyen ok miatt eltérünk. Az SML-ből kilépni a készenléti jelre adott többféle válasszal lehet: a quit() függvényhívással, Windows alatt a ctrl-z, majd az enter leütésével, unix (linux) alatt a ctrl-d leütésével, a Process.exit arg függvényhívással, ahol arg-nak Process.status típusú értéknek kell lennie.
12 1. FEJEZET. BEVEZETÉS Az SML-értelmező kalkulátorként is használható, pl. - 2+2; > val it = 4 : int - 3.2-2.3; > val it = 0.9 : real - Math.sqrt 2.0; > val it = 1.41421356237 : real Math.sqrt a Math könyvtárbeli sqrt függvényt jelöli. Egyes SML-könyvtárak tartalmát a B. függelékben ismertetjük. 1.6. Információforrások A funkcionális programozásnak magyar nyelvű irodalma alig van, az SML-nek e jegyzeten és korábbi kiadásain kívül egyáltalán nincs. Az angol nyelvű könyvek közül különösen a következőket javasoljuk: 1. Jeffrey D. Ullman: Elements of ML Programming (2nd Edition, ML97). MIT Press 1997. <http://www-db.stanford.edu/~ullman/emlp.html> 2. Lawrence C Paulson: ML for the Working Programmer (2nd Edition, ML97). Cambridge University Press 1996. ISBN: 0-521-56543-X (paperback), 0-521-57050-6 (hardback). <http://www.cl.cam.ac.uk/users/lcp/mlbook/> 3. Hal Abelson, Jerry Sussman, Julie Sussman: Structure and Interpretation of Computer Programs (MIT Press, 1984; ISBN 0-262-01077-1) <http://mitpress.mit.edu/sicp> Letölthető a teljes könyv elektronikus változata! 4. Richard Bosworth: A Practical Course in Functional Programming Using Standard ML. McGraw- Hill 1995. ISBN: 0-07-707625-7. Az on-line információforrások közül javasoljuk a következőket: 1. Andrew Cumming: A Gentle Introduction to ML. <http://www.dcs.napier.ac.uk/course-notes/sml/manual.html> 2. Stephen Gilmore: Programming in Standard ML 97: An On-line Tutorial. <http://www.dcs.ed.ac.uk/home/stg/notes> 3. Hal Abelson, Jerry Sussman, Julie Sussman: Structure and Interpretation of Computer Programs <http://mitpress.mit.edu/sicp> 4. Robert Harper: Programming in Standard ML <http://www-2.cs.cmu.edu/ rwh/smlbook/offline.pdf> 5. Gerd Smolka: Programmierung. Eine Einführung in die Informatik. <http://www.ps.uni-sb.de/courses/prog-ws04/script/index.html> 6. COMP.LANG.ML Frequently Asked Questions and Answers. <http://www.faqs.org/faqs/meta-lang-faq/> Az mosml, az smlnj, a Poly/ML és az Alice honlapjának címe: 1. Moscow ML: <http://www.dina.kvl.dk/~sestoft/mosml.html> 2. Standard ML of New Jersey: <http://www.smlnj.org/> 3. Poly/ML: <http://www.polyml.org/> 4. Alice: <http://www.ps.uni-sb.de/alice>
1.7. VÁLTOZÁSOK AZ ELŐZŐ KIADÁSHOZ KÉPEST 13 1.7. Változások az előző kiadáshoz képest A jegyzet 4. kiadásához képest a fontosabb változások a következők: A bevezető egyszerű SML-példákat bemutató szakasza bővült és önálló fejezet lett. A korábbi Kifejezések c. fejezet első szakasza átkerült az átszerkesztett Nevek, függvények, egyszerű típusok c. fejezetbe. A korábbi Kfejezések c. fejezet második szakaszából és a korábbi Lokális kifejezés, lokális és egyidejű deklaráció c. fejezetből Kiértékelés, deklaráció címmel egy fejezet lett. A korábbi Lusta lista c. fejezet helyébe lépő Lusta kifejezések c. fejezet az Alice-nyelv bővített SMLszintaxisát alkalmazza. A korábbi Polimorfizmus c. fejezet két szakasza az átszerkesztett Ennesek, rekordok, polimorf típusok, harmadik szakasza a Listák c. fejezetbe került át. A korábbi Részlegesen alkalmazható függvények c. fejezet anyaga bekerült a Magasabbrendű függvények c. fejezetbe. Új az Egy egyszerű fordítóprogram SML-ben c. fejezet. A Válogatás az SML Alapkönytárából c. fejezet további struktúrák Binarymap, Binaryset, ListPair, Random, Regex, Splaymap, Splaytree és StringCvt rövid leírását tartalmazza. A többi fejezet szövegében és szerkezetében is vannak kisebb-nagyobb változások. 1.8. Köszönetnyilvánítás Köszönet illeti az ML for the Working Programmer c. könyv szerzőjét, Lawrence C. Paulsont: a könyvből sok érdekeset tanultam az SML-ről, többek között a jegyzetben bemutatott példák és megoldások jó része is ebből a könyvből származik; a Moscow ML értelmező/fordító program szerzőit, Peter Sestoftot és Szergej Romanyenkót az oktatási célra (is) kitűnő SML-megvalósításért. 1.9. Hibajelentés A szerző köszönettel fogad a hanak@inf.bme.hu címre érkező bármilyen (sajtóhibákra, tartalomra vonatkozó) észrevételt a jegyzettel kapcsolatban.
2. fejezet Egyszerű példák SML-ben Ebben a fejezetben, ízelítőként, néhány egyszerű programot mutatunk be SML-ben. 2.1. Egész szám négyzete fun square x = x * x; val square = fn : int -> int A square függvény típusa: int -> int. A square : int -> int kifejezést a square függvény szignatúrájának nevezzük. Alapértelmezés szerint az aritmetikai műveletekben az operandusoknak int a típusa. 2.2. Legnagyobb közös osztó Nézzük a jól ismert euklideszi algoritmus egy megvalósítását a legnagyobb közös osztó kiszámítására! Matematikai definíciója (feltesszük, hogy 0 m n): gcd(0,n) = n gcd(m,n) = gcd(n mod m,m), ha m > 0 Egy lehetséges kódolása Pascalban: function gcd(m, n: integer): integer; var prevm: integer; begin while m <> 0 do begin prevm := m; m := n mod m; n := prevm end; gcd := n end (* gcd ; és egy változata SML-ben: fun gcd (m, n) = if m=0 then n else gcd(n mod m, m); Az utóbbihoz hasonló programot Pascalban is lehet írni, csakhogy a Pascalban kevésbé hatékony a rekurzió megvalósítása, és több töltelékszöveget kell írnunk: 14
2.3. INTERVALLUMÖSSZEG 15 function gcd (m,n: integer): integer; begin if m = 0 then gcd := n else gcd := gcd (n mod m, m) end; SML-ben az eredeti matematikai definícióra nagyon hasonlító programot is írhatunk: fun gcd(0, n) = n gcd(m, n) = gcd(n mod m, m); mod az egészosztás maradékát adja eredményül. 2.3. Intervallumösszeg Adott az s 1 egész szám. Határozzuk meg azt a lehető leghosszabb [i, j] zárt intervallumot, amelyre 1 i j és az s az [i,j] intervallumba eső számok összegével egyenlő. Kézenfekvőnek látszik a következő algoritmus: az intervallum alsó és felső határát is 1-ről indítjuk. Egy ciklusban addig növeljük a felső határt, amíg az intervallumba eső számok összege kisebb s-nél, ill. addig növeljük az alsó határt, amíg a számok összege nagyobb s-nél. Ciklus helyett rekurzív segédfüggvényt használunk az SML-ben. Ahelyett, hogy az intervallumba eső számokat minden lépésben újból és újból összeadnánk, akkumulátort (másnéven gyűjtőargumentumot) használunk az összeg képzésére. Valahányszor az alsó határt növeljük meg, az értékét kivonjuk akkumulátorból, és valahányszor a felső határt növeljük meg, az értékét hozzáadjuk az akkumulátorhoz. intvalsum két számpárt adjon eredményül: az első számpár a zárt intervallum alsó és felső határa, a második számpár első tagja az intervallum hossza, második tagja pedig a rekurzív hívások a szükséges lépések száma legyen. Ha az s < 1 argumentumra alkalmazzuk az intvalsum függvényt, ((0, 0), (0, 0)) legyen a visszaadott érték. (* ivs (s, i, j, t, n) = ((0, 0), (0, 0)) s < 1-re, egyébként ((a, b), (c, d)), ahol [a,b] az a lehető leghoszabb zárt intervallum, amely elemeinek összege s, hossza c, a meghatározásához szükséges rekurzív hívások száma pedig d ivs : int * int * int * int * int -> (int * int) * (int * int) PRE : (i, j, t, n) = (1, 1, 1, 1,) fun ivs (s, i, j, t, n) = if s < 1 then ((0, 0), (0, 0)) else if t < s then ivs (s, i, j+1, t+j+1, n+1) else if t > s then ivs (s, i+1, j, t-i, n+1) else ((i, j), (j-i+1, n+1)); A PRE szócska az algoritmus helyes működésének előfeltételét (precondition) adja meg. (* intvalsum s = ((0, 0), (0, 0)) s < 1-re, egyébként ((a, b), (c, d)), ahol [a,b] az a lehető leghoszabb zárt intervallum, amely elemeinek összege s, hossza c, a meghatározásához szükséges rekurzív hívások száma pedig d
16 2. FEJEZET. EGYSZERŰ PÉLDÁK SML-BEN intvalsum : int -> ((int * int) * (int * int)) fun intvalsum s = ivs(s, 1, 1, 1, 1); A kézenfekvő megoldásnak bizonyos esetekben elég rossz a hatékonysága. Például s = 1 + 2 + 3 +... + (n 1) + n = (1 + n) n/2 alakú számok esetén csupán n lépésre van szükség, mert csak a felső határt kell növelni, az alsó határ változatlan marad. Ezzel szemben egyelemű intervallumok esetén, ahol s = i = j (ilyen szám például 8192, 67108864 = 8192 8192 és 268435456 = 16384 16384 is) a szükséges lépések száma 2s, mert mindkét határt addig kell növelni, amíg kisebbek s-nél. Az utóbbi két intervallum meghatározása még a mai gyors processzorokat is alaposan igénybe veszi... Nézzük, mit kapunk eredményül az említett esetekben: intvalsum 8192; > val it = ((8192, 8192), (1, 16384)) : (int * int) * (int * int) intvalsum 67108864; > val it = ((67108864, 67108864), (1, 134217728)) : (int * int) * (int * int) intvalsum 268435456; > val it = ((268435456, 268435456), (1, 536870912)) : (int * int) * (int * int) Az 536 870 912 lépés megtételéhez még egy 1.3 GHz-es CPU-n is kb. egy percre volt szüksége az mosmlnek. Az smlnj egy nagyságrenddel gyorsabban, mindössze 6 s alatt állította elő az eredményt. Van mit javítani az algoritmus hatékonyságán! Nézzük, mit tehetünk. A lépések számát radikálisan csökkenthetjük, ha legfeljebb annyi lépést teszünk meg, amennyi a keresett intervallum hossza. Az azonos összegű intervallumok közül az lesz a leghosszabb, amelynek az alsó határa a lehető legkisebb, mert ilyenkor összegezzük a lehető legkisebb számokat. A keresett intervallum hosszának tehát van felső korlátja: annak az intervallumnak a hossza, amely 1-től kezdődik, m-ig tart, és az elemeinek összege nem kisebb s-nél. Képlettel: (1 + m) m/2 s, azaz m 2 +m 2s, azaz m 2s m. Ha felső korlátnak az f = 2s számot választjuk, legfeljebb néhány lépéssel kell többet megtennünk, cserébe egyszerűbb lesz az r kiszámítása. Az egész számok körében maradva az f-et az alábbi lenlimit függvénnyel számíthatjuk ki: (* lenlimit (s, h) = felső korlát az s összegű zárt intervallum hosszára lenlimit : int * int -> int PRE : f = 1 fun lenlimit (s, h) = if h*h div 2 < s then lenlimit(s, h+1) else h-1; A h*h < 2*s helyett a h*h div 2 < s kifejezést számítjuk ki a függvényben, mert így valamivel később ütközünk az egész számok ábrázolásának felső korlátjába. div az egészosztás hányadosát adja eredményül. Ezek után a feltételt kielégítő intervallumot a lehető leghosszabbtól kezdve kereshetjük. Amíg nem találjuk meg, minden lépésben eggyel csökkentsük a jelölt h hosszát, és vizsgáljuk meg a k = s (1 + h) h/2 különbséget! Akkor van meg a megoldás, amikor a k a h egész számú többszörösévé válik, mert ilyenkor az intervallumot k/h-val felfelé eltolva valóban a lehető leghosszabb s összegű intervallumot kapjuk.
2.4. PÉNZVÁLTÁS 17 (* ivs (s, h, n) = ((0, 0), (0, 0)) s < 1-re, egyébként ((a, b), (c, d)), ahol [a,b] az a lehető leghoszabb zárt intervallum, amely elemeinek összege s, hossza c, a meghatározásához szükséges rekurzív hívások száma pedig d ivs : int * int * int * int * int -> (int * int) * (int * int) PRE : n = 1, h = felső korlát az s összegű zárt intervallum hosszára fun ivs (s, h, n) = if s < 1 then ((0, 0), (0, 0)) else let val k = s - h * (h+1) div 2 in if k mod h <> 0 then ivs(s, h-1, n+1) else ((k div h + 1, k div h + h), (h, n)) end; Az s - h * (h+1) div 2 kifejezés értékére többször is szükség van a függvény törzsében, ezért előre kiszámítjuk és elnevezzük k-nak. mod az egészosztás maradékát adja eredményül. k mod h <> 0 helyett vizsgálhatnánk a k - k div h * h > 0 feltételt is. fun intvalsum s = ivs(s, lenlimit(s, 1), 1); Nézzük, mit kapunk most eredményül a korábban már vizsgált esetekben! intvalsum 8192; > val it = ((8192, 8192), (1, 127)) : (int * int) * (int * int) intvalsum 67108864; > val it = ((67108864, 67108864), (1, 11585)) : (int * int) * (int * int) intvalsum 268435456; > val it = ((268435456, 268435456), (1, 23170)) : (int * int) * (int * int) A megfelelő algoritmus a megoldást több nagyságrenddel kevesebb lépésben, egy szemvillanásnyi idő alatt állítja elő. A tanulság az lehet, hogy a programjaink végrehajtási idejét nem a rekurzió használata, hanem az ügyetlenül megválasztott algoritmusok viselkedése növeli meg tetemesen. 2.4. Pénzváltás Adott különböző címletű pénzérmék érték szerint csökkenő sorrendű listája, pl. [20, 10, 5, 2, 1] Most olyan SML-programot írunk, amely tetszőleges összeget apróra vált, és az eredményt ugyancsak listaként adja vissza! Feltesszük, hogy a megadott összeg mindig felváltható, azaz az érmék között van 1-es értékű. Az SML-program tanulmányozása előtt azt javasoljuk az olvasónak, hogy oldja meg a feladatot valamilyen általa ismert programozási nyelven. A feladatot érdemes rekurzió alkalmazásával megoldani. Két esetet kell megkülönböztetnünk:
18 2. FEJEZET. EGYSZERŰ PÉLDÁK SML-BEN 1. a felváltandó összeg 0, 2. a felváltandó összeg nem 0. 1 Az 1. (triviális) esetben semmit nem kell tennünk, a feladat meg van oldva. A 2. esetben megpróbáljuk visszavezetni a feladatot egy már ismert részfeladatra. fun change(0, coins) = [] change(sum, coin :: coins) = if sum >= coin then coin :: change(sum - coin, coin :: coins) else change(sum, coins); > val change = fn : int * int list -> int list Nézzük a jelöléseket! A példában fun, if, then, else, val és fn a nyelv kulcsszavai, betűvel írt terminális szimbólumai. A [], más néven nil az üres lista (üres sorozat) jele. A (vonás) klózokat választ el egymástól. A :: (négyespont) a bal oldalán álló elemet fűzi a jobb oldalán álló listához. A fun kulcsszó után a definiálandó függvény neve (most: change) áll, a nevet egy vagy több (most két) paraméter követi. Később látni fogjuk, hogy az SML-ben minden függvényt egyparaméteresnek tekintünk. A paraméterek megengedett értékei alapján mintaillesztéssel választunk az esetek közül. Ha a paraméter konkrét érték (pl. most 0), akkor az SML-értelmező az adott klózt csak akkor hajtja végre, ha a függvényt pontosan ilyen értékű argumentummal hívjuk meg. Ha a paraméter név (más szóval azonosító, most pl. coins vagy sum), akkor az tetszőleges mintára illeszkedik. A (coin :: coins) olyan összetett minta, amely legalább egyelemű (most: egész számokból álló) listára illeszkedik: coin-nak egy elemre, coins-nak egy esetleg üres listára kell illeszkednie. (Jelölésrendszerünket később egyszerűsíteni fogjuk.) A program helyessége is könnyen belátható. Ha a felváltandó összeg 0, az eredmény az üres lista. Ha a felváltandó összeg nem 0, két további esetet kell megkülönböztetnünk az if-then-else feltételes operátor alkalmazásával. (Használhatnánk-e itt mintaillesztést? Használhatnánk-e feltételes kifejezést mintaillesztés helyett a 0 és a nem 0 esetek megkülönböztetésére?) Ha a felváltandó összeg (sum) nem kisebb a soron következő címletnél (coin), akkor ez jó érték, és be kell rakni annak az eredménylistának az elejére, amelyet úgy kapunk, hogy a maradék összeget (sum - coin) is megpróbáljuk felváltani ugyanilyen és nála kisebb értékű érmékkel (coin::coins). De ha a felváltandó összeg kisebb a soron következő címletnél, akkor ez nem jó érték, és a váltást a soron következő címlettel kell megpróbálni. Jegyezzük meg, hogy a fenti függvénydefinícióban a klózok sorrendje nem közömbös, mivel a sum azonosító minden egész típusú mintára, így a 0 állandóra is illeszkedik. A kifejezések kiértékelési sorrendje balról jobbra, fentről lefelé garantálja, hogy ha az aktuális paraméter illeszkedik a 0 mintára, akkor a sum mintát tartalmazó klózra ne kerüljön sor. 1 Feltesszük, hogy felváltandó összegnek csak nemnegatív számot adunk meg. Később látni fogjuk, hogy mit tehetünk a hibás bemeneti adatok kiszűréséért.
3. fejezet Nevek, függvények, egyszerű típusok 3.1. Értékdeklaráció Deklaráció: valamilyen értéknek (pl. egésznek, valósnak, karakternek, füzérnek, függvénynek), típusnak, szignatúrának, struktúrának, funktornak stb. nevet adunk, kötést hozunk létre. 1 Az SML-ben a kötés statikus: fordítási időben jön létre a név és az érték között. (A futási időben létrejövő dinamikus kötés az objektum-orientált programozási nyelvek jellemzője.) 3.1.1. Névadás állandónak Egy állandó lehet tartós állandó (pl. π, pi), átmeneti állandó (pl. valamilyen részeredmény). SML-példák (az SML-értelmező válaszát nem minden esetben adjuk meg): - val seconds = 60; > val seconds = 60 : int - val minutes = 60; - val hours = 24; - seconds * minutes * hours; > val it = 86400 : int A 60, a 24 és a 86400 tovább nem egyszerűsíthető, ún. kanonikus kifejezések. Az SML-értelmező válasza minden esetben kanonikus kifejezés. Van egy kitüntetett szerepű azonosító, az it, amely mindig a legfelső szintű kifejezés értékét veszi fel. A fenti kifejezéssorozat kiértékelése után pl. az it értéke 86400. - it; > val it = 86400 : int - it div 24; > val it = 3600 : int it értékét elrakhatjuk későbbre, pl. - val secsinhour = it; > val secsinhour = 3600 : int 1 Az SML-ben rendszerint nem teszünk olyan éles különbséget definíció és deklaráció között, mint a C-ben. 19
20 3. FEJEZET. NEVEK, FÜGGVÉNYEK, EGYSZERŰ TÍPUSOK A nevekben a kis- és nagybetűk, a decimális számjegyek, az aláhúzás-jel (_) és a percjel (, más néven felülvessző, aposztróf) használhatók. Jegyezzük meg, hogy az SML különbséget tesz a kis- és nagybetűk között! 3.1.2. Névadás függvénynek Legyen 2 - val pi = 3.14159; akkor - val r = 2.0; - val area = pi * r * r; > val area = 12.56636 : real vagy függvényként - fun area (r) = pi * r * r; > val area = fn : real -> real ahol r a (formális) paraméter, pi * r * r pedig a függvény törzse. Az SML-ben a függvény maga is: érték! A fun definíció tulajdonképpen rövidítés, az értékdefiníció egy változata. Az area függvényt így is definiálhatjuk: - val area = fn r => pi * r * r; > val area = fn : real -> real Talán meglepő, de az fn r => pi * r * r maga is kanonikus kifejezés, hiszen tovább nem egyszerűsíthető! 3 Egy függvény argumentumának típusa mint halmaz tartalmazza az értelmezési tartományát (domain), eredményének típusa mint halmaz pedig az értékkészletét (range). 4 Gyakran előfordul ugyanis, hogy az argumentum típusa által megengedett értékek egy részére a függvény nincs értelmezve (pl. az egész számokra értelmezett div függvény, ha 0 az osztója), vagy az eredmény típusa által megengedett értékek közül nem mindet állítja elő a függvény (pl. az egész típusú eredményt adó sqrt, amely csak nemnegatív eredményt állíthat elő). A függvényt leképezésnek, transzformációnak (angolul mappingnek) is nevezzük. A függvény típusa adja meg, hogy milyen típusú értéket milyen típusú értékké képez le. Pl. az area függvény típusa: real -> real. Vegyük észre, hogy a függvény eredményének típusa nem azonos a függvény típusával! Amikor az SML-ben egy függvényt egy argumentumra alkalmazunk, az argumentumként megadott kifejezést csak akkor kell zárójelbe tenni, ha erre precedenciaokok miatt szükség van. Helyesek tehát az alábbi példák: - area(2.0); - area 1.0; - fun area r = pi * r * r 2 A pi állandó a Math könyvtárban is megvan. 3 Az fn jelölésről részletesen egy későbbi fejezetben szólunk. Az fn jelet sokszor lambdának ejtjük, ami az eredetére (ti. a λ-kalkulusra) utal. λ-kalkulusbeli jelöléssel a fenti függvény: λr π r r. A kétargumentumú szorzásfüggvény definíciója a λ-kalkulusban: λx λy x y, SML-jelöléssel: fn x => fn y => x*y. 4 A típus és a halmaz rokonértelmű fogalmak: a típus határozza meg azoknak az értékeknek a halmazát, amelyeket az adott típusba tartozó azonosítók, nevek felvehetnek.
3.1. ÉRTÉKDEKLARÁCIÓ 21 Az állandókat tekinthetjük függvényeknek is, mégpedig argumentum nélküli függvényeknek. A jól ismert unáris (egyoperandusú, monadikus) és bináris (kétoperandusú, diadikus) operátorok (műveleti jelek) szintén függvények. Az unáris operátor olyan függvény jele, amelynek egyetlen argumentuma (operandusa) van; az operátor az operandus előtt, ún. prefix helyzetben van. A bináris operátor olyan függvény jele, amelynek két argumentuma (operandusa) van; az operátor a két operandus között, ún. infix helyzetben van. Természetesen vannak kettőnél több operandusú műveletek is, például az if-then-else. 3.1.3. Nevek újradefiniálása Tetszőleges értéknek adhatunk nevet az SML-ben. A név egy érték, esetleg egy másik név szinonimája. Név, szinonima helyett gyakran azonosítóról, ritkábban (matematikai értelemben vett) változóról beszélünk. Az utóbbi elnevezés egyes programozók számára félrevezető lehet, ugyanis az SML-beli változók másképpen viselkednek, mint az imperatív nyelvekből jól ismert társaik: nem frissíthetők, azaz nem kaphatnak új értéket a megszokott értékadással. Nem frissíthető változók esetén érték-szemantikáról, frissíthető változók esetén hivatkozás-szemantikáról beszélünk. Ha az SML-ben egy azonosítót újradefiniálunk, az nincs hatással az azonosító korábbi alkalmazásaira: az értékdeklaráció statikus (és nem dinamikus) kötést hoz létre. Az alábbi példában hiába definiáljuk újra pi-t, az area függvény definíciójába a pi korábbi értéke (3.14259) van beépítve, és nem a hivatkozás a pi néven tárolt értékre. - val pi = 0.0; - area 1.0; > val it = 3.14159 : real Jegyezzük meg, hogy ha egy programban egy függvényt újradefiniálunk, az egész programot újra le kell fordítanunk, különben a változtatás hatástalan maradhat. 3.1.3.1. Nevek képzése A nevek (azonosítók) tetszőleges hosszúságúak lehetnek. Az SML-ben alfanumerikus (azaz kis- és nagybetűkből, számjegyekből, aláhúzás-jelből, valamint percjelből) és írásjelekből képzett (azaz egyéb jelekből álló, angolul symbolic) neveket különböztetünk meg. Az írásjelekből képzett nevekben 20-féle jel (ún. tapadó jel) fordulhat elő:! % & $ # + - * / : < = >? @ \ ~ ^ Egyes jelsorozatoknak különleges jelentésük van, ezeket fenntartott azonosítóknak vagy szintaktikai jeleknek nevezzük. Példák: 5 - = => -> # abs val fun fn int real list + - * / ~ Lássunk egy példát írásjelekből képzett nevek deklarálására! - val +-+-+ = 1415; > val +-+-+ = 1415 : int Az SML-ben csak egyes belső függvények (abs, +, * stb.) neve többszörös terhelésű, a programozó nem definiálhat többszörösen terhelt neveket. Ez azért van így, mert az SML tervezői az automatikus típuslevezetés (type inference) megvalósítását fontosabbnak tartották a vele ütköző többszörös terhelésnél (overloading). A nevek többszörös terhelésének csökkent a jelentősége a moduláris programozás elterjedésével, hiszen a modulnevek (az SML-ben a struktúra- és a funktornevek) szelektorként, a nevek előtagjaként használhatók. 5 int típusnév, list típusoperátor, real pedig egyidejűleg típusnév is és függvénynév is. Jegyezzük meg, hogy ugyanaz a név egyidejűleg jelölhet értéket, típust, modult (struktúrát, ill. funktort), valamint rekordmezőt.