Luku 9 Oliokielten erityispiirteitä Oliosuuntautuneisuus (engl. object orientation) ja olio-ohjelmointi (engl. object oriented programming) ovat laajassa käytössä olevia sanoja, joilla on tietty merkitys, mutta tuo merkitys on yksityiskohdiltaan varsin sumuinen jokaisella ekspertillä on oma käsityksensä siitä, mitä olio-ohjelmointi on. Booch (1994, s. 38) määritteli olio-ohjelmoinnin seuraavasti (suomennus minun): Olio-ohjelmointi on toteutusmenetelmä, jossa ohjelmat rakentuvat kokoelmina yhteistyötä tekeviä olioita, jotka jokainen edustaa jonkin luokan ilmentymää, ja jonka luokat ovat kaikki perintäsuhteiden kautta yhdistyneen luokkahierarkian jäseniä. Taivalsaari (1993, s. 30) luonnehtii olio-ohjelmointia seuraavasti (suomennus minun): mikä tahansa systeemi, joka tukee oliokäsitettä ja tarjoaa keinot olioiden inkrementaaliselle muokkaukselle, voidaan tulkita oliosuuntautuneeksi. Armstrong (2006) hahmottelee puolimuodollisessa kirjallisuuskatsauksessaan oliopohjaisen kehityksen keskeiset termit seuraavasti (suomennos minun): olio (engl. object): yksittäinen, yksilöity kappale (engl. item), joko todellinen tai abstrakti, joka sisältää tietoa siitä itsestään sekä kuvauksia kyseisen tiedon käsittelystä. luokka (engl. class): yhden tai useamman samankaltaisen olion yhteisen rakenteen ja toiminnan kuvaus. kapselointi (engl. encapsulation): luokkien ja olioiden suunnittelutekniikka, joka rajoittaa pääsyä tietoon ja käyttäytymiseen määrittelemällä rajoitetun joukon viestejä, jotka kyseisen luokan olio voi vastaanottaa. 87
88 LUKU 9. OLIOKIELTEN ERITYISPIIRTEITÄ metodi (engl. method): tapa asettaa, hakea taikka käsitellä olion tietoa. viestinvälitys (engl. message passing): prosessi jolla olio lähettää tietoa toiselle oliolle tai pyytää tätä herättämään (engl. invoke) tietyn metodin. polymorfismi (engl. polymorphism): eri luokkien kyky vastata samaan viestiin ja toteuttaa se sopivalla kullekin tavalla. abstraktio (engl. abstraction): luokkien luominen todellisuuden osaalueiden yksinkertaistamiseksi käyttäen ongelmaan sinänsä kuuluvia eroavaisuuksia. Tässä monisteessa tarkastellaan oliokielten erityispiirteitä. Monisteessa rakennetut mallikielet ovat muunnelmia Abadin ja Cardellin (1996) sigmalaskennosta. 9.1 Olio Booch (1994) ja Taivalsaari (1993) määrittelevät olion (engl. object) samalla tavalla: oliolla on tila (engl. state), käyttäytyminen (engl. behavior) ja identiteetti (engl. identity). Olio on dynaaminen käsite. Olion identiteetillä tarkoitetaan sitä olion ominaisuutta (tai ominaisuuksia yhdistelmää), joka erottaa olion toisesta oliosta. Identiteetti ilmenee identiteettivertailuoperaattorin (esimerkiksi Javan ==) toiminnassa. Identiteetti myös mahdollistaa olion löytämisen jos tiedät olion identiteetin, pääset olioon käsiksi ja näin muistuttaa eräiden mytologioiden todellinen nimi -ajatusta (jos tiedät jonkun todellisen nimen, sinulla on valta hänen ylitseen). Käytännössä olion identiteetti on yleensä oliolle varatun muistialueen alkuosoite. Joskus se ei riitä, esimerkiksi jos kielessä on tuki hajautetuille olioille, jolloin olion identiteettiin tulee lisätä myös jonkinlainen tieto siitä, missä koneessa olio sijaitsee. Hajauttamattomassakaan järjestelmässä olion osoite ei välttämättä ole hyvä identiteetti: ajatellaanpa nyt vaikkapa kopioivaa muistinsiivousta taikka pysyviä olioita, jotka tallennetaan levylle ja ladataan takaisin muistiin, yleensä eri osoitteisiin. Useimmissa oliokielissä olion identiteetti on implisiittinen, mutta joissakin kielissä se voi olla myös eksplisiittinen. Olion tilalla tarkoitetaan sen sisältämää dataa, mukaanlukien sen tiedossa olevia muiden olioiden identiteettejä. Yleensä olion tila koostuu yh-
9.2. YKSINKERTAINEN KIELI 89 destä tai useammasta nimetystä muuttujasta eli attribuutista, jotka jokainen tallettaa jonkin arvon. Tässä mielessä olio muistuttaa tietuetta. Se, että olion tila ja identiteetti mainitaan määritelmässä erikseen, tarkoittaa, että olion tila ei (välttämättä) riipu sen identiteetistä eli olion tila voi muuttua ohjelman suorituksen edetessä. Toisin sanoen kielessä tulee olla tuki sijoituslauseelle ja sivuvaikutuksille. Olion käyttäytyminen tarkoittaa sitä, että olio voi vastaanottaa toiselta oliolta viestin ja reagoida siihen muuttamalla tilaansa taikka lähettämällä muita viestejä. Se, että käyttäytyminen liitetään nimenomaan olioon, dynaamiseen käsitteeseen, tarkoittaa, että tuo käyttäytyminen voi periaatteessa olla jokaisella oliolla erilainen, toisin sanoen käyttäytymisen määrittelevä ohjelmakoodi on jollakin tapaa liitettävä olioon itseensä. Oliolle lähetettävä viesti sisältää tavallisesti viestin nimen sekä jonon viestiin liittyvää dataa. Tavallisesti viestin lähettäjä odottaa oliolta vastausta, joka sisältää (tavallisesti) yhden arvon paluutietona tätä sanotaan synkroniseksi viestiksi. Synkroninen viestinvälitys voidaan nähdä aliohjelmakutsun varianttina, jolloin viestin lähetys vastaa aliohjelmakutsua ja vastausviesti vastaa aliohjelmasta palaamista. Jos viestiin ei odoteta vastausta (asynkroninen viesti), vastaanottava olio käsittelee viestin jossakin muussa yhteydessä, esimerkiksi uudessa säikeessä. Koodia, joka käsittelee viestin, on se synkroninen tai asynkroninen, sanotaan olion metodiksi. Metodi voi periaatteessa olla kullekin oliolle erikseen koodattu, mutta silloin uusia olioita ei voida dynaamisesti luoda. Parempi tapa on parametrisoida metodi yli olion, jolloin se saa itseviitteen (engl. self-reference), eli sen olion identiteetin, johon se kuuluu, (implisiittisenä tai eksplisiittisenä) parametrinaan. 9.2 Yksinkertainen kieli Merkiantätekninen huomautus: Seuraavassa käytetään lyhennysmerkintää (T i ) i=1 n tarkoittamaan listaa T 1,..., T n, mikä T sitten kulloinkin mahtaa olla. Jos listan kokoa ei ole tarpeen kirjoittaa näkyviin, käytetään merkintää (T i ) i (mikäli tyhjä lista on sallittu) tai (T i ) + i (muuten). Mikäli indeksimuuttujaa i ei tarvita, se voidaan kahdessa jälkimmäisessä jättää kirjoittamatta näkyviin. Kaikissa kolmessa merkinnässä sulut ovat osa metakieltä, ei kohdekieltä. Lyhennysmerkinnän poistaminen vaatii yleensä listan kirjoittamisen auki. Kieliopissa tälllaisten lyhennysmerkintöjen poistaminen vaatii sen sijaan listan määrittelevän nonterminaalin ja produktioiden lisäämisen.
90 LUKU 9. OLIOKIELTEN ERITYISPIIRTEITÄ Yksinkertainen oliokieli voidaan määritellä seuraavasti: f, l Labels x, y, z Variables t, u Terms t, u ::= x { (l i = t i ) i, ( f i = m i ) i } olion luonti t.l attribuutin valinta t. f ((t) ) viestin lähetys t.l := u attribuutin korvaus t; u peräkkäistys m Method m ::= ςxλ(y) t Metodikonstruktori ςxλ(y) t on ainoa konstruktio, joka kielessä sitoo muuttujia (itseviittaus x ja parametrit (y) ). Korvausoperaattori [ := ] sekä metafunktio FV määritellään tavanomaiseen tapaan tähän perustuen; en kirjoita määritelmiä tähän auki. Operationaalisen merkitysopin välitiloissa ei ole tarvetta muuttujille vaan olion identiteeteille (viitteille kekoon). Sen takia on tarpeen määritellä erityinen välitermien syntaktinen luokka: r References τ IntermediateTerms τ ::= r { (l i = τ i ) i, ( f i = m i ) i } τ.l τ. f ((τ) ) τ.l := τ τ; τ Muuttujien puuttuessa välitermeistä ei nyt tarvita ympäristöä, joka yhdistäisi muuttujia arvoihin kuten aiemmissa kielissä. Sen sijaan käytetään taltiota (engl. store), joka yhdistää olioiden identiteetit (viitteet) olioiden tiloihin. Oliotilat puolestaan ovat oliolausekkeen kaltaisia vekottimia, mutta niissä attribuutit sisältävät olioviitteitä, ei termejä tai välitermejä. Yksin-
9.2. YKSINKERTAINEN KIELI 91 kertaisuuden vuoksi oliotilat sisältävät myös olioiden metodit: v, w Values v, w ::= r o ObjectStates o ::= { (l i = v i ), ( f i = m i ) } Huomaa, että ObjectStates IntermediateTerm. Arvojen joukko ja sen metamuuttuja (v) on otettu kieleen, jotta kieltä olisi helppo laajentaa muilla arvoilla kuin olioviitteillä, esimerkiksi lukuarvoilla. Nyt välitilat kuuluvat joukkoon IntermediateTerms (References ObjectStates). Lopputilat puolestaan kuuluvat joukkoon Values (References ObjectStates). Nyt voidaan määritellä kielen operationaalinen merkitysoppi: (o, σ) (r, σ { (r, o) }) σ(r) (9.1) (r.l k, σ) (v k, σ) σ(r) = { (l i = v i ) n i=1, ( f i = m i ) i } 1 k n (9.2) (r.l k ((v j ) n j=1 ), σ) (t, σ) σ(r) = { (l i = w i ), ( f i = m i ) n i=1 } 1 k n m i = ςxλ(y j ) n j=1 t FV(t) {(y j ) n j=1 } t = t[x := r]([y j := v j ]) n j=1 ) (9.3) (r.l k := v, σ) (r, σ ) σ(r) = { (l i = v i ) n i=1, ( f i = m i ) i } 1 k n σ = σ { (r, { (l i = v i ) k 1 i=1, l k = v, (l i = v i ) n i=k+1, ( f i = m i ) i } (9.4) (v; τ, σ) (τ, σ) (9.5)
92 LUKU 9. OLIOKIELTEN ERITYISPIIRTEITÄ Edellisten varsinaisten laskentasääntöjen lisäksi tarvitaan myös sääntöjä, jotka määrittelevät, missä järjestyksessä saa laskea. Tässä kielessä lasketaan vasemmalta oikealle: (τ k, σ) (τ k, σ ) ({ (l i = v i ) k 1 i=1, l k = τ k, (l i = τ i ) i=k+1 n }, σ) ({ (l i = v i ) k 1 i=1, l k = τ k, (l i = τ i ) i=k+1 n }, σ ) (τ, σ) (τ, σ ) (τ.l, σ) (τ.l, σ ) (τ k, σ) (τ k, σ ) (r. f ((v i ) k 1 i=1, τ k, (τ i ) i=k+1 n ), σ) (r. f ((v i) k 1 i=1, τ k, (τ i) i=k+1 n ), σ ) (τ 1, σ) (τ 1, σ ) (τ 1.l := τ 2, σ) (τ 1.l := τ 2, σ ) (τ, σ) (τ, σ ) (r.l := τ, σ) (r.l := τ, σ ) (τ 1, σ) (τ 1, σ ) (τ 1 ; τ 2, σ) (τ 1 ; τ 2, σ ) (9.6) (9.7) (9.8) (9.9) (9.10) (9.11) Tässä kielessä (laajennettuna tavanomaisella aritmetiikalla) laskuriolio voitaisiin määritellä seuraavasti: { n = 0, clear = ςsλ s.n := 0, inc = ςsλ s.n := s.n + 1 } 9.3 Inkrementaalinen muokkaus Edellä tarkasteltu kieli ei tue vielä inkrementaalista muokkausta, jota Taivalsaari (1993) pitää oliokielen olennaisena ominaisuutena. Yksinkertainen tapa sallia inkrementaalinen muokkaus on lisätä kieleen metodinkorvausoperaatio t. f := σxλ(y i ) t, joka toimii kuten sijoitus paitsi että se
9.3. INKREMENTAALINEN MUOKKAUS 93 muuttaa metodia, ei attribuuttia (Abadi ja Cardelli, 1996, luku 10). Seuraavassa tarkastellaan muita vaihtoehtoja. 9.3.1 Prototyyppiperintä Prototyyppiperinnässä yksittäisen olion käyttäytymistä ei voi muokata (kuten metodinkorvausmenetelmässä voi), mutta oliosta voidaan tehdä muokattuja kopioita. Tässä ideana on lisätä muokkaava kloonausoperaatio t, u ::= t { (l i = t 1 ) i, ( f i = m 1 ) i } jonka ideana on luoda olio kopiona toisesta oliosta (sen prototyypistä) poiketen siitä kuitenkin määrätyllä tavalla. Konkreettisessa toteutuksessa attribuutit kopioidaan aina mutta metodeille voidaan tehdä kolme erilaista ratkaisua: Metoditkin (tai osoittimet niihin) voidaan kopioida uuteen olioon. Uuteen olioon lisätään delegaatioviite vanhaan olioon, ja uuteen olioon tallennetaan vain muokatut metodit. Mikäli uudelle oliolle lähetetään viesti, jota se ei itse osaa käsitellä, delegoi se kyseisen viestin vanhalle oliolle (kuitenkin niin, että metodin itseviittaus viittaa uuteen, ei vanhaan olioon). 9.3.2 Luokkaperintä Tavallisempi tapa hoitaa inkrementaalinen muokkaus on lisätä kieleen tuki luokille (engl. class), joilla on seuraavat ominaisuudet: 1. Jokainen olio kuuluu yhteen (ja vain yhteen) luokkaan. 2. Jokaisella luokan oliolla on sama rakenne ja samat metodit. Luokat, toisin kuin oliot, ovat yleensä staattisia otuksia, mutta ne voivat olla dynaamisiakin (onpa joissakin kielissä niinkin, että luokatkin ovat olioita 1 ). 1 Mikä on luokan luokka?
94 LUKU 9. OLIOKIELTEN ERITYISPIIRTEITÄ Seuraavassa kielessä luokat ovat dynaamisia otuksia mutta eivät olioita: f, l Labels x, y, z Variables m Method t, u Terms t, u ::= x { (l i = t i ) i, ( f i = m i ) i } luokan luonti new t olion luonti t.l attribuutin valinta t.l((t) ) viestin lähetys t.l := u attribuutin korvaus t; u peräkkäistys Se konstruktio, joka edellisessä kielessä loi olion, luo nyt luokan. Ajon aikana luokka voidaan esittää tietueena, joka sisältää sen metodit mutta ei attribuutteja, sekä viittauksen ohjelmapätkään (konstruktori), joka luo luokasta olion. Laajemmissa kielissä mutta ei tässä konstruktoreita voi olla useita ja ne sattavat olla parametrisoituja. Olio luodaan luokasta operaattorilla new, jolle annetaan luokka operandiksi: se kutsuu luokan konstruktoria. Luokkaa ei saa käyttää attribuutin valinnan, attribuutin korvauksen taikka viestin lähetyksen ensimmäisenä operandina, sillä luokka ei ole olio. Inkrementaalinen muokkaus tulee mahdolliseksi tällaisessa luokkakielessä luokan perinnällä: t, u ::= t { (l i = t i ) i, ( f i = m i ) i } Tässä t:n tulee olla luokka; se luo luokan, jossa on kaikki t:n attribuutit ja metodit sekä kaikki luetellut attribuutit ja metodit, kuitenkin siten, että lueteltu attribuutti tai metodi korvaa uudessa luokassa alkuperäisen luokan attribuutin tai metodin. Huomaa, että tyypitetyssä kielessä luokan ei tarvitse olla sama asia kuin tyyppi. Ero on samankaltainen kuin Javan luokkien ja rajapintojen välillä: tyyppi kertoo, mitä viestejä on lupa lähettää, kun taas luokka kertoo, minkälaisia oliot ovat. Tarkastellaan kieliä, joissa on seuraavat ominaisuudet: 1. Kieli on staattisesti tyypitetty. 2. Kieli soveltaa nimiyhtäläisyyttä.
9.4. TIEDONPIILOTUS 95 3. Oliotyyppiin täsmälleen yksi luokka. 4. Luokalla on vain yksi yliluokka. Tällaisia kieliä ovat (moniperintä sivuuttaen) esimerkiksi C++ ja Java. Tällaisessa kielessä luokka voidaan esittää ajonaikana metodiosoittimien taulukkona, jossa on ensiksi kaikki yliluokan metodit ja sitten esittelyjärjestyksessä luokan omat metodit. Kääntäjä kykenee tällöin staattisesti antamaan jokaiselle metodille yksikäsitteisen järjestysnumeron (joka on riippumaton siitä, mihin aliluokkaan olio kuuluu), jolloin metodikutsu voidaan kääntää nopeaksi sekvenssiksi: 1. Hae kohdeolion luokka. 2. Etsi kyseisestä luokasta n:s metodi, missä n on kutsuttavan metodin (käännösaikana selvitetty) järjestysnumero. 3. Kutsu kyseistä metodia. 9.4 Tiedonpiilotus Tiedonpiilotus (engl. information hiding), jota jotkut kutsuvat kapseloinniksi (engl. encapsulation), tarkoittaa kielen ominaisuutta, jossa osa olion vastaanottamista viesteistä (taikka sen attribuutit) piilotetaan niin, että olion identiteetin tietäjä ei pysty niitä käyttämään. Luokkakielissä tämä voidaan tehdä siten, että attribuutille tai metodille (joita yhteisesti sanotaan ominaisuuksiksi) annetaan annotaatio public (kaikki saavat käyttää), protected (luokka itse ja sen aliluokat saavat käyttää) ja private (vain luokka itse saa käyttää). Joissakin kielissä voidaan antaa tarkempiakin määrittelyjä: C++:ssa voidaan sallia protected- ja private-ominaisuuksien käyttäminen nimetyille luokille käyttämällä friendmäärittelyä. Eiffelissä voidaan erikseen luetella luokat, jotka pääsevät käsittelemään tiettyjä ominaisuuksia. Tiedonpiilotus voidaan toteuttaa myös erillisenä ominaisuutena modulijärjestelmän (joka on toki mahdollinen myös muissa kuin oliokielissä) avulla. Moduli on (yleensä) staattinen tai (harvoin) dynaaminen konstruktio, joka sisältää joukon määrittelyitä, joista osa ovat modulin sisäisiä ja osa ovat julkisia. Moduli voidaan ottaa käyttöön (engl. import) toisessa modulissa erityisellä käyttöönottokonstruktiolla, minkä jälkeen kyseisen modulin julkiset määrittelyt ovat käyttöön ottaneen modulin sisällä käytettävissä.
96 LUKU 9. OLIOKIELTEN ERITYISPIIRTEITÄ Tiedonpiilotus mahdollistaa erillisen kääntämisen: jokainen moduli tai luokka voidaan kääntää erillään toisesta siten, että vain julkisen rajapinnan muutos vaatii käyttäjämodulien uudelleen kääntämisen. Tällä on suurissa järjetelmissä olennainen merkitys käännösajan kannalta. 9.5 Moniperinnän ongelma Luku perustuu lähteeseen Ducasse et al. (2006). Perinnällä on olio-ohjelmoinnissa useita käyttötarkoituksia, joista tässä mainittakoon is a-suhteen mallintaminen ja toteutuksen uudelleenkäyttö. Edellisessä on kyse olennaisesti samasta asiasta kuin tyyppiteorian subsumptioperiaatteessa aliluokan olion tulee kelvata kaikkiin paikkoihin, joissa kaivataan yliluokan oliota ja jälkimmäisessä tarkoituksena on tietyn ominaisuuden toteuttavan koodin keskittämisen yhteen paikkaan. Is a-suhteeseen riittää normaali hierarkia eli yksiperintä, mutta jälkimmäisen kanssa tarvitaan usein moniperintää. Tarkastellaan esimerkkinä luokkia ReadStream ja WriteStream. Edellinen toteuttaa syötevirran, jälkimmäinen tulostevirran. Näistä voisi olla järkevää muodostaa yhdistämällä syöte- ja tulostevirtaluokka ReadWrite- Stream, mutta yksinperintää tukevassa kielessä se ei onnistu. Niinpä näissä kielissä tulee houkutus kääntää hierarkia ylösalaisin toteuttaa ensin ReadWriteStream ja sitten periä siitä poistamalla metodeita käytöstä Read- Stream ja WriteStream. Jotkut kielet (Eiffel ja C++ tunnetuimpina esimerkkeinä) sallivat luokan perimisen useasta eri luokasta (moniperintä). Monperinnällä ReadWriteStream luonnistuu oikein mukavasti. Tarkastellaan nyt tilannetta, jossa on kaksi toisistaan riippumatonta luokkaa, A ja B, joilla molemmilla on read- ja write-metodit. Luokasta A saadaan helposti perittyä luokka SyncA, joka käyttää jonkinlaista lukkoa lukemisen ja kirjoittamisen synkronointiin. Vastaavasti luokasta B saadaan helposti perittyä luokka SyncB. Itse lukituskoodi on molemmissa samanlainen C++:n syntaksilla int SyncA::read() { lock.acquirelock(); int rv = A::read (); lock.releaselock(); return rv; } void SyncA::write(int c) {
9.5. MONIPERINNÄN ONGELMA 97 } lock.acquirelock(); A::write(c ); lock.releaselock(); modulo viittaukset luokkien nimiin. Luonnollista olisi abstrahoida tämä koodi omaksi luokakseen, SyncReadWrite. Ongelmaksi muodostuu se, että yläluokan metodin kutsu sidotaan näissä kielissä tavallisesti staattisesti: esimerkiksi C++:ssa ohjelmoijan on eksplisiittisesti nimettävä, mitä yläluokkaa tarkoitetaan. C++:ssa tämä voidaan ratkaista käyttämällä mixin-patternia eli antamalla tällaiselle SyncReadWrite-luokalle yläluokka template-parametrina: template <class S> class SyncReadWrite : public S { Lock lock; public: virtual int read() { lock.acquirelock(); int rv = S :: read (); lock.releaselock(); return rv; } virtual void SyncA::write(int c) { lock.acquirelock(); S :: write(c ); lock.releaselock(); } }; class SyncA : public SyncReadWrite<A> {}; class SyncB : public SyncReadWrite<B> {}; Kielessä, jossa yläluokkaa ei voi parametrisoida, ongelmalle ei ole näin siistiä ratkaisua. Ongelmaksi jää se, että perintähierarkia linearisoituu: käsitteellisesti SyncReadWrite ja A eivät ole perintäsuhteessa keskenään, mutta mixinratkaisussa SyncReadWrite<A> peritään A:sta. Monimutkaisemmissa tapauksissa kuin ylläoleva esimerkki linearisointi voi johtaa isoihinkin ongelmiin, koska perintäjärjestyksen muuttaminen voi muuttaa lopullisen luokan toimintaa olennaisestikin ja toisaalta linearisoinnissa alatasolla olevaan mixin-luokkaan lisätty metodi saattaa vahingossa korvata korkeam-
98 LUKU 9. OLIOKIELTEN ERITYISPIIRTEITÄ malla olevan mixin-luokan aivan toista tarkoitusta varten olevan metodin. Ducasse et al. (2006) ehdottavat ratkaisuksi uutta luokkien koostamistapaa, jota he kutsuvat nimellä trait, jonka voisi suomentaa vaikkapa piirteeksi. Piirre on joukko metodien määrittelyjä. Piirteen sanotaan tarvitsevan ne metodit, joita se käyttää määrittelemättä niitä itse. Piirteitä voidan yhdistää käyttämällä seuraavia koostinoperaattoreita: Ylikirjoittaminen Jos T on piirre ja d on joukko metodeita, niin d T on piirre, jossa on d:n määrittelemät metodit ja ne T:n määrittelemät metodit, jotka d:ssä on jätetty määrittelemättä. Summaus Jos T ja U ovat piirteitä, niin T + U on sellainen piirre, jossa on jokainen T:n ja U:n metodi. Mikäli T ja U määrittelevät saman metodin eri tavoilla, tällöin kyseinen metodi on T + U:ssa konfliktissa. Aliasointi Jos T on piirre ja m ja m ovat (eri) metodinimiä, niin T[m m ] on piirre, jossa on kaikki T:n metodit ja lisäksi m, joka käyttäytyy samalla tavalla kuin m. Mikäli T jo sisältää erilaisen metodin m, niin T[m m ]:ssa m on konfliktissa. Poissulkeminen Jos T on piirre ja m on metodinimi, niin T m on piirre, joka on muuten samanlainen kuin T paitsi että metodi m ei ole siinä määritelty. Piirteitä tukevassa kielessä luokkamäärittely koostuu yliluokan nimeämisestä, joukosta attribuuttimääritelmiä, joukosta metodeita (d) sekä piirrelausekkeesta (T). Luokan metodisto muodostetaan ylikirjoituksella d T. Näin syntynyt luokka on hyvin määritelty, jos jokainen piirteiden vaatima metodi on määritelty ja jos luokkaan ei jää konfliktissa olevia metodeja. Konfliktit tulee ohjelmoijan ratkaista ylikirjoituksen, aliasoinnin ja poissulkemisen avulla. Piirteillä on tasoittumisominaisuus (engl. flattening property): luokka, joka on koostettu piirteistä, on täysin ekvivalentti sellaisen luokan kanssa, jossa vastaavat metodit on suoraan määritelty. Erityisesti piirteiden summausjärjestyksellä ei ole merkitystä. Edellä esitetty synkronisointiesimerkki voitaisiin kirjoittaa piirteiden avullä seuraavasti: trait SyncReadWrite { virtual int read() { lock (). acquirelock(); int rv = super.read();
9.6. BINÄÄRIMETODIEN ONGELMA 99 lock (). releaselock(); return rv; } virtual void SyncA::write(int c) { lock (). acquirelock(); super.write(c ); lock (). releaselock(); } }; class SyncA : public A { Lock thelock; Lock lock() { return thelock; } public: uses { SyncReadWrite } } class SyncB : public B { Lock thelock; Lock lock() { return thelock; } public: uses { SyncReadWrite } } Tässä uses-lohko luettelee luokan tarvitsemat piirteet ja tarvittaessa soveltaa niihin aliasointia ja poissulkemista (joita kumpaakaan ei esimerkissä tarvita). Koska piirteellä ei voi olla omia attribuutteja, joudutaan lukkoattribuutti lisäämään SyncA- ja SyncB-luokkiin käsin. Bergel et al. (2008) esittävät tavan laajentaa piirretekniikkaa sallimaan myös piirteiden sisäiset attribuutit. 9.6 Binäärimetodien ongelma Moni Java-ohjelmoija on törmännyt equals-metodin ongelmaan: Olisi mukavaa, jos public boolean equals(object o) { if (!( o instanceof ThisClass)) return false ; ThisClass other = (ThisClass) o; voitaisiin kirjoittaa public boolean equals(thisclass other) {
100 LUKU 9. OLIOKIELTEN ERITYISPIIRTEITÄ mutta se ei ole Javassa mahdollista. Yhtäsuuruusvertailumetodi on binäärimetodi. Muita esimerkkejä ovat binääristen operaattoreiden kuten yhteenlasku ja kertolasku toteuttavat metodit. Kaikille niille on yhteistä se, että metodin toiminta riippuu kahden parametrin luokasta: equals-metodin tapauksessa eriluokkaiset oliot johtavat aina tulokseen false, yhteenlaskun tapauksessa vaaditaan vähintään tyyppimuunnos avuksi, jotta yhteenlasku voidaan suorittaa. Yksi ratkaisu binäärimetodien ongelmaan on laajentaa kieltä ymmärtämään multimetodeita eli metodeita, joilla on useampi kuin yksi kohdeolio (Bruce et al., 1995). Syntaktisesti se muistuttaa C++:n ja Javan metodiylikuormitusta, mutta olennaista multimetodeissa on, että suoritettava metodi valitaan dynaamisesti argumenttien luokkien perusteella. Staattisesti tyypitetyissä kielissä voidaan myös sallia This-avainsanan joka on tyyppitason vastine this-avainsanalle käyttö metodiparametreissa. Tällöin Javan Object-luokassa voitaisiin määritellä public class Object {... public boolean equals(this other) { return this == other }... } Ideana on, että This on aina viittaus this-olion luokkaan riippumatta siitä, missä luokassa määrittely sijaitsee.
Luku 10 Samanaikaisuus Tehtävät ovat samanaikaiset (engl. concurrent), jos ne etenevät (näennäisesti tai tosiasiallisesti) yhtä aikaa samalla toistensa kanssa kommunikoiden. Tehtävät ovat rinnakkaiset (engl. parallel), jos ne etenevät yhtä aikaa toisistaan täysin riippumatta. Kumpikin voi tarkoittaa suoritusta eri prosessoreissa tai eri prosessoriytimissä taikka lomittamalla samassa prosessoriytimessä. Kannattaa tosin huomata, että on varsin yleistä sekoittaa nämä kaksi termiä toisiinsa. Samanaikaisuus on arkipäivää nykyisissä tietojenkäsittelytilanteissa. Arkipäivän henkilökohtaisissa tietokoneissa on jo useita toisistaan (ohjelmoijan näkökulmasta) riippumattomia prosessoriytimiä ja tehokkaammissa palvelimissa on tavallisesti useita prosessoreita. Tietokoneet on kytketty toisiinsa suhteellisen nopeaksi tietokoneverkostoksi, ja erittäin tavallista on, että ohjelma suoritetaan kahden tietokoneen (asiakas nykyisin enimmäkseen WWW-selain ja palvelin) yhteistyönä. Samanaikaisesti ajossa olevat tehtävät voivat (yleensä) kommunikoida yhteisen muistin kautta (yhteismuistisamanaikaisuus, engl. shared memory concurrency) ja lähettämällä toisilleen viestejä (viestinvälityssamanaikaisuus, engl. message-passing concurrency). Koska yleensä sama tehtävä toimii eri tilanteissa lähettäjän ja vastaanottajan roolissa, puhutaan yleensä yleisesti agenteista, jotka kommunikoivat. Viestinvälityksen toimintatapoja on monia, jotka perustuvat viestintävälineen ominaisuuksiin: Viestinvälitys voi olla täysin vapaata: viestin lähettäjä voi aina lähettää viestin ja vastaanottaja voi vastaanottaa viestin aina, kun on ainakin yksi vastaanottamaton viesti. Viestien vastaanottojärjestys on epädeterministinen. Tällainen välitystapa on lähinnä teoreettinen: se olettaa viestintävälineeltä rajattoman tallennuskapasiteetin. 101
102 LUKU 10. SAMANAIKAISUUS Edellisestä voidaan tehdä muunnelma, jossa matkan varrella voi olla kerrallaan enintään jokin ennalta kiinnitetty määrä viestejä. Voidaan tehdä myös muunnelma, jossa osa lähetetyistä viesteistä katoaa matkan varrelle. (Esimerkiksi UDP/IP.) Voidaan tehdä sellainenkin muunnelma, että viestit vastaanotetaan lähetysjärjestyksessä. (Esimerkiksi TCP/IP.) Muunnelma, jossa viestit vastaanotetaan lähetysjärjestyksessä, mutta on olemassa jokin kiinteä raja sille, kuinka monta viestiä voi olla matkalla kerrallaan. (Tyypillinen puskuri.) Edellisestä muunnelma, jossa kyseinen raja on 1. (Synkronoitu muistipaikka.) Rajaksi voidaan asettaa myös 0. Tällöin lähettäjä voi lähettää vain, jos vastaanottaja on valmis vastaanottamaan. Tätä sanotaan kohtaamiseksi (engl. rendezvous). Kaikki muut viestinvälitystavat voidaan mallittaa kohtaamisen avulla. Tällöin ajatuksena on se, että lähettäjän ja vastaanottajan väliin jää kolmas agentti, joka toteuttaa viestinvälitysprotokollan. Ohjelmointikielissä samanaikaisuuden haaste liittyy sen hallinnan abstrahointiin: millä tavalla ohjelmoijalle voidaan tarjota samanaikaisuuden hallintaan vahvat työkalut, jotka vähentävät ohjelmointivirheiden todennäköisyyttä mutta jotka eivät liikaa häiritse ohjelmoijan päätehtävää, eli ohjelmointiongelman ratkaisemista? Luvun päälähteet ovat Reynolds (1998, luvut 8 ja 9) ja Scott (2000, luku 12). 10.1 Samanaikainen suoritus Yksinkertaisin yhteismuuttujakieli saadaan laajentamalla while-kieltä uudella konstruktiolla s t, joka on lause, jossa lauseet s ja t suoritetaan samanaikaisesti ja molemmilla on pääsy sillä hetkellä näkyviin muuttujiin: x, y, z Variable e, f, g Expressions s, t, u Statements s, t, u ::= s t
10.1. SAMANAIKAINEN SUORITUS 103 (s, σ) λ (s, σ ) (s t, σ) λ (s t, σ ) (t, σ) λ (t, σ ) (s t, σ) λ (s t, σ ) (t, σ) λ σ (s t, σ) λ (s, σ ) (s, σ) λ σ (s t, σ) λ (t, σ ) Tämä konstruktio vastaa Algol 68 -kielessä käytössä ollutta par begin -lohkorakennetta. Jokaisella tehtävällä on usein myös tarvetta omille muuttujilleen. Tähän kieleen (kuten myöskin tavalliseen while-kieleen) voidaan varsin yksinkertaisesti lisätä tuki paikallisille muuttujille: x, y, z Variable e, f, g Expressions v Values s, t, u Statements s, t, u ::= {x = e ; s} (s, σ {(x, v)}) λ (s, σ ) ({x = v ; s}, σ) λ ({x = σ (x) ; s }, σ {(x, σ(x))}) (s, σ {(x, v)}) λ σ ({x = v ; s}, σ) λ σ {(x, σ(x))}) (e, σ) (e, σ) ({x = e ; s}, σ) ({x = e ; s}, σ) Tässä joudutaan lyhytaskelsemantiikassa temppuilemaan, jotta paikallinen muuttuja tosiaan pysyy paikallisena eikä esimerkiksi sotke samanaikaista tehtävää. Vastaava, tosin varsin erilainen, temppuilu joudutaan tekemään myös kielen toteutuksissa.
104 LUKU 10. SAMANAIKAISUUS Tällainen yhtäaikaisten tehtävien malli vaatii, että molemmat tehtävät tulevat loppuun suoritetuksi ennen kuin suoritus voi jatkua -operaation jälkeen. On myös hyödyllistä voida luoda taustatehtäviä, joiden päättyminen ei ole tarpeen, jotta suoritus voisi jatkua tehtävän luovan operaation jälkeen. 10.2 Kriittiset lohkot Näin yksinkertaisella yhteismuuttujakielellä on kuitenkin yksi erittäin iso ongelma: kuinka estetään samanaikaisia tehtäviä astumasta toistensa varpaille? Erityisen iso ongelma se on silloin, kun tehtävien on tarkoitus kommunikoida yhteismuuttujien avulla. Tavallista on, että itse informaatio välitetään eri muuttujassa kuin tieto informaation valmistumisesta ja informaation tulemisesta käytetyksi. Nyt esimerkiksi tuottajatehtävä saattaisi toimia seuraavasti: (while empty do skip); datum ; empty false Tässä skip on lause, joka ei tee mitään. Vastaavasti kuluttajatehtävä toimisi seuraavasti: (while empty do skip); x datum; empty true Niin kauan kuin tuottajia ja kuluttajia on vain kaksi, tämä voi vielä toimia, mutta jos tuottajia tai kuluttajia on useita, voi tilanne muuttua kaaoottiseksi. Yksinkertaisin tapa korjata tilanne on lisätä kieleen tuki atomisille lohkoille (engl. atomic blocks) eli kriittisille alueille (engl. critical regions), joiden
10.2. KRIITTISET LOHKOT 105 suorituksen aikana muut tehtävät on pysäytetty: x, y, z Variable e, f, g Expressions v Values s, t, u Statements s, t, u ::= atomic c c, d CriticalStatement c, d ::= x e c ; d if e then c else d {x = e ; c} (c, σ) σ (atomic c, σ) σ Huomaa, atomisen lohkon sisällä ei sallita samanaikaisuutta, siirräntää eikä silmukoita. Tämä on tosin liian rajoittunut ratkaisu, sillä kuluttajaa ja tuottajaa ei voi atomisena lohkona kirjoittaa silmukan kiellon takia. Muutenkin tällainen busy-wait-tekniikka on tietokoneen resursseja tuhlaavaa, ja on parempi lisätä kieleen tuki ehdolliselle atomisuudelle: x, y, z Variable e, f, g Expressions v Values c, d CriticalStatement s, t, u Statements s, t, u ::= await e then c (e, σ) (true, σ) (c, σ) σ (await e then c, σ) σ (e, σ) (false, σ) (await e then c, σ) (await e then c, σ)
106 LUKU 10. SAMANAIKAISUUS Käytännön toteutuksessa await-ehdon tuottaessa epätoden tehtävä kirjataan await-ehdon lukemien muuttujien odotusjonoon ja jätetään odottamaan, ja kun jokin toinen tehtävä sijoittaa muuttujaan, se samalla aktivoi kaikki kyseisen muuttujan odotusjonossa olevat tehtävät. Nyt tuottaja voidaan kirjoittaa ja kuluttaja await empty then (datum ; empty false) await empty then (x datum; empty true) Ehdolliset atomiset lohkot riittävät klassisten semaforien toteutukseen. 10.3 Monitorit Edellä esitettyjen tekniikoiden ongelmana on, että ne ovat varsin matalan tason ratkaisuja. Hansen (1972) ja Hoare (1974) kehittivät rakenteisen ratkaisun, jota sanotaan monitoriksi. Monitorin ideana on, että yleensä olennaista on estää tietyn muuttujajoukon yhtäaikainen käsittely ja että tuolla muuttujajoukolla on vain pieni joukko operaatioita, jotka tarvitsevat suoraa pääsyä joukkoon. Monitorilla on kaksi tilaa: se on joko vapaa tai varattu. Jos se on varattu, niin on olemassa jokin tehtävä, joka on sen varannut. Jos joku tehtävä kutsuu monitorin operaatiota ja monitori on jonkin toisen tehtävän varaama, kyseinen tehtävä jää jonoon odottamaan monitorin vapautumista. Jos monitori onkin vapaa, kyseinen tehtävä pääsee suorittamaan kyseistä operaatiota, ja monitori on varattu niin kauan kuin tuon operaation suoritus kestää. Kun operaatio on valmis, tehtävä vapauttaa monitorin ja jokin jonossa odottavista tehtävistä pääsee jatkamaan suoritustaan. Monitorissa voi olla ehtomuuttujia. Tällöin monitorin varannut tehtävä voi jäädä odottamaan ehtomuuttujaan, jolloin monitori vapautuu ja kyseinen tehtävä jää odottamaan kyseisen ehtomuuttujan jonoon. Jokin toinen tehtävä, pitäessään monitoria varattuna, voi signaloida ehtomuuttujaa, jolloin tämä tehtävä jää odottamaan ja jokin ehtomuuttujan jonossa
10.4. TRANSAKTIOMUISTI 107 oleva tehtävä päästetään jatkamaan monitorissa, ja signaloinut tehtävä jatkaa heti monitorin vapauduttua. Joissakin monitoritoteutuksissa signaloiva tehtävä jatkaa välittömästi ja ehtomuuttujassa odottava tehtävä jatkaa heti signaloivan tehtävän vapautettua monitorin. Monitorit on helppo yhdistää oliokielten kanssa, sillä olio voi hyvin toimia monitorina. Javassa synchronized-luokan oliot ovat monitoreita. 10.4 Transaktiomuisti Viimeisen kymmenen vuoden aikana on kehitetty ohjelmointikieliä, joissa työmuistia käsitellään tietokantaohjelmista tuttujen transaktioiden avulla (Harris et al., 2005). Transaktioiden pääetu on, että niitä voidaan yhdistellä mikään muu yhteismuistin hallintaan kehitetyt menetelmät eivät ole turvallisesti yhdistyvä. Transaktiomuisti tarvitsee kielessä tuen kahdelle konstruktiolle. Ensimmäinen, atomic s, sanoo, että s on yksi transaktio. Toinen, retry, sanoo, että transaktio epäonnistui ja se on aloitettava alusta. Kielensuunnittelijan kannalta oleellinen haaste on, kuinka kieli yhdistää peruutettavat transaktiot ja peruuttamattomat sivuvaikutukset. 10.5 CSP Hoaren vuonna Hoare (1978) esittelemä hahmotelma viestinvälityspohjaiseksi samanaikaisuusohjelmointikieleksi, Communicating Sequential Processes eli CSP on yksi ensimmäisistä yrityksistä muodostaa viestinvälityksen kielellistä teoriaa. CSP:n ideana on laajentaa imperatiivista ohjelmointikieltä (esimerkiksi while-kieltä) kanavilla, joiden kautta kohtaaminen tapahtuu, samanaikaisuudella sekä kanavapohjaisilla syöttö- ja tulostuslauseilla. Laajennuksen abstrakti syntaksi on annettu kuvassa 10.1 ja sen laskentasäännöt ovat kuvassa 10.2. (Reynolds, 1998, luku 9) Uusi kanava luodaan lausekkeella, jonka arvo sijoitetaan yleensä heti muuttujaan. Kanavaan kirjoitetaan lauseella e! f s, joka kirjoittaa kanavaan e viestin f ja suorittaa sitten lauseen s. Kanavaa luetaan lauseella e?x s, joka lukee kanavasta e viestin, tallettaa sen muuttujaan x ja suorittaa sitten lauseen s. Luku- ja kirjoitusoperaatio onnistuu vain, jos samalla kanavalla on yhtä aikaa sekä lukija että kirjoittaja. Useita luku- ja kirjoituslauseita voidaan yhdistää -operaattorilla; tällöin suoritettavaksi tulee niistä ensimmäinen, jonka suoritus onnistuu; mikäli mikään niistä ei ole
108 LUKU 10. SAMANAIKAISUUS x, y, z Variables h ChannelHandles e, f, g Expressions e, f, g ::= uusi kanava s, t, u Statements s, t, u ::= s t samanaikainen suoritus c vieestintä c, d Communication c, d ::= e?x s vastaanotto e! f s lähetys c d vaihtoehto v Values v ::= h λ Messsage λ ::= ɛ ei viestiä h?v viestin vastaanotto h!v viestin lähetys Kuva 10.1: CSP-laajennuksen abstrakti syntaksi
10.5. CSP 109 (, σ) (h, σ) h uusi (10.1) (h?x s, σ) h?v (s, σ {(x, v)}) (10.2) (h!v s, σ) h!v (s, σ) (10.3) (c, σ) λ (s, σ ) (c d, σ) λ (s, σ ) (d, σ) λ (s, σ ) (c d, σ) λ (s, σ ) (s, σ) h!v (s, σ) (t, σ) h?v (t, σ ) (s t, σ) ɛ (s t, σ ) (s, σ) h?v (s, σ ) (t, σ) h!v (t, σ) (s t, σ) ɛ (s t, σ ) (10.4) (10.5) (10.6) (10.7) (e, σ) (e, σ ) (e?x s, σ) ɛ (e?x s, σ ) (e, σ) (e, σ ) (e! f s, σ) ɛ (e! f s, σ ) ( f, σ) ( f, σ ) (e! f s, σ) ɛ (e! f s, σ ) (c, σ) λ (c, σ ) (c d, σ) λ (c d, σ ) (d, σ) λ (d, σ ) (c d, σ) λ (c d, σ ) (s, σ) λ (s, σ ) (s t, σ) λ (s t, σ ) (t, σ) λ (t, σ ) (s t, σ) λ (s t, σ ) (10.8) (10.9) (10.10) (10.11) (10.12) (10.13) (10.14) Kuva 10.2: CSP-laajennuksen laskentasäännöt
110 LUKU 10. SAMANAIKAISUUS juuri nyt suoritettavissa, suoritus jää odottamaan tilanteen muuttumista. Samanaikaisesti suoritettavat lauseet yhdistetään -operaattorilla. Esimerkki 5. Synkronoiva lukumuistipaikka voidaan toteuttaa CSP:llä laajennetussa while-kielessä (laajennettuna paikallisilla muuttujilla) seuraavasti. locwrite ; locread ; ({ loc 0 ; locempty true; while true do fi od }) if locempty then else locwrite?loc locempty false locread!loc locempty true Nyt kolmen pisteen paikalla voi olla koodia, joka lukee muistipaikkaa lukemalla kanavaa locread ja kirjoittaa muistipaikkaan kirjoittamalla kanavaan locwrite. 10.6 Piilaskento Milnerin, Parrowin ja Walkerin (1990) luoma piilaskento (engl. pi calculus, π-calculus) on yksi tunnetuimmista prosessialgebroista. Tämä luku perustuu Piercen artikkeliin (1996). Piilaskennon tarkoituksena on olla viestinvälityspohjaiselle ohjelmoinnille samanlainen perusteoria kuin lambdakieli on funktio-ohjelmoinnille. Tämä tavoite näkyy laskennon pelkistetyssä muodossa, jossa kaikki itse päätarkoituksen kannalta epäoleellinen on jätetty pois. Puhtaassa piilaskennossa on varsinaisesti vain kahdenlaisia otuksia agentit ja viestintäkanavia joita kielessä edustavat prosessilausekkeet ja muuttujat. Sitä voidaan toki laajentaa samaan tapaan kuin lambdakieltä ymmärtämään erilaisia tietorakenteita, mutta nuo laajennokset voidaan myös määritellä kielessä itsessään. Piilaskennon abstrakti syntaksi on esitetty kuvassa 10.3. Prosessilausekkeita on kuudenlaisia: 0, pysähtynyt (engl. inert) prosessi, ei tee mitään.
10.6. PIILASKENTO 111 x, y, z Variables p, q Processes p, q ::= 0 pysähtynyt prosessi x(y) p vastaanottava prosessi xy p lähettävä prosessi p q rinnakkaisprosessi (νx) p rajoitettu prosessi!p toistuva prosessi Vastaanottava ja rajoitettu prosessi sitovat sulkeissa mainitun muuttujan; muutoin muuttujat esiintyvät vapaana. Funktio FV : Processes P(Variables), joka liittää prosessiin siinä vapaana esiintyvät muuttujat, määritellään tavanomaiseen tapaan; samoin tavalliseen tapaan määritellään muuttujan korvaaminen toisella prosessissa p[x := y]. Tavanomaiseen tapaan prosessilausekkeita, jotka eroavat vain sidottujen muuttujien nimissä, pidetään syntaktisesti samoina lausekkeina. Kuva 10.3: Piilaskennon abstrakti syntaksi x(y) p, vastaanottava prosessi, ottaa vastaan viestin kanavalla x ja sitoo viestin sisällön paikalliseen muuttujaan y ja suorittaa sitten prosessin p. xy p, lähettävä prosessi, lähettää muuttujan y kanavalla x ja suorittaa sitten prosessin p. p q, rinnakkaisprosessi, suorittaa p:n ja q:n samanaikaisesti. (νx) p, rajoitettu prosessi, luo paikallisen muuttujan x.!p, toistuva prosessi, toistaa prosessia p rajattomasti. Piilaskennon muodollinen semantikka muodostuu kuvassa 10.4 esitetystä rakenteellisen ekvivalenssin säännöstöstä sekä kuvassa 10.5 esitetystä lyhytaskelsemantiikasta. Rakenteellisen ekvivalenssin tarkoituksena on hävittää kielestä sen tekstuaalisen esityksen aiheuttama lineaarinen bias: ei ole oikeasti väliä, kirjoitetaanko p q vai q p. Piilaskennossa muuttujat edustavat viestintäkanavia, ja mitään muuta kielessä ei olekaan. Kaikki viestit ovat itse viestintäkanavia (vertaa puhtaan lambda-kielen ajatukseen, jossa kaikki arvot ovat itse funktioita), ja kaikki viestintäkanavat kelpaavat viesteiksi. Piilaskennon perusteella on luotu mm. Pict-ohjelmointikieli (Pierce ja Turner, 2000).
112 LUKU 10. SAMANAIKAISUUS p q q p (10.15) (p q) r p (q r) (10.16)!p p!p (10.17) x FV(q) ((νx)p) q (νx)(p q) (10.18) Kuva 10.4: Piilaskennon rakenteellisen ekvivalenssin säännöstö xy p x(z) q p q[z := x] (10.19) p p p q p q (10.20) p p (νx)p (νx)p (10.21) p q p q p p q q (10.22) Kuva 10.5: Piilaskennon lyhytaskelsemantiikka