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.
Esipuhe Sisältö Luku1. Johdanto 1 1. Kieltenjaotteluja 1 2. Kielenvalinta 6 3. Ohjelmointikieltensuunnitteluperiaatteita 7 4. Kielenmäärittely 8 5. Toteutustekniikoista 10 Osa 1. Imperatiivinen ohjelmointi 11 Luku 2. Konekielinen ohjelmointi 13 1. Konekielet 13 2. Symbolisetkonekielet 15 3. Abstraktitkoneet 16 4. Neloskielenidea 18 5. Systeemikutsut 23 6. Konekielelläohjelmoinnista 24 Luku3. Suoraviivaohjelmat 27 1. Suoraviivaohjelmanelementit 27 2. Abstraktisyntaksi 35 3. Denotationaalinensemantiikka 38 4. Tyyppijärjestelmä 46 5. Konkreettikielioppi 51 6. Toteutuksesta 57 Luku 4. Paikallinen kontrollivuon ohjaus 61 1. 61 Luku 5. Epädeterministinen ohjelmointi 63 Luku6. Samanaikaisuus 65 1. Jaettumuisti 65 2. Erilliset kommunikoivat prosessit 65 Luku7. Aliohjelma-abstraktio 67 Osa 2. Applikatiivinen ohjelmointi 69 Luku8. Funktioabstraktio 71 iii v
iv SISÄLTÖ Luku9. Tyypitys 73 Luku 10. Rakenteiset arvot 75 Luku11. Synteesi 77 Osa 3. Laajuuden hallinta 79 Luku 12. Abstraktit tietorakenteet ja modulit 81 Luku13. Olioabstraktio 83 Luku14. Polymorfismi 85 Kirjallisuutta 87 Liite A. Neloskoneen systeemikutsut 89 Liite B. Ohjelmointikielten historia 95 1. Kaksi ensimmäistä sukupolvea: ennen vuotta 1955 95 2. Automaattinen ohjelmointi ja ohjelmointikielten synty: 1955 1960 95 3. Baabelin torni: 1960-luku 97 4. Modernismi:1970-luku 98 5. Postmodernismi:1980-luku 99 6. Internetin nousu: 1990-luku 100 7. Sukupolvista 100 Liite C. Semanttiset alueet 101 1. Tuloalueet 103 2. Summa-alueet 104 3. Funktioalueet 105 Liite D. ALKEIS-suoran kääntäjä 109 1. Pääohjelma 109 2. Selain 113 3. Apuluokat 119 4. Lauseet 123 5. Tyypit 128 6. Lausekkeenjäsennin 135 7. Primäärilausekkeet 138 8. Operaattorilausekkeet 142 9. Muunnoslausekkeet 147
Osa1 Imperatiivinen ohjelmointi
LUKU 3 Suoraviivaohjelmat Suoraviivaohjelmilla tarkoitetaan ohjelmia, joissa ei ole lainkaan silmukoita tai muunlaisia hyppyoperaatioita. Suoraviivaohjelmien hyödyllisyys sinänsä on toki varsin vähäistä, mutta jokainen imperatiivinen ohjelma koostuu joukosta hyppyoperaatioin tai silmukoin yhdistetyistä pienistä suoraviivaohjelmista. Oppi alkaa juuresta, joten on hyödyllistä aloittaa kielten tutkiskelu suoraviivaisten ohjelmien kielestä. Samalla voimme tutustua yleisemminkin siihen, mitä ohjelmointikielet oikeastaan ovat. Luvussa esitellään kieli ALKEIS-suora, joka on yksinkertaisin AL- KEIS-kieli. Sillä voi ilmaista suoraviivaohjelmia, jotka koostuvat peräkkäistetyistä sijoituslauseista, tulostuslauseista ja syötteenlukulauseista. 1. Suoraviivaohjelman elementit Suoraviivaohjelmoinnin perusperiaate on seuraavanlainen: ohjelmakoostuulauseista 1 (engl.statements),jotkayhdistetäänperäkkäistyksellä(engl. sequencing). Tärkein lause on sijoituslause(engl. assignment statement), joka asettaa muuttujalle(engl. variable) jonkin uuden arvon(engl. value), jonka se saa laskemalla jonkin tietyn lausekkeen(engl. expression) arvon senhetkisessä ympäristössä(engl. environment). Käykäämme nyt edellisessä kappaleessa esille tulleet käsitteet yksitellen läpi. 1.1. Arvo. Arvot ovat abstrakteja, matemaattisia käsitteitä. Arvoja ovat esimerkiksi eri kokonaisluvut, merkkijonot ja muut sellaiset. Ne ovat ajattomia ja paikattomia: ei ole mielekästä pohtia, milloinnollasyntyitaimilloinsana lakkaaolemasta.arvon lukumäärääkään ei voida mielekkäästi laskea(kuinka monta ykköstä on?). Arvot eivät myöskään muutu(oho, ykkönen onkin nyt kakkonen vaionko?). On tärkeää käsittää, että arvot eivät näy missään. Tietokoneohjelman lähdekoodissa esiintyvä merkkijono ei ole arvo 42; se edustaa 1 Huomaa:käskyt(engl.instructions)kuuluvatkonekieliohjelmointiin.Korkean tason kielissä puhutaan lauseista. 27
28 3. SUORAVIIVAOHJELMAT tai merkitsee(engl. denote) sitä. Myöskään ohjelman ruudulle tulostama eiolemerkkijonoarvovaanseedustaasitä. Tästä huolimatta arvoilla on merkittävä rooli tietokoneohjelmien teoriassa ja ymmärtämisessä. Enempää arvoista on vaikea sanoa, ennen kuin muita käsitteitä on esitelty. 1.2. Muuttuja. Muuttuja on paikka, jossa voi säilyttää yhtä ja vain yhtä arvoa. Imperatiivisessa ohjelmoinnissa on oleellista, että muuttujan sisältämä arvo voidaan korvata toisella. Toisin kuin arvo, muuttuja on ajassa ja paikassa kiinni: muuttuja syntyy joskus ja kuolee joskus, ja se sijaitsee aina jossain paikassa. Muuttuja on muistialueen abstraktio. Elinaikanaan muuttuja on sidottu(engl. bound) johonkin tiettyyn muistialueeseen. Muistialueella on osoite ja pituus, kumpikin tavallisesti etumerkittömiä kokonaislukuja. Muuttujalla sen sijaan on yleensä nimi(nimettömät muuttujat jätämme toistaiseksi huomiotta) ja tyyppi, joista voidaan johtaa, mihin muistialueeseen muuttuja on sidottu. Muuttujalla on monia ominaisuuksia(engl. attributes). Tärkeimmät niistä on jo mainittu: nimi, tyyppi, arvo, muuttujan muistialueen osoite ja muuttujan muistialueen pituus. Nämä jakautuvat siististi kahteen eri lokeroon: muuttujan ominaisuus voi olla joko staattinen(engl. static) tai dynaaminen(engl. dynamic). Dynaaminen ominaisuus on sellainen, joka vaihtelee eri suorituskertojen välillä; staattinen ominaisuus on sama suorituskerroista riippumatta. Dynaamisia ominaisuuksia ovat arvo sekä muuttujan muistialueen osoite ja pituus. Staattisia ominaisuuksia ovat nimi ja tyyppi. Muuttujan muistialueen pituus on yleensä määritettävissä muuttujan tyypistä. Usein muuttuja samastetaan nimeensä. Tämä on yleensä järkevää, mutta tarkkana pitää olla, ettei synny sekaannuksia, kun sama nimi voi eri yhteyksissä tarkoittaa eri muuttujaa. 1.3. Ympäristö. Jokaisella lauseella on ympäristö, joka yhdistää muuttujien nimet niihin muuttujien ominaisuuksiin, jotka ovat kyseisen lauseen kohdalla voimassa. Kuten muuttujien ominaisuudet, myös ympäristöt luokitellaan staattisiin ja dynaamisiin: staattiset ympäristöt sisältävät staattisia(pysyviä) ominaisuuksia ja dynaamiset ympäristöt sisältävät dynaamisia(muuttuvia) ominaisuuksia. 1.4. Lauseke. Lauseke on ohjelmointikielen ilmaisu, joka edustaa arvoa. Yksi tärkeimmistä piirteistä, joka erottaa(symbolisen) konekielen korkean tason ohjelmointikielistä on tuki mielivaltaisen monimutkaisille lausekkeille, joita ohjelmoijan ei itse tarvitse jakaa yksittäisiin operaatioihin, joiden välitulokset hänen pitäisi tallentaa jonnekin. Maailman vanhimman korkean tason ohjelmointikielen nimi on FORTRAN, formula translator, syystä.
1. SUORAVIIVAOHJELMAN ELEMENTIT 29 Sallittujen lausekkeiden joukko riippuu kielestä. Käytännössä kaikki ohjelmointikielet tukevat ainakin tavallisia aritmeettisia lausekkeita: (1) Lukuvakio on aritmeettinen lauseke. (2) Muuttuja, joka on nykyisessä ympäristösssä sidottu lukuarvoon, on aritmeettinen lauseke. (3)Josejae ovataritmeettisialausekkeita,niine+e,e e,e e jae/e ovataritmeettisialausekkeita. (4) Jos e on aritmeettinen lauseke, niin e ja(e) ovat aritmeettisia lausekkeita. Yllä esitetyt lauseketyypit voidaan jaotella primäärilausekkeisiin (engl. primary expressions), unaarilausekkeisiin(engl. unary expressions) sekä binäärilausekkeisiin(engl. binary expressions). Primäärilausekkeita ovat sulkulausekkeet(e) sekä lukuvakiot ja muuttujat lausekkeina. Unaarilausekkeet jäsentyvät siten, että niissä on ensin jokin operaattori(engl. operator) ja sitten alilauseke(ns. operandi(engl. operand)); unaarilausekkeita ovat muotoa e, missä operaattori on ja operandi on e. Binäärilausekkeet alkavat alilausekkeella(vasen operandi),jonkajälkeentuleeoperaattori(+,, tai /)jalopuksitoinen alilauseke(oikea operandi). Lauseketta, jossa binäärinen operaattori voi sijaita operandiensa välissä, sanotaan infix-lausekkeeksi. Ongelmana tällaisissa lausekkeissa on, että ei ole aina aivan selvää, miten lauseke pitäisi ymmärtää. Esimerkki tällaisesta moniselitteisestä(engl. ambiguous) lausekkeestaon1 +2 3:pitääköseymmärtääsamoinkuin(1 +2) 3 vaikentiessamoinkuin1 +(2 3)?Jomuinaisetmatemaatikottiesivät tähän ratkaisun: pitää määritellä, mikä on eri operaattoreiden presedenssi(engl. precedence) ja assosiatiivisuus(engl. associativity). Jos tarkasteltavanaonlausekemuotoae e e,missäoperaattorilla on korkeampi presedenssi kuin operaattorilla, sen sovitaan tarkoittavan(e e ) e ;jostaasoperaattorilla onmatalampipresedenssi kuinoperaattorilla,sensovitaantarkoittavane (e e ).Josoperaattoreilla ja on sama presedenssi, niin lausekkeen tulkinta riippuu niiden assosiatiivisuudesta. Jos molemmat assosioituvat vasemmalle(engl.associatetotheleft),lauseketulkitaan(e e ) e,jajosmolemmat assosioituvat oikealle(engl. associate to the right), lauseke tulkitaane (e e ).Josneassosioituvaterisuuntaantaiainakaantoinen ei assosioidu lainkaan, lausekkeen todetaan olevan kielen sääntöjen vastainen. Jos sama operaattori voi esiintyä sekä unaarisena että binäärisenä(esimerkiksi ), tulee sen unaarinen ja binäärinen versio pitää erillään; niillä on yleensä eri presedenssi. Presedenssi- ja assosiointisäännöt vaihtelevat kielestä toiseen. Yleensä lienee järkevää, että matematiikasta tutut aritmeettiset operaattorit(esimerkiksi yhteen-, vähennys-, kerto- ja vähennyslaskuoperaattorit sekä vastalukuoperaattori) noudattavat matematiikasta tuttua
30 3. SUORAVIIVAOHJELMAT (unaarinen) / + TAULUKKO 1. Tavanomaisten aritmeettisten operaattoreiden normaali presedenssirelaatio presedenssiä: vastalukuoperaattorilla(unaarinen ) on korkeampi presedenssi kuin kerto- ja jakolaskuoperaattoreilla, joilla puolestaan on korkeampi presedenssi kuin yhteen- ja vähennyslaskuoperaattoreilla. Kerto- ja jakolaskuoperaattoreilla on sama presedenssi, ja samoin yhteen- ja vähennyslaskuoperaattoreilla on sama presedenssi. Kaikki nämä operaattorit assosioituvat vasemmalle paitsi vastalukuoperaattori, joka assosioituu oikealle. Presedenssi muodostaa operaattoreiden välille osittaisjärjestyksen 2.Yleensäkuitenkinpresedenssirelaatioontäydellinenjärjestys ja se esitetään tavallisesti taulukkona, jossa korkeamman presedenssin omaavat operaattorit ovat korkeammalla kuin matalamman presedenssin omaavat operaattorit. Edellä esitetyt tavanomaiset presedenssisäännöt on esitetty taulukossa 1. Kannattaa huomata, että presedenssi ja assosiatiivisuus eivät määrittele laskujärjestystä vaan jäsennyksen, vaikka toisin usein ajatellaankin.esimerkiksilausekkeessa2+3+4 5voidaanlaskeaensin2+3 siitä huolimatta, että koulussa opetettiin, että kertolasku lasketaan ensin. Vaikka infix-lausekkeet ovatkin kaikista tutuimpia, eivät ne ole ainoat mahdollisuudet. Voidaan esimerkiksi käyttää ns. puolalaisia (engl. Polish) eli prefix-lausekkeita, joissa operaattori tulee aina ensin ja vasta sitten operandit. Tämän esitystavan etu on, että sulkuja tai presedenssi- ja assosiatiivisuussääntöjä ei tarvita, jos operaattorien operandimäärä on kiinteä(eli jos sama operaattori ei ole sekä unaarinen että binäärinen): + 2 3 4 5 tarkoittaa yksiselitteisesti samaakuininfix-lauseke(2 3) +(4 5).Vastaavastivoidaankäyttää käänteisesti puolalaisia(engl. reverse Polish) eli postfix-lausekkeita, joissa operaattori tulee operandien jälkeen. Tuo sama lauseke olisi postfix-lausekkeena23 45 +. 3 2 Muistakaamme,ettäosittaisjärjestys(engl.partialorder)onrelaatio,jokaon refleksiivinen(a apäteeaina),antisymmetrinen(josa bjab apätevät,niin a =bpätee)jatransitiivinen(josa bjab cpätevät,niina cpätee).täydellinen järjestys(engl. total order) vaatii lisäksi totaalisuuden(a b tai b a pätee aina). 3 Postfix-lausekkeetovatkäytössäForth-jaPostscript-kielissä.Prefix-lausekkeiden muunnelma, ns. Cambridgen-puolalainen lauseketyyppi, jossa operaattorit tulevat aina operandien edellä mutta lausekkeiden ympärillä on aina sulut, on käytössä Lisp-sukuisissa kielissä.
1. SUORAVIIVAOHJELMAN ELEMENTIT 31 + 2 3 6 / 7 8 KUVA1. Lausekkeen(2 +3) 6 7/8rakennepuu Lausekkeita onkin paras ajatella puina, jotka vain kirjoitetaan näkyviin infix-, prefix- tai postfix-tyylillä. Näissä puissa operaattorit ovat sisäsolmuja ja operandit ovat operaattorinsa alipuita. Puun lehdet muodostuvat muuttujista ja lukuvakioista. Tällaista puuta sanotaan lausekkeen rakennepuuksi(engl. structural tree); eräs sellainen on kuvattu kuvassa 1. 1.5. Sijoituslause. Sijoituslause on imperatiivisen ohjelmoinnin toinen kulmakivi. Se on yleensä muotoa x e(tarkka ilmiasu vaihtelee kielittäin), ja sen tehtävänä on laskea lauseke e nykyisessä ympäristössä ja muuttaa sitten ympäristöä siten, että muuttujan x arvoksi tulee lausekkeen e arvo. 1.6. Peräkkäistys. Peräkkäistys on toinen imperatiivsen ohjelmoinnin kulmakivi. Se on yleensä hieman piilossa, sillä se esiintyy yleensä ohjelmissa vain muunlaisten kontrollirakenteiden käyttämättä jättämisenä. Peräkkäistyksen tehtävänä on suorittaa jono lauseita peräjälkeen: s 1 ;...;s n tarkoittaa,ettäs 1 suoritetaantämänperäkkäistyslauseen alussavoimassaolevassaympäristössäjayleisestis i suoritetaanlauseens i 1 muokkaamassaympäristössä;peräkkäistyslauseenlopussa voimassaonseympäristö,jokaonvoimassas n :nlopussa. 1.7. Tyyppi. Tyyppi(engl. type) on muuttujan tai lausekkeen ominaisuus. Sillä on kolme tehtävää: se kertoo, minkä joukon alkio muuttujan tai lausekkeen arvo on; lisäksi se kertoo, miten se tavujono, joka muodostaa sen muistialueen, johon muuttuja on sidottu, tulkitaan arvoksi; lausekkeen tyyppi kertoo myös, mitkä operaatiot ovat kyseiselle lausekkeelle sallittuja. Olioihin kohdistuu operaatioita. Operaatiot olettavat, että kohteena oleva olio on jotain tiettyä tyyppiä. Tyyppivirheellä(type error) tarkoitetaan sitä, että jonkin suoritettavan operaation kohteena on olio, jonka tyyppi ei ole se, mitä operaatio olettaa. Huomaamatta jäävät tyyppivirheet johtavat ohjelman sekoamiseen: koska olio
32 3. SUORAVIIVAOHJELMAT on eri tyyppiä kuin oletetaan, sen tulkinta arvoksi on täysin päätön ja tuloksena on roskaa. Mikäli ohjelmointikieli(eli oikeasti sen määrittely) vaatii toteutukseltaan, että se diagnosoi(ilmoittaa käyttäjälle) kaikki tyyppivirheet, kieli on vahvasti tyypitetty(strongly typed). Muussa tapauksessa kieli on heikoisti tyypitetty(weakly typed). Joitakin kieliä voidaan myös verrata keskenään sen mukaan, mitä tyyppivirheitä ne vaativat diagnosoitavaksi. Esimerkiksi C ja C++ ovat heikosti tyypitettyjä(molemmissa on mahdollista kirjoittaa tyyppivirheellinen ohjelma, jonka tyyppivirhettä ei toteutus huomaa), mutta C++ on paljon vahvemmin tyypitetty kuin C. Java, Haskell ja Scheme ovat esimerkkejä vahvasti tyypitetyistä kielistä. Kielen toteutus voi diagnosoida tyyppivirheen joko ennen suoritusta tai suoritusaikana. Ensin mainitussa tapauksessa on kyse staattisesta tyyppitarkastuksesta(static typechecking), jälkimmäisessä dynaamisesta tyyppitarkastuksesta(dynamic typechecking). Staattiseen tyyppitarkastukseen palataan myöhemmin; nyt tarkastellaan dynaamista tyyppitarkastusta. Mikäli dynaamista tyyppitarkastusta halutaan, tulee olion sisällä olla tieto siitä, mitä tyyppiä se on. Oliokielissä(joissa tämä tieto tarvitaan jo metodien dynaamisen sidonnan toteuttamiseen) tämä toteutetaan jo aiemmin mainitulla luokkaoliolla, jonka osoite tallennetaan jokaisen tätä tyyppiä olevan olion alkuun. Tyyppitarkastus voidaan tehdä tarkastamalla, mihin luokkaolioon olion alussa on osoitin. Vastaava tekniikka toimii myös muissa kielissä. Dynaamisissa funktiokielissä(mm. Lisp), joissa tyyppejä on varsin vähän, on tapana käyttää oliona konesanaa, josta varataan muutama bitti tyyppitunnisteeksi(isommat oliot laatikoidaan). Joissakin kielissä on hyvin rikas tyyppijärjestelmä; palaamme näihin myöhemmin applikatiivisen ohjelmoinnin yhteydessä. Seuraavat tyypit ja niiden muunnelmat ovat yleisiä imperatiivisissa kielissä(konkreettisuuden vuoksi käytän niistä C-kielen tyylisiä nimityksiä): int:kokonaislukuväliltä 2 n 1,...,2 n 1 1,jokaesitetäänkahden komplementtina yhdessä konesanassa.(tässä n on konesanan pituus bitteinä.) unsignedint:kokonaislukuväliltä0,...,2 n 1,jokaesitetään etumerkittömänä yhdessä konesanassa.(tässä n on konesanan pituus bitteinä.) byte:kokonaislukuväliltä 2 n 1,...,2 n 1 1,jokaesitetään kahden komplementtina yhdessä tavussa.(tässä n on tavun pituusbitteinä,tavallisesti8,jolloinlukualueon 128,...,127.) unsignedbyte:kokonaislukuväliltä0,...,2 n 1,jokaesitetään etumerkittömänä yhdessä tavussa.(tässä n on tavun pituusbitteinä,tavallisesti8,jolloinlukualueon0,...,255.)
1. SUORAVIIVAOHJELMAN ELEMENTIT 33 float: Liukuluku, joka esitetään tavallisesti IEEE 754-standardin yksinkertaisen tarkkuuden liukulukuna neljän tavun tavujonossa. double: Liukuluku, joka esitetään tavallisesti IEEE 754-standardin kaksinkertaisen tarkkuuden liukulukuna kahdeksan tavun tavujonossa. T[]: Taulukko, jonka alkiot ovat tyyppiä T. Useimmat tyyppijärjestelmät laskevat näistä intin, unsigned intin, byten sekä unsigned byten kokonaislukutyypeiksi(engl. integral types) (siltä osin kuin tukevat näitä tyyppejä). Kaikki tyyppijärjestelmät sallivat aritmeettisten lausekkeiden rakentamisen niin, että jokaisen alilausekkeen tyyppi on sama; tällöin kokonaisen aritmeettisen lausekkeen tyyppi on tuo tyyppi. Vastaavasti aritmeettisissa lausekkeissa on yleensä sallittu liukulukutyyppien(float ja double) käyttäminen, jos kaikilla alilausekkeilla on sama tyyppi. Tiukka tyyppijärjestelmä kieltää tyyppien sekakäytön. Monet varsinkin vanhemmat imperatiiviset ohjelmointikielet määrittelevät kaikenlaisia automaattisia tyypinmuunnoksia(engl. coercions) näiden tyyppien välille: aina silloin, kun tiukka tyyppijärjestelmä olisi kieltämässä jonkin lausekkeen, muunnoksia käyttävä tyyppijärjestelmä pyrkii korjaamaan lausekkeen tyypityksen lisäämällä siihenmuunnosoperaatioita.esimerkiksic-kielessä[13]byte 4 voidaan muuttaa tarvittaessa int-tyypiksi, float voidaan muuttaa doubleksi ja mikä tahansa unsigned-tyyppi voidaan muuttaa vastaavaksi etumerkilliseksi tyypiksi, ja näitä muunnoksia voidaan tarvittaessa ketjuttaa. C-kielen vuoden 1999 versiossa myös bool voidaan muuttaa tarvittaessa unsigned byteksi. Automaattiset tyypinmuunnokset ovat käteviä, mutta niillä on myös kääntöpuolensa. Tyyppijärjestelmän yksi tehtävä on suojata ohjelmoijaa virheiltä, jotka johtuvat tyyppien sekoittamisesta; jos kieli tekee automaattisia tyypinmuunnoksia, se todennäköisesti tekee niitä myös tilanteissa, joissa ohjelmoija ei niitä odota. Erityisen ikävä voi olla esimerkiksi kokonaislukutyypin muuttuminen yllättäen liukuluvuksi, mikä saattaa vähentää laskentatarkkuutta, tai tämän muunnoksen tapahtumatta jääminen silloin, kun ohjelmoija sitä odottaa(esimerkiksi C-kielessä jakolasku antaa tulokseksi 0, koska molemmat operandit ovat kokonaislukutyyppisiä; useampikin ohjelmoija on odottanut tuossa tilanteessa tapahtuvan automaattisen muunnoksen liukuluvuksi, jolloin tulos olisi 0.5). 1.8. Taulukoista. Taulukoiden(engl. arrays) hyväksyminen kieleen vaatii hieman tarkennusta edellä esitettyihin käsitteisiin. 4 C-kielessäoikeastibyteonnimeltäänsignedcharjaunsignedbyteonnimeltään unsigned char.
34 3. SUORAVIIVAOHJELMAT Taulukkoja tukevan kielen sallittuihin lausekkeisiin pitää totta kai lisätä indeksointi a[i]; tässä vaaditaan, että a:n tyyppi on T[] jollekin tyypille T ja että i:n tyyppi on jokin kokonaislukutyyppi. Tällöin indeksointilausekkeen tyyppi on T ja sen arvo on se arvo, joka on tallennettuna taulukon a kohtaan i. Taulukoiden kanssa nousee ensimmäistä kertaa esille ohjelmointikielen turvallisuuden ongelma. Taulukot ovat äärellisiä, joten herää kysymys, mitä tapahtuu, jos taulukon koko on 5, taulukon indeksit alkavat nollasta ja indeksinä käytetään vaikkapa 42:ta. Yksinkertaisinta olisi toki unohtaa koko ongelma ja jättää asiasta huolehtiminen kokonaan ohjelmoijan vastuulle. Näin toimivat ainakin C ja C++. Jos ohjelmoija kuitenkin vahingossa(tai tahallaan!) indeksoi taulukon ulkopuolelle, vähintäänkin lausekkeen arvo on arvaamaton; hyvällä onnella käyttöjärjestelmä tai prosessori huomaa tämän kielletyn indeksoinnin ja lopettaa ohjelman. Vastuullisempaa olisi luultavasti se, että kielen toteutus tarkistaa indeksoinnin kunnollisuuden juuri ennen itse indeksointioperaatiota eli tekee rajatarkastuksen(engl. bounds check); tämä tosin hidastaa ohjelman suoritusta. TEHTÄVÄ 3.1. Pascal-kielessä taulukon indeksirajat kuuluvat taulukon tyyppiin: VARa:ARRAY[5..10]OFINTEGER määrittelee muuttujan a, joka on kokonaislukuja sisältävä taulukko, jossa onkuusialkiotajaindeksiionsallittu,jos5 i 10pätee.Indeksirajojen täytyy tietenkin olla vakioita, koska ne ovat osa a:n tyyppiä. Mitä hyötyjä ja mitä haittoja tästä on? Jos taulukon on tarkoitus olla hyödyllinen osa imperatiivista kieltä, pitää olla mahdollista päivittää taulukon yksittäisen alkion arvoa. Tämä tarkoittaa, että indeksointi pitää sallia myös sijoituslauseen vasemmalla puolella muuttujan paikalla. Yksinkertaisinta varmaankin olisisalliasijoituslause,jokaonmuotoaa[i] e,missäajaiovatkuten yllä ja e on sopivantyyppinen lauseke. Yleisemmin kuitenkin voi ollajärkevääsanoa,ettäsijoituslauseone e,missäejae ovatsamantyyppisiälausekkeita.eikuitenkaanolejärkevääsanoa,ettäe :n arvo sijoitetaan e:n arvoon. Christopher Strachey[32] ehdotti 1960-luvulla arvon käsitteen jakamista kahtia: lausekkeella on kaksi arvoa, vasen arvo(engl. left value)jaoikeaarvo(engl.rightvalue) 5.Lausekkeenoikeaarvoonse, mitä on edellä kutsuttu pelkäksi arvoksi. Muuttujalausekkeen vasen arvo kuvaa sen muistialueen alkuosoitetta, joka on tuohon muuttujaan sidottu, ja yleisemmin lausekkeen vasen arvo kertoo, mihin muistialueeseen lausekkeen oikea arvo on tallennettu. Nämä nimet 5 Nämäenglanninkielisettermitlyhennetäännykyisinyleensälvaluejarvalue.
2. ABSTRAKTI SYNTAKSI 35 tulevat tietenkin siitä, että lausekkeen vasenta arvoa käytetään, kun lauseke esiintyy sijoituslauseen vasemmalla puolella, ja oikeaa arvoa käytetään, kun lauseke esiintyy sijoituslauseen oikealla puolella. Edellä esitetty vasemman arvon määritelmä on kuitenkin turhan salliva. Jos jokaisella lausekkeella olisi vasen arvo, niin se olisi myös lukuvakiolla 42. Jos näin on, niin sijoituslauseen 42 2 tulisi olla sallittu. Mitä se tekisi? Itse asiassa joissakin FORTRAN-kielten toteutuksissa on todella ollut mahdollista muuttaa lukuvakion arvoa niin, että tuollaisen sijoituslauseen jälkeen jokainen viittaus lukuvakioon 42 tarkoittaisikin 2:a. Tämä ei ole yleensä kuitenkaan järkevää, joten tavallisesti sovitaan, että kaikilla lausekkeilla ei ole vasenta arvoa. Tarkastelemistamme lausekkeista vasen arvo on vain muuttujalausekkeella sekä indeksointilausekkeella a[i], jos a:lla on vasen arvo. Kun taulukkon alkioon voi sijoittaa, on indeksoinnin rajatarkastus yllättävänkin tärkeää. Esimerkiksi jos taulukkoon luetaan verkosta dataa, saattaa rajatarkastuksen puuttuminen tehdä ohjelmointivirheestä tietoturvaongelman. Kannattaa huomata, että koska T[] on tyyppi, jos T on tyyppi, niin taulukot voivat koostua taulukoista. 2. Abstrakti syntaksi Kielen määritteleminen lähtee sen pohtimisesta, mikä on sen abstrakti syntaksi(engl. abstract syntax): minkälaisia konstruktioita kielessä on, kun jätetään huomiotta sellaiset yksityiskohdat kuten mikä tarkkaan ottaen kelpaa muuttujan nimeksi taikka mikä on operaattoreiden presedenssi. Abstraktin syntaksin tasolla kielellä kirjoitettu ohjelma hahmotetaan enemmänkin tietorakenteina, jotka kuvaavat ohjelmaa, kuin merkkijonona. Tästä saatiin jo aiemmin esimerkki: abstraktin syntaksin tasolla lauseke hahmotetaan rakennepuunaan.
36 3. SUORAVIIVAOHJELMAT Abstrakti syntaksi voidaan määritellä käyttäen kontekstittomista kieliopeista tuttua merkintätapaa. Esimerkiksi ALKEIS-suoran abstrakti syntaksi on seuraavanlainen: π Programs π::=var δ begin γ end ohjelma: muuttujien esittelyt ja sitten lause δ Declarations δ::=ι: τ muuttujan esittelyssä on nimi ja tyyppi δ 1 ;δ 2...taikaksiesittelyäperäkkäin τ Types τ::=int unsigned int sana kokonaisluvuksi tulkittuna byte unsigned byte tavu kokonaisluvuksi tulkittuna float double liukuluku τ[ν] taulukko γ Statements γ::=ε 1 ε 2 write ε read ε sijoituslause kirjoituslause lukulause γ 1 ;γ 2 peräkkäistys ε Expressions ε::=ι lausekevoiolla...muuttuja ν...lukuvakio ε...vastalukuoperaatio ε 1 ε 2...kertolasku ε 1 /ε 2...jakolasku ε 1%ε 2...jakojäännös ε 1 + ε 2...yhteenlasku ε 1 ε 2...vähennyslasku ε 1 [ε 2 ]...indeksointi Tässä välikesymboleina käytetään kreikkalaisia kirjaimia, jotka on valittu niin, että ne muistuttavat sitä, mitä ne edustavat(esimerkiksi ε, epsilon on lauseke eli expression). Metasyntaktiset symbolit ovat: ::= erottaa produktion vasemman ja oikean puolen. erottaa produktion vaihtoehtoiset oikeat puolet.
2. ABSTRAKTI SYNTAKSI 37 Muut symbolit(mukaan lukien lihavoidut sanat) ovat päätesymboleita. Välikesymboleilta näyttävät ι Ids(muuttujannimet, identifiers) ja ν Constants(lukuvakiot, numeric constants) ovat oikeasti päätesymboleita, jotka edustavat mielivaltaista muuttujannimeä(ι) ja mielivaltaista lukuvakiota(ν). Abstrakti syntaksi eroaa tavanomaisesta(konkreetista) syntaksista siten, että abstrakti syntaksi on tavallisesti merkkijonokieliopiksi tulkittuna hyvin moniselitteinen. Tämä ei kuitenkaan haittaa, sillä ajatuksena on, että abstrakti syntaksi katsotaan puukieliopiksi(engl. tree grammar) eli että se kuvaa sallittuja puita sallittujen merkkijonojen asemesta. Puukielioppina abstrakti syntaksi on hyvinkin yksiselitteinen, kunhan seuraava ehto pätee[30, s. 2]: Kunkin välikesymbolin produktioilla on kullakin oma päätesymbolijono. Toisin sanoen, ei ole olemassa kahta saman välikesymbolin produktiota siten, että kun näistä produktioista poistetaan kaikki välikesymbolit, jäljelle jäävien päätesymbolien muodostamat jonoteivätolesamajono. Puukieliopiksi abstrakti syntaksi tulkitaan siten, että kukin produktio kertoo, miten puu voidaan konstruoida: juuri laputetaan sillä päätesymbolien jonolla, joka jää jäljelle, kun produktion oikeasta puolesta poistetaan välikesymbolit, ja kutakin produktion oikean puolen välikesymbolia vastaa kyseisen välikesymbolin puukieliopin mukainen alipuu. Alipuut pidetään järjestyksessä. Abstraktia syntaksia käytetään ahkerasti erityisesti kielten teoreettisessa tarkastelussa. Silloin tarvitaan tapa kirjoittaa abstraktin syntaksin puut merkkijonoiksi jollakin tavalla, sillä puupiirrokset ovat ikävän tilaavieviä ja kömpelöitä teoreetikkojen kannalta. Käytännössä tässä toimitaan niin, että abstrakti syntaksi tulkitaan merkkijonokieliopiksi ja sulkeita sekä epämuodollisia presedenssi- ja assosiatiivisuussopimuksia käytetään ratkaisemaan moniselitteisyysongelmat. Merkkijonoon saatetaan jättää alaindeksoituja välikesymboleja ilmaisemaan paikkoja, johon voi laittaa minkä tahansa merkkijonon(tai puun), joka sopii kyseisen välikesymboliin. Tämän ei kuitenkaan ole tarkoitus olla kielen varsinainen konkreetti kielioppi, vaan kyseessä on puhtaasti sopimus, joka mahdollistaa abstraktin syntaksin puiden kirjoittamisen tiiviisti ja luettavasti. Abstrakti kielioppi on keskeinen käsite ohjelmointikielten teoriassa, mutta se on merkittävä myös käytännössä, sillä kielenkäsittelimien kuten kääntäjien tai tulkkien kannattaa yleensä käsitellä ohjelmia ainakin osan aikaa juuri rakennepuina(ja sallitut rakennepuut määrittelee juuri abstrakti kielioppi). Rakennepuu on suhteellisen yksinkertaista esittää tietorakenteena kieltä käsittelevässä ohjelmassa. Jos esimerkiksi toteutuskielenä
38 3. SUORAVIIVAOHJELMAT on Java, on selkeintä tehdä jokaisesta välikesymbolista abstrakti luokka tai rajapinta ja periä siitä final-luokka jokaiselle produktiolle. Produktioluokalle annetaan attribuutti jokaista produktion oikealla puolella esiintyvää välikesymbolia kohti. Ohjelma rakentuu sitten tämän luokkarakenteen ympärille jommalla kummalla seuraavista strategioista: (1) Jokaista rakennepuulle tehtävää operaatiota kohti laitetaan välikesymboliluokkiin tai-rajapintoihin metodi. Tästä on se etu, että saman kielikonstruktion kaikki toiminnallisuus on yhdistetty samaan luokkaan. Kielen muuttaminen on tässä rakenteessa helppoa, mutta uuden operaation lisääminen vaatii kaikkien luokkien muuttamista. (2) Luokkarakenne varustetaan Visitor-suunnittelumallin[9] tukimetodeilla ja-rajapinnoilla. Kukin operaatio toteutetaan sitten luokkana, joka toteuttaa rakennepuuluokaston visitorrajapinnan. Tämän hyöty on, että uusia operaatioita voidaan luoda toisistaan riippumatta ja koskematta rakennepuuluokastoon. Vastaavasti kielen muuttaminen on vaivalloista. Valinnan tueksi sopii siis nyrkkisääntö, että visitor-malli sopii paremmin vakiintuneen ja varsin hitaasti muuttuvan ohjelmointikielen kyvykkääseen kääntäjään ja että metodimalli sopii paremmin uuden, nopeasti muuttuvan ohjelmointikielen yksinkertaiseen prototyyppikääntäjään. Toki molempia menetelmiä voi käyttää samassakin kääntäjässä tarpeen mukaan: esimerkiksi kääntäjä, joka jakautuu kahteen vaiheeseen, jotka kommunikoivat erityisen välikielen(engl. intermediate language) avulla, kannattanee toteuttaa niin, että lähdekielen prosessointi tehdään metodimallilla ja välikielen käsittely(esimerkiksi optimoinnit) tehdään visitor-mallilla. Näin siksi, että välikielen voisi ajatella muuttuvan hitaammin kuin nuoren ohjelmointikielen. Kuvassa 2 on esitetty eräs mahdollinen luokkarakenne, jolla AL- KEIS-suoran rakennepuut voitaisiin esittää. 3. Denotationaalinen semantiikka Kielelle pitää määritellä myös merkitys, eli mitä mikäkin konstruktio tekee. Tavallisimmin tämä tehdään luonnollisella kielellä, esimerkiksi englanniksi, mutta yksistään se on turhan epätarkka keino. Parempi mutta vaivalloisempi tapa on tehdä kielestä matemaattinen malli eli formaali semantiiika(engl. formal semantics). Tässä on kaksi pääasiallista lähestymistapaa: denotationaalinen ja operationaalinen. Seuraavassa on esitetty ALKEIS-suora-kielelle denotationaalinen semantiikka.
SequencingStatement KUVA 2. ALKEIS-suoran abstraktin kieliopin staattinen analyysivaiheen UML-malli left value VarExpression Expression +name: string ConstantExpression +value IndexingExpression NegationExpression UnsignedIntType UnsignedType UnsignedByteType ByteType SignedType IntType right value BinaryOperatorExpression IntegralType AssignmentStatement Statement ReadStatement WriteStatement Declaration RemainderExpression DivisionExpression SimpleDeclaration MultiplicationExpression +name: string SubtractionExpression AdditionExpression Type ArrayType +length: integer NumericType FloatType FloatingPointType DoubleType PairDeclaration Program 3. DENOTATIONAALINEN SEMANTIIKKA 39
40 3. SUORAVIIVAOHJELMAT Denotationaalisen semantiikan idea on liittää kielen jokaiseen konstruktioon matemaattinen funktio, joka kertoo, miten kyseinen konstruktio käyttäytyy. 3.1. Arvojoukot. Olkoon B tavun pituus bitteinä(tavallisesti 8) ja olkoon W sanan pituus tavuina. Määritellään funktio V, joka liittää kuhunkin tyyppiin sen arvojoukon: V:Types 2 RValue V byte = { n 2 B 1 n<2 B 1 } { } V unsignedbyte = { n n<2 B } { } V int = { n 2 WB 1 n<2 WB 1 } { } V unsignedint = { n n<2 WB } { } V float = {x x F(24, 126,+127) } {+,,NaN, } V double = {x x F(53, 1022,+1023) } {+,,NaN, } V T[] = {f V T n : i :(f(i) = i n) } { } missä { F(p,e min,e max ) = 2 e s p k=1 2 k f k s { 1,1} e } e min e e max f 1 {0,1} f p {0,1} Jokaisessa arvojoukossa esiintyvä eli pohja(engl. bottom) tarkoittaa jotain sellaista kuvitteellista arvoa, joka ilmaisee virheellistä tai loputonta laskemista. Esimerkiksi kokonaisluvun jakaminen kokonaislukunollalla tuottaa :n. Se on lähinnä teoreettinen merkintätapa; todellisissa ohjelmissa se ilmenee ohjelman päättymisenä, omituisena käyttäytymisenä tai jumittumisena. Kokonaislukutyyppien arvojoukot ovat selkeitä: pohjan lisäksi niihin kuuluvat asiaankuuluvat kokonaislukuarvot. Liukulukutyypit ovat hankalampia, eikä tarkoitus olekaan, että niiden määrittelevää kaavaa alettaisiin tankata tarkasti. Idea on, että liukulukutyypin määrittää kolme parametria: mantissan koko p sekä eksponentinrajate min jae max.näistäkaavaf(p,e min,e max )määrittääliukulukutyypin esitettävissä olevat reaaliluvut. Näiden parametrien arvot on otettu IEEE 754-standardista[1]. Muut arvot liukulukutyypeissä ovat pohja, kaksi äärettömyyttä sekä NaN(engl. Not a Number), joka ilmaisee liukulukulaskun epäonnistumista esimerkiksi nollalla jaon vuoksi 6. 6 Selkeämpääolisitokikäyttääpohjaamyösliukulukuepäonnistumistenilmaisemiseen, mutta NaN on vakiokamaa liukulukujen kanssa. Syy tähän on, että liukulukulaskijat saavat käytetystä NaN-esitystavasta selville, mikä meni laskussa vikaan, mikä ei pohjan tapauksessa onnistu.
3. DENOTATIONAALINEN SEMANTIIKKA 41 Taulukkotyypin arvona käytetään funktiota luonnollisilta luvuilta taulukon alkiotyypin arvoille. Indeksoinnin rajallisuus ilmaistaan siten, että kullakin taulukkoarvolla on koko n, jonka käyttäminen indeksinä, kuten myös minkä tahansa sitä suuremman luvun käyttäminen indeksinä, tuottaa pohjan. Näistä arvojoukoista saadaan koottua määritelmä oikeiden arvojen joukolle: RValue =V byte +V unsignedbyte +V int +V unsignedint +V float +V double + V T T τ Tässä yhteenlasku tarkoittaa erillistä yhdistettä(engl. disjoint union), joka muuten muistuttaa tavallista joukkojen yhdistettä mutta lopputuloksesta on mahdollista selvittää, mistä joukosta alkio oli peräisin. Tarkasti tämä ja muut tässä tarvittavat matemaattiset temput määritellään liitteessä C. Vasemmat arvot vaativat oman määritelmänsä. Kuten muistetaan, vasen arvo edustaa sitä muistialuetta, joka sisältää lausekkeen oikean arvon. Luonnollista on siis julistaa, että vasen arvo on yksinkertaisesti osoite: LValue = { }. 3.2. Ympäristö. Jos tiedetään lausekkeen vasen arvo, miten saadaan sen oikea arvo? Koska vasen arvo antaa osoitteen, josta alkaa se muistialue, joka sisältää tuon halutun oikean arvon, pitää olla jokin tapa ilmaista muodollisesti, mitä muistissa on milloinkin, pitää voida mallittaa ympäristö. Koska ympäristö liittää muistiosoitteisiin(vasempiin arvoihin) oikeita arvoja, lienee parasta mallittaa ympäristö matemaattisena funktiona LValue RValue. Samalla kuitenkin pitää liittää muuttujien nimiin niitä vastaavat vasemmat arvot, Ids LValue, joten ympäristön pitäisi koostua näistä kahdesta: Env =(Ids LValue) (LValue RValue). Jos jokin vasen arvo on epäkelpo, niin on luonnollista, että silloin sitä vastaava oikea arvo ympäristössä on pohja; erityisesti, jos kyseinen vasen arvo on pohja: (σ l,σ r ) Env:σ r ( ) =.
42 3. SUORAVIIVAOHJELMAT 3.3. Lausekkeet. Lausekkeen laskemista(engl. evaluation) mallittaa matemaattinen funktio, joka kuvaa lausekkeet matemaattisiksi funktioiksi, jotka kuvaavat ympäristöt kyseisen lausekkeiden oikeiksi arvoiksi: E r :Expressions (Env RValue) E r ι (σ l,σ r ) = σ r (σ l ι ) E r ν (σ l,σ r ) = ν 2 WB E r ε (σ l,σ r ) 1 jose r ε (σ l,σ r ) V unsignedint 2 B E r ε (σ l,σ r ) 1 jose r ε (σ l,σ r ) V unsignedbyte E r ε (σ l,σ r ) jose r ε (σ l,σ r ) V int \ { 2 WB 1 } E r ε (σ l,σ r ) jose r ε (σ l,σ r ) V byte \ { 2 B 1 } E r ε (σ E r ε (σ l,σ r ) = l,σ r ) jose r ε (σ l,σ r ) V float E r ε (σ l,σ r ) jose r ε (σ l,σ r ) V double + jose r ε (σ l,σ r ) = jose r ε (σ l,σ r ) = + NaN jose r ε (σ l,σ r ) =NaN muuten E r ε 1 ε 2 (σ l,σ r ) =... E r ε 1 /ε 2 (σ l,σ r ) =... E r ε 1%ε 2 (σ l,σ r ) =... E r ε 1 + ε 2 (σ l,σ r ) =... E r ε 1 ε 2 (σ l,σ r ) =... E r ε 1 [ε 2 ] (σ l,σ r ) = σ r (E l ε 1 [ε 2 ] (σ l,σ r )) Tässä oletetaan yksinkertaisuuden vuoksi, että lukuvakiot ovat abstraktin syntaksin tasolla lukuja itse niin, että lukuvakio edustaa kaavoissa sitä lukua, mitä se edustaa ohjelmissa. Useimmat aritmeettiset operaattorit on tästä jätetty pois, sillä kuten vastaluvun tapauksesta näkee, ne ovat varsin sotkuisia, koska niiden tapauksessa pitää miettiä lukualueen ylityksen ja liukulukujen tapauksessa pyöristyksen vaikutuksia. Oikeassa ohjelmointikielessä nämä onkin syytä määritellä tarkasti, mutta tässä ne ovat lähinnä tiellä. Taulukon indeksoinnin oikea arvo on tässä määritelty vasemman arvon perusteella niin, että annetaan vasemman arvon laskea muistipaikka, josta tieto löytyy ja oikea arvo saadaan hakemassa se ympäristöstä.
3. DENOTATIONAALINEN SEMANTIIKKA 43 Vasemmat arvot ovat, taulukon indeksointia lukuunottamatta, reilusti helpompia: E l :Expressions (Env LValue) E l ι (σ l,σ r ) = σ l ι E l ν (σ l,σ r ) = E l ε (σ l,σ r ) = E l ε 1 ε 2 (σ l,σ r ) = E l ε 1 /ε 2 (σ l,σ r ) = E l ε 1%ε 2 (σ l,σ r ) = E l ε 1 + ε 2 (σ l,σ r ) = E l ε 1 ε 2 (σ l,σ r ) = E l ε 1 (σ l,σ r ) +m E r ε 2 (σ l,σ r ) jose l ε 1 (σ l,σ r ) ja E r ε 1 (σ l,σ r ) V τ[m][n] ja E r ε 2 (σ l,σ r ) V unsignedint ja E r ε 2 (σ l,σ r ) <m E l ε 1 [ε 2 ] (σ l,σ r ) = E l ε 1 (σ l,σ r ) +E r ε 2 (σ l,σ r ) jose l ε 1 (σ l,σ r ) ja E r ε 1 (σ l,σ r ) V τ 1 [n] ja E r ε 2 (σ l,σ r ) V unsignedint ja τ 2 Types: m : τ 1 τ 2 [m] muuten Tässä on tehty sellainen päätös, että taulukot esitetään joukkona muistipaikkoja niin, että yksiulotteisen taulukon(eli taulukon, jonka alkiotyyppi ei ole itse taulukko) muistipaikat muodostavat yhtenäisen alueen ja taulukon indeksoinnissa on vain kyse indeksin lisäämisestä taulukon alkuosoitteeseen. Moniulotteiset taulukot katsotaan olevan yksiulotteisia taulukoita, joiden alkiot ovat itse taulukoita. 3.4. Lauseet. Lauseen tehtävä on muuttaa ohjelman tilaa. Lauseella ei ole arvoa kuten lausekkeella mutta sillä on vaikutus(engl. effect). Denotationaalisessa semantiikassa tämä vaikutus ilmaistaan kuvaamalla lauseet funktioiksi, jotka ottavat ympäristön ja antavat uuden
44 3. SUORAVIIVAOHJELMAT ympäristöntilalle 7 : C:Statements (Env (Env { })) (σ l,σ r {(E l ε 1 (σ l,σ r ),E r ε 2 (σ l,σ r ))}) jose l ε 1 (σ l,σ r ) ja C ε 1 ε 2 (σ l,σ r ) = E r ε 2 (σ l,σ r ) muuten C read ε (σ l,σ r ) =C ε read[readinx];readinx readinx +1 (σ l,σ r ) C write ε (σ l,σ r ) =C write[writeinx] ε;writeinx writeinx +1 (σ l,σ r ) { C γ 2 (C γ 1 (σ C γ 1 ;γ 2 (σ l,σ r ) = l,σ r )) josc γ 1 (σ l,σ r ) muuten Syöttö ja tulostus on tässä esitetty hieman huijaten, mutta kunnollinen esitys vaatisi tämän kurssin kannalta aivan liian raskasta matematiikkaa. Idea tässä esityksessä on, että syöte ja tuloste ovat erityisiin muuttujiin sidottuja taulukoita, joita syöttö- ja tulostuslauseet käyttävät. Tämän esitystavan hankalimmat ongelmat liittyvät interaktiivisuuteen ja syötteen pituuteen: näin esitettynä syötteen tulee olla äärellinen ja olla kokonaan annettu ennen ohjelman suorituksen alkua, ja tulosteen koko on ennalta rajattu. Nämä ongelmat ovat kyllä ratkaistavissa, mutta keinot ovat tälle kurssille turhan raskaita. Sijoituslauseen määrittelyssä kannattaa huomata, että sijoituslause muuttaa sen muistipaikan arvoa, johon vasen arvo osoittaa, sen sijaan, että se vaihtaisi vasemman arvon(sikäli kuin se on nimi) osoittamaan muualle. Tätä sanotaan erinäisistä syistä arvosemantiikaksi(engl. value semantics). Toinen vaihtoehto olisi todellakin muuttaa vasemmalla puolella olevan nimen sidonta niin, että kyseinen nimi olisikin sidottu oikeanpuoleisen lausekkeen vasempaan arvoon. Tätä puolestaan sanotaan yleensä viitesemantiikaksi(engl. reference semantics). Peräkkäistysoperaation määritelmää tarkkaan katsoessa huomannee, että siinä on kyse oikeastaan funktioiden yhdistämisestä eli ketjutuksesta: ensimmäisen lauseen jälkeinen tila on toisen lauseen alkutila. Monimutkaisuutta tähän tuo lähinnä virhetilanteen mahdollisuus, joka ilmenee pohjana. ALKEIS-suorassa on ollut koko ajan tiukka ero lauseiden ja lausekkeiden välillä: lauseilla on vaikutus mutta ei arvoa, ja lausekkeilla on arvo mutta ei vaikutusta. Olisi mahdollista valita toisinkin, nimittäin lausekkeelle voidaan määritellä sivuvaikutus(engl. side effect), jolloin 7 Tässä σ ςtarkoittaarelaationpäällekirjoitusta(engl.relationoverwriting): σ ς = ς {(x,y) σ y:(x,y) ς }
3. DENOTATIONAALINEN SEMANTIIKKA 45 lausekkeella on sekä vaikutus että arvo. Sivuvaikutuksen päällimmäisin ongelma on se, että vähänkin monimutkaisemman lausekkeen osalausekkeiden laskujärjestys ei yleensä ole määrätty sen tarkemmin kuin mitä lausekkeen rakenne määrää, ja tämä johtaa siihen, että sivuvaikutusten tapahtumisjärjestys ei ole määrätty. Klassinen esimerkki tämän aiheuttamasta ongelmasta on C-kielen ilmaisu Josi:narvoonennentämänlauseensuorittamista 3, niin mikä on i:n arvo lauseen suorittamisen jälkeen ja mistä indekseistätaulukoitaajabluetaan? 8 3.5. Muuttujanesittelyt. Muuttujanesittelyiden tehtävänä on kertoa käytettävien muuttujien tyypit sekä ilmoittaa, että niille tulee varata muistia. ALKEIS-suorassa esittelyt ovat ohjelman alussa var- ja begin-avainsanojen välissä; joissakin muissa kielissä ne voivat olla muuallakin, kunhan mitään muuttujaa ei käytetä ennen sen esittelyä (tosin joissakin kielissä muuttujan ensimmäinen käyttö on samalla sen esittely). Formaalisti muuttujanesittelyt käsitellään lauseiden tapaan, mutta tällä kertaa mukana pidetään myös laskuria, joka kertoo, mikä on ensimmäinen vielä vapaa muistipaikka: missä D:Declarations (Env ) D ι:τ (n) =(({(ι,n)}, { n LValue }),n + l(τ)) D δ 1 ;δ 2 (n) =((σ, { n LValue }),m 2 ) σ = σ 1 σ 2, (σ 1,m 1 ) = δ 1 (n), (σ 2,m 2 ) = δ 2 (m 1 ), l:types, l(int) =1, l(unsigned int) = 1, l(byte) =1, l(unsigned byte) = 1, l(float) =1, l(double) =1ja l(τ[n]) =n l(τ). Tässä l kertoo, kuinka paljon tilaa kukin tyyppi vie. 8 Vastaus: C-kielessä tällainen lauseen käyttäytyminen on määrittelemätön (engl. undefined behaviour); tässä monisteessa käytetyin termein tämä tarkoittaa, että lauseen suorituksen jälkeen tila on. Tällaisessa tilanteessa mitä tahansa voi tapahtua.
46 3. SUORAVIIVAOHJELMAT 3.6. Ohjelma. Ohjelman denotationaalinen semantiikka on nyt lopuksi varsin helppo määritellä: missä P:Programs (V unsignedbyte [n] V unsignedbyte [m]) P var δbegin γend (I) =O O = {(n,o) V unsignedbyte o=e r write[n] (σ l,σ r) } (σ l,σ r) =C read[0] I(0); ;read[n] I(n);γ (σ l,σ r ) ((σ l,σ r ),k) =D δ;write:unsignedbyte[m];read:unsignedbyte[n] (0) Sotkuiseksi tämän määritelmän tekee taas siirrännän määrittely, jota tässäkin on vähän huijaamalla yksinkertaistettu. Samoin tässä on yksinkertaisuuden vuoksi jätetty huomiotta rikkinäiset ohjelmat eli sellaiset ohjelmat, joiden tila muuttuu jossain kohtaa pohjaksi. Kannattaa huomata, kuinka ohjelman tila on sen sisäinen asia: ulospäin näkyy vain syöte ja tuloste. Tällä on se merkitys, että yleensä katsotaan, että toteutuksen on noudatettu määriteltyä semantiikkaa vain siltä osin, että semantiikan antama ulkoinen käyttäytyminen(tässä tuloste syötteen funktiona) on sama kuin toteutuksen ulkoinen käyttäytyminen. 4. Tyyppijärjestelmä Tyyppijärjestelmällä on kolme merkittävää tehtävää: (1) rajata pois tietyt virhetyypit, (2) mahdollistaa muistin tehokas käyttö sekä (3) auttaa ohjelman abstraktin rakenteen kuvauksessa. Kolmas tehtävä tulee näkyviin parhaiten vasta myöhemmin, kun puhutaan rikkaista tyyppijärjestelmistä. Nyt puheena on perinteisen imperatiivisen kielen perustyyppijärjestelmä. Formaalisti tyyppijärjestelmä hahmotetaan yleensä formaaliksi päättelyjärjestelmäksi. Se koostuu siten aksioomista ja päättelysäännöistä, jotka kertovat, millä tavoin on luvallista päätellä, että jokin ohjelma tai kielen konstruktio on hyvinmuodostettu(engl. well-formed). ALKEIS-suorantyyppijärjestelmäonannettukuvassa3 9. Tyyppijärjestelmässä käytetään sisäisesti yhtä tyyppiä lvalue(τ), joka ilmaisee, että sillä lausekkeeella, jolla on tuo tyyppi, on vasen arvojasenoikeanarvontyyppion τ. 9 Tyyppijärjestelmässäesiintyyoperaattoridom,jokamääritelläänseuraavasti: domx = {x mod y:(x,y) X }.
4. TYYPPIJÄRJESTELMÄ 47 Γ δ Γ γ var δbegin γend dom Γ 1 dom Γ 2 = Γ 1 δ 1 Γ 2 δ 2 Γ 1, Γ 2 δ 1 ;δ 2 ι:int ι:int ι:unsignedint ι:unsignedint ι:byte ι:byte ι:unsignedbyte ι:unsignedbyte ι:float ι:float ι:double ι:double ι:τ[n] ι:τ[n] ι:τ ι :τ Γ,ι:τ ι :lvalue(τ) 0 value(ν ) <2 B Γ ν :unsignedbyte 2 B value(ν ) <2 WB Γ ν :unsignedint 2 B 1 value(ν ) <2 B 1 Γ ν :byte 2 WB 1 value(ν ) < 2 B 1 2 B 1 value(ν ) <2 WB 1 Γ ν :int Γ ν :double Γ ε :lvalue(τ) Γ ε :τ Γ ε :unsignedbyte Γ ε :unsignedint Γ ε :byte Γ ε :int Γ ε :float Γ ε :double τ {unsigned byte, byte, unsigned int, int, float, double} Γ ε :τ Γ ε :τ τ {unsigned byte, byte, unsigned int, int, float, double} {+,,,/,%} Γ ε 1 :τ Γ ε 2 :τ Γ ε 1 ε 2 :τ Γ ε 1 :lvalue(τ[n]) Γ ε 2 :unsignedint Γ ε 1 [ε 2 ] :lvalue(τ) Γ ε 1 :lvalue(τ) Γ ε 2 :τ Γ ε 1 ε 2 Γ ε :lvalue(unsignedbyte) Γ read ε Γ ε :unsignedbyte Γ write ε Γ γ 1 Γ γ 2 Γ γ 1 ;γ 2 KUVA 3. ALKEIS-suoran tyyppijärjestelmä
48 3. SUORAVIIVAOHJELMAT Lukuvakiopäätesymbolia ν on tyyppijärjestelmässä tarkennettu alaviitteillä, sillä lausekkeen ν tyyppi riippuu lukuvakion ilmiasusta. Tässä tarvitaan kykyä erottaa etumerkittömät kokonaislukuvakiot ν, etumerkilliset kokonaislukuvakiot ν sekä desimaalilukuvakiot ν toisistaan. Lisäksi tarvitaan tietoa lukuvakiopäätesymbolin lukuarvosta, jota ilmaistaan value(ν). Tyyppijärjestelmän toimintaidea on seuraava. Kirjoitetaan paperilleensiksityyppitarkastettavaohjelma -merkinperään -sulkeiden sisään: varx:unsignedbytebeginreadx;writexend Seuraavaksi säännöstöstä etsitään sääntö, jonka alin rivi on muodoltaan tuon ohjelman näköinen(täsmällisesti: jos säännön alimmalla rivillä välikesymbolit korvataan sopivasti valituilla konstruktioilla niin, että kukin välikesymboli korvataan välikesymboliin abstraktin syntaksin mukaan sopivalla konstruktiolla). Mikäli säännössä on viivan yläpuolella väittämiä(ns. reunaehdot, engl. side conditions), niiden totuus tarkastetaan: sääntöä saa käyttää vain, jos kaikki sen reunaehdot pätevät kyseisessä tilanteessa. Tässä tapauksessa ainoa sopiva sääntö on Γ δ Γ γ var δbegin γend Tarkasteltavaan paperille kirjoitettuun riviin merkitään seuraavaksi, mikä osa rivistä vastaa mitäkin säännön alimmalla rivillä esiintyvää välikesymbolia: varx:unsignedbyte }{{} δ beginreadx;writex }{{} γ end Sitten paperille kopioidaan valitun säännön viivan yläpuolella olevat rivit, korvaten välikesymbolit niillä konstruktioilla, jotka edellä merkittiin, ja tarkastelun alla ollut rivi merkitään käsitellyksi. end varx:unsignedbyte }{{} δ beginreadx;writex }{{} γ Γ x:unsignedbyte }{{} δ Γ readx;writex }{{} γ Seuraavaksi tarkasteluun otetaan jokin merkitsemätön rivi, vaikkapa ylin sellainen. Edellä esitetty toistetaan sille riville. Sopiva sääntö on ι:unsignedbyte ι:unsignedbyte
4. TYYPPIJÄRJESTELMÄ 49 Koska säännössä ei ole viivan yläpuolella rivejä(eli sääntö on aksiooma, voidaan ylempi rivi merkitä käsitellyksi saman tien. Samalla on kuitenkin korvattava Γ kaikkialla sillä tekstillä, johon se säännössä sopi: varx:unsignedbytebeginreadx;writexend }{{} x : unsigned byte }{{} x :unsignedbyte ι } {{ } ι Γ }{{} x : unsigned byte readx;writex ι } {{ } Γ Jälleen käsitellään jokin merkitsemätön rivi; niitä on nyt vain yksi, joten valinta on helppoa. Tällä kertaa sopiva sääntö on Γ γ 1 Γ γ 2 Γ γ 1 ;γ 2 Mikäli olemassaolevilla riveillä ja säännössä esiintyisi sama välikesymboli, tulee sääntöä muuttaa varustamalla kyseinen välikesymboli jollakin alaindeksillä, joka ei vielä esiinny paperilla. Nyt tätä ei tarvitse tehdä. Paperille lisätään kaksi uutta riviä: varx:unsignedbytebeginreadx;writexend x:unsignedbyte x:unsignedbyte x:unsignedbyte readx }{{};writex }{{} γ 1 γ 2 x:unsignedbyte }{{} Γ x:unsignedbyte }{{} Γ readx }{{} γ 1 writex }{{} γ 2 Nyt ylempään merkitsemättömään riviin sopii sääntö ja alempaan Γ ε :lvalue(unsignedbyte) Γ read ε Γ ε :unsignedbyte Γ write ε
50 3. SUORAVIIVAOHJELMAT ja tuloksena ovat rivit varx:unsignedbytebeginreadx;writexend x:unsignedbyte x:unsignedbyte x:unsignedbyte readx;writex x:unsignedbyte }{{} Γ x:unsignedbyte }{{} Γ x x:unsignedbyte }{{} Γ x:unsignedbyte }{{} Γ }{{} ε x read x }{{} ε write x }{{} ε :lvalue(unsignedbyte) }{{} ε :unsignedbyte Ylempään merkitsemättömään riviin sopii sääntö Γ,ι:τ ι :lvalue(τ) Nyt Γ:aa sovitettaessa astuu voimaan erityinen sääntö, joka koskee -symbolin vasenta puolta: se nimittäin koostuu pilkuilla erotetuista muuttujien tyypityksistä, jotka ovat muotoa ι: τ; erityissääntö on nyt, ett tyypitysten järjestyksellä tai saman tyypityksen toistolla ei ole merkitystä. Tuloksena ovat rivit(nyt jätän kirjoittamatta aiemmin merkityt rivit, sillä niihin ei enää tule muutoksia): ) }{{} x : unsigned byte }{{} ι τ x }{{} ι :lvalue(unsignedbyte }{{} τ x:unsignedbyte x :unsignedbyte Viimeiseen merkitsemättömään riviin sovelletaan sääntöä Tuloksena ovat seuraavat rivit: ι:τ ι :τ x:unsignedbyte x :lvalue(unsignedbyte) }{{} x : unsigned byte }{{}}{{} x :unsignedbyte }{{} ι τ ι τ Koska enää ei ole jäljellä yhtään merkitsemätöntä riviä, tyyppitarkastus päättyy ja ohjelma todetaan hyvinmuodostetuksi. Yleisemmin, jos on olemassa ainakin yksi tapa käyttää sääntöjä niin, että tuloksena tulee jono rivejä, jotka on kaikki merkitty, ohjelma on hyvinmuodostettu. Mikäli tällaista ei ole, se hylätään.
5. KONKREETTI KIELIOPPI 51 ALKEIS-suoran tyyppijärjestelmän suunnittelussa on tehty useita ei kovin itsestäänselviä valintoja. Esimerkiksi muotoa Γ ε :unsignedbyte Γ ε :unsignedint olevat säännöt ovat juuri niitä edellä mainittuja automaattisia tyypinmuunnoksia(engl. coercions). Vaihtoehtona olisi ollut vaatia esimerkiksi, että tyypinmuunnos tapahtuu ainoastaan sijoituksen yhteydessä. TEHTÄVÄ 3.2. Muuta ALKEIS-suoran tyyppijärjestelmää siten, että tyypinmuunnokset ovat sallittuja vain sijoituksen yhteydessä(toisin sanoen oikeanpuolimmainen lauseke lasketaan ensin ilman muunnoksia ja muunnokset tehdään vasta juuri ennen sijoituksen tapahtumista). Olisimyösollutmahdollistamääritelläalityyppirelaatio τ 1 τ 2,jokatarkoittaa,ettäjokainen τ 1 :narvoonmyös τ 2 :narvojaettä τ 1 käy kaikkiallamissä τ 2 :kin.tällainenrelaatioonosittaisjärjestys(ks.sivun 30 alaviite), mikä on kirjoitettava näkyviin tyypityssääntöihin. TEHTÄVÄ 3.3. Muuta ALKEIS-suoran tyyppijärjestelmää siten, että tyypinmuunnosten sijasta käytetäänkin alityyppirelaatiota. 5. Konkreetti kielioppi Kielen määrittely ja tutkiminen onnistuu aivan hyvin abstraktin kieliopin pohjalta, kuten edellä huomattiin. Käytännön käyttöön on kuitenkin kiinnitettävä jokin konkreetti kielioppi(engl. concrete syntax), joka kuvaa, mitkä merkkijonot ovat sallittuja kielellä kirjoitettuja ohjelmia ja samalla määrää, miten merkkijonot kuvautuvat abstraktin kieliopin mukaisiksi rakennepuiksi. Tapana on määritellä konkreetti kielioppi kahdessa osassa: ensin määritellään kielen sanasrakenne(engl. lexical structure) ja sitten käyttäen sanasrakenteen sanasia päätesymboleina, kontekstiton kielioppi, johon synteettisten attribuuttien avulla liitetään rakennepuiden kokoamisohje. On olemassa ohjelmointikieliä, jotka eivät ilmaise ohjelmia merkkijonoina. Tällaisia ovat esimerkiksi Prograph ja Fenfiren Clangit[14]. Tällaiset kielet käyttävät jotain aivan muuta ohjelmien konkreettiseen esittämiseen. Useimmat tällaiset, ei-tekstuaaliset kielet voidaan kuvata kuitenkin abstraktin kielioppinsa ja semantiikkansa osalta samaan tapaan kuin muutkin kielet. Emme puutu näihin kieliin. 5.1. Merkit. Ohjelman teksti kirjoitetaan merkkijonona. Ohjelmointikielet määrittelevät joukon merkkejä, joita voidaan käyttää ohjelmien kirjoittamiseen. Tavallisesti rajoitutaan ECMA-6 IRV-merkistöön[2](tunnetaan paremmin nimellä ASCII). Tällöin käytettävissä ei ole mm. ääkkösiä. Monet kielet sallivat jonkin tuon merkistön