Luku 4 Kontrolli Tässä viimeisessä luvussa paneudutaan ohjelman kontrollin hallintaan. Keskeisin asia on aliohjelmakäsite, joka on teoreettisesti niin vahva, että valtaosa muista aiheeseen liittyvistä käsitteistä on siitä johdettavissa. 4.1 Kontrollivuo ja kontrollinohjaus Useimmat nykykielet ovat luonteeltaan peräkkäistäviä (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 (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 (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. 65
66 LUKU 4. KONTROLLI Suoritusaikana on kullakin hetkellä kaksi merkittävää tietoa: missä nyt ollaan ja minne seuraavaksi mennään. Jälkimmäistä sanotaan jatkeeksi (continuation). Koko staattinen kontrollivuo voidaan mallittaa kertomalla, mitä missäkin ohjelman paikassa tehdään ja mikä on siinä jatke. 4.1.1 Käskykielten kontrollinohjaus Käskykielten merkittävin kontrollia ohjaava konstruktio unohtuu helposti, sillä se on pääasiassa piilossa. Tarkoitan tässä peräkkäistämistä (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 (topdown) -suunnittelu, koodin modularisointi, rakenteiset tyypit, muuttujien selkeä nimeäminen ja laaja kommentointi. Rakenteisen ohjelmoinnin liikkeen yhtenä osana kampanjoitiin Dijkstra 1 etunenässä go to -lauseen hävittämisen puolesta (mutta myös kompromisseja etsittiin 2 ). 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). 1. Edsger W. Dijkstra. Go To Statement Considered Harmful. Communications of the ACM (letters to the editor), vol. 11 no. 3, March 1968. 2. Donald E. Knuth. Structured Programming with go to Statements. Computing Surveys, vol. 6, no. 4, December 1974.
4.1. KONTROLLIVUO JA KONTROLLINOHJAUS 67 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. Käskykielissä on käytössä myös jonkinlainen aliohjelmarakenne ja usein myös poikkeustenkäsittely. 4.1.2 Funktiokielten kontrollinohjaus Funktiokielissä ainoa varsinainen kontrollia ohjaava konstruktio on aliohjelmarakenne. Jos kielen aliohjelmat voivat olla väljiä jonkin argumenttinsa suhteen, mitään muuta ei tarvita periaatteessa jopa ehtolauseke voidaan rakentaa aliohjelmien avulla muussa tapauksessa lisäksi tarvitaan erillinen tuki ehdolliselle haarautumiselle. Kaikki muut rakenteet ovat merkitysopillisesti kielioppimakeisia (syntactic sugar), ne voidaan palauttaa merkityksen muuttumatta funktion määrittelyihin ja kutsuihin. 4.1.3 Logiikkakielten kontrollinohjaus Tyypillisen logiikkakielen abstrakti syntaksi on simppeli 3 : Program: γ ::=φψ ψ Clause: φ ::=φφ π : π sääntö π. fakta Predicate: π ::=ξ(α + ) ξ 3. Lloyd Allison, emt., s. 107
68 LUKU 4. KONTROLLI Atom: α ::=ν ξ Ξ ξ(α + ) lukuliteraali vakionimi (alkaa pienellä kirjaimella) muuttujanimi (alkaa isolla kirjaimella) funktion käyttö Query: ψ ::=? π. Tässä γ + tarkoittaa, että γ esiintyy tässä kohtaa ainakin kerran, mahdollisesti useammankin kerran peräkkäin. Tässä γ tarkoittaa muuten samaa, mutta on mahdollista, ettei γ esinny tässä kohtaa kertaakaan. Toisin sanoen logiikkaohjelma koostuu jonosta faktoja ja sääntöjä. Fakta on predikaatti, jossa voi esiintyä muuttujia; sääntö on muotoa jos nämä niin tämä (operaattori : luetaan jos ). Ohjelma voi myös sisältää kyselyjä, joilla on faktajonon muoto. Logiikkaohjelman kontrollivuo koostuu todistusyrityksistä ja peruutuksesta (backtracking). Suoritus alkaa kyselystä. Kysely koostuu predikaateista, joiden konjunktio pitäisi todistaa. Tämä tapahtuu todistamalla kukin predikaatti erikseen, järjestyksessä. Predikaatti todistetaan etsimällä ohjelmasta sellaista faktaa, jonka muoto (kun muuttujat tulkitaan jokerimerkeiksi) on sama kuin todistettavalla predikaatilla, tai sellaista sääntöä, jonka vasen puolen muoto on sama kuin todistettavalla predikaatilla. Muodon samuus todetaan käyttämällä erityistä samastusalgoritmia (unification). Mikäli löytyy tällainen fakta, predikaatti on todistettu. Mikäli löytyy tällainen sääntö, otetaan muistiin, mitä atomia kukin säännön vasemmalla puolella esiintyvä muuttuja vastaa todistettavassa predikaatissa. Tämäkin kuuluu samastusalgoritmin tehtäviin. Predikaatti on todistettu, mikäli säännön oikea puoli, kun vasemmalla puolella esiintyneet muuttujat korvataan siinä niitä vastaavilla todistettavan predikaatin atomeilla. Jos todistus epäonnistuu, etsitään seuraava sopiva fakta tai sääntö. Mikäli sopivaa faktaa tai sääntöä ei löydy, todetaan, että predikaattia ei voida todistaa. Kontrollin liikettä seuraamalla huomataan, että kukin sääntö (sekä triviaalilla tavalla kukin fakta) on aliohjelman määritelmä, ja kukin todistettava predikaatti on aliohjelman kutsu. Peruutuksella tarkoitetaan sitä tilannetta, jossa
4.2. ALIOHJELMAT 69 säännön oikean puolen todistaminen epäonnistuu. Tällöin kontrolli palaa kyseisestä säännöstä, ja kokeillaan seuraavaa sopivaa faktaa tai sääntöä. 4.2 Aliohjelmat Aliohjelma (subroutine) on useimpien kielten tärkein kontrollin abstrahointikeino. 4.2.1 Kutsusekvenssit Aliohjelmaan kontrolli siirtyy sen kutsun (call) kautta. Kun aliohjelman suoritus päättyy, kontrolli siirtyy takaisin sinne, missä kutsu tehtiin toisin sanoen aliohjelman jatkeena on kutsun jatke. Tämä johtaa siihen, että aliohjelmien kutsurakenne on lifo-tyyppinen (last in, first out), ja toteutustekniikaksi sopii mainiosti pino. Konekielitasolla kutsuoperaatio laittaa aktivaatiopinoon (activation stack) itseään seuraavan käskyn osiotteen ja sitten siirtää kontrollin aliohjelmalle (eli hyppää aliohjelman alkuun, suorittaa go to -lauseen, jonka kohteena on aliohjelman alku). Viimeisenä toimenaan aliohjelma ottaa pinosta päällimmäisenä olevan osoitteen ja hyppää tuohon osoitteeseen. Tavallisesti aliohjelmat ovat parametrisoituja. Parametrisoidun aliohjelman kutsu välittää aliohjelmalle dataa, jota aliohjelman odotetaan käyttävän hyväksi. Tätä dataa kutsutaan aliohjelman argumenteiksi (argument) kielitasolla tarkastellen kyse on lausekkeista, joiden arvo annetaan aliohjelmalle. Myös argumentit on tapana antaa aliohjelmalle pinon välityksellä. Konekielitasolla siis kutsuoperaatio laittaa pinoon paluuosoitteen jälkeen viimeisen argumentin, sitten toiseksi viimeisen argumentin, ja lopulta se laittaa pinoon ensimmäisen argumentin. Sitten se suorittaa hypyn. Joissakin koneissa argumentit sijoitetaan pinoon ennen paluuosoitetta, joissakin koneissa ne laitetaan rekistereihin, joissakin myös paluuosoite laitetaan rekisteriin. Tällaiset eroavuudet ovat väistämättömiä. Säännöt siitä, mitä pinossa ja rekistereissä tarkkaan ottaen tulee olla kutsuun liittyvän hypyn hetkellä, sekä siitä, miten kutsun jäljet siivotaan aliohjelman päätyttyä, muodostavat kutsusekvenssin (calling sequence). On tärkeää, että kutsuja ja aliohjelma noudattavat samaa kutsusekvenssiä. Erityisen tarkkana tämän kanssa pitää olla, jos aliohjelma ja kutsu on tuotettu eri kääntäjillä. Tässä monisteessa oletetaan yksinkertaisuuden vuoksi seuraavanlainen kutsusekvenssi: paluuosoite on pinossa alimmaisena ja sitten tulevat argumentit käännetyssä järjestyksessä, aliohjelma poistaa argumentit ja paluuosoitteen pinosta ennen paluuhyppyä. Teemme tähän myöhemmin muutoksia.
70 LUKU 4. KONTROLLI Tärkeää on nyt huomata, että paluuosoite voidaan ajatella aliohjelmalle annettuna ylimääräisenä, viimeisen argumentin jälkeen tulevana argumenttina. Näin aliohjelmakutsu on tulkittavissa argumentteja välittävänä go to -lauseena! 4 Tämä havainto on tärkeä, sillä kutsun tulkitseminen hypyksi mahdollistaa monia merkittäviä optimointeja. Esimerkiksi jos jokin aliohjelma kutsuu toista aliohjelmaa viimeisenä tekonaan, voidaan tätä yksinkertaistaa reilusti: ensiksi se poistaa itselleen tulleet argumentit (paluuosoitetta lukuunottamatta) pinosta, sitten se laittaa kutsun argumentit (paluuosoitetta lukuunottamatta) pinoon. Pino on nyt sellaisessa tilassa, että näyttäisi kuin tämän aliohjelman kutsuja olisi tekemässä tätä kutsua. Nyt voidaan tehdä hyppy kutsuttavaan aliohjelmaan. Koska pinossa oleva paluuosoite on suoraan kutsuvan aliohjelman kutsujaan, ei hypyn jäljessä tarvitse olla enää yhtään koodia, aliohjelma päättyy tuohon hyppyyn. Myöskin on niin, että kutsuvasta aliohjelmasta ei jää merkkiäkään pinoon, jolloin tällaisia häntäkutsuja (tail call) voidaan tehdä loputtomasti ilman, että pinon koko kasvaisi sen seurauksena rajattomasti. Tätä optimisaatiota sanotaan häntäkutsun poistoksi (tail call elimination). Sillä on myös merkitysopillista merkitystä. Kun häntäkutsu (eli aliohjelman lopuksi tehtävä kutsu) ei kasvata pinon kokoa, sitä voidaan käyttää iteraation ilmaisemiseen. Tunnetuin esimerkki kielestä, jossa iteraatio ilmaistaan häntärekursiona (tail recursion), on Scheme, jonka määrittelydokumentti vaatii jokaiselta Scheme-toteutukselta luotettavan häntäkutsun poiston. Schemessä olisi esimerkiksi kertoman laskeminen luonnollisinta ilmaista näin: (define (! n) (define (loop i accu) (if (eq? i 1) accu (loop (- i 1) (* accu i)))) (loop n 1)) Jotkut aliohjelmat palauttavat (return) tietoa kutsujalle päätyttyään. Tämä tieto laitetaan paluun yhteydessä pinoon paluuosoitteen tilalle (joissakin koneissa se laitetaan rekisteriin). Aliohjelmaa, joka palauttaa tietoa kutsujalle, sanotaan funktioksi (function). Muut aliohjelmat ovat proseduureja (procedure). Joissakin kielissä kaikkii aliohjelmat ovat funktioita, ja paluuarvoton tilanne hoidetaan palauttamalla arvo, jolla ei ole väliä. Joissakin kielissä on erityinen tyyppi tällaista paluuarvoa varten. Funktion määrittelyssä on kieliopillinen pulma. Onko funktion runko lauseke vai lause? Jos se on lauseke, funktion paluuarvo on luonnollisesti tuon lausekkeen arvo, mutta jotta tällainen funktio olisi hyödyllinen, tulee lausekkeisiin 4. Guy Lewis Steele, Jr, emt.