1 Tämän dokumentin tarkoitus Tämä dokumentti ei kuulu millään tavoin tenttialueeseen, enkä ota vastuuta sen lukemisen aiheuttamista vahingoista. Tässä dokumentissa esitetään esimerkin kautta, miten matematiikan kielessä voi harrastaa ns. funktionaalista ohjelmointia. Tarkemmin, esitetään eräs versio ns. lisäyslajittelu-algoritmista matematiikan kielellä, määrittelemällä se funktiona sanoista sanoihin rakentellisella induktiolla. Lisäksi todistetaan, että algoritmi toimii. Monisteen Luvussa 1.6 tehdään samaa asiaa hieman eri kielellä ja eri esimerkkiongelmille. Lauseen 1 todistus on kirjoitettu Proof-of-concept -mielessä, koska luennoitsija itse halusi nähdä onnistuuko induktiotodistus tässä esimerkissä suoraviivaisesti (näyttäisi onnistuvan). Todistuksen yksityiskohtainen lukeminen ei välttämättä ole pedagogisesti hyödyllistä. 2 Sanoja järjestävän funktion ohjelmoiminen Olkoon Σ äärellinen 1 aakkosto, jossa on määritelty jokin täysi järjestys, eli kun a, b Σ, niin tasan yksi kolmesta relaatiosta a < b, a = b, a > b pätee. Esimerkiksi voi olla Σ N. Esimerkeissä aakkosto on Σ = {0, 1, 2} (eli aakkoston Σ kirjaimet ovat 0, 1, 2), ja valitaan järjestykseksi 0 < 1 < 2. Nyt määritellään induktiivisesti kaksi funktiota, s : Σ Σ on sanan järjestävä funktio, eli esim. halutaan s(010) = 001. Tämä funktio määritellään seuraavasti käyttäen apuna funktiota i: s(ɛ) = ɛ, s(a u) = i(a, s(u)). Funktio i : Σ Σ Σ on apufunktio, jonka idea on, että jos u on jo järjestyksessä ja a Σ, niin i(a, u) = v, missä v on muuten sama kuin u, mutta siihen on sijoitettu a oikeaan paikkaan Esimerkiksi i(1, 00112) = 001112. Määritellään i induktiivisesti: { abv jos b a i(a, ɛ) = a, i(a, b v) = b i(a, v) jos b < a Pythonilla sama näyttäisi tältä: # järjestysfunktio, jossa sama rekursio kuin kaavoissamme def s(w): if len(w) == 0: # tyhjä sana vastaa 0-pituista sanaa, sille ei tarvitse tehdä mitään return w else: # yleisessä tapauksessa w = a. u leikataan ulos a ja u a = w[0] u = w[1:] # s(w) = s(a.u) = i(a, s(u)) kuten kaavassa return i(a, s(u)) 1 Homma toimisi äärettömälle samoin ohjelmoinnissa tyypillisesti Σ olisi mielivaltainen tyyppi jossa on määritelty järjestysrelaatio, ja samoin voi tehdä matematiikassa. 1
# "insert"-funktio, taas samoilla kaavoilla määritelty def i(a, u): if len(u) == 0: # i(a, epsilon) = a return a else: b = u[0] v = u[1:] if b >= a: # Pythonissa sanoja pistetään yhteen +-merkillä eikä kertomerkillä. return a + b + v else: # i(a, b.u) = b. i(a, u) jos b < a return b + i(a, v) print (s("abbabaa")) # tämä printtaa aaaabbb kun ohjelma ajetaan Matemaattiset määritelmät voi tässä nähdä kyseisen Python-koodin käännöksenä matematiikan kielelle, jotta sen ominaisuuksia voi todistaa. (Tosin tein itse asian toisinpäin eli käänsin matemaattisen määritelmän Pythoniksi.) 3 Testi Lasketaan s(20110), jossa pitäisi siis olla 2, 0, 1, 1, 0 kasvavassa järjestyksessä. Näin todella käy: s(20110) = i(2, s(0110)) = i(2, i(0, s(110))) = i(2, i(0, i(1, s(10)))) = i(2, i(0, i(1, i(1, s(0))))) = i(2, i(0, i(1, i(1, i(0, ɛ))))) = i(2, i(0, i(1, i(1, 0)))) = i(2, i(0, i(1, 0 i(1, ɛ)))) = i(2, i(0, i(1, 0 1))) = i(2, i(0, 0 i(1, 1))) = i(2, i(0, 0 11)) = i(2, 0 011)) = 0 i(2, 0 11)) = 00 i(2, 1 1)) = 001 i(2, 1)) = 0011 i(2, ɛ)) = 00112 Tässä on aina laskettu sisin termi auki määritelmää käyttäen, ja ylimääräiset kertomerkit on pudoteltu pois välivaiheissa. Välivaiheita tuli tässä 15 kpl, ja 2
niiden määrää voisi vähän pudottaa todistamalla sopivia aputuloksia, tai tekemällä useampi sievennys kerralla. Joka tapauksessa tällä algoritmilla välivaiheita tulee monta, nimittäin pahimmillaan Ω(n 2 ) kappaletta, eli enemmän kuin Cn 2 jollekin vakiolle C, missä n on sanan pituus. Parempien algoritmien keksiminen ei kuitenkaan ole tämän kurssin asiaa. 4 Määritelmiä: Mitä tarkoittaa järjestää? Ennen kuin voidaan todistaa, että funktiomme todella järjestää sanan, täytyy määritellä, mitä tarkoittaa järjestää. Määrittelemme tämänkin induktiolla. Intuitiivisesti funktio s : Σ Σ on sanan järjestävä funktio, jos kaikille sanoille u, s(u) sisältää samat kirjaimet kuin u, ja s(u):ssa kirjaimet ovat järjestyksessä. Kirjaimet ovat järjestyksessä, jos peräkkäiset kirjaimet ovat aina kasvavassa järjestyksessä, eli u Σ on järjestyksessä jos kaikille i, u i u i+1, missä u i tarkoittaa u:n i:nnettä kirjainta. Voimme kirjoittaa tämän induktiolla: Määritellään järjestyksessä olevien sanojen joukko S Σ induktiivisesti: ɛ S, a S kaikille a Σ, ja jos b u S ja a b, a Σ, niin abu S. Nyt sana u on järjestyksessä jos u S, merkitään tätä ominaisuutta P (u). Todistetaan melko triviaali apulause. Lemma 1. Jos a, b Σ ja a b, niin ab S. Todistus. Suoraan määritelmästä b S eli b ɛ S, ja tällöin S:n määritelmän induktioaskeleesta saadaan a (b ɛ) = ab S. Määritellään sitten mitä tarkoittaa, että v:ssä ja u:ssa on samat kirjaimet. Yksi tapa määritellä tämä on, että kaikille kirjaimille a Σ, kirjainten a lukumäärä u:ssa ja v:ssä on sama. Merkitään tätä määrää u a ja määritellään se induktiivisesti: { 1 + u a jos b = a ɛ a = 0, b u a = u a muuten. Nyt sanotaan, että u:ssa ja v:ssä on samat kirjaimet, jos kaikille a Σ, u a = v a. Merkitään tätä ominaisuutta Q(u, v). Nyt halutaan, että funktiolla s : Σ Σ, joka määriteltiin Luvussa 2, on seuraava ominaisuus: u Σ : P (s(u)) Q(u, s(u)) eli s järjestää syötteeksi saamansa sanan u. Todistamme tämän seuraavassa luvussa. 5 Funktio s on todella järjestysfunktio Olemme määritelleet induktiivisesti funktion s ja määritelleet mitä tarkoittaa olla järjestyksessä ja sisältää samat kirjaimet. Nyt voimme todistaa induktiolla, että s on järjestysfunktio. Theorem 1. Olkoon u Σ mielivaltainen sana. Tällöin P (s(u)) ja Q(u, s(u)), eli s(u) on järjestyksessä ja sisältää samat kirjaimet kuin u. 3
Todistus. Todistetaan ensin ominaisuus P (s(u)), eli että s:n arvo s(u) on järjestyksessä jokaisella syötteellä. Pitää siis todistaa, että s(u) kuuluu joukkoon S, joka määriteltiin edellisessä luvussa. Todistetaan tämä rakenteellisella induktiolla: Lähtökohta on s(ɛ) = ɛ S. Induktioaskel on s(a u) = i(a, s(u)), missä induktio-oletuksesta seuraa, että s(u) S (koska u on yksinkertaisempi sana kuin a u). Riittää siis todistaa, että funktiolla i on ominaisuus, että jos v S ja a Σ, niin i(a, v) S. Todistetaan tämä rakenteellisella induktiolla v:n suhteen (mikä onnistuu, koska S määriteltiin induktiolla): Lähtökohdat ovat i(a, ɛ) ja i(a, b) kun a, b Σ. Tapaus i(a, ɛ) = a S on selvä. Tapauksessa i(a, b) joko a b tai b < a. Jos a b, niin i(a, b) = ab suoralla laskulla (eli suoraan määritelmästä), ja tällöin ab S Lemman 1 nojalla. Jos b < a, niin suoralla laskulla taas i(a, b) = ba ja taas ba S Lemman 1 nojalla. Joukon S määritelmässä askel on (kun nimetään vähän uudelleen kirjaimia), että jos c u S ja b c, b Σ, niin bcu S. Eli todistettaessa i:lle induktioaskelta tarvitsee käsitellä vain tapaus i(a, b (c u)) S missä c u S ja b c. Jos a b, niin suoralla laskulla (eli suoraan määritelmästä) i(a, b (c u))) = abcu, ja abcu S taas kerran suoraan S:n määritelmästä (bcu S ja a b joten tämä on vain S:n määritelmän induktioaskel). Muuten a > b jolloin i(a, b (c u)) = b i(a, c u) missä induktio-oletuksesta seuraa i(a, c u) S koska c u S on yksinkertaisempi kuin b (c u), ja koska b < a, erityisesti b a, joten S:n induktiivisesta määritelmästä seuraa i(a, b (c u)) = b i(a, c u) S. Olemme todistaneet, että todella P (s(u)) kaikille sanoille u. Todistetaan vielä Q(u, s(u)) kaikille sanoille u Σ. Tämä tehdään yllättäen rakenteellisella induktiolla u:n suhteen. Lähtötapaus Q(ɛ, s(ɛ)) on triviaali, koska s(ɛ) = ɛ ja selvästi ɛ a = ɛ a kaikille a Σ. Sitten induktioaskel: Olkoon a Σ, ja b u Σ. Nyt s(b u) = i(b, s(u)), ja induktio-oletuksesta seuraa u a = s(u) a kaikille a. Koska b u a = u a + 1 jos a = b ja b u a = u a jos a b, riittää todistaa, että i:llä on se ominaisuus, että jos a b niin i(b, v) a = v a ja jos a = b niin i(b, v) a = v a + 1 (eli toisin sanoen, i(b, v):ssa on v:n kirjaimet sekä yksi ylimääräinen b). Tämä todistetaan yllättäen rakenteellisella induktiolla v:n suhteen. Käsitellään tässä vain tapaukset b = a, tapaukset a b menevät aina samaan tyyliin: Lähtökohdat i(b, ɛ) a = b a = 1 = ɛ a + 1 kun b = a ja i(b, c) {bc, cb} 4
joten i(b, c) a = c a + 1 suoralla laskulla kun b = a (ja tosiaan tapaukset b a samaan malliin). Sitten induktioaskel S:n induktiivista määritelmää pitkin: Olkoon v = c (d v ) S, c d, c, d Σ. Jos b c niin i:n määritelmästä laskemalla saadaan Jos taas b > c niin i(b, c (d v ) a = bcdv a = 1 + cdv a = v a + 1 i(b, c (d v ) a = c i(b, d v ) a. Tässä i(b, d v ) a = d v + 1. Jos nyt c = a, niin v a = d v + 1 jolloin i(b, c (d v ) a = c i(b, d v ) a = 1 + i(b, d v ) a = d v + 2 = v a + 1 ja jos c a, niin lasku menee samaan tyyliin. 6 Lyhyesti formaalisista metodeista Formaalit metodit ovat tietojenkäsittelytieteen haara, jossa tutkitaan erityisesti ohjelmien toimivuuden aukotonta todistamista. Tämä pohjautuu siihen, että matematiikan kielellä voi ohjelmoida kaiken, minkä voi ylipäätään ohjelmoida, mutta matematiikassa voi mennä ohjelmointia pidemmälle, ja todistaa, että ohjelma toimii. Tässä on kaksi vaihetta: 1. Formalisoidaan ominaisuus, joka halutaan todistaa (esimerkissämme tämä ominaisuus on, että s järjestää sanan kirjaimet kasvavaan järjestykseen, eli Luvun 4 ominaisuudet P ja Q). 2. Todistetaan (esim. induktiolla) että algoritmilla on todella tämä formalisoitu ominaisuus. (Lause 1.) Ensimmäinen vaihe näistä on myös ohjelmointia, koska meidän on matematiikan kielellä ohjelmoitava, mitä tarkoittaa olla järjestyksessä. Tässä vaiheessa voi myös sattua virheitä! Onko hommassa siis mitään järkeä, jos virhe voi yhtä hyvin sattua siinä vaiheessa kun päätämme mitä haluamme todistaa? On! Jos myöhemmin vaihdamme algoritmia, jolla järjestämme sanan, esimerkiksi nopeampaan QuickSort- tai MergeSort-algoritmiin, 2 jotka myös on helppo ohjelmoida matematiikan kielellä, niin meidän ei tarvitse uudestaan määritellä mitä tarkoittaa olla järjetyksessä, vaan voimme suoraan lähteä todistamaan näiden algoritmien oikeellisuutta samalla määritelmällä. Eli siinä, missä algoritmi voi muuttua (koska sitä pitää esim. nopeuttaa, tai muuten tehostaa), määritelmät kuten olla järjestyksessä eivät yleensä muutu (koska ne ovat vain määritelmiä, eikä niiden tarvitse olla tehokkaita ). Eli Lukua 4 ei tarvitsisi muuttaa lainkaan, vaikka funktion s induktiivinen/rekursiivinen määritelmä vaihdettaisiin tehokkaampaan! 2 Näistä löytyy esim. Wikipediasta selkeät esitykset. 5
Toinen vaihe, jossa ominaisuus todistetaan, voi myös teoriassa mennä vikaan, jos sen tekee käsin: Joskus todistus on vaikeampi ymmärtää, kuin intuitiivinen selitys, eikä sitä hullukaan jaksa lukea. Esimerkiksi yllä oleva todistus, että järjestysfunktio s todella järjestää syötteensä, on suoraan sanottuna aika hirvittävä (eikä edes käsittele kaikkia tapauksia). Oikeassa elämässä algoritmit ovat kuitenkin paljon monimutkaisempia, ja algoritmista ei välttämättä oikeasti tiedä toimiiko se, ennen kuin todistaa asian. Tällöin todistuksessa hypätään tylsät osat yli, ja selitetään vain olennainen (mutta tällä tarkkuudella tehtynä Lauseessa 1 ei olisi mitään todistamista). Kuitenkin jo QuickSortin tapauksessa helpoin tapa selittää toimivuus, on selittää miksi funktion rekursiivinen kutsu tuottaa validin induktioaskeleen. Jos taas ollaan kiinnostuttu yksinkertaisista algoritmeista kuten s:lle annettu kaava, ja halutaan olla todella varmoja, että ohjelma todella toimii oikein (kaikissa tilanteissa), voi todistuksen voi kirjoittaa jossain todistustyökalussa (esim. Isabelle, Coq, HOL, Mizar...) ja käskeä tietokoneen tarkistamaan sen. Tämä vaatii toistaiseksi enemmän työtä, kuin todistuksen selittäminen ihmiselle, mutta todistustyökalut muuttuvat kaiken aikaa helppokäyttöisemmiksi, ja osaavat jo nyt usein automatisoida monia ilmeisiä askelia todistuksessa. Yllä annetun kaltainen induktiotodistus sopii periaatteessa melko suoraan tällaiseen työkaluun, joskin vaatii työkalun hyvää teknistä hallintaa. 6