Ohjelmointikielten periaatteet Syksy 2004 Antti-Juhani Kaijanaho
Copyright c 2002, 2004 Antti-Juhani Kaijanaho Tästä teoksesta saa valmistaa kappaleita ja sen saa saattaa yleisön saataviin, muuttamattomana tai muutettuna, käännöksenä tai muunnelmana, toisessa kirjallisuus- tai taidelajissa taikka toista tekotapaa käyttäen, mikäli kaikki seuraavat ehdot tulevat täytetyksi: (1) Kun teoksesta valmistetaan muutettu kappale tai muutettu teos kokonaan tai osittain saatetaan yleisön saataviin, on teoksen muuttajan nimi, salanimi tai nimimerkki ilmoitettava tekijän nimen rinnalla. (2) Kun teoksesta valmistetaan muutettu kappale tai muutettu teos kokonaan tai osittain saatetaan yleisön saataviin, on pidettävä huoli, ettei muutettua teosta voi hyvällä tahdolla sekoittaa alkuperäiseen. (3) Teoksen tekijän nimeä ei saa ilman hänen eri lupaansa käyttää teoksen markkinoinnissa. Erikseen korostettakoon, että teoksen tekijä ei vaadi maksua teoskappaleiden valmistamisesta tai teoksen saattamisesta yleisön saataviin. Teoskappaleen valmistaja tai teoksen yleisön saataviin saattaja saa periä tästä haluamansa maksun. Tekijänoikeuslaki (404/1961) 3 1 mom: Kun teoksesta valmistetaan kappale tai teos kokonaan tai osittain saatetaan yleisön saataviin, on tekijä ilmoitettava sillä tavoin kuin hyvä tapa vaatii.
Sisältö Esipuhe v Luku 1. Johdanto 1 1. Kielten jaotteluja 1 2. Kielen valinta 6 3. Ohjelmointikielten suunnitteluperiaatteita 7 4. Kielen määrittely 8 5. Toteutustekniikoista 10 Luku 2. Konekielinen ohjelmointi 11 1. Konekielet 11 2. Symboliset konekielet 13 3. Abstraktit koneet 14 4. Neloskielen idea 16 5. Systeemikutsut 21 6. Konekielellä ohjelmoinnista 22 Luku 3. Suoraviivaohjelmat 25 1. Suoraviivaohjelman elementit 25 2. Abstrakti syntaksi 33 3. Denotationaalinen semantiikka 36 4. Tyyppijärjestelmä 44 5. Konkreetti kielioppi 49 6. Toteutuksesta 55 Luku 4. Paikallinen kontrollivuon ohjaus 59 1. Kontrollivuo ja kontrollinohjaus 59 2. Testit 60 3. Valintalauseet 62 4. Toistolauseet eli silmukat 66 5. Vahtikomennot 71 6. Väitteet 73 Luku 5. Aliohjelmat 75 1. Kutsusekvenssit 75 2. Parametrinvälitysmekanismit 78 3. Staattinen ja dynaaminen vaikutusalue 79 4. Aktivaatiotietue 80 5. Vuorottaisrutiinit 82 iii
iv SISÄLTÖ 6. Ohjelmointi laskentana 82 Luku 6. Samanaikaisuus 83 1. Jaettu muisti 83 2. Erilliset kommunikoivat prosessit 83 Luku 7. Rakenteiset arvot 85 Luku 8. Abstraktit tietorakenteet ja modulit 87 Luku 9. Olioabstraktio 89 Luku 10. Polymorfismi 91 Kirjallisuutta 93 Liite A. Neloskoneen systeemikutsut 95 Liite B. Ohjelmointikielten historia 101 1. Kaksi ensimmäistä sukupolvea: ennen vuotta 1955 101 2. Automaattinen ohjelmointi ja ohjelmointikielten synty: 1955 1960 101 3. Baabelin torni: 1960-luku 103 4. Modernismi: 1970-luku 104 5. Postmodernismi: 1980-luku 105 6. Internetin nousu: 1990-luku 106 7. Sukupolvista 106 Liite C. Semanttiset alueet 107 1. Tuloalueet 109 2. Summa-alueet 110 3. Funktioalueet 111 Liite D. ALKEIS-suoran kääntäjä 115 1. Pääohjelma 115 2. Selain 119 3. Apuluokat 126 4. Lauseet 131 5. Tyypit 138 6. Lausekkeen jäsennin 145 7. Primäärilausekkeet 148 8. Operaattorilausekkeet 152 9. Muunnoslausekkeet 157
LUKU 4 Paikallinen kontrollivuon ohjaus Edellisessä luvussa esiteltiin imperatiivisen ohjelmointikielen perusrakenne suoraviivaohjelman mallia noudattaen. Luvussa rakennetun ALKEIS-suoran pahin puute on, että sillä ei pysty tekemään kummoisempia ohjelmia: valintojen tekeminen on mahdotonta, ja samoin on mahdotonta suorittaa mitään vaihtuvan monta kertaa. Tässä luvussa pohditaan mahdollisia laajennuksia ALKEIS-suoraan, jotka korjaavat tämän puutteen. Tuloksena syntyvät ALKEIS-jos, AL- KEIS-silmukka ja ALKEIS-vahti. 1. Kontrollivuo ja kontrollinohjaus Useimmat nykykielet ovat luonteeltaan peräkkäistäviä (engl. sequential): ohjelmatekstissä mainitut tehtävät tehdään yksi kerrallaan toinen toisensa jälkeen. Sitä järjestystä, jossa tehtävät tehdään jollakin tietyllä suorituskerralla, sanotaan kontrollivuoksi (engl. control flow). Sillä tarkoitetaan toisinaan myös sitä, missä kaikissa järjestyksissä asiat voidaan tehdä, kun tarkastellaan kaikkia mahdollisia suorituskertoja. Sekaannuksen välttämiseksi ensimmäistä merkitystä kutsutaan tässä monisteessa dynaamiseksi kontrollivuoksi tai lyhyesti kontrolliksi ja jälkimmäistä staattiseksi kontrollivuoksi. Kääntäjät tekevät usein laajoja staattisen kontrollivuon analyysejä optimointiensa tueksi ja myös tiettyjen merkitysopillisten ongelmien (mm. käyttämättömät muuttujat) tunnistamiseksi. Jotkin kielet tukevat yhtäaikaisuutta (engl. concurrency), jolloin ohjelmassa on useita rinnakkaisia dynaamisia kontrollivoita. Tällöinkin ohjelmalla on vain yksi staattinen kontrollivuo, joka kuvaa yhteisesti ohjelman kaikkien suorituskertojen kaikkia dynaamisia kontrollivoita. Jokaisessa ohjelmointikielessä on konstruktioita, joiden tehtävänä on ohjata dynaamisen kontrollivuon kulkua. Tässä monisteessa kutsutaan näitä konstruktioita kontrollia ohjaaviksi (tälle ei ole tietääkseni englanninkielistä termiä) ja näiden konstruktioiden käyttöä kontrollinohjaukseksi. Suoritusaikana on kullakin hetkellä kaksi merkittävää tietoa: missä nyt ollaan ja minne seuraavaksi mennään. Jälkimmäistä sanotaan jatkeeksi (engl. continuation). Koko staattinen kontrollivuo voidaan mallittaa kertomalla, mitä missäkin ohjelman paikassa tehdään ja mikä on siinä jatke. 59
60 4. PAIKALLINEN KONTROLLIVUON OHJAUS Käskykielten merkittävin kontrollia ohjaava konstruktio unohtuu helposti, sillä se on pääasiassa piilossa. Tarkoitan tässä jo edellisessä luvussa tutuksi tullutta peräkkäistämistä (engl. sequencing), joka ottaa kaksi lausetta järjestyksessä. Kun kontrolli tulee peräkkäistyskonstruktioon (mukanaan jatke ilmaisemassa, mitä normaalitilanteessa pitäisi tehdä tämän konstruktion jälkeen), se antaa jälkimmäiselle osalauseelle jatkeeksi oman jatkeensa ja antaa ensimmäiselle jatkeeksi tuon yhdistelmän. Tälle yhdistelmälle, joka koostuu ensimmäisestä osalauseesta, jonka jatke on toinen osalause, jonka jatkeena on koko konstruktion jatke, konstruktio antaa kontrollin. Toisin sanoen se suorittaa ensin ensimmäisen lauseen ja sitten toisen lauseen. Abstraktissa kieliopissa peräkkäistystä merkitään yleensä lauseiden väliin sijoitetulla puolipisteellä. Aikoinaan ohjelmointikielten näkyvin tärkein kontrollia ohjaava konstruktio oli go to -lause. Se oli pesiytynyt kieliin symbolisten konekielten hyppykäskyistä. Go to ohjasi kontrollin (usein numerolla) nimettyyn paikkaan ohjelmassa. Sen lisäksi käytettiin lähinnä haarautuvaa if-lausetta, joka valitsee lausekkeen arvon perusteella, kumpi sen osalauseista suoritetaan. Tuloksena on vaikeasti ymmärrettävää ohjelmakoodia, ns. spagettikoodia. 1970-luvulla muotia oli rakenteinen ohjelmointi (structured programming), Se oli reaktio spagettikoodia vastaan lääkkeinä nähtiin ylhäältä alas (engl. top-down) -suunnittelu, koodin modularisointi, rakenteiset tyypit, muuttujien selkeä nimeäminen ja laaja kommentointi. Rakenteisen ohjelmoinnin liikkeen yhtenä osana kampanjoitiin Dijkstra [9] etunenässä go to -lauseen hävittämisen puolesta (mutta myös kompromisseja etsittiin [22]). Nykyisin näyttää siltä, että go to on vähintäänkin uhanalainen laji: Java varaa sen avainsanaksi vain siksi, että kääntäjät voisivat antaa parempia virheilmoituksia... Toisaalta niissäkin kielissä, jotka eivät go to -nimistä lausetta sisällä, on sen tapaisia lauseita kuitenkin eri nimillä (Javan break on varsin voimakas heikennetty versio go to -lauseesta; Schemen call-with-current-continuation on sekin hieman rajoitettu mutta erittäin voimakas versio go to -lauseesta). Rakenteisen ohjelmoinnin myötä go to -lauseen tilalle on vakiintunut joukko lohkorakenteisia kontrollia ohjaavia konstruktioita. Jokin for-lauseen muunnelma löytyy kaikista käskykielistä. Samoin kielissä on jokin while-lauseen muunnelma sekä täysin haarautuva if... else-lause. 2. Testit Kontrollivuon ohjaus perustuu siihen, että ohjelma pystyy havainnoimaan omaa tilaansa ja tekemään sen perusteella päätöksiä. Tämän perustana on testit:
2. TESTIT 61 Kokonaislukutyyppisten ja liukulukutyyppisten lausekkeiden arvojen vertailu (pienempi-kuin, suurempi-kuin, yhtäsuurikuin ym.). Taulukoiden vertailu alkioittain. Koosteiset testit, jotka muodostuvat useista testeistä, jotka on yhdistetty loogisin operaattorein. Testit esitetään abstraktissa syntaksissa yleensä lausekkeina. Tätä varten pitää lisätä yksi uusi tyyppi, totuusarvotyyppi (engl. truth value type), yleensä nimeltään boolean. Tässä tyypissä on kaksi arvoa, tosi ja epätosi, jotka syntaksissa esitetään tavallisesti avainsanoina: τ ::=... boolean totuusarvotyyppi ε ::=... lauseke voi olla myös... true false... totuusvakio ε 1 = ε 2... yhtäsuuruusvertailu ε 1 ε 2... erisuuruusvertailu ε 1 < ε 2... pienempi kuin -vertailu ε 1 ε 2... pienempi kuin tai yhtäsuuri -vertailu ε 1 > ε 2... suurempi kuin -vertailu ε 1 ε 2... suurempi kuin tai yhtäsuuri -vertailu ε... looginen negaatio ε 1 ε 2... konjunktio ε 1 ε 2... disjunktio ε 1 ε 2... implikaatio ε 1 ε 2... ekvivalenssi Tavallisesti loogiset operaattorit, ja ovat oikosuljettuja (engl. shortcircuited), jolloin oikean operandin arvo lasketaan vain, jos koko lausekkeen arvo ei ole selvä jo pelkän vasemman oprandin arvon perusteella. Tämän järjestelyn etuna on, että testeissä voidaan testata jotain sellaista asiaa, joiden laskeminen on jo virhe, kunhan vain ensin samassa testissä rajataan pois tilanteet, joissa tällaiset virheet syntyvät. Esimerkiksi taulukon indeksointi testissä voidaan tehdä, vaikka vasta aiemmin samassa testilausekkeessa on tarkistettu, että indeksi on sallittu. TEHTÄVÄ 4.1. Lisää testit ALKEIS-suoran denotationaaliseen semantiikkaan ja tyyppijärjestelmään. TEHTÄVÄ 4.2. Lisää testit ALKEIS-suoran konkreettiin kielioppiin. Kiinnitä huomiota järkevien presedenssien asettamiseen (perustele valintasi).
62 4. PAIKALLINEN KONTROLLIVUON OHJAUS Mitä tarkoittaa kaava 2 < 3 < 4? Saisitko rakennettua kielen niin, että se tarkoittaa sitä mitä se matematiikassa tarkoittaa? 3. Valintalauseet Ohjelmoinnissa on kaksi pääasiallista valintatilannetta: suoritettava lause pitää valita joko jonkin testin totuusarvon mukaan tai sitten jonkin toisen lausekkeen arvon mukaan. Toki jälkimmäinen riittäisi yksin, sillä edellinen on sen erityistapaus, mutta edellinen on niin tärkeä erityistapaus, että sille on oma konstruktionsa. If-lauseen semantiikka on kaikille tuttu: selvitetään annetun testin totuusarvo if-lauseen kohdalla ja sitten suoritetaan jompi kumpi annetusta vaihtoehtoisesta lauseesta. Sen abstrakti syntaksi on myös simppeli: γ ::= if ɛ then γ 1 else γ 2 Toteutuksessakaan ei ole juuri yllätyksiä. If-lauseen ainoa kiinnostusta herättävä piirre on sen konkreetti syntaksi. Aivan vanhimmissa kielissä if-lauseen then- ja else-osassa sai olla enintään yksi lause kummassakin; tällöin yhtään monimutkaisempien rakenteiden ilmaisemisessa tarvittiin konekielisestä ohjelmoinnista tuttu goto-lause. Siitä ei kuitenkaan pidetä korkean tason ohjelmoinnissa 1, joten moderneissa ohjelmointikielissä if-lauseen then- ja else-lause voivat olla mielivaltaisen monimutkaisia, yleensä lohkoja. Tyypillinen konkreetti syntaksi on Pascalin käyttämän tyyppinen if x 0 then if x < 0 then writeln("negatiivinen") else writeln("nolla") else writeln("positiivinen") tai C:n tyylinen: if (x 0) if (x < 0) puts("negatiivinen\n"); else puts("nolla\n"); else puts("positiivinen\n"); Jos kumpi tahansa näistä kirjoitetaan ilmiselvällä tavalla konkreetiksi kieliopiksi ja else-osan pois jättäminen sallituaan, kuten on tapana esimerkiksi 1 Ks. esim. Dijkstran kommentti [9] sekä Knuthin sovitteleva artikkeli [22].
3. VALINTALAUSEET 63 statement: if ( expression ) statement if ( expression ) statement else statement niin tällainen konkreetti kielioppi on moniselitteinen: kumpaan if-lauseeeseen else-osa liittyy lauseessa if (x >= 0) if (x == 0) printf("1"); else printf("0");, eli tulostaako se 0 silloin kun x on negatiivinen vai silloin kun se on positiivinen? Tätä ongelmaa sanotaan orvoksi elseksi (engl. dangling else), ja siihen on useita ratkaisuja: Orvon elsen ongelma juontuu siitä, että if-lauseen loppu ei ole yksikäsitteisesti määrätty. Tämä voidaan korjata niin, että lisätään if-lauseen loppuun jokin lopetusavainsana; tyypillisiä ovat endif, end if ja fi (if väärin päin). Näin toimivat esimerkiksi Bourne Shell ja Algolit. Edellisen ratkaisun tyyliin voidaan tehdä myös niin, että thenlauseen tai jopa myös else-lauseen pitää olla lohko. Tällöin ei myöskään jää epäselväksi, mihin if-lause päättyy. Näin toimii esimerkiksi Perl. Vielä yksi edellisten muunnelma on ilmaista ohjelman lohkorakenne sisennyksillä niin, että sisennyksillä on semanttista merkitystä. Tämän seurauksena kieliopin näkökulmasta jokainen then- ja else-lause on lohko, joten ratkaisu on sukua edelliselle ratkaisulle. Näin toimivat esimerkiksi Python ja Haskell. Yleisin ratkaisu lienee kuitenkin julistaa, että if-else-parit löydetään etsimällä ensin sisimmälle if-lauseelle sitä tekstuaalisesti lähinnä oleva else ja sitten toiseksi sisimmälle if-lauseelle sitä tekstuaalisesti lähinnä oleva vielä käyttämätön else jne. Tämä vastaa sitä, mitä ohjelmoijat yleensä tarkoittavat. TEHTÄVÄ 4.3. Kirjoita denotationaalinen semantiikka if-lauseelle. ALKEIS-suora varustettuna testeillä ja if-lauseella on ALKEIS-jos. TEHTÄVÄ 4.4. Suunnittele ja speksaa (abstrakti ja konkreetti syntaksi sekä denotationaalinen semantiikka ja tyypitys) ALKEIS-josiin case-lause matkien C-sukuisten kielten switch-case-lausetta. Älä kuitenkaan matki sitä niin tarkasti, että toistaisit näiden kielten mokaa: break-lauseen vaatiminen useimpien haarojen loppuun on erittäin virhealtista. Case-lauseen speksaaminen jätettiin harjoitustehtäväksi, koska se on tuttu eikä sen speksissä ole juuri yllätyksiä. Sen sijaan sen toteuttamiseen liittyy muutama asia, jotka on syytä mainita. Tässä oletetaan, että case toimii vain lukutyyppisillä lausekkeilla. Jos caselauseen haarojen arvot ovat lähellä toisiaan, kannattaa kääntäjän käyttää hyppytaulukkoa, joka koostuu hyppykäskyistä, ja sopivaan hyppykäskyyn hypätään case-lauseen lausekkeen arvon perusteella. Seuraavassa listauksessa on käytetty tätä tekniikkaa:
64 4. PAIKALLINEN KONTROLLIVUON OHJAUS 1 ; A hand compilation of the following ALKEIS style code 2 ; using a jump table: 3 ; var 4 ; a : unsigned byte; 5 ; begin 6 ; read a; 7 ; case a 8 ; 1 : write 1 ; 9 ; 2 : write 2 ; 10 ; 3 : write 3 ; 11 ; esac 12 ; end 13 14 tmp: zero 15 zero 16 a: zero 17 ; read a 18 SP < SP 12 19 [SP+0] < 0 ; stdin 20 [SP+4] < a ; buffer 21 [SP+8] < 1 ; buffer length 22 syscall 3 ; SYS_READ 23 if [SP+0] = 0 goto fail 24 SP < SP + 4 25 ; case a 1: write 1 ; 2: write 2 ; 3: write 3 ; esac 26 if [a] < 49 goto fail (B) ; 49 = 1 27 if [a] > 51 goto fail (B) ; 51 = 3 28 [tmp] < 0 29 [tmp] < [a] 49 (B) 30 [ tmp] < [tmp] 16 ; 16 bytes for an instruction 31 [tmp] < [tmp] + jumptable 32 goto [ tmp] 33 jumptable: noinstr 34 goto case_1 35 goto case_2 36 goto case_3 37 case_1: noinstr 38 ; write 1 39 AR < tmp 40 [AR+0] < 49 (B) ; 1 41 [AR+1] < 10 (B) ; line feed 42 SP < SP 12
3. VALINTALAUSEET 65 43 [SP+0] < 1 ; stdout 44 [SP+4] < tmp ; buffer 45 [SP+8] < 2 ; buffer length 46 syscall 4 ; SYS_WRITE 47 SP < SP + 4 ; ignore errors 48 goto exit 49 case_2: noinstr 50 ; write 2 51 AR < tmp 52 [AR+0] < 50 (B) ; 2 53 [AR+1] < 10 (B) ; line feed 54 SP < SP 12 55 [SP+0] < 1 ; stdout 56 [SP+4] < tmp ; buffer 57 [SP+8] < 2 ; buffer length 58 syscall 4 ; SYS_WRITE 59 SP < SP + 4 ; ignore errors 60 goto exit 61 case_3: noinstr 62 ; write 3 63 AR < tmp 64 [AR+0] < 51 (B) ; 3 65 [AR+1] < 10 (B) ; line feed 66 SP < SP 12 67 [SP+0] < 1 ; stdout 68 [SP+4] < tmp ; buffer 69 [SP+8] < 2 ; buffer length 70 syscall 4 ; SYS_WRITE 71 SP < SP + 4 ; ignore errors 72 goto exit 73 fail : SP < SP 4 74 [SP+0] < 1 ; EXIT_FAILURE 75 syscall 11 ; SYS_EXIT 76 exit : SP < SP 4 77 [SP+0] < 0 ; EXIT_SUCCESS 78 syscall 11 ; SYS_EXIT Hyppytaulun sijasta voi käyttää myös rakennetta, jossa on taulukossa eri tapausten alkuosoitteet ja sitten ko. taulukosta haetaan hypyn kohde. Jos taas arvot ovat hajallaan ympäri arvoaluetta, voi olla järkevää tehdä lineaarinen tai binäärinen haku. Jos arvot muodostavat
66 4. PAIKALLINEN KONTROLLIVUON OHJAUS useita klustereita, voi olla järkevää tehdä ensin lineaarinen tai binäärinen haku klustereiden joukosta ja sitten valitussa klusterissa käyttää hyppytaulukkoa. TEHTÄVÄ 4.5. Lisää ALKEIS-josin kääntäjään tuki speksaamallesi caserakenteelle. If- ja case-rakenteet ovat hyödyllisiä myös lausekkeina, jolloin niitä käytetään valitsemaan jokin useista vaihtoehtoisista lausekkeista. C-sukuisten kielten?:-operaattori on oleellisesti if-lauseke. TEHTÄVÄ 4.6. Speksaa ja toteuta if-lauseke ALKEIS-josiin. 4. Toistolauseet eli silmukat Imperatiivisen ohjelmoinnin ytimessä on sijoituslauseiden ja peräkkäisyyden lisäksi silmukat (engl. loops) eli toistolauseet (engl. repetition statements). ALKEIS-jos laajenee ALKEIS-silmukaksi, kun siihen lisätään toistolauseet. Varsin harvinainen mutta yleiskäyttöisin rakenne on loop. Sen abstrakti syntaksi on seuraavanlainen: γ ::= loop γ 1 exit if ε γ 2 Tämän silmukan intuitiivinen semantiikka on, että joka iteraatiolla suoritetaan ensin γ 1, sitten katsotaan, onko ε tosi ja jos on, silmukka päättyy, muuten suoritetaan γ 2 ja aloitetaan uusi iteraatio. Tämän denotationaalinen semantiikka vaatii sen verran raskasta matematiikkaa, että jätämme sen tällä kertaa väliin. 4.1. Operationaalinen semantiikka. Tästä lähtien abstraktissa syntaksissa ei käytetä enää γ-kirjainta mihinkään tarkoitukseen. Lauseiden tunnukseksi otetaan sen tilalle κ. Silmukkarakenteen formaalin semantiikan määrittelemme nyt käyttäen Plotkinin rakenteista operationaalista semantiikkaa (engl. structural operational semantics) eli lyhytaskelista operationaalista semantiikkaa (engl. smallstep operational semantics) eli siirtymäsemantiikkaa (engl. transition semantics). Hyvä esitys aiheesta on esimerkiksi Reynoldsin mainiossa kirjassa [37]. Operationaalisen semantiikan intuitio on tilakone (engl. state machine): koneella on joukko tiloja joiden välillä on sallittuja tilasiirtymiä. Tämä tilakone ei tavallisesti ole äärellinen vaan tiloja on yleensä ainakin numeroituvasti ääretön määrä. Ohjelman suoritus alkaa jostain alkutilasta ja etenee tilasiirtymiä pitkin päätyäkseen, jos hyvin käy, johonkin lopputilaan. Keskeisessä asemassa on siis tila (engl. state, configuration), joka on oleellisesti sama kuin denotationaalisessa semantiikassa käytetty
4. TOISTOLAUSEET ELI SILMUKAT 67 ympäristö. Kaikkien tilojen joukkoa merkitään Γ. Tila voi olla lopputila (engl. terminal state) tai välitila (engl. nonterminal state). Lopputilojen joukkoa merkitään Γ T ja välitilojen joukkoa merkitään Γ N ; nämä ovat keskenään pistevieraita, ts. mikään tila ei ole sekä lopputila että välitila. Toinen keskeinen käsite on siirtymä (engl. transition), joka määritellään relaationa joukosta Γ N joukkoon Γ. Kielen operationaalinen semantiikka määritellään määrittelemällä Γ, Γ T ja siirtymärelaatio. ALKEIS-silmukan tilajoukko määritellään seuraavasti: Γ T = Σ Σ (Ids LValue) (LValue RValue) Γ N = (Statements Expressions) Γ T Γ = Γ T Γ N Σ = V unsigned byte missä operaattori ( ) tarkoittaa äärellisten jonojen joukkoa, toisin sanoen X koostuu X:n alkioista muodostetuista äärellisistä jonoista. Tyhjää jonoa merkitään, alkioiden a ja b muodostamaa jonoa merkitään a, b ; jonoa, jossa jonot s ja t on laitettu peräkkäin, merkitään st (toisin sanoen a, b c, d = a, b, c, d ), jonon s alkiota, jonka järjestysnumero on n (laskeminen aloitetaan nollasta), merkitään s(n) ja jonon s osajonoa, joka muodostuu s:n alkioista, joiden järjestysnumerot ovat n:n ja m:n välillä, n mukaanlukien, merkitään s(n : m) (toisin sanoen a, b, c, d, e (2 : 4) = c, d ). Plotkinin idea oli, että semantiikan siirtymärelaatio esitetään päättelysäännöstönä: siirtymä γ γ, missä γ Γ N ja γ Γ ovat tiloja, on mahdollinen, jos se voidaan johtaa päättelysäännöistä (vrt. tyyppijärjestelmän päättelysääntöjen toiminta). Kutakin kielikonstruktiota kohti annetaan yksi tai useampi päättelysääntö. Päättelysäännöt on muotoiltava niin, että niitä voidaan soveltaa ainoastaan sellaisessa tilanteessa, jossa vastaavan operaation tekeminen on haluttua. Esimerkiksi laskujärjestys voidaan haluttaessa määritellä tarkasti, mutta ihan yhtä hyvin se voidaan jättää suhteellisen auki. Keskeisenä tekijänä tässä ohjauksessa ovat kaksi uutta abstraktin syntaksin välikesymbolia, ν R ja ν L edellinen edustaa muodollisesti lauseketta, joka on jonkin oikean arvon vakio, ja jälkimmäinen
68 4. PAIKALLINEN KONTROLLIVUON OHJAUS edustaa vastaavasti muodollisesti lauseketta, joka on jonkin vasemman arvon vakio. Pohjaa eli virhetilannetta kumpikaan ei kuitenkaan hyväksy. Näin lausekkeiden abstrakti syntaksi muuttuu seuraavanlaiseksi: ν R RValue \ { } ν L LValue \ { } ε Expressions ε ::= ν R ν L Konkreettiin syntaksiin ei kuitenkaan tuoda mitään keinoa ilmaista vasempia arvoja, vaan tämä abstraktin syntaksin muutos on puhtaasti muodollinen lisäys, jonka tarkoituksena on tukea operationaalista semantiikkaa. Sijoituslauseen osalta riittää antaa seuraavat säännöt: (ν L ν R, (σ I, σ O, σ L, σ R )) (σ I, σ O, σ L, σ R (ν L, ν R )) (ε 1, σ) (ε 1, σ) (ε 1 ε 2, σ) (ε 1 ε 2, σ) (ε 2, σ) (ε 2, σ) (ε 1 ε 2, σ) (ε 1 ε 2, σ) Säännöt johtavat siihen vaatimukseen, että molemmat lausekkeet lasketaan kokonaan (onnistuneesti) ennen kuin sijoitusta aletaan tehdä. Sen sijaan lausekkeiden keskinäinen laskujärjestys jää auki. If-lauseelle tarvitaan useampi sääntö: (if true then κ 1 else κ 2, σ) (κ 1, σ) (if false then κ 1 else κ 2, σ) (κ 2, σ) (ε, σ) (ε, σ) (if ε then κ 1 else κ 2, σ) (if ε then κ 1 else κ 2, σ) Muuttujan arvon ottaminen tuottaa ulos vasemman arvon, jos muuttuja on määritelty: σ L (ι) = ν L (ι, (σ I, σ O, σ L, σ R )) (ν L, (σ I, σ O, σ L, σ R ))
4. TOISTOLAUSEET ELI SILMUKAT 69 Vasen arvo muuttuu oikeaksi arvoksi helposti: σ R (ν L ) = ν R (ν L, (σ I, σ O, σ L, σ R )) (ν R, (σ I, σ O, σ L, σ R )) Merkin kirjoittaminen mallinnetaan nyt helpommin kuin aiemmin, koska tilaan on otettu erikseen σ O, tulostejono: Peräkkäistys: ν R Σ (write ν R, (σ I, σ O, σ L, σ R )) (σ I, ν R σ O, σ L, σ R ) (ε, σ) (ε, σ) (write ε, σ) (write ε, σ) (κ 1, σ) σ (κ 1 ; κ 2, σ) (κ 2, σ ) (κ 1, σ) (κ 1, σ ) (κ 1 ; κ 2, σ) (κ 1; κ 2, σ ) Lohkot: ei ole sellaista ι jolle σ L (ι ) = a σ R (a) = (ε, σ) ν R (κ, (σ I, σ O, σ L (ι, a), σ R (a, ))) (σ I, σ O, σ L, σ R) (var ι : τ begin κ end, (σ I, σ O, σ L, σ R )) (σ I, σ O, σ L (ι, σ L (ι)), σ R (a, )) (var δ 1 ; δ 2 begin κ end, σ) (var δ 1 begin var δ 2 begin κ end end, σ) Ohjelman suoritus (engl. execution) on operationaalisen semantiikan kannalta tilojen (mahdollisesti ääretön) jono, jolle pätee se, että peräkkäisten tilojen γ ja γ välillä on siirtymärelaatio γ γ. Jos on olemassa äärellinen suoritus, joka alkaa tilasta γ ja päättyy tilaan γ, niin tämä merkitään γ γ. Jos suorituksen viimeinen tila ei ole lopputila eikä suoritusta voi enää jatkaa (eli ei ole olemassa tilaa, johon kyseisen suorituksen viimeisestä tilasta pääsisi sallitulla siirtymällä), suorituksen sanotaan olevan jumissa (engl. stuck). Virhetilanteet operationaalinen semantiikka hoitaa niin, että kukin sääntö sallii (joko oletustensa tai muotonsa avulla) käytön vain silloin, kun virhetilannetta ei tule. Näin virhetilanteessa ei enää voida käyttää yhtään sääntöä ja suoritus jää jumiin. Virhetilanne ilmenee siis tilakoneen jumittumisena. TEHTÄVÄ 4.7. Täydennä edellä annettu ALKEIS-josin operationaalinen semantiikka.
70 4. PAIKALLINEN KONTROLLIVUON OHJAUS Silmukan operationaalinen semantiikka on seuraavanlainen: (loop κ 1 exit if ε κ 2, σ) (κ 1 ; if ε then (κ 2 ; loop κ 1 exit if ε κ 2 ), σ) 4.2. Muut silmukkarakenteet. Silmukkarakenteista kuuluisin on varmastikin while-rakenne: κ ::= while ε do κ (while ε do κ, σ) (loop exit if ε κ, σ) Tarpeellinen on usein myös perinteinen for-silmukka: κ ::= for ι : τ ε 1 to ε 2 step ε 3 do κ (for ι : τ ε 1 to ε 2 step ε 3 do κ, σ) (var ι : τ begin ι ε 1 ; while (ι < ε 2 ) (κ; ι ι + ε 3 ) end, σ) Ohjelmointikieltä, joka on suunnilleen samanlainen kuin ALKEISjos varustettuna while-silmukoilla, sanotaan while-kieleksi. Sillä on keskeinen merkitys ohjelmoinnin teoriassa imperatiivisen ohjelmointikielen arkkityyppinä. Tämä johtuu siitä, että while-kielelle on olemassa selkeitä ohjelmien oikeaksi todistamiseen liittyviä formalismeja ja siitä, että while-kieli on yksinkertaisimmasta päästä kieliä, joka on laskennallisesti yhtä ilmaisuvoimainen kuin Turingin kone eli kykenee laskemaan kaikki (yleis)rekursiiviset (engl. general recursive) funktiot Vastaavasti kieltä, joka on suunnilleen samanlainen kuin ALKEISjos varustettuna for-silmukoilla niin, että silmukkamuuttujaan ei voi sijoittaa silmukan sisällä, sanotaan for-kieleksi. Se ei, yllättäen, ole yhtä ilmaisuvoimainen kuin while-kieli, sillä sillä ei voi tehdä päättymätöntä silmukkaa eikä ylipäätään mitään silmukkaa, jonka iteraatioiden lukumäärä ei ole etukäteen tiedossa. Niinpä for-kieli kykenee laskemaan primitiivirekursiiviset (engl. primitive recursive) funktiot ja on Turingin konetta aidosti heikompi.
5. VAHTIKOMENNOT 71 5. Vahtikomennot Mielenkiintoinen variantti ALKEIS-silmukasta saadaan, kun kaikki if-lauseet ja silmukkarakenteet korvataan Dijkstran vahtikomennoilla [10]: κ ::= if ξ guarded conditional do ξ guarded loop ξ GuardedCommand ξ ::=ε κ guard ξ 1 ξ 2 guarded choice Vahtikomento ε κ ilmaisee, että lause κ suoritetaan vain jos ε on tosi. Jos ε on epätosi, vahtikomennon sanotaan olevan suorituskelvoton (engl. infeasible). Vahtikomento ξ 1 ξ 2 valitsee vahtikomennoista ξ 1 ja ξ 2 suoritettavaksi jomman kumman, kuitenkin niin, että suorituskelvotonta ei valita. Käytännössä siis vahtikomento ε 1 κ 1 ε n κ n toimii niin, että se valitsee jonkin sellaisen i:n, jolla vahtilauseke ε i saa arvokseen true, ja suorittaa sitten κ i :n; jos sellaista i:tä ei ole, koko vahtikomento on suorituskelvoton ja jos sellaisia i:tä on useita, niistä valitaan joku. Vahtikomennot tuovat ohjelmointikieleen epädeterminismin (engl. nondeterminism): täsmälleen samalla syötteellä sama ohjelma voi käyttäytyä eri tavoin. Epädeterminismiä on kahdenlaista: enkeliepädeterminismiä (engl. angelic nondeterminism) ja demoniepädeterminismiä (engl. demonic nondeterminism). Näiden kuvaavien nimien takaa löytyy kysymys siitä, miten suoritettava lause valitaan, kun vaihtoehtoja on useita. Enkeliepädeterminismin taustalla on vertauskuva, jossa valinnan tekee enkeli: tehdään siis ohjelman käyttäjän kannalta suotuisin valinta (toisin sanoen, jos valintakohdassa toinen vaihtoehto johtaa virhetilanteeseen ja toinen antaa oikean tuloksen, valitaan se oikeaan tulokseen johtava). Demoniepädeterminismin vertauskuvana on vastaavasti demoni valintaa tekemässä. Tämä ei ole varsinaisesti enkeliepädeterminismin vastakohta, sillä silloin valittaisiin aina väärin ; pikemminkin kyse on ilkeilevästä demonista, joka saattaa joskus valita oikeinkin, mutta siihen ei voi luottaa. Käytännössä tämä tarkoittaa, että demoniepädeterminismissä ohjelman kirjoittajan tulee olettaa pahin mahdollinen valinta varmuuden vuoksi, mutta siihen ei voi luotaa. Enkeliepädeterminismin toteuttaminen tietokoneohjelmassa vaatii käytännössä sitä, että valintaa tehtäessä arvataan ja jos arvaus
72 4. PAIKALLINEN KONTROLLIVUON OHJAUS osoittautuu myöhemmin vääräksi, valinta tehdään uudestaan. Tällaista toimintaa sanotaan peruutukseksi (engl. backtracking) ja sitä käytetään lähinnä logiikkaohjelmoinnissa. Toinen tapa toteuttaa enkeliepädeterminismi on valintatilanteen edessä valita kaikki mahdollisuudet ja suorittaa kaikkia vaihtoehtoja toisistaan riippumatta. Jotkut vaihtoehdot osoittautuvat vääriksi valinnoiksi, jolloin niiden suorittaminen lakkaa; ohjelman lopuksi (toivottavasti) hengissä on ainakin yksi suoritus ja niistä voidaan sitten valita umpimähkään, mikä tulos annetaan ulos (tai, haluttaessa voidaan myös antaa ulos kaikki tulokset, jos vain käyttäjä ymmärtää tämän). Vahtikomentojen epädeterministisyys on käytännössä aina demonista, sillä demoniepädeterminismi on helpoin toteutettava. Koska valitsijaksi katsotaan oikukas demoni, voidaan ajatella, että valinnan tekemiseen annetaan kääntäjälle täysi vapaus: se voi esimerkiksi valita aina ensimmäisen niistä, aina viimeisen niistä tai sitten se voi esimerkiksi heittää arpaa niiden välillä (hui!). Fiksu optimoiva kääntäjä saattaa valita aina sen, joka on kevyin suorittaa. Pääsääntö on siis, että valinnanvapaus annetaan kääntäjälle (ja sen toteuttajalle) ja ohjelmoijan pitää hyväksyä se, että mikä tahansa suorituskelpoisista vaihtoehdoista valitaan. Vahtikomentopohjainen if-lause valitsee vaihtoehdoista demonisesti jonkin suorituskelpoisen ja suorittaa sitä vastaavan lauseen. Jos mikään vaihtoehdoista ei ole suorituskelpoinen, if-lauseen suoritus epäonnistuu (operationaalisen semantiikan näkökulmasta suorittava kone jää jumiin, denotationaalisen semantiikan näkökulmasta tila muuttuu pohjaksi). Vahtikomentopohjainen do-silmukka suorittaa vahtikomentonsa toistuvasti kunnes vahtikomento muuttuu suorituskelvottomaksi. Siis silmukan suoritus loppuu, kun kaikki vahtilausekkeet saavat arvokseen false. ESIMERKKI 4.1. Seuraava vahtikomentopohjainen if-lause valitsee muuttujien a ja b arvoista pienemmän ja sijoittaa sen muuttujaan min: if a b min a a b min b ESIMERKKI 4.2. Seuraavassa ohjelmapätkässä a, b ja c ovat lukutaulukoita ja sekä a että b on järjestetty kasvavaan suuruusjärjestykseen. Ohjelmanpätkä yhdistää a:n ja b:n c:hen niin, että c:ssä on jokainen a:n ja b:n alkio ja c on kasvavassa suuruusjärjestyksessä. Muuttujassa n on taulukon a koko ja muuttujassa m on taulukon b koko. Taulukko c on riittävän iso. var i : unsigned int; j : unsigned int; k : unsigned int; begin i 0U;
6. VÄITTEET 73 j 0U; k 0U; do i < n (j m a[i] b[j]) begin c[k] a[i]; i i + 1; k k + 1 end j < m (i n a[i] b[j]) begin c[k] b[j]; j j + 1; k k + 1 end end Vahtikomentojen semantiikan sivuutamme ajanpuutteen vuoksi TEHTÄVÄ 4.8. Suunnittele, speksaa ja toteuta ALKEIS-vahti. Voit jättää formaalin semantiikan kirjoittamatta. 6. Väitteet Väitteet (engl. assertions) ovat testejä, joiden odotetaan saavan arvokseen aina true. Niiden tarkoituksena on dokumentoida ohjelmoijien tekemiä oletuksia ja osoittaa, milloin näitä oletuksia rikotaan. Ne ovat pääsääntöisesti testauksen apuvälineitä (testioraakkeleita). Yleiskäyttöisin tapa lisätä väitteet kieleen on lisätä siihen assertlause: κ ::= assert ε (ε, σ) (ε, σ) (assert ε, σ) (assert ε, σ) (assert true, σ) σ Huomaa, kuinka operationaalisesta semantiikasta puuttuu sääntö assert false -tapaukselle: se on tarkoituksellista ja se johtaa siihen, että assert false aiheuttaa jumin niin kuin pitääkin. Hyödyllinen olisi lohkorakenne pre ε 1 begin κ post ε 2, jossa ε 1 on väitelauseke, jonka tulee olla totta lohkoon tultaessa, ja jossa ε 2 on väitelauseke, jonka tulee olla totta lohkosta poistuttaessa. Väitettä ε 1 sanotaan esiehdoksi (engl. precondition) ja väitettä ε 2 sanotaan jälkiehdoksi (engl. postcondition). Ajatus on, että tämä lohkorakenne ilmaisee sopimuksen: lohkon käyttäjän vastuulla on, että esiehto pätee, ja lohkon itsensä vastuulla on, että jälkiehto pätee. Jos lohkon käyttäjä rikkoo sopimuksen ja esiehto ei pädekään, lohkolla ei ole mitään vastuuta eikä sen tarvitse huolehtia jälkiehdosta. Näin esimerkiksi voidaan kirjoittaa seuraavasti:
74 4. PAIKALLINEN KONTROLLIVUON OHJAUS pre a 0 b > 0 begin q a / b; r a % b; post a = q b + r 0 r r < b end Muita hyödyllisiä väitteitä ovat ns. pysyväisväittämät (engl. invariants), joista ei ajanpuutteen vuoksi tämän enempää. Tony Hoare on kirjoittanut mielenkiintoisia artikkeleita [14, 15] väitteiden käytöstä teoriassa ja käytännössä.
Kirjallisuutta [1] IEEE standard for binary floating-point arithmetic, 1985. ANSI/IEEE Std 754-1985. [2] 7-Bit coded Character Set, 1991. Standard ECMA 6. WWW: http://www.ecma. ch/ecma1/stand/ecma-006.htm. [3] Addison-Wesley, Reading, MA. The Unicode Standard, Version 4.0, 2003. [4] Alfred V. Aho, Ravi Sethi, ja Jeffrey D. Ullman. Compilers: Principles, Techniques and Tools. Addison-Wesley, Reading, MA, 1988. [5] Andrew W. Appel ja Jens Palsberg. Modern Compiler Implementation in Java. Cambridge University Press, Cambridge, 2 laitos, 2002. [6] John Backus. Can programming be liberated from the von Neumann style? A functional style and its algebra of programs. Communications of the ACM, 21(8), Elokuu 1978. [7] C. J. Burgess. Software quality issues when choosing a programming language. Kirjassa Software Quality Management III Vol. 2, sivua 25 31. Computational Mechanics Publications, 1995. [8] O.-J. Dahl, E. W. Dijkstra, ja C. A. R. Hoare. Structured Programming. Academic Press, Lontoo, 1972. [9] Edsger W. Dijkstra. Go to statement considered harmful. Communications of the ACM, 11(3), Maaliskuu 1968. Letter to the editor. [10] Edsger W. Dijkstra. A Discipline of Programming. Prentice-Hall, Englewood Cliffs, NJ, 1976. [11] Erich Gamma, Richard Helm, Ralph Johnson, ja John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, Boston, 1995. [12] C. A. R. Hoare. Hints on programming language design. Kirjassa C. A. R. Hoare ja C. B. Jones (toim.), Essays in Computer Science. Prentice-Hall, New York, 1989. [13] C. A. R. Hoare. A hard act to follow. Higher-Order and Symbolic Computation, 13(1 2):71 72, 2000. [14] C. A. R. Hoare. Assertions: a personal perspective. IEEE Annals of the History of Computing, 25(2), 2003. [15] Tony Hoare. Towards the verifying compiler. Kirjassa Olaf Owe, Stein Krogdahl, ja Tom Lyche (toim.), From Object-Orientation to Formal Methods: Essays in Memory of Ole-Johan Dahl, nide 2635 sarjasta Lecture Notes in Computer Science. Springer, 2004. [16] International Organization for Standardization. Information lechnology Syntactic metalanguage Extended BNF, 1996. ISO/IEC 14977:1996(E). [17] International Organization for Standardization. Programming languages C, 1999. ISO/IEC 9899:1999. [18] Antti-Juhani Kaijanaho ja Benjamin Fallenstein. Totally different structural programming: Programming languages in ZigZag. WWW: http://www.mit. jyu./antkaij/plinzz.pdf, Elokuu 2001. An invited talk presented at the First 93
94 KIRJALLISUUTTA International ZigZag Conference, part of ACM Hypertext Conference 2001 in Århus, Denmark on August 14, 2001. [19] Richard Kelsey, William Klinger, Jonathan Rees, et al. Revised 5 report on the algorithmic language Scheme. ACM SIGPLAN Notices, 33(9):26 76, Syyskuu 1998. [20] Heiko Kießling ja Uwe Krüger. Blocks and procedures. SIGPLAN Notices, 28(11), 1993. [21] Donald E. Knuth. Backus normal form vs. backus naur form. Communications of the ACM, 7(12), Joulukuu 1962. Letter to the editor. [22] Donald E. Knuth. Structured programming with go to statements. Computing Surveys, 6(4), Joulukuu 1974. [23] Donald E. Knuth ja Luis Trabb Pardo. The early development of programming languages. Kirjassa J. Belzer, A. G. Holzman, ja A. Kent (toim.), Encyclopedia of Computer Science and Technology, nide 6. Dekker, New York, 1977. Julkaistu myös kirjassa [27]. [24] Thomas E. Kurtz. BASIC. ACM SIGPLAN Notices, 13(8):103 118, Elokuu 1978. Preprints, First ACM SIGPLAN History of Programming Languages Conference. [25] Patricia K. Lawlis. Guidelines for choosing a computer language: Support for the visionary organization, Elokuu 1997. [26] John McCarthy. History of LISP. ACM SIGPLAN Notices, 13(8):217 223, Elokuu 1978. Preprints, First ACM SIGPLAN History of Programming Languages Conference. [27] N. Metropolis, J. Howlett, ja Gian-Carlo Rota (toim.). A History of Computing in the Twentieth Century. Academic Press, New York, 1980. [28] Robin Milner. A theory of type polymorphism in programming. Journal of Computer and System Sciences, 17(3):348 375, 1978. [29] Hanspeter Mössenböck. Treating statement sequences as block objects. SIGPLAN Notices, 27(8), 1992. [30] Peter Naur et al. Revised report on the algorithmic language ALGOL 60. Communications of the ACM, 6(1):1 17, Tammikuu 1963. [31] John K. Ousterhout. Scripting: Higher-level programming for the 21st century. Computer, 31(3):23 30, Maaliskuu 1998. [32] Alan J. Perlis. Epigrams on programming. ACM SIGPLAN Notices, 17(9), Syyskuu 1982. [33] Simon Peyton Jones (toim.). Haskell 98 Language and Libraries: The Revised Report. Cambridge University Press, Huhtikuu 2003. [34] Simon Peyton Jones. Haskell 98 language and libraries: The revised report. Journal of Functional Programming, 13(1), Tammikuu 2003. [35] Simon L. Peyton Jones. The Implementation of Functional Programming Languages. Prentice-Hall, New York, 1987. [36] Eric S. Raymond. The Art of Unix Programming. Addison-Wesley, Boston, 2003. [37] John C. Reynolds. Theories of Programming Languages. Cambridge University Press, Cambridge, 1998. [38] Guy Lewis Steele, Jr. Debunking the expensive procedure call myth or procedure call implementations considered harmful or, LAMBDA: The ultimate GOTO. Kirjassa James K. Ketchel et al. (toim.), Proceedings of the 1977 annual conference, sivua 153 162. ACM Press, 1977. [39] Christopher Strachey. Fundamental concepts in programming languages. Higher-Order and Symbolic Computation, 13:11 49, 2000. Perustuu Stracheyn vuonna 1967 pitämiin luentoihin.
KIRJALLISUUTTA 95 [40] A. van Wijngaarden et al. (toim.). Revised Report on the Algorithmic Language Algol 68. Springer, Berliini, 1976. [41] Larry Wall. Perl, the first postmodern computer language. http://www.wall. org/ larry/pm.html, 1999. A talk given in Linux World Spring 1999.