TIES542 kevät 2009 Oliokielten erityiskysymyksiä Antti-Juhani Kaijanaho 16. maaliskuuta 2009 1 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 ReadWriteStream, 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ä ReadStream 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() 1
lock.acquirelock(); int rv = A::read(); return rv; void SyncA::write(int c) lock.acquirelock(); A::write(c); 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(); return rv; virtual void SyncA::write(int c) lock.acquirelock(); S:: write(c); ; class SyncA : public SyncReadWrite<A> ; class SyncB : public SyncReadWrite<B> ; Kielessä, jossa yläluokkaa ei voi parametrisoida, ongelmalle ei ole näin siistiä ratkaisua. 2
Ongelmaksi jää se, että perintähierarkia linearisoituu: käsitteellisesti SyncReadWrite ja A eivät ole perintäsuhteessa keskenään, mutta mixin-ratkaisussa Sync- ReadWrite<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 korkeammalla 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 saadaan ylikirjoituksella d T. Luokka on hyvin määritelty, jos jokainen piirteiden vaatima metodi on lopullisessa luokassa 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 ratkaista piirteillä seuraavasti: trait SyncReadWrite virtual int read() 3
lock (). acquirelock(); int rv = super.read(); 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. 2 Binäärimetodien ongelma Jokainen Java-ohjelmoija lienee törmännyt equals-metodin ongelmaan. Lähes jokainen equals-metodi alkaa public boolean equals(object o) if (!( o instanceof ThisClass)) return false; 4
ThisClass other = (ThisClass) o; Olisi mukavaa, jos sen voisi kirjoittaa public boolean equals(thisclass other) mutta se ei ole Javassa mahdollista. Yhtäsuuruusvertailu on yksi esimerkki binäärimetodista. 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. Valitettavasti tällöin Object-luokasta periminen rikkoo alityypityksen subsumptioperiaatteen (harjoitustehtävä!). Siksipä on otettava käyttöön alityypitystä heikompi relaatio, matching (Bruce et al., 1995), jossa This-tyyppinen argumentti käyttäytyy kuten on tarkoitus. Matching ei salli aliluokan olion käyttämistä yliluokan olion tilalla, mutta matchingia voidaan käyttää parametrisen polymorfismin avulla useimmissa tilanteissa, joissa alityypitystä tarvittaisiin. Viitteet Alexandre Bergel, Stéphane Ducasse, Oscar Nierstrasz, and Roel Wuyts. Stateful traits and their formalization. Computer Languages, Systems & Structures, 34 5
(2 3):83 108, 2008. http://dx.doi.org/10.1016/j.cl.2007.05.003. Kim Bruce, Luca Cardelli, Giuseppe Castagna, Jonathan Eifrig, Scott Smith, Valeri Trifonov, Gary T. Leavens, and Benjamin Pierce. On binary methods. Theory and Practice of Object Systems, 1(3), 1995. http://citeseerx.ist. psu.edu/viewdoc/download?doi=10.1.1.132.298&rep=rep1&type=pdf. Stéphane Ducasse, Oscar Nierstrasz, Nathanael Schärli, Roel Wuyts, and Andrew P. Black. Traits: A mechanism for fine-grained reuse. ACM Transactions on Programming Languages and Systems, 28(2):331 388, March 2006. http://doi.acm.org/10.1145/1119479.1119483. 6