TIES542 kevät 2009 Tyyppiteorian alkeet Antti-Juhani Kaijanaho 9. helmikuuta 2009 [Staattinen t]yyppijärjestelmä on ratkeava, kieliopillinen menetelmä, jota käytetään todistamaan tiettyjen käytösten puuttuminen ohjelmasta luokittelemalla ilmaisuja niiden laskemien arvojen laadun perusteella. Benjamin C. Pierce (2002, sivu 1) Käsitteellä tyyppi (engl. type) on ohjelmointikielten alueella kaksi toisiinsa liittyvää merkitystä: Tyyppi määrittelee, mitä operaatioita johonkin arvoon taikka muistipaikkaan voidaan kohdistaa. Tyyppi määrittelee, miten jonkin olion tila (joka on pohjimmiltaan bittijono) tulkitaan arvoksi. Esimerkiksi kokonaislukuun voidaan kohdistaa aritmeettisia operaatioita mutta ei esimerkiksi aliohjelmana kutsumista toisaalta monien kielten kokonaislukutyyppinen muistialue tulkitaan luvuksi käyttäen kahden komplementti -esitystä. Tyyppivirhe (engl. type error) tarkoittaa tilannetta, jossa ohjelman suorituksen aikana yritetään kohdistaa johonkin arvoon taikka muistipaikkaan sellaista operaatiota, joka ei ole sen tyypin mukaan sallittua. Hyvä esimerkki useimmissa kielissä tyyppivirheestä on kokonaisluvun kutsuminen ikään kuin se olisi funktio. Tärkeää on kuitenkin huomata, että tyyppivirheen määritelmä riippuu kielestä (esimerkiksi ei ole mahdotonta kuvitella kieltä, joka määrittelee kokonaisluvulle myös funktiomerkityksen). Ohjelmointikieli voi ottaa tyyppeihin kantaa kahdella dimensiolla: olematon heikko vahva: Ohjelmointikieli on vahvasti tyypitetty, jos se diagnosoi kaikki tyyppivirheet; heikosti tyypitetty, jos se diagnosoi osan tyyppivirheistä; ja tyypitön, jos se ei diagnosoi yhtään tyyppivirhettä. 1
Huomaa, että tämä dimensio on osittainjärjestetty: kieli voi olla vahvemmin tyypitetty kuin toinen, vaikka kumpikaan ei olisi vahvasti tyypitetty. staattinen dynaaminen: Ohjelmointikieli on staattisesti tyypitetty, jos se diagnosoi tyyppivirheet (tai osan niistä) ennen suorituksen alkua, esimerkiksi käännösaikana; ja dynaamisesti tyypitetty, jos se diagnosoi tyyppivirheet (tai osan niistä) suorituksen aikana, tyypillisesti silloin, kun tyyppivirheellistä operaatiota ryhdytään yrittämään suorittaa. Kieli voi toki olla molempia. Tyyppivirheen diagnosointi tarkoittaa, että kielen toteutus estää kyseisen virheen sisältävän konstruktion suorittamisen ja antaa virheestä ohjelmoijalle (tai loppukäyttäjälle) palautetta. Esimerkki 1. Symboliset konekielet ovat (pääsääntöisesti) tyypittömiä. C on staattisesti heikosti tyypitetty ja dynaamisesti tyypitön kieli. C++ on C:tä vahvemmin tyypitetty sekä staattisesti että dynaamisesti. Java on C++:aa vahvemmin tyypitetty sekä staattisesti että dynaamisesti. Python ja Perl ovat staattisesti tyypittömiä ja dynaamisesti vahvasti tyypitettyjä kieliä. Haskell 98 (ilman laajennoksia) on staattisesti vahvasti tyypitetty. Ohjelmointikielen tyypitys on aina kompromissi tyyppiturvallisuuden ja ilmaisuvoiman välillä. Jokainen tyyppivirhe on useimmissa ohjelmissa aidosti ongelma, mutta on ohjelmia (erityisesti käyttöjärjestelmätasolla), joissa tyyppivirhe on tarkoituksellinen ja tarpeellinen. Mikäli kielen tyypitys on ilmaisuvoimaltaan heikkoa, voi aivan tavallistenkin ohjelmointiongelmien ratkaiseminen luonnollisella tavalla olla mahdotonta. Tässä monisteessa tarkastellaan tyyppiteoriaa, jossa on kyse nimenomaan staattisen tyyppijärjestelmän teoriasta. Alan tutkimuksen päätavoite on löytää vahvoja tyyppijärjestelmiä, jotka hylkäävät tyyppivirheinä yhä vähemmän käyttökelpoisia ohjelmointikonstruktioita. Täydellinen tyyppijärjestelmä on matemaattinen mahdottomuus, mutta täydellisyyttä voidaan lähestyä rajatta. Aiheen peruskirjallisuutta ovat Cardelli (2004) ja Pierce (2002). 1 Tyypitetty while-kieli Aiemmassa monisteessa määriteltiin while-kieli: siinä oli aritmeettiset lausekkeet ja ehtolausekkeet, sijoituslause, peräkkäistys, valintalause ja while-silmukka. Yksi 2
ikävä puute kielessä oli: muuttujaan ei voinut sijoittaa testin tulosta. Korjataanpa tämä ongelma. Jos muuttujaan voi sijoittaa testin tuloksen, pitää ratkaista, mitä tapahtuu, jos tällaisen muuttujan arvon antaa aritmeettiselle operaatiolle. Vastaavasti pitää sallia muuttujan käyttäminen suoraan testinä ja niinpä pitää ratkaista, mitä tapahtuu, jos lukuarvoista muuttujaa käytetään testinä. Joissakin kielissä tämä ratkaistaan siten, että testit ovat lukuja, ja nolla tarkoittaa epätotta. Tällä kertaa ei tehdä tuota ratkaisua, vaan pidetään luvut ja ehdot erillään. Määritellään, että kielessä on kaksi tyyppiä, luvut ja ehdot: T, U Types T, U ::= num bool Kielen syntaksissa ei enää erotella toisistaan luku- ja ehtolausekkeita. c N x, y, z Var e, f, g Expr e, f, g ::= c x e e + f e f e f e = f e f e < f e f e > f e f e f e f s, t Stmt s, t ::= x e s ; t if e then s else t while e do s Lausekkeiden staattinen tyyppijärjestelmä tavallisesti määritellään päättelyjärjestelmänä (vrt. lyhytaskelsemantiikka), jossa pääteltävänä on relaatio Γ e : T, missä 3
Γ: Var Types on osittaisfunktio muuttujilta tyypeille, ns. tyyppiympäristö (engl. type environment); e on jokin lauseke; ja T on jokin tyyppi. Relaatio Γ e : T luetaan Jos muuttujilla on ne tyypit, mitkä on Γ:ssa annettu, niin e:n tyyppi on T. Aritmeettisten operaattoreiden tyyppisäännöt ovat yksinkertaiset: operandien tulee olla lukuja ja tulos on luku: Γ e : num Γ e : num Γ e + f : num Γ e f : num Γ e f : num Vertailuoperaattoreillakin tilanne on yksinkertainen: (T-neg) (T-add) (T-sub) (T-mul) Γ e = f : bool Γ e f : bool Γ e < f : bool Γ e f : bool Γ e > f : bool Γ e f : bool (T-eq) (T-noteq) (T-lt) (T-le) (T-gt) (T-ge) Samoin loogiset operaattorit: Γ e : bool Γ f : bool Γ e f : bool Γ e : bool Γ f : bool Γ e f : bool (T-and) (T-or) 4
Kaikista yksinkertaisin tilanne on vakioilla: Γ c : num (T-const) Muuttujan kohdalla joudutaan tekemään hieman lisää työtä, sillä muuttujan tyyppi täytyy etsiä tyyppiympäristöstä. Tässä yhteydessä merkintätapa Γ, x : T tarkoittaa tyyppiympäristöä, jossa x:n tyyppi on T ja joka muuten on Γ 1 : Γ, x : T x : T (T-var) Lauseiden tapaukesssa tyypitysrelaatio on Γ s jos muuttujien tyypit ovat kuten Γ:ssa, niin s on hyvin tyypitetty lause. Γ, x : T e : T Γ, x : T x e Γ s Γ t Γ s ; t Γ e : bool Γ s Γ t Γ if e then s else t Γ e : bool Γ s Γ while e do s (T-ass) (T-seq) (T-if) (T-while) Tyyppijärjestelmän soveltaminen tapahtuu rakentamalla päättelypuita: x: num x : num x: num 1 : num x: num x : num x: num 0 : num x: num x 1 : num x: num x 0 : bool x: num x x 1 x: num while x 0 do x x 1 Jokaiseen puun vaakaviivaan liittyy jokin yllä annetuista säännöistä (esimerkiksi alimmassa on sovellettu T-whileä). Tehtävä 1. Merkitse näkyviin, mitä sääntöä kussakin vaakaviivassa on edellä sovellettu. Yllä kuvattu while-kielen tyyppijärjestelmä on ratkeava (engl. decidable), sillä jokaisessa säännössä viivan yläpuolella on pienempiä tehtäviä kuin viivan alapuolella; ja kielioppiohjautunut (engl. syntax-directed), sillä jokaiselle (abstraktin) kieliopin produktiolle on oma sääntönsä. 1. Eli Γ, x : T = Γ {(x, T ). 5
Nämä kaksi ominaisuutta tarkoittaa, että säännöstö on suhteellisen helppo kääntää tyyppitarkastusohjelmaksi. Kumpikin tyypitysrelaatio lausekkeiden Γ e : T ja lauseiden Γ s kääntyy omaksi aliohjelmakseen, jossa kukin ylläoleva sääntö tulee yhdeksi tapaukseksi. 2 Yksinkertaisesti tyypitetty lambda-kieli Aiemmassa monisteessa käsitelty lambda-kieli on tyyppiteorian arkkikieli: pääosa teoreettisista tyyppijärjestelmistä on rakennettu lambda-kielen päälle. Syy tähän lienee enimmälti historiallinen, mutta on myös niin, että lambda-kieli toimii varsin mainiosti eräänlaisena kaikkien ohjelmointikielten äitinä. Tyyppiteorian perusteoria on ns. yksinkertaisesti tyypitetty (engl. simply typed) lambda-kieli Λ, joka määritellään seuraavasti: x, y, z Var T, U Types T, U ::= T U t, u Λ t, u ::= x tu λx: T. t Vapaat muuttujat F V, korvausoperaattori [ := ] sekä α-ekivivalenssi ja β- sievennys määritellään samaan tapaan kuin tyypittömässä lambda-kielessä. Tyyppisäännöt ovat seuraavat: Γ, x : T x : T Γ t : U T Γ u : U Γ tu : T Γ, x : T t : U Γ (λx : T. t) : T U Huomaa, kuinka funktion parametrille on annettu tyyppi, ja huomaa, kuinka abstraktiotermin tyypityksesä parametrille annettua tyyppiä käytetään abstraktion rungon tyypityksessä laajentaen tyyppiympäristöä. 6
/ Types / enum type { T_NONE / signals type error /, T_BOOL, T_NUM ; / Expressions / enum exp_kind { E_CONST, / constants / E_VAR, / variables / E_NEG, / e / E_ADD, / e+f / E_SUB, / e f / E_MUL, / e f / E_EQ, / e = f / E_NQ, / e!= f / E_LT, / e < f / E_LE, / e >= f / E_GT, / e > f / E_GE, / e >= f / E_LAND, / e && f / E_LOR / e f / ; struct exp { enum exp_kind kind; union { char variable; / E_CONST / long constant; / E_VAR / struct exp unary_operand; / E_NEG / struct { struct exp left ; struct exp right; binary_operands; / the rest / u; ; / Statements / enum stmt_kind { S_ASSIGN, S_SEQ, S_IF, S_WHILE ; struct stmt { enum stmt_kind kind; union { struct { char x; struct exp e; assign ; struct { struct stmt left ; struct stmt right; seq; struct { struct exp test; struct stmt if_true; struct stmt if_false; if_; struct { struct exp test; struct stmt body; 7
/ Typecheck expressions; type error is returned as T_NONE / enum type tc_exp(struct type_env env, struct exp e) { switch (e >kind) { case E_CONST: return T_NUM; case E_VAR: { enum type ty = lookup_var(env, e >u.variable); if (ty == T_NONE) { fprintf ( stderr, "undefined variable %s \n", e >u.variable); return ty; case E_NEG: if (tc_exp(env, e >u.unary_operand)!= T_NUM) { fprintf ( stderr, "type error in negation\n"); return T_NONE; return T_NUM; case E_ADD: case E_SUB: case E_MUL: if (tc_exp(env, e >u.binary_operands.left)!= T_NUM tc_exp(env, e >u.binary_operands.right)!= T_NUM) { fprintf ( stderr, "type error in arithmetic\n"); return T_NONE; return T_NUM; case E_EQ: case E_NQ: case E_LT: case E_LE: case E_GT: case E_GE: if (tc_exp(env, e >u.binary_operands.left)!= T_NUM tc_exp(env, e >u.binary_operands.right)!= T_NUM) { fprintf ( stderr, "type error in comparison\n"); return T_NONE; return T_BOOL; case E_LAND: case E_LOR: if (tc_exp(env, e >u.binary_operands.left)!= T_BOOL tc_exp(env, e >u.binary_operands.right)!= T_BOOL) { fprintf ( stderr, "type error in comparison\n"); return T_NONE; return T_BOOL; / not reached / abort(); Listing 2: Tyyppitarkastusta C:llä, osa 2 8
/ Typecheck statements; returns nonzero if no type errors. / int tc_stmt(struct type_env env, struct stmt s) { switch (s >kind) { case S_ASSIGN: if (tc_exp(env, s >u.assign.e)!= lookup_var(env,s >u.assign.x)) { fprintf ( stderr, "type error in assignment\n"); return 0; return 1; case S_SEQ: return tc_stmt(env, s >u.seq.left) && tc_stmt(env, s >u.seq.right); case S_IF: if (tc_exp(env, s >u.if_.test)!= T_BOOL) { fprintf ( stderr, "a test must be boolean\n"); return 0; return tc_stmt(env, s >u.if_.if_true) && tc_stmt(env, s >u.if_.if_false); case S_WHILE: if (tc_exp(env, s >u.while_.test)!= T_BOOL) { fprintf ( stderr, "a test must be boolean\n"); return 0; return tc_stmt(env, s >u.while_.body); / not reached / abort(); Listing 3: Tyyppitarkastusta C:llä, osa 3 9
Tällä tavalla määritelty yksinkertaisten tyyppien lambda-kieli on sinänsä täysin typerä, sillä yhtään tyyppiä ei ole määritelty! Senpä takia toisin kuin tyypittömässä lambda-kielessä pitää aina määritellä joitakin perustyyppejä, esimerkiksi luvut: c Z x, y, z Var T, U Types T, U ::= T U Num t, u Λ Num t, u ::= x tu λx: T. t c t t + u t u t u Vapaat muuttujat F V, korvausoperaattori [ := ] sekä α-ekivivalenssi määritellään samaan tapaan kuin tyypittömässä lambda-kielessä. Laskentasääntö ( ) on β-sievennyksen yleistys ja sisältää myös aritmetiikan laskemisen. Tyyppisäännöt ovat seuraavat: Γ, x : T x : T Γ t : U T Γ u : U Γ tu : T Γ, x : T t : U Γ (λx : T. t) : T U Γ c : Num Γ t : Num Γ t : Num Γ t : Num Γ u : Num Γ t + u : Num Γ t : Num Γ u : Num Γ t u : Num 10
Γ t : Num Γ u : Num Γ t u : Num Tässä kielessä f(x) = x + 1 kirjoitettaisiin λx : Num. x + 1. Viitteet Luca Cardelli. Type systems. In Allen B. Tucker, editor, CRC Handbook of Computer Science and Engineering. CRC, 2 edition, 2004. Benjamin C. Pierce. Type and Programming Languages. MIT Press, Cambridge, MA, 2002. 11