TIES542 kevät 2009 Rekursiiviset tyypit Antti-Juhani Kaijanaho 17. helmikuuta 2009 Edellisessä monisteessa esitetyt tietue- ja varianttityypit eivät yksinään riitä kovin mielenkiintoisten tietorakenteiden toteuttamiseen. Useimmissa ohjelmissa tarvitaan erilaisia puurakenteita, jotka on luonnollisinta esittää suoraan käyttämällä itseviittaavia tyyppejä. Esimerkiksi yksöislinkitetty kokonaislukulista voidaan määritellä C-kielessä seuraavasti: struct intlist { int datum; struct intlist next; }; Tyypitetyn (ja rakenteisilla tyypeillä laajennetun) lambda-kielen syntaksilla se voitaisiin kirjoittaa esimerkiksi näin: NumList = null : (), notnull : { datum : Num, next : NumList }... paitsi että tuohon kieleen ei kuulu minkäänlaista tapaa antaa nimiä (saati rekursiivisia määritelmiä!) tyypeille. Tässä monisteessa käsitellään rekursiivisten tyyppimääritelmien problematiikkaa ja sen ratkaisumalleja. Monisteen päälähde on Pierce (2002, luvut 21 ja 22). 1 Rekursiiviset termit tyypitetyssä lambda-kielessä Rekursiivisten tyyppien käsittelyssä tarvitaan rekursiivisia termejä. Koska yksinkertaisesti tyypitetyssä lambda-kielessä ei ole valmista kiintopisteoperaattoria, on sellainen lisättävä: 1
t, u ::= µx : T t Γ, x : T t : T Γ µx : T t : T µx : T t t[x := µx : T t] 2 Rekursiiviset tyypit lambda-kielessä Tyyppiteorian tapa lisätä rekursiiviset tyypit lambda-kieleen on ottaa käyttöön tyyppioperaattori µ, joka mahdollistaa rekursion: α TyVars T, U ::= α µα T Esimerkiksi lukulistan tyyppi voitaisiin kirjoittaa µα null : (), notnull : (Num, α) ; operaattorin µα vaikutusalueella tyyppimuuttuja α edustaa koko pitkää µ:lla alkavaa tyyppilauseketta. Esimerkki 1. NumList = µα null : (), notnull : (Num, α) sum = µf : NumList Num λl : NumList case l of null = x 0 notnull = x let (a, r) = x in a + fr Esimerkissä nimet ovat vain lyhennysmerkintöjä; rekursiota ei niiden kautta sallita. Tämän laajennuksen tyypitys- ja laskentasäännöt vaativat jonkin verran pohdintaa. Olisi nimittäin kiva, jos tyyppien yhtäläisyys noudattaisi kiintopisteperiaatetta µα T (α) = T (µα T (α)) missä T (α) on jokin tyyppilauseke, jossa α esiintyy. Lukulistan tapauksessa sama kiintopisteyhtälö saa muodon µα null : (), notnull : (Num, α) = null : (), notnull : (Num, µα null : (), notnull : (Num, α) ) 2
Rekursiivisten tyyppien ongelma on, että on varsin hankalaa määritellä algoritmi, joka toteuttaa sellaisen tyyppien yhtäläisyysvertailun, jolle tuo yhtälö pätee. Algoritmi, joka toteuttaa tämän yhtälön sellaisenaan, toteuttaa ekvirekursion (engl. equirecursion). 3 Nimiyhtäläisyys Yksinkertaisin ja ohjelmointikielissä yleisin tapa ratkaista rekursiivisuuden ongelma on vaatia, että jokainen tyyppi nimetään, ja sallia tällaisten nimettyjen määritelmien keskinäinen rekursio (yleensä siten rajoitettuna, että rekursion tulee kulkea osoittimen kautta, mikäli kielessä on erillinen osoitintyyppi). Lisäksi tässä ratkaisussa julistetaan, että kaksi eri nimistä tyyppiä ovat eri tyyppejä vaikka niillä olisikin sama rakenne. Tätä ratkaisutapaa sanotaan nimiyhtäläisyydeksi (engl. name equivalence). 4 Isorekursio Hieman monimutkaisempi ja edellä annettuun lambda-kielen laajennokseen sopiva mutta kuitenkin täyttä ekvirekursiota helpompi ratkaisu on isorekursio (engl. isorecursion) 1 Ajatuksena on, että ohjelmoija merkitsee näkyviin paikat, missä on tarpeen tehdä muunnos tyypistä µα T (α) tyyppiin T (µα T (α)) ja takaisin käyttämällä kielen sisään rakennettavia konversiofunktioita unfold[µα T (α)]: µα T (α) T (µα T (α)) fold[µα T (α)]: T (µα T (α)) µα T (α) Nyt edellä esimerkkinä annettu sum-funktio jouduttaisiin kirjoittamaan seuraavasti: sum = µf : NumList Num λl : NumList case unfold[numlist] l of null = x 0 notnull = x let (a, r) = x in a + fr Toisaalta käytännön ohjelmointikielessä tämä ei ole niin ongelmallista, koska fold- ja unfold-funktiot voidaan niissä piilottaa osaksi kielen muuta sotkuisuutta. Esimerkiksi jos kieli vaatii, että tyyppirekursion tulee aina kulkea jonkin varianttityypin kautta, 2 voidaan varianttityypin käsittelyn sisälle piilottaa tarvittavat fold- ja unfold-operaatiot. 1. Etuliite iso- ei tässä viittaa kokoon vaan isomorfismiin. 2. Tämä ei ole kovinkaan vakava vaatimus, sillä käytännössä tyyppirekursio joka tapauksessa kulkee (lähes) aina variantin kautta. 3
Isorekursiiviset tyypit voidaan määritellä seuraavasti: α TyVars T, U ::= α µα T t, u ::= fold[t ] unfold[t ] Nyt myös tyypeille pitää määritellä korvausoperaattori T [α := U] ja vapaat muuttujat F V (T ). Määritelmät kirjoitetaan samaan tapaan kuin termeille (harjoitustehtävä). T = µα U Γ unfold[t ] : T U[α := T ] T = µα U Γ fold[t ] : U[α := T ] T unfold[t ](fold[t ] v) v 5 Ekvirekursio Ekvirekursiivinen lähestymistapa on kaikista simppelein ohjelmoijan kannalta, mutta vaatii varsin raskasta koneistoa tyyppijärjestelmään. Pääongelmana on, että tyyppitarkastimen pitää nyt päätellä, mihin fold- ja unfold-operaatiot kuuluvat, ilman ohjelmoijalta saatavaa apua. Ekvirekursiivisesti tyypitetty lambda-kieli voidaan määritellä seuraavasti: α, β, γ TyVars x, y, z Vars T, U ::= T U α µα T t, u ::= x tu λx : T t 4
Γ, x : T x : T Γ t : U 1 T Γ u : U 2 U 1 U 2 Γ tu : T Γ, x : T t : U Γ (λx : T t) : T U Tyypityssäännöt ovat siis samat kuin yksinkertaisesti tyypitetyssä lambda-kielessä lukuunottamatta applikaation sääntöä. johon on lisätty eksplisiittinen tyyppien yhtäläisyysvaatimus. Laskentasäännöt eivät muutu, joten niitä ei tässä tarvitse toistaa. Ekvirekursiivisten tyyppien yhtäläisyyden idea on avata µ-tyypit äärettömiksi tyypeiksi ja sitten verrata, ovatko nämä äärettömät tyypit samoja. Yksinkertaistaen siis: expand(α) = α expand(t U) = expand(t ) expand(u) expand(µα T ) = T [x := expand(µα T )] T U expand(t ) = expand(u) Käytännössä tämä on tietenkin käyttökelvoton algoritmi, sillä se vaatisi äärettömän tietorakenteen läpikäymistä. Brandt ja Henglein (1998) esittävät yhden mahdollisen algoritmin asian ratkaisemiseksi. Seuraavissa päättelysäännöissä Γ on tyyppiparien (joita merkitään T U) joukko. Γ T T Γ, T U T U Γ U T Γ T U Γ T 1 T 2 Γ T 2 T 3 Γ T 1 T 3 Γ µα T T [α := µα T ] Γ, T 1 T 2 U 1 U 2 T 1 U 1 Γ, T 1 T 2 U 1 U 2 T 2 U 2 Γ T 1 T 2 U 1 U 2 Päättelysäännöstöstä on mahdollista johtaa seuraavanlainen algoritmi: 5
Algoritmi 1 (Brandt ja Henglein (1998)). Syötteenä tyyppiparien joukko Σ sekä kaksi tyyppiä, tuloksena joko tosi tai epätosi. Sovelletaan järjestyksessä ensimmäistä sopivaa yhtälöä: S(Σ, µt.t, U) = S(Σ, T [t := µt.t ], U) S(Σ, T, µt.u) = S(Σ, T, U[t := µt.u]) S(Σ, T, U) = true (1) S(Σ, T T, U U ) = S(Σ, T, U) S(Σ, T, U ) (2) S(Σ, t, t) = true (3) S(Σ T, U) = false (1) jos (T, U) Σ (2) missä Σ = Σ {(T T, U U )} (3) t I Algoritmin (ja sen oikeellisuustodistuksen) taustalla on koinduktiivinen metodi, joka sivuutetaan tässä. 6 Rekursiivisten tyyppien seurauksia Esimerkki 2 (Nälkäiset funktiot). Tarkastellaan tyyppiä Hungry = µα Num α. Yksi mahdollinen tuohon tyyppiin kuuluva funktio on f = µf : Num Hungry λx : Num f. Tällainen funktio tarvitsee äärettömän monta argumenttia. Esimerkki 3 (Virrat). Hyödyllisempi on tyyppi NumStream = µα () (Num, α) joka palauttaa joka kutsulla luvun ja uuden virran: ones = µf : NumStream λx : () (1, f) nats = (µf : Num NumStream λx : Num λy : () (x, f(x + 1))) 0 6
Esimerkki 4 (Yksinkertaiset oliot). Määritellään tyyppi Counter = µα { get : Nat, inc : () α } Nyt voidaan määritellä tehdasfunktio createcounter = µf : { x : Num } Counter λs : { x : Num } { get = s.x, inc = λy : () f({ x = s.x + 1 }) } ja sen avulla olio createcounter { x = 0 }. Tällä oliolla on tosin kaksi puutetta: sillä ei ole tilasta riippumatonta identiteettiä (jokainen muutos luo uuden olion) ja alityypitys ei luonnistu. Kumpikin ongelma on ratkaistavissa identiteetti käyttämällä osoitintyyppiä, alityypityksestä puhutaan myöhemmällä luennolla. Mainittakoon vielä, että rekursiivisesti tyypitetty lambda-kieli on yhtä vahva kuin tyypittämätön lambda-kieli, eli siinäkin on mahdollista määritellä kiintopisteoperaattori muiden ominaisuuksien avulla. Viitteet Michael Brandt and Fritz Henglein. Coinductive axiomatization of recursive type equality and subtyping. Fundamenta Informaticae, 33(4):309 338, April 1998. Benjamin C. Pierce. Type and Programming Languages. MIT Press, Cambridge, MA, 2002. 7