TIES542 kevät 2009 Tyyppijärjestelmän laajennoksia Antti-Juhani Kaijanaho 16. helmikuuta 2009 Tyypitetyt ohjelmointikielet sisältävät paljon muitakin konstruktioita kuin yksinkertaisesti tyypitetyn lambda-kielen, vaikka ne tyypillisesti sisältävät senkin (ainakin rajoitettuna versiona). Monisteen päälähde on Pierce (2002). 1 Yksikkötyyppi Kielissä, jotka sallivat globaalit sivuvaikutukset (kuten esimerkiksi siirrännän), on usein tarpeen kirjoittaa aliohjelmia, joilla ei ole (mielenkiintoista) paluuarvoa. C-sukuisissa kielissä kuten C, C++ ja Java tällaiseen käyttöön on varattu voidtyyppi, joka näiden kielten perinteen mukaan ei sisällä yhtään arvoa pidän tätä perinteistä näkemystä virheellisenä (tyyppiä, jossa ei ole arvoa, oleva lauseke ei voi tyyppiteorian näkökulmasta koskaan tulla lasketuksi loppuun). Tyyppiteoriassa sama ajatus mallinnetaan yksikkötyypillä (engl. unit type), jossa on yksi arvo. Kun tyypissä on yksi arvo ja jos staattinen tyyppijärjestelmä on eheä tarvitaan arvon esittämiseen log 2 1 eli nolla bittiä. Tyypistä ja sen arvosta käytetään toisinaan notaatiota Unit ja unit, toisinaan kummastakin notaatiota (). Jälkimmäinen toimii mainiosti yhteen myöhemmin esiteltävien monikkotyyppien kanssa. T, U ::= () t, u ::= () w ::= () 1
Yksikkötyypillä ei ole laskentasääntöjä. Γ () : () 2 Paikalliset muuttujat Useimmissa aliohjelmia tukevissa kielissä on tuki myös paikallisille muuttujille muuttujille, jotka näkyvät vain aliohjelmassa tai sen osassa. Tyypillinen rakenne on Pascalin var x : real begin end taikka C90:n {double x; }. Lambda-kielessä (ja siten tyyppiteoriassa) muuttujiin ei voi sijoittaa, vaan niille täytyy antaa alkuarvo heti kättelyssä. Lambda-kielen paikallisia muuttujia luova laajennus on ns. let-lauseke: t, u ::= let x = t in u Laskentasäännöstöjä let-lausekkeelle voidaan antaa kahdenlaisia. Ensimmäinen on innokkaasti laskeva joka vastaa useimpien kielien toimintatapaa: Toinen on laiskasti laskeva: Tyypityssääntö on yksinkertainen: let x = v in u u[x := v] t t let x = t in u let x = t in u let x = t in u u[x := t] Γ t : T Γ, x : T u : U Γ let x = t in u : U Jos tyypitys jätettäisiin huomiotta, let-lauseke voitaisiin ymmärtää beeta-redeksinä: let x = t in; u = (λxu)t. Yksinkertaisten tyyppien kanssa vastaavuus ei ole yhtä yksiselitteinen, sillä let-lausekkeessa ei ole tarpeen kertoa muuttujan tyyppiä, kun taas abstraktiossa se vaaditaan. Let-lauseke voidaan yleistää hyväksymään useita muuttujien määrittelyjä tyyliin let x 1 = t 1 ; x 2 = t 2 in u. Tässä on kuitenkin tehtävä valinta sen mukaan, näkyvätkö määriteltävät muuttujat muissa määritelmissä (taikka omassa määritelmässään) vai ei toisin sanoen, sallitaanko (keskenään) rekursiiviset määritelmät 2
vaiko ei. Tavanomainen let-lauseke ei tavallisesti rekursiota salli, ja tällöin se voidaankin ymmärtää kielioppimakeisena, joka tarkoittaa sisäkkäisiä yksinkertaisia let-lausekkeita: (let x 1 = t 1 ; x 2 = t 2 in u) = (let x 1 = t 1 in let x 2 = t 2 in u). Toisinaan rekursio sallitaan tällöin puhutaan ns. letrec-lausekkeesta. 3 Monikot Yksi ohjelmoinnin tärkeimmistä työkaluista on rakenteiset arvot ja niitä vastaavat rakenteiset tyypit toisin sanoen säilöt containers. Yksinkertaisin esimerkki on monikko (engl. tuple), jossa on sisällä järjestykseen asetettuna kaksi tai useampaa eri tyyppistä arvoa, joita päästään käsittelemään järjestysnumeron avulla. Monikossa on kuitenkin aina staattisesti tiedossa komponenttien lukumäärä ja tyyppi sekä mitä komponenttia ollaan kulloinkin hakemassa. Monikot sinänsä eivät ole kovin yleisiä ohjelmointikielissä, mutta niitä käytetään usein auttamaan tiettyjen konstruktioiden hahmottamisessa. Esimerkiksi moniparametrinen funktio voidaan ymmärtää toisaalta curry attyna yksiparametrisena funktiona, joka palauttaa funktion, mutta toisaalta myös (ja tavallisemmin) yksiparametrisena funktiona, jonka parametri on monikko. Samoin monen arvon palauttaminen (mikä on mahdollista joissakin kielissä) voidaan ymmärtää monikon palauttamisena. Monikon konstruointisyntaksina käytetään lähes aina sulkeita ja pilkkuja: (1, kissa, 3) toisinaan kulmasulkeita ja pilkkuja: (1, kissa, 3). Arvojen käsittelyyn voidaan määritellä projisointioperaattoreita, mutta hyödyllisempää mielestäni on laajentaa let-lausekkeen muuttujakäsitystä seuraavaan tapaan: T, U ::= (T 1,..., T n ) n 2 t, u ::= (t 1,..., t n ) n 2 let (x 1,..., x n ) = t in u n 2 w ::= (v 1,..., v n ) n 2 Γ t 1 : T 1 Γ t n : T n Γ (t 1,..., t n ) : (T 1,..., T n ) Γ t : (T 1,... T m ) Γ, x 1 : T 1,..., x n : T n u : U i j x i x j n = m Γ let (x 1,..., x n ) = t in u : U t k t k (t 1,..., t k,..., t n ) (t 1,..., t k,..., t n ) 3
t t let (x 1,..., x n ) = t in u let (x 1,..., x n ) = t in u z 1,..., z n uusia i j x i x j n = m let (x 1,..., x n ) = (v 1,..., v m ) in u u[x 1 := z 1 ] [x n := z n ][z 1 := v 1 ] [z n := v n ] Muuttujanvaihdos viimeisessä laskusäännössä on tarpeen, jotta sidotut ja vapaat muuttujat eivät menisi sekaisin (jos esimerkiksi x 2 esiintyy vapaana v 1 :ssä). Näin määritelty monikkoja ymmärtävä let-lauseke on innokkaasti laskeva. Laiskasti laskeva määrittely on toki myös mahdollinen. 4 Tietueet Monikosta on varsin yksinkertainen hyppäys tietueisiin, joissa komponentteihin ei viitata järjestysnumerolla, vaan niille annetaan (staattisesti tiedossa olevat) nimet. Tietueet esiintyvät useimmissa tyypitetyissä kielissä, yleensä tietueen tai struktuurin nimellä ja tietue on myös tyypillisen olioluokan pohjalla. Myös tietueille on mahdollista määritellä purkava let-lause, kuten monikoille tehtiin yllä, mutta tässä valitaan toinen (ja yleisemmin käytössä oleva) ratkaisu: l Labels T, U ::= {l 1 : T 1,..., l n : T n } t, u ::= {l 1 = t 1,..., l n = t n } t.l w ::= {l 1 = v 1,..., l n = v n } Γ t 1 : T 1 Γ t n : T n Γ {l 1 = t 1,..., l n = t n } : {l 1 : T 1,..., l n : T n } Γ t : {l 1 : T 1,..., l k : T k,..., l n : T n } l = l k T = T k Γ t.l : T t k t k {l 1 = t 1,..., l k = t k,..., l n = t n } {l 1 = t 1,..., l k = t k,..., l n = t n } l = l k {l 1 = v 1,..., l k = v k,..., l n = v n }.l v k 4
Tämäkin määrittely laskee innokkaasti laiska määritelmä olisi myös mahdollinen. Tässä määritelmässä tietuetyypit ovat samat, jos niissä esiintyvät sama määrä samannimisiä kenttiä samantyyppisinä samassa järjestyksessä. Määritelmä vastaa C-kielessä käytössä olevaa järjestelyä. Sen hyöty on siinä, että kääntäjä voi staattisesti selvittää pelkän tyypin avulla, kuinka kaukana tietueen alusta haluttu kenttä on. Semanttisesti voisi olla järkevämpää sallia kenttien järjestyksen vaihteleminen, ja toisinaan näin tehdäänkin mutta silloin joudutaan tekemään ajonaikana työtä sen selvittämiseksi, missä kohtaa tietuetta haluttu kenttä on. 5 Variantit Proseduraalisessa ja funktionaalisessa ohjelmoinnissa joissa perintä ei ole käytettävissä on tarpeen kyetä luettelemaan vaihtoehtoisia rakenteita samalle tyypille. C-kielessä tämä toteutetaan käyttämällä unionia (engl. union), Pascalissa varianttitietueita variant records ja Haskellissa algebrallisia tietotyyppejä (engl. algebraic data types). Näille kolmelle yhteinen teoriatausta saadaan varianttityypistä (engl. variant type), joka on muuten samanlainen kuin tietue paitsi että vain yksi kenttä on olemassa kerrallaan. Lisäksi variantin sisällön tarkasteleminen vaatii suorituksen haarautumista sen mukaan, mikä variantin kentistä on kulloinkin käytössä. l Labels T, U ::= l 1 : T 1,..., l n : T n t, u ::= l = t case t of l 1 = x 1 t 1 l n = x n t n w ::= l = v Γ t : T ( k {1,..., n}: l = l k T = T k ) Γ l = t : l 1 : T 1,..., l n : T n Γ, x 1 : T 1 t : T Γ, x n : T n t : T Γ t : l 1 : T 1,..., l n, T m n = m Γ case t of l 1 = x 1 t 1 l n = x n t n : T t t l = t l = t 5
l = l k case l = v of l 1 = x 1 t 1 l n = x n t n t k [x k := v] Näin määritelty varianttityypit sisältävä tyyppijärjestelmä on ensimmäinen kurssilla vastaan tuleva järjestelmä, jossa tyyppisäännöistä ei voi suoraan takaperin lukemalla löytää tyyppitarkastusalgoritmia. Syy on siinä, että varianttilausekkeen l = t tyyppi ei ole yksikäsitteinen. Elegantti ratkaisu ongelmalle on käyttää Hindley Milnerin tyyppipäättelyä. Toinen vaihtoehto on laajentaa kieltä siten, että varianttilausekkeelle tulee aina eksplisiittisesti annettu tyyppi. C-kielen unionityyppi poikkeaa edellä esitetystä siinä, että unionin mitä tahansa kenttää on syntaktisesti mahdollista käyttää milloin vain, eikä tyyppijärjestelmä sen paremmin staattisesti kuin dynaamisestikaan virheellistä käyttöä huomaa. Vastaava ongelma on käytännössä myös Pascalin varianttitietueilla. Haskell-kielen algebrallisissa tyypeissä on kyse varianttityypeistä, joissa kunkin vaihtoehdon tyyppi on automaattisesti joko monikko tai tietue. Tällainen tyyppi määrittelee olennaisesti universaalialgebrasta tutun operaattoriston mikä selittääkin nimen. Luettelotyypit (kuten C:n enum-tyypit, mutta myös kokonaislukutyypit, totuusarvotyyppi jne.) voidaan nähdä varianttityypin erikoistapauksena, jossa kunkin varianttikentän tyyppinä on yksikkötyyppi. 6 Viitetyypit Imperatiivisessa ohjelmoinnissa välttämätön ominaisuus on muuttuja, jonka arvoa voi suoritusaikana muuttaa. Lambdakielessä ja tyyppiteoriassa tämä mallinnetaan erityisenä viitetyyppinä (engl. reference type), jossa muuttujan arvona voi olla viite muistipaikkaan, jonka arvon muuttaminen ajonaikana on mahdollista. Seuraavassa käytetään C++-kielen osoittimista tuttua syntaksia. 6
r Reference T, U ::= T () t, u ::= new t t t u () t; u w ::= r Huomaa: Ensimmäistä kertaa selkäranka (ja siten arvo) ei ole termin erikoistapaus, sillä viittausta edustava r ei ole sallittu termi. Γ t : T Γ new t : T Γ t : T Γ t : T Γ t : T Γ u : T Γ t u : () Γ t : () Γ u : U Γ t; u : U Γ () : () Laskentasäännöissä joudutaan laajentamaan välitilan käsitettä aiemmasta lisäämällä välitiloihin keko (engl. store), joka on osittaisfunktio viittauksista arvoihin. Jokainen aiempi laskentasääntö pitää myös laajentaa kuljettamaan kekoa mukanaan, esimerkiksi: (λx : T t, σ) (λx : T t, σ ) Muut lambda-kielen laskentasäännöt muuttuvat vastaavasti. Uusien konstruktioiden laskentasäännöt ovat seuraavat: r dom σ (new v, σ) (r, σ (r, v)) 7
r dom σ ( r, σ) (σ(r), σ) r dom σ (r v, σ) (r, σ (r, v)) (new t, σ) (new t, σ ) ( t, σ) ( t, σ ) (t u, σ) (t u, σ ) (u, σ) (u, σ ) (t u, σ) (t u, σ ) (t; u, σ) (t ; u, σ ) Jotta tyyppijärjestelmän eheys voitaisiin todistaa etenemis- ja säilymislemmojen avulla, pitää välitiloille ja siten keolle määritellä tyypitys. Sivuutetaan se tässä (Pierce, 2002, 13.4). Viitteet Benjamin C. Pierce. Type and Programming Languages. MIT Press, Cambridge, MA, 2002. 8