Luku 2 Ohjelmointi laskentana Funktio-ohjelmoinnin, olio-ohjelmoinnin ja käskyohjelmoinnin ero on löydettävissä niiden pohjalla olevista laskennan mallista. Automaattisen tietojenkäsittelyn yksi historiallinen lähtökohta on viime vuosisadan alkupuolen metamatemaatikkojen ja loogikkojen kysymys: voidaanko looginen ajattelu mekanisoida? Vastatakseen tähän kysymykseen Alonzo Church ja Alan Turing kehittivät toisistaan tietämättä mekaanisen laskennan mallit. Turingin malli on kaikille tuttu Turingin kone, Churchin malli tunnetaan nimellä λ-laskenta. Pian osoitettiin, että nämä ovat siinä suhteessa ekvivalentteja, että ne pystyvät ratkaisemaan täsmälleen samat ongelmat. Turingin konetta (tai oikeastaan sen sivistynyttä versiota, hajasaantikonetta) voidaan pitää von Neumannin koneen idealisaationa. Siinä mielessä se on käskyohjelmoinnin pohjalla oleva laskennan malli: tietoa käsitellään sana kerrallaan. Olio-ohjelmoinnin laskennan malli perustuu ajatukseen yhteistyössä toimivista olioista, jotka kommunikoivat keskenään. Funktio-ohjelmoinnin pohjalla oleva laskennan malli on laskento eli kalkyyli. Kynällä ja paperilla pyöritetään kaavoja: yritetään ennalta annettujen sääntöjen mukaisesti muuttaa kaava toiseen, yhtäpitävään muotoon. Perinteisesti näinä ennalta annettuina sääntöinä on funktio-ohjelmoinnissa pidetty λ-laskennan sääntöjä. Aloitamme kuitenkin yksinkertaisemmin. 2.1 Laskento Palataanpa hetkeksi peruskoulun (tai mitä koulua ikinä kävittekään, minä kävin Steinerin:) penkille ja mietitään laskentoa. 7
8 LUKU 2. OHJELMOINTI LASKENTANA Laskento on pohjimmiltaan symbolijonojen käsittelyä. Kun tehtäväksi annetaan laskea yhteen neljäkymmentäkaksi ja kaksikymmentäneljä, kirjoitetaan paperille symbolijono 42 + 24. Tämän symbolijonon voi kirjoittaa myös muotoon (4, 2) + (2, 4) näin ei tosin koulussa koskaan kirjoitettu, mutta asiallisesti ottaen niin sitä siellä ajateltiin. Nyt sitten eräs laskennon sääntö sanoo, että tuo voidaan kirjoittaa muotoon (4 + 2, 2 + 4), ja eräs toinen sääntö ( yhteenlaskutaulu ) taas sallii tuon kirjoittamisen muotoon (6, 6), joka on totuttu kirjoittamaan muotoon 66. Palataanpa takaisin yliopistoon. Täällä yllä oleva saatettaisiin, jos laskentoa täällä opetettaisiin, kirjoittaa muotoon 42 + 24 = (4, 2) + (2, 4) = (4 + 2, 2 + 4) = (6, 6) = 66 Yhtäsuuruusmerkki ( = ) ilmaisee, että vasen puoli saadaan sallitulla säännöllä oikeanpuolisesta. Mitkä nämä laskennon säännöt sitten ovat? Lähdetään ajatuksesta, että käytössämme on kaksi numeromerkkiä (siis operoimme binäärijärjestelmässä): 0 ja 1. Niiden tavanomaista merkitystä emme ajattele! paitsi silloin, kun valitsemme sääntöjä. Ne ovat vain merkkejä. Oletetaan, että numeromerkkien kirjoittaminen peräkkäin on vain lyhennysmerkintä sille, että ne erotetaan toisistaan pilkuilla. Oletetaan lisäksi, että pilkku assosioi vasemmalle: toisin sanoen 1, 0, 1 tarkoittaa (1, 0), 1. Nämä oletukset sanottuamme voimme asettaa seuraavat määritelmät: x = 0x (2.1) 0 + 0 = 0 (2.2) 0 + 1 = 1 (2.3) 1 + 0 = 1 (2.4) 1 + 1 = 10 (2.5) (x, 0) + (y, 0) = (x + y), 0 (2.6) (x, 0) + (y, 1) = (x + y), 1 (2.7) (x, 1) + (y, 0) = (x + y), 1 (2.8) (x, 1) + (y, 1) = (x + (y + 1)), 0 (2.9)
2.2. FUNKTIOT 9 Nyt voimmekin laskea 42 + 24 sääntöjen mukaan: 101010 + 011000 =(10101), 0 + (01100), 0 sääntö 2.6 =((1010), 1 + (0110), 0), 0 sääntö 2.8 =((101), 0 + (011), 0), 1, 0 sääntö 2.6 =((10), 1 + 01, 1), 0, 1, 0 sääntö 2.9 =(10 + (01 + 1)), 0, 0, 1, 0 sääntö 2.1 =(10 + (0, 1 + 0, 1)), 0, 0, 1, 0 sääntö 2.9 =(10 + (0 + (0 + 1)), 0), 0, 0, 1, 0 sääntö 2.3 =(10 + (0 + 1), 0), 0, 0, 1, 0 sääntö 2.3 =(1, 0 + 1, 0), 0, 0, 1, 0 sääntö 2.6 =(1 + 1), 0, 0, 0, 1, 0 sääntö 2.5 =1, 0, 0, 0, 0, 1, 0 =1000010 Näin tuli siis määriteltyä binääriluvuille yhteenlasku. Tämä tapahtui antamalla joukko yhtälöitä, joiden määrätään olevan voimassa. Voisimme määritellä muutakin 1 : x 0 = 0 x y = (x (y 1)) + x = + 1 Tästä lähtien käytämme normaaleja kymmenjärjestelmän lukuja; edellä käytiin binäärijärjestelmässä lähinnä määritelmien helpottamisen vuoksi. Samoin jätämme kirjoittamatta perusaritmetiikan välivaiheita. 2.2 Funktiot Voidaan myös määritellä funktioita: nelio x = x x Tässä nelio on funktion nimi. Kyseisen funktion ominaisuuden, että se ottaa yhden lukuparametrin ja palauttaa lukuarvon, kirjoitamme seuraavasti: nelio :: 1. Oletamme tässä, että vähennyslasku on määritelty erikseen.
10 LUKU 2. OHJELMOINTI LASKENTANA Tässä vaiheessa on syytä asettaa rajoitus, että funktion nimen perään tulee laskuissa aina kirjoittaa argumentti. Voimme laskea näillä määritelmillä yksinkertaisia laskuja: nelio (2 + 3) = nelio 5 = 5 5 = 25 Kannattaa huomata käytäntö, jota yllä käytimme: toisin kun matematiikassa yleensä, emme ympäröi funktion argumentteja suluilla, jos argumenttina on pelkkä luku tai muuttuja. Itse asiassa yhteenlaskukin voidaan ymmärtää funktiona: lisaa :: lisaa(x, y) = x + y Tavallisemmin tämä määritellään kuitenkin seuraavalla tyylillä, jota kutsutaan curryamiseksi Haskell B. Curryn mukaan mutta jonka oikeastaan keksi Moses Schönfinkel: lisaa :: lisaa x y = x + y Näitä myös käytetään eri tavoin: lisaa(1, 2) = 3 mutta lisaa 1 2 = 3. Itse asiassa operaattorit ylipäätään ovat myös funktioita. Haskellia seuraten otamme käyttöön seuraavat käytännöt. Jos operaattori kirjoitetaan yksin sulkeiden sisään, niin silloin operaattori muuttuu tavalliseksi funktioksi: 2+3 = 5 mutta (+) 2 3 = 5. Voidaankin kirjoittaa (+) :: ( ) :: Vastaavasti mikä tahansa kaksi argumenttia ottava funktio voidaan kirjoittaa operaattoriksi laittamalla sen nimi takahipsuihin: lisaa 2 3 = 5 ja 2 lisaa 3 = 5. 2.3 λ-laskento Tarkastellaanpa nyt hieman oudompaa laskentoa, nimittäin λ-laskentoa.
2.3. λ-laskento 11 λ-lausekkeiden syntaksi voidaan esittää vaikkapa seuraavasti: Λ ::= µ (muuttuja) Λ 1 Λ 2 (applikaatio) λµ.λ (abstraktio) µ ::= mikä tahansa kirjain Toisin sanoen λ-lausekkeita ovat 1. kaikki kirjaimet (muuttujat), 2. kaikki kaksi λ-lauseketta peräkkäin kirjoittamalla saadut merkkijonot (applikaatiot), ja 3. kaikki merkkijonot, jotka ovat muotoa λx.e, missä x on jokin kirjain (muuttuja) ja E on jokin λ-lauseke (abstraktiot). Sulkuja käytetään tavanomaiseen tapaan ilmaisemaan lausekkeen rakennetta. Applikaatio ryhmitetään sulkujen uupuessa vasemmalle, joten abc tarkoittaa (ab)c eikä a(bc). Abstraktio ulottuu oikealle niin pitkälle kuin mahdollista, joten λx.abc tarkoittaa λx.(abc) eikä (λx.a)bc. Esimerkki 1 Seuraavat ovat λ-lausekkeita: 1. e (muuttuja) 2. fe (applikaatio, jonka osat ovat muuttujia) 3. feh (applikaatio, jonka osat ovat toinen applikaatio ja muuttuja, ja sen toisen applikaation osat ovat muuttujia) 4. λx.x (abstraktio, jonka alilausekkeena on muuttuja) 5. λx.xy (abstraktio, jonka alilausekkeena on applikaatio) 6. (λx.x)y (applikaatio, jonka vasemmanpuoleisena osana on abstraktio) Seuraavat eivät ole λ-lausekkeita: 1. λ 2. λx 3. λx. 4. λ.x 5. xyλ Abstraktiolausekkeen λx.e sanotaan sitovan muuttujan x. Tällöin kaikki E:n sisällä olevat x:n esiintymät ovat sidottuja. Jokainen sellainen muuttujan esiintymä, joka ei ole sidottu, on vapaa. Jos kaavassa on yksikin muuttujan x vapaa esiintymä, x:n sanotaan esiintyvän ko. kaavassa vapaana ja olevan ko. kaavan vapaa muuttuja.
12 LUKU 2. OHJELMOINTI LASKENTANA Esimerkki 2 Tutkitaan vapaita ja sidottuja muuttujia. 1. Lausekkeen x ainoa vapaa muuttuja on x. Siinä ei ole sidottuja muuttujia. 2. Lausekkeen xy vapaat muuttujat ovat x ja y. Siinä ei ole sidottuja muuttujia. 3. Lausekkeessa λx.x ei ole vapaita muuttujia. Muuttuja x esiintyy siinä sidottuna. 4. Lausekkeen λx.y ainoa vapaa muuttuja on y. Muuttuja x esiintyy siinä sidottuna. 5. Lausekkeen xλx.x ainoa vapaa muuttuja on x. Se myös esiintyy siinä sidottuna. Merkinnällä E[x/F] (luetaan E, jossa x korvataan F:llä ) tarkoitetaan lauseketta, joka on muuten sama kuin lauseke E mutta siinä olevat kaikki vapaat x:n esiintymät korvataan (F):llä. Esimerkki 3 Joitakin korvauksia: 1. Korvaus x[x/y] tarkoittaa lauseketta y. 2. Korvaus z[x/y] tarkoittaa lauseketta z. 3. Korvaus (λx.x)[x/y] tarkoittaa lauseketta λx.x. 4. Korvaus (xλx.x)[x/y] tarkoittaa lauseketta yλx.x. 5. Korvaus (xλx.x)[x/xy] tarkoittaa lauseketta xyλx.x. 6. Korvaus (xλx.x)[x/(λx.x)] tarkoittaa lauseketta (λx.x)(λx.x). Abstraktion sitoma muuttuja voidaan vaihtaa lausekkeen merkityksen muuttumatta. Tätä sanotaan α-muunnokseksi. Formaalisti sanoen: λx.e = λy.e[x/y]. Tässä on tärkeä poikkeus: muunnosta ei saa tehdä, jos abstraktion sitoma muuttuja muuttuu jossakin osalausekkeessa vapaasta sidotuksi (tämä on helpointa välttää siten, että valitsee sidotun muuttujan uudeksi nimeksi sellaisen, joka ei lainkaan esiinny koko lausekkeessa). Itse λ-laskennan merkittävin muunnostyyppi eli laskusääntö on β-muunnos, joka saadaan tehdä β-redexille eli applikaatiolausekkeelle, jonka vasen osa on abstraktio: (λx.e)f = E[x/F] Tässä on kuitenkin tärkeä poikkeus: Jos jokin F:n vapaa muuttuja on abstraktiossa sidottu, tätä β-muunnosta ei saa tehdä. Tämä rajoitus voidaan kiertää helposti α-muunnoksella. Beeta-muunnoksen oikeaa suuntaa sanotaan β- reduktioksi ja vasempaa suuntaa β-abstraktioksi.
2.4. NORMAALIMUOTO, POHJA JA LASKENTAJÄRJESTYS 13 Toisinaan voi olla syytä kirjoittaa α- tai β-kirjain yhtäsuuruusmerkin yläpuolelle kertomaan, mitä muunnosta käytetään. Esimerkki 4 1. (λx.x)y β = y 2. (λx.λy.xy)y α = (λx.λz.xz)y β = λz.yz. (Jos kiellosta huolimatta olisi tehty β-muunnos saman tien, olisi tulokseksi tullut λy.yy.) Edellä esitetty λ-laskento on ns. puhdas λ-laskento. Käytännössä λ-laskennossa käytetään myös δ-muunnosta. Siihen liittyen lausekkeiden määritelmään lisätään aritmeettiset ynnä muut sellaiset lausekkeet. Delta-reduktio sanoo, että mikä tahansa δ-redex eli sellainen lauseke, joka sopii jonkin annetun määritelmäyhtälön (kuten ne, jotka yllä aiemmin annettiin) vasemmaksi puoleksi, voidaan korvata ko. yhtälön oikealla puolella. Jos useampi yhtälö sopii, niistä valitaan se, jonka vasen puoli on spesifein. Delta-abstraktio on sama prosessi toiseen suuntaan; δ-muunnoksella tarkoitetaan näitä molempia. Delta-muunnos siis käytännössä mahdollistaa perinteisen aritmeettisen laskennan: Esimerkki 5 1. (λx.x + 1) 3 β = 3 + 1 δ = 4. 2. (λx.λy.x + y) 3 4 β = (λy.3 + y) 4 β = 3 + 4 δ = 7. Tässä vaiheessa ehkä huomaakin jo, mistä λ-laskennossa on kyse: se on funktiolaskentaa puhtaimmillaan. Applikaatiolauseke edustaa funktion käyttöä (funktion kutsua ), kun taas abstraktio luo nimettömän funktion. Itse asiassa kaikki aiemmin asetetut määritelmät voitaisiin kirjoittaa uusiksi, jos niin haluttaisiin: voidaan kirjoittaa myös nelio x = x x nelio = λx.x x 2.4 Normaalimuoto, pohja ja laskentajärjestys Lausekkeen normaalimuodolla tarkoitetaan sellaista muotoa, johon ei voi soveltaa β-reduktiota eikä δ-reduktiota. Kun laskennossa pyritään saamaan pelkkä luku vastaukseksi, λ-laskennossa pyritään löytämään normaalimuoto. Valitettavasti kaikilla lausekkeilla ei ole normaalimuotoa.
14 LUKU 2. OHJELMOINTI LASKENTANA Esimerkki 6 Lausekkeella (λx.xxx)(λx.xxx) ei ole normaalimuotoa, sillä (λx.xxx)(λx.xxx) β = (λx.xxx)(λx.xxx)(λx.xxx). Otetaan käyttöön uusi lauseketyyppi, (luetaan: pohja). Merkitään sillä sellaisen laskun tulosta, joka ei koskaan päädy normaalimuotoon. Toisin sanoen edustaa päättymätöntä laskua. Sitä käytetään myös merkitsemään virheellisen laskun, esimerkiksi nollalla jakamisen, tulosta. Funktiot on tapana jakaa kahteen luokkaan sen mukaan, miten ne käyttäytyvät pohjan suhteen. Jos f = pätee, f on tiukka (strict) funktio. Jos f pätee, f on väljä (non-strict) funktio. Funktion väljyyden tai tiukkuuden usein määrää se, missä järjestyksessä laskut tehdään. Esimerkiksi lausekkeella (λx.y)((λx.xxx)(λx.xxx)) on normaalimuoto y, mutta se, että saavutetaanko se, riippuu siitä, missä järjestyksessä muunnokset suoritetaan. Kaksi järjestystä vaativat erityisen huomion: Normaalijärjestyksessä valitaan uloin, vasemmanpuolisin β- tai δ-redex ja sovelletaan siihen β- tai δ-reduktiota. Applikaatiivisessa järjestyksessä valitaan sisin, vasemmanpuolisin β- tai δ- redex ja sovelletaan siihen β- tai δ-reduktiota. Applikatiivisen järjestyksen perusominaisuus on se, että siinä funktion argumentit lasketaan ennen kuin funktion arvoa aletaan selvittämään. Tällöin, jos argumentti on, koko funktion arvoksi muodostuu. Normaalijärjestys puolestaan laskee argumentit mahdollisimman myöhään, jolloin funktio, joka ei käytä argumenttiaan, voi saada muunkin arvon kuin, vaikka argumenttina olisikin. Itse asiassa normaalijärjestys löytää normaalimuodon, jos sellainen on olemassa.