TIES542 kevät 2009 Aliohjelmien formalisointia lambda-kieli Antti-Juhani Kaijanaho 3. helmikuuta 2009 Lambda-kielen taustalla on 1900-luvun alkuvuosikymmenien matematiikan perusteiden tutkimus ja siihen liittyvä logiikan kehittyminen. Noiden aikojen suuri kysymys oli, voidaanko koko matematiikka formalisoida mekaaniseksi laskentajärjestelmäksi niin, ettei siihen jää piileviä ristiriitoja 1 Alonzo Church oli yksi heistä, joka pyrki tämän ongelman ratkaisemaan. Hänen ratkaisuehdotelmaansa kehittivät eteenpäin myös hänen oppilaansa Stephen C. Kleene ja J. Barkley Rosser. Kleene ja Rosser osoittivat varsin pian, että Churchin järjestelmä on logiikaksi käyttökelvoton (ristiriitainen). Church oli kuitenkin jo oivaltanut, että se kykenee määrittelemään kaikki mekaanisesti laskettavissa olevat funktiot, ja vain ne. Käyttäen tätä ajatusta hyväkseen hän ratkaisi loogiikkoja monta kymmentä vuotta vaivanneen ratkeavuusongelman (ns. Entscheidungsproblem) Church osoitti, että ei ole mahdollista mekaanisesti selvittää, onko jokin mielivaltainen looginen väittämä tosi vai ei. 2 Churchin järjestelmä, kun siitä on siivottu ns. illatiiviset osat eli ne, jotka tarvitaan logiikkapuuhasteluun, on nimeltään lambda-kieli taikka lambda-laskento (engl. lambda calculus), ja se on yksi maailman vanhimmista ohjelmointikielistä kehitetty ennen ohjelmoitavia tietokoneita. Nykyaikana lambda-kieli on yksi teoreettisen ohjelmointikielitutkimuksen päätyökaluista. Esimerkiksi staattisten 1. Edelleen varsin tunnettu, yksi vanhimmista alkuaikojen loogisten järjestelmien piilevistä ristiriidoista on Bertrand Russellin paradoksi: kuuluuko se joukko itseensä, johon kuuluvat kaikki ne joukot, jotka eivät kuulu itseensä? 2. Saman ongelman ratkaisi suunnilleen samoihin aikoihin myös muuan Alan Turing, käyttäen nykyisin Turingin koneena tunnettua formalismia. Church ehti julkaista ensin, mutta Turing saa jostain syystä nykyisin suuremman osan kunniasta. Jopa tuo ongelmatyyppi tunnetaan nykyisin nimellä (Turingin koneen) pysähtymisongelma (engl. halting problem). 1
tyyppijärjestelmien teoria on kehittynyt pääosin lambda-kielen ympärille. Funktiokielet ovat puolestaan kaikki olennaisesti laajennettuja lambda-kieliä. Lambda-kielen keskeinen idea on mallittaa matemaattinen funktio (ja samalla myös aliohjelma) ymmärrettynä laskentaohjeena. Kielestä on siivottu pois häiritsemästä kaikki muu. Yllättäen kieli on tästä huolimatta erittäin ilmaisuvoimainen. Klassikkokirjoja lambda-kielestä ovat Church (1985), Curry and Feys (1968). Suhteellisen kattava teos lambda-kielestä on Barendregt (1984). 1 Syntaktiset määritelmät Lambda-kielen abstrakti syntaksi on seuraavanlainen: x, y, z Var t, u Λ t, u ::= x (muuttuja) tu (applikaatio) λxt (abstraktio) Joukon Λ alkioita sanotaan usein (lambda-)termeiksi. Abstraktion λxt intuitiivinen tulkinta on termi t muuttujan x funktiona. Vastaavasti applikaatio tu tarkoittaa intuitiivisesti funktion t arvo kohdassa u. Lambda-kielessä ei ole moniparametrisia funktioita, vaan niitä simuloidaan curry malla (λxλyt)u 1 u 2. Applikaatio tavanomaisesti assosioi vasemmalle ja abstraktio oikealle; applikaatiolla on suurempi presedenssi kuin abstraktiolla. Varsin usein käytetään lyhennysmerkintää λx 1 x 2 x n.t, joka tarkoittaa termiä λx 1 λx 2 λx n t. Huomaa, kuinka lyhennysmerkinnässä muuttujalistan jälkeen tulee piste. Esimerkki 1. 1. λxx on intuitiivisesti identiteettifunktio. Siitä käytetään toisinaan nimeä I. 2. λxy.x on funktio, joka palauttaa vakiofunktion. Siitä käytetään toisinaan nimeä K. 3. λxyz.xz(yz) on nimeltään S. Termit λx.xz ja λy.yz ovat intuitiivisesti samoja, mutta termit λx.xz ja λx.xy eivät ole. Syynä tähän on se, että λn jälkeen tulevan muuttujan nimellä ei ole mitään merkitystä tilanne on sama kuin esimerkiksi integraalissa 2 f(x) dx = 2 f(y) dy 1 1 2
Tätä eroa merkitsemään on luotu kaksi termiä vapaa muuttuja (engl. free variable) (kuten y termissä λxxy) ja sidottu muuttuja (engl. bound variable) (kuten x termissä λxxy). Vapaasta muuttujast sanotaan myös, että se esiintyy vapaana (engl. occurs free) termissä. Muuttuja esiintyy vapaana jossain termissä, jos se ylipäätään esiintyy kyseisessä termissä ja ainakin yksi sen esiintymistä ei ole sellaisen λn alla, jonka vieressä kyseinen muuttuja on. Muodollisesti määritellään (meta)funktio 3 F V : Λ P(Var) seuraavasti F V x = {x} F V tu = F V t F V u F V λxt = F V t \ {x} F V t on siis t:ssä esiintyvien vapaiden muuttujien joukko. Jatkossa tullaan tarvitsemaan myös korvausoperaattoria (engl. substitution operator), joka on (meta)funktio Var Λ Λ Lambda. Sen tulosta merkitään tässä monisteessa t[x := u], ja se korvaa kaikki x:n vapaat esiintymät t:ssä u:lla. Muita kirjallisuudessa esiintyviä merkintöjä ovat [x := u]t, t[u/x] ja S x u t. Korvausoperaattorin täsmällinen määritelmä on jokseenkin sotkuinen, koska sen pitää välttää ns. muuttujan kaappausta (engl. variable capture) kyse on olennaisesti paikallisen muuttujan staattisen sidonnan varmistamisesta: y[x := t] = { t y jos x = y jos x y (t 1 t 2 )[x := u] = t 1 [x := u] t 2 [x := u] (2) λyt jos x = y tai x F V t (λyt)[x := u] = λy(t[x := u]) jos x y ja x F V t ja y F V u (3) λz(t[y := z][x := u]) jos x y ja x F V t ja y F V u missä z on uusi muuttuja (engl. fresh variable) eli muuttuja, jota ei ole vielä käytetty mihinkään (oikeastaan riittää z x ja z F V u ). (1) 2 Denotationaalinen semantiikka Lambda-laskennon denotationaalinen semantiikka on erittäin yksinkertainen. Olkoon D sellainen hila, että D sisältää kaikki Scott-jatkuvat funktiot D D 3. Notaatio P(S) tarkoittaa S:n potenssijoukkoa eli sen osajoukkojen joukkoa. 3
(tällaisen joukon olemassaolo on todistettu). Nyt voidaan määritellä semanttinen funktio seuraavasti: E : Λ (Var D) D E x σ = σ(x) { E t σ(e u σ) E tu σ = E λxt σ = f jos E t σ D D muuten missä f(a) = E t (σ {(x, a)}) 3 Sievennykset Ns. α-muunnos (engl. α-conversion) sallii sidotun muuttujan nimen vaihtamisen. Se määritellään (pienaskelsemantiikan keinoin) seuraavasti: λxt α λy(t[x := y]) jos y F V t t α t tu α t u u α u tu α tu t α t λxt α λxt Tehtävä 1. Osoita, että ( α ) on ekvivalenssirelaatio. Jos t α u pätee, sanotaan, että t ja u ovat α-ekvivalentteja. Käytännössä niitä pidetään samana terminä toisin sanoen termejä käsitellään α-muunnoksen ekvivalenssiluokkina. Koko lambda-laskennon ydin sisältyy β-sievennykseen (engl. β-reduction), joka 4
määritellään seuraavasti: (λxt)u β t[x := u] t β t tu β t u u β u tu β tu t β t λxt β λxt Muotoa (λxt)u sanotaan β-redeksiksi (engl. β-redex). Jos termi ei sisällä yhtään β-redeksiä, sen sanotaan olevan normaalimuoto (engl. normal form). Jos t β u pätee 4 ja u on normaalimuoto, niin u:n sanotaan olevan t:n normaalimuoto. Teoreema 1 (Church ja Rosser). Jos t β u 1 ja t β u 2 pätevät, niin on olemassa α-ekvivalentit normaalimuodot t 1 ja t 2 siten, että u 1 β t 1 ja u 2 β t 2 pätevät. Todistus. Sivuutetaan. Tämä Church Rosser-teoreema sanoo siis, että jos lauseella on ylipäätään normaalimuoto, se on yksikäsitteinen (modulo α-ekvivalenssi). Jos t β u taikka u β t pätee, sanotaan t:n ja u:n olevan β-ekvivalentteja, merkitään t = β u. 4 Churchin koodaukset Yllättäen lambda-laskennosta on muodostunut ohjelmointikielten teorian keskeisimmistä kulmakivistä! Tämän taustalla on muun muassa havainto, että lambdalaskennossa voi tehdä kaikenlaista kivaa. Esimerkiksi voidaan asettaa seuraavat 4. Jos ( ) on relaatio, niin ( ) on sen refleksiivis-transitiivinen sulkeuma, eli t 0 t n tarkoittaa joko, että t 0 ja t n ovat samat tai että on olemassa t 1,..., t n 1 siten, että t i t i+1 pätee kaikilla i = 0,..., n 1. 5
lyhennysmerkinnät: zero = λfx.x succ = λnfx.f(nfx) add = λmnfx.mf(nfx) mult = λmnf.m(nf) Laskemalla muutamia esimerkkilaskuja voidaan huomata, että zero tarkoittaa nollaa, succ on funktio, joka lisää argumenttiinsa ykkösen, add on funktio, joka laskee kaksi argumenttiaan yhteen, ja mult on vastaava kertolaskufunktio. Yleisesti, jos meillä on mikä tahansa yhteen- ja kertolaskutehtävä, se voidaan koodata lambda-lausekkeeksi edellä esitettyjen määritelmien avulla, ja tämän lausekkeen normaalimuoto esittää laskutehtävän vastausta! Yleisesti tässä koodauksessa, jota kutsutaan Churchin numeraaleiksi, lukua n vastaa lambda-lauseke eli succ(succ(... (succ(zero)... ) }{{} n kpl λf.λx. f(f... (f x)... ). }{{} n kpl On myös mahdollista asettaa seuraavanlaiset määritelmät: true = λxy.x false = λxy.y if = λx.x Nämä todella käyttäytyvät nimensä mukaisesti totuusarvoina ja if-lausekkeena! Ehkä kuitenkin hätkähdyttävin havainto on, että rekursiota on mahdollista simuloida lambda-laskennossa ilman, että rekursiivisia määritelmiä varsinaisesti sallittaisiin! Ideana on etsiä ns. kiintopisteoperaattori F, jolle seuraava β-ekvivalenssi pätee: F g = β g(f g) Nimensä tällainen (hypoteettinen) kiintopisteoperaattori saa siitä, että ns. kiintopisteyhtälön x = β gx ratkaisuksi x:n suhteen kelpaa aina x = F g. Sattuu olemaan niin, että lambdalaskennossa kiintopisteoperaattoreita on ääretön määrä; yksi niistä on Y-kombinaattoriksi kutsuttu λf(λxf(xx))(λxf(xx)). 6
Rekursion simulointi onnistuu kiintopisteoperaattorin avulla seuraavasti: Funktio f määritellään rekursiivisesti antamalla yhtälö f = E jossa f esiintyy myös yhtälön oikealla puolella, siis lausekkeessa E, vapaana. Määritellään apufunktio f = λf.e. Nyt funktio F f, missä F on kiintopisteoperaattori, on funktio f eli edellä annetun rekursioyhtälön ratkaisu! Koska lambda-laskento kykenee esittämään aritmetiikan ja simuloimaan rekursiota, on se Turing-täydellinen. 5 Laskujärjestykset Edellä määritelty β-sievennysrelaatio on epädeterministinen. Vaikka Churchin Rosserin teoreema takaakin, että mahdollinen normaalimuoto on yksikäsitteinen, mikään ei takaa, että mielivaltaisessa järjestyksessä tehty sievennys päättyy. Esimerkki 2. Sievennetään argumentti ensin: (λxy) ((λxxxx)(λxxxx)) β (λxy) ((λxxxx)(λxxxx)(λxxxx)) β (λxy) ((λxxxx)(λxxxx)(λxxxx)(λxxxx)) Sievennetään uloin redeksi ensin: (λxy) ((λxxxx)(λxxxx)) β y Voidaan todistaa, että normaalijärjestys (engl. normal order) ( N ) löytää aina normaalimuodon, jos se on olemassa: (λxt)u N t[x := u] t N t tu N t u jos t ei ole abstraktio u N u tu N tu jos t on normaalimuoto t N t λxt N λxt 7
Normaalijärjestys siis valitsee uloimman redeksin, tai jos on useampia uloimpia redeksejä, jotka eivät ole toisiinsa nähden sisäkkäisiä, niistä vasemmanpuolisimman. Normaalijärjestys on siis turvallinen, mutta se on usein tehoton, koska se saattaa aiheuttaa argumentin kopioitumisen ja siten työmäärän tuplautumisen. Käytössä on myös applikatiivinen järjestys (engl. applicative order) ( A ), jolla ei ole vastaavaa tehottomuusongelmaa: (λxt)u A t[x := u] jos t ja u ovat normaalimuotoja t A t tu A t u u A u tu A tu jos t on normaalimuoto t A t λxt A λxt Applikatiivisen järjestyksen heikko (engl. weak) variantti, joka ei sievennä abstraktion sisällä, vastaa ohjelmointikielen aliohjelman argumentin arvovälitystä. Vastaavasti normaalijärjestyksen heikko muoto (joka myöskään ei sievennä abstraktion sisällä) vastaa nimivälitystä. Viitteet H. P. Barendregt. The Lambda Calculus. Its Syntax and Semantics. Number 103 in Studies in logic and the foundation of mathematics. Elsevier, Amsterdam, revised edition, 1984. Alonzo Church. The Calculi of Lambda Conversion. Princeton University Press, 1985. Haskell B. Curry and Robert Feys. Combinatory Logic, volume 1. North-Holland, 1968. 8