TÍPUSKIKÖVETKEZTETÉSI MÓDSZEREK THE METHODS OF TYPE INFERENCE Csörnyei Zoltán 1, Nagy Sára 2 ELTE Informatikai Kar, Programozási Nyelvek és Fordítóprogramok Tanszék 1 ELTE Informatikai Kar, Algoritmusok és Alkalmazásaik Tanszék 2 Összefoglaló Előadásunkban a típuselmélet egyik speciális területével, a típuskikövetkeztetéssel foglalkozunk. Nem minden programnyelvben kötelező a típusok megadása, vannak olyan nyelvek is, ahol egyáltalán nem kell a programozónak a típusokkal foglalkoznia. Viszont a típusok, a lehetséges típusok ismerete feltétlenül szükséges a program helyességének a vizsgálatához. A típuskikövetkeztetés feladata a típusok pontos meghatározása. Először a legegyszerűbb módszert, a Curry-típuskikövetkeztetést mutatjuk be, majd a polimorfikus let-utasításon tanulmányozzuk a Hindley-Milner-algoritmust. Megadunk környezetfüggő és környezetfüggetlen módszereket, és megmutatjuk a korlátozások generálásának és az egységesítésnek az alkalmazását. Foglalkozunk a rekurzióval is, megadjuk a mai modern programnyelvekben alkalmazott Hindley-Mycroft-típuskikövetkeztető eljárást is. Típuskikövetkeztetéssel, különböző részletességgel, az ELTE Doktori Iskolájában, a programtervező informatikus MSc-képzésben foglalkozunk, és egy rövid összefoglaló szerepel a programtervező matematikus szak tantervében is. Kulcsszavak típuselmélet, típuskikövetkeztetés, fordítóprogramok Abstract We will discuss a particular area of the type theory, the type inference. Prescribing the types is not always mandatory: There are languages in which not all of types have to be given. In some languages the programmer does not need to consider types at all. However, the knowledge of the possible types is essential for checking the correctness of softwares. Type inference describes the types exactly. First we present the simplest method, the Curry-inference method, then we study the Hindley- Milner algorithm on the polymorphic let-statement. We present context dependent and context free methods, and the applications of generating constrains and of unification. We will consider recursion, and will present the Hindley-Mycroft type inference that is used by modern programming languages. In Eotvos University, we teach type inference - in different depths in PhD courses and in an MSc course. The curriculum of BSc degree also includes a brief summary of type inference. Keywords type theory, type inference, compilers 1
Bevezetés A típus már a kezdetektől fogva a számítástudomány központi fogalma. A típusrendszerek vizsgálata az utóbbi évek fontos kutatási területévé vált, az eredmények sikeresen alkalmazhatók a programozási nyelvek tervezésében, nagyhatékonyságú fordítóprogramok implementációjában és a típus nem utolsósorban a biztonságos programok létrehozásának egyik eszköze. Közismert Robin Milnernek az a mondása, hogy egy jól típusozott program nem futhat rosszul. Ezt az állítást már sokan finomították, pontosabbá tették, precízebben megfogalmazták, de ez a kijelentés jól mutatja a típusnak, a típuselméletnek a programozási nyelvekben betöltött szerepét. 1. A Curry-típusrendszer Nem minden programnyelvben kötelező a típusok megadása, vannak olyan nyelvek, ahol nem kell minden típust megadni, és vannak, ahol egyáltalán nem kell a programozónak a típusokkal foglalkoznia. A típusok, a lehetséges típusok ismerete viszont feltétlenül szükséges a program helyességének a vizsgálatához, a típuskikövetkeztetés feladata a típusok pontos meghatározása. A Curry-típusos λ-kalkulust röviden Curry-típusrendszernek nevezzük. A Currytípusrendszer típusai nem alaptípusokból, hanem típusváltozókból épülnek fel. A típusváltozók konkrét típusértéket majd a típuskikövetkeztetés folyamán kapnak. A Curry-típusrendszer szintaxisa: <típus>::= <típuváltozó> ( <típus> <típus>) <λ kifejezés >::= <változó> ( λ<változó>.<λ kifejezés > ) ( <λ kifejezés > <λ kifejezés >) A kifejezések típusát mindig egy <váltózó>:<típus> párokból álló típuskörnyezetből kiindulva a Curry-típusrendszer szabályainak felhasználásával bizonyítjuk. A kifejezések szabad változóinak típusa a típuskörnyezetből olvasható ki. A környezet üres is lehet. A Curry-típusrendszerben is háromfajta következtetés van, az első kettő a szintaktikus helyességgel foglalkozik, a harmadik egy jól formált kifejezés típusát adja meg: Ha -ból E:τ, akkor -ban az E kifejezés típusaτ. Azt mondjuk, hogy egy E:τ következtetés érvényes a típuskörnyezetben, ha bizonyítható a Curry-típusrendszer szabályaival. A típus meghatározásával kapcsolatban három kérdés merül fel, ezekben a típuskörnyezet mindig üres: 1. típuskikövetkeztetés: ha adott az E kifejezés, van-e olyan A típus, melyre E:A, 2. típusellenőrzés: ha adott az E kifejezés és az A típus, akkor teljesül-e az, hogy E:A, 3. típus-reprezentáció: ha adott az A típus, van-e olyan E kifejezés, melyre E:A. 2
2. A Curry-típuskikövetkeztetés A fentiek szerint legyen E a Curry-típusrendszer egy kifejezése, és azt vizsgáljuk, hogy van-e olyan A típus, hogy E:A. Típusváltozók szimultán helyettesítését az [α 1 :=A 1,, α n := A n ] leírással adjuk meg, és ha s egy típusváltozó helyettesítés, akkor az A típus s helyettesítését As alakban írjuk le. Azt mondjuk, hogy az A típus a B típus variánsa, ha van olyan s 1 és s 2 helyettesítések, hogy As 1 B és Bs 2 A. Ha pedig As Bs, akkor s az A és B egyesítő helyettesítése. Az A típus általánosabb típus a B típusnál, ha létezik olyan s helyettesítés, hogy B As. Ezt a tulajdonságot B A val jelöljük. Ha üres típuskörnyezetből következik, hogy E:A és A a legáltalánosabb típus, akkor A-t principális típusnak nevezzük. A feladat tehát az, hogy meghatározzuk, kikövetkeztessük az E kifejezés principális típusát. A típuskikövetkeztetés két műveletre, a korlátozások generálására és az egyesítésre bontható fel. A korlátozások típusok közötti egyenlőségek. A korlátozások generálása azt jelenti, hogy a típusrendszer levezetési szabályainak felhasználásával az E kifejezésre és részkifejezéseire nem adunk meg konkrét típusokat, hanem új típusváltozók alkalmazásával egyenleteket generálunk. A korlátozások egyenletrendszerét az egyesítés lépése oldja meg. Az egyesítés a generált típusváltozókhoz konkrét típuskifejezéseket rendel, az E-hez tartozó típusváltozóhoz rendelt típuskifejezés lesz majd az E típusa. A korlátozások generálását a T(,E,A ) leképezéssel végezzük, ahol egy típuskörnyezet, E egy kifejezés és A egy típus. A T(,E,A ) leképezést az E szerkezete szerint adjuk meg: 1. T(,x,A ) {A = (x)} 2. T(, x.e,a ) T({,x: },E, ) {A = },ahol és új típusváltozók, 3. T(,EF,A ) T(,E, A ) T(,F, ),ahol új típusváltozó. Az egyesítés feladat a korlátozások egyenletrendszerének olyan megoldása, ami a korlátozásokat adó kifejezés principális típusa lesz. Ha egy E kifejezés korlátozásait a fenti leképezéssel határozzuk meg és eredményül egy egyenletrendszert kapunk, akkor az egyesítés eljárással meghatározzuk az egyenletrendszer legáltalánosabb megoldását, és ebből már meg tudjuk határozni a principális típust. 3. A let-kifejezés bevezetése 3.1. Az egyszerű let-kifejezés és típuskikövetkeztetése A Curry-típusos λ-kalkulust egy új kifejezéssel bővítjük, a let-kifejezéssel. Ezt az indokolja, hogy a let-kifejezés a legtöbb funkcionális programnyelvben benne van, és kezelése az eddig megismert módszerekkel nem valósítható meg. A let-kifejezés szintaktikája: let <változó> = <λ kifejezés 1 > in <λ kifejezés 2 > 3
Először csak olyan let-kifejezéssel foglalkozunk, amelyekben a <változó> nem szerepel a <λ kifejezés 1 > - ben, azaz nincs rekurzió, és feltesszük, hogy a <változó> típusa monomorf. A let-kifejezés operációs szemantikája: let x = E in F F[x := E], és a let-kifejezés típusszabálya: Ha Γ-ból E:A és Γ,x:A-ból F:B, akkor Γ-ból let x=e inf :B. Látható, hogy a λ absztrakcióhoz hasonlóan, a típuskörnyezet x:a eleme beépül a letkifejezésbe. Ennek a let-kifejezésnek a korlátozás-generálása nagyon egyszerű. Legyen α egy új típusváltozó, és tegyük fel, hogy a let-kifejezés típusa A, ekkor a let-kifejezéshez a következő két korlátozás-generálás adható meg: T( Γ, E,α ) és T( {Γ, x:a }, F, A ), ezeknek a megoldása pedig az előző pontban már szerepelt. A korlátozások generálása után a principális típus most is az egységesítés műveletével határozható meg. 3.2. Probléma a let-tel, a polimorfikus let-kifejezés bevezetése A fenti let-kifejezés nagyon jónak tűnik, de sajnos már olyan egyszerű kifejezésre sem alkalmazható, mint például a következő: let f = λx.x in pair (f 0) (f True). A λx.x típusa α α, így az (f 0) típusa Nat Nat, de az (f True) típusa Bool Bool. Az f monomorf típusa miatt ez nem lehetséges. Ezért itt (de csak itt) meg kell engedni a változó polimorf típusát. Ez a let-polimorfizmus, és ez először az ML programnyelvben jelent meg. Ha az f polimorf, akkor a pair (f 0) (f True) kifejezés típusozható, és típusa Pair_Nat_Bool. Ehhez azonban a típus fogalmát is módosítani kell. Az eddigi típust egyszerű típusnak nevezzük, és bevezetjük a típusséma, azaz a polimorfikus típus fogalmát: <típusséma> := <egyszerű típus> <típusváltozó>. <típusséma> A típussémát a σ jellel jelöljük Látható, hogy a kvantor csak a típussémák baloldalán szerepelhet. A kvantor egy kötést valósít meg, a <típusváltozó> kötött lesz az utána következő <típussémá>-ban. Ezt a kötést a típusváltozó helyettesítésekor is figyelembe kell venni. Megadhatjuk egy típusséma gen(γ,σ) általánosítását úgy, hogy univerzális kvantorokkal kötjük a szabad típusváltozóit, és beszélhetünk egy típusséma inst(σ) példányosításáról: ahol a kvantorokkal kötött típusváltozókat egyedi új típusváltozókkal helyettesítjük. Ezeknek a fogalmaknak a bevezetésével már megadhatjuk a polimorfikus let-kifejés típuskikövetkeztetését. 4
4. A Hindley-Milner-típuskikövetkeztetés A típuskikövetkeztetés módszerét egymástól függetlenül 1969-ben Hindley, majd 1978- ban Milner publikálta, innen származik Hindley-Milner típuskikövetkeztetés elnevezés. Milner volt az első, aki Robinson egységesítését is alkalmazta, beépítve a típuskikövetkeztést az ML fordítóprogramjának elemző algoritmusai közé. Curry is foglalkozott a típuskikövetkeztetéssel, ezért ezt a módszert gyakran Curry--Hindley típuskikövetkeztetésnek is nevezik. A principális típus tétel bizonyítását Damas adta meg, ezért sokan ennek a módszernek a Damas-Milner típuskikövetkeztetés nevet adták. 4.1. A típusrendszer szabályai Csak a három legfontosabb típusszabályt adjuk meg: Ha Γ-ból E:σ, akkor Γ-ból E:gen(Γ,σ), ha Γ-ból E:σ, akkor Γ-ból E:inst(σ), ha Γ-ból E:σ és Γ,x:σ-ból F: σ, akkor Γ-ból let x = E in F :σ. Látható, hogy a típusséma előfordulhat a kifejezések típusában is, nem csak a típuskörnyezetekben. Ez nehézkessé teszi a típuskikövetkeztetést, ráadásul a kifejezés szintaktikájából egyáltalán nem látszik, hogy mikor kell az általánosítás vagy a példányosítás szabályát alkalmazni. Ezért módosítsuk a let-kifejezés típusszabályát úgy, hogy ez a kifejezés írja bele a típuskörnyezetbe a polimorfikus típust, és hogy a polimorfikus típus ne szerepeljen kifejezések típusában, csak a típuskörnyezetben. ha Γ-ból E:σ és {Γ,x: gen(γ,σ)}-ból F: σ, akkor Γ-ból let x = E in F :σ. A típuskörnyezetből egy változó típusát kiolvashatjuk, de ekkor mindig a példányosítás műveletét kell alkalmazni. 4.2. A W-algoritmus Milner két algoritmust is adott a típuskikövetkeztetésre. Először a W-algoritmussal foglalkozunk. Az algoritmust mintákkal, a kifejezés szerkezete szerint adjuk meg. Az algoritmus egy típuskörnyezetre és a kifejezésre egy típusváltozó helyettesítését határozza meg, vagy hibajelzéssel megáll. Az algoritmust nem részletezzük, csupán két mintára vonatkozó lépését adjuk meg: W(Γ,x) = (Id, inst(σ)), ahol {x:σ} a Γ-ban van. W(Γ,let x=e inf) = (s 1, s 2, τ ), ahol W(Γ,E) = (s 1,τ ) és W({Γs 1,x:gen(Γs 1,τ },F) = (s 2,τ ) A megadott két lépésből is látható, hogy az univerzális kvantoros típusséma a let-kifejezés feldolgozásánál kerül bele a típuskörnyezetbe, és egy változó típusának a típuskörnyezetből való meghatározásánál a példányosítás műveletét alkalmazzuk. Ezek biztosítják azt, hogy például a fenti 5
f = λx.x in pair (f 0) (f True) kifejezésben az f típusa α.α α lesz, és az f 0 kifejezésben az f típusának meghatározásakor a példányosítás művelete az α-hoz a Nat, az f True kifejezésben pedig a Bool típust rendeli hozzá A W-algoritmus helyes, azaz ha W(Γ,E) = (s,τ), akkor Γs-ből valóban az következik, hogy E:τ, és az algoritmus teljes, vagyis a legáltalánosabb típust határozza meg. 4.3. A J-algoritmus A J-algoritmust is Milner definiálta 1978-ban, mint a W-algoritmus egy egyszerűbb formáját, ugyanabban a cikkben, amelyben a W-algoritmus is először szerepel. A J- algoritmust a Haskell programnyelv implementációjában használták. A típuskikövetkeztetés is használ egy s helyettesítést, mint egy globális változót, a típuskikövetkeztetés indításakor s = Id. A helyettesítést azonban csak az egységesítés argumentumainak megadásában használjuk, és ellentétben a W-algoritmussal, a helyettesítés a principális típus végső meghatározásában már nem szerepel. Most is csak két mintára vonatkozó lépést adunk meg: J(Γ,x) = inst(σ), ahol {x:σ} a Γ-ban van, J(Γ,let x=e inf) = τ, ahol J(Γ,E) = τ és W({Γ,x:gen(Γs_1,τ s},f) = τ. Az algoritmus közvetlenül a kifejezés típusát adja meg, és bizonyítható, hogy az algoritmus helyes és teljes. 4.4. Az M-algoritmus Mind a W-, mind a J-algoritmus az EF applikációra úgy működik, hogy először végigszámolta az E-t, majd végigszámolja az F-t, és csak ezután ellenőrzi, hogy az E típusa függvény-e, vagyis az E típus α α alakú-e. Ezért ezeket a típuskikövetkeztetéseket környezetfüggetlen típuskikövetkeztetéseknek nevezzük, hiszen a kifejezések típusát anélkül határozzák meg, hogy megvizsgálnák, azok milyen környezetben vannak. Ezeket az algoritmusokat alulról-felfelé haladó algoritmusoknak is tekinthetjük, hiszen például ha az EF applikációt gráffal ábrázoljuk, az algoritmusok először az E-t, a bal oldali részfát járják be, utána foglalkoznak a jobb oldali F részfájával, és csak ezután lépnek egy szinttel feljebb az applikáció műveletéhez, és csak most vizsgálják meg az applikáció lehetőségét. Ez a működés a hiba felfedezésére is erősen kihat, hiszen ha az applikáció nem lehetséges, ezt a kikövetkeztető algoritmus csak sok munka után, az E és F teljes feldolgozása után fedezi fel. A típuskikövetkeztető rendszer sokkal hamarabb észrevenné a hibát, ha egy EF applikáció esetén az E feldolgozása után azonnal ellenőrizné, hogy az E típusa vajon függvény-e. Ez a gondolat az alapja a típuskikövetkeztetés M-algoritmusának, amit Lee és Yi dolgozott ki 1998-ban. 6
Az M-algoritmus a környezetből meghatározza a várható típust, ezt a típust továbbítja a kifejezés felé, és hibát jelez, ha a kifejezés típusa nem felel meg a várt típusnak. Az algoritmust ezért környezetfüggő típuskikövetkeztetésnek nevezzük. A módszer lényegében egy felülről-lefelé haladó típuskikövetkeztetés. Az EF applikáció gráfját tekintve az algoritmus először meghatározza az applikáció elemeinek várható típusát, és csak ezután járja be az E bal oldali részfát. Csak akkor folytatja a típuskikövetkeztetést a jobb oldali részfával, azaz az F kifejezéssel, ha az E típusa megfelel a várt típusnak. 5. A rekurzió bevezetése és a Milner-Mycroft típuskikövetkeztetés A rekurzív kifejezések típusának leírására a fenti rendszerek nem alkalmasak, ezért bővítsük a rendszerünket a rekurziót leíró fix-kifejezéssel. A fix-kifejezés szintaktikája: fix <változó>. <λ kifejezés>, és operációs szemantikája fix x. E E [x:= fix x. E ]. A fix-kifejezés klasszikus monomorf típusszabályát Mycroft módosította polimorfikus típusokra: ha Γ,x:σ-ból E: σ, akkor Γ-ból fix x. E:σ. A típusok tehát típussémák, és ez biztosítja a polimorfikus típusos rekurzív függvények leírását. Ezt a típusrendszert nevezzük Milner-Mycroft típusrendszernek. A let-kifejezéshez hasonlóan, a fix-kifejezés típusszabálya is módosítható úgy, hogy a kvantoros típussémák csak a típuskörnyezetben szerepeljenek. Ez a típuskikövetkeztetés szintaxisvezérelt végrehajtását teszi lehetővé. A Milner-Mycroft típusrendszer megtartja a Hindley-Milner típusrendszer legtöbb tulajdonságát, például itt is teljesül a tárgy-redukció tétele, és itt is igaz, hogy a típusosan helyes kifejezések nem okoznak run-time típus-hibát. Azonban, míg a Curry- és a Hindley- Milner típusrendszer típus-kikövetkeztetése eldönthető és a probléma általában polinomiális, néhány speciális esetben exponenciális időbonyolultságú, addig a Milner-Mycroft típuskikövetkeztetés nem eldönthető, azaz vannak olyan problémák, amelyekre a kikövetkeztetés nem terminál. Irodalomjegyzék [1] L. Cardelli: Type Systems, Handbook of Computer Science and Engineering, CRC Press, 1997. [2] Z. Csörnyei: Lambda-kalkulus, Typotex, Budapest, 2007. [3] Z. Csörnyei: Típuselmélet (kézirat), 2008. [4] B. C. Pierce: Types and Programming Languages, The MIT Press, Cambridge, 2002. [5] B. C. Pierce (editor):advanced Topics in Types and Programming Languages, The Mit Press, 2005. 7