Luku 11 Logiikkaohjelmoinnin idea Logiikkaohjelmoinnin perusidea on esittää ohjelmointiongelma faktoina (aksioomina) ja päättelysääntöinä sekä kyselynä, joka kohdistuu näiden muodostamaan tietojoukkoon. Tässä monisteessa on jo esitetty runsaasti logiikkaohjelmaesimerkkejä, sillä jokainen operationaalinen merkitysoppi on aika lähellä logiikkaohjelmaa. 11.1 Logiikkakieli Määritellään aluksi yksinkertainen logiikkakieli: A,..., O, Q, R, T,..., Z VariableNames a,..., o, v,..., z ConstantNames t, u Terms t, u ::= X f ((t) + ) a p, q Predicates p, q ::= f ((t) + ) a r, s Rule r, s ::= (p) p P Programs P ::= r ; p Ohjelma koostuu jonosta sääntöjä p 1,..., p n p (jossa n = 0 on täysin mahdollinen), joita seuraa predikaatti eli tavoite (engl. goal) tai kysely (engl. query). Sääntö tulkitaan väitteeksi jos jokainen p i (i = 1,..., n) pätee, niin p pätee ; siinä p on johtopäätös ja p i premissejä. Sääntö sitoo kaikki 113
114 LUKU 11. LOGIIKKAOHJELMOINNIN IDEA siinä esiintyvät muuttujat; muissa yhteyksissä muuttujat ovat aina vapaita. Kielen konkreetti syntaksi voi olla melkein mitä vain. Prolog käyttää - symbolin sijalla merkintää :- ja sijoittaa johtopäätöksen ja premissit toisin päin merkkiin nähden sekä lisäksi tukee koko joukkoa erityismerkintätapoja, mutta olennaisilta osiltaan se käyttää yllä kuvattua abstraktia kielioppia. Puolimuodolliset operationaaliset merkitysopit, kuten tässä monisteessa esitetyt, esittävät säännöt kaksiulotteisesti, jolloin -symbolin paikalla on päättelysäännön vaakasuora viiva; näissä kielissä myös termit ja predikaatit käyttävät usein erityismerkintätapoja. Pohjimmiltaan niissäkin on kuitenkin kyse edellä kuvatusta abstraktista kielestä. Ohjelman tuloksena on vastaus seuraavaan kysymykseen: Millä kaikilla tavoitteessa esiintyvien muuttujien arvoilla tavoite on osoitettavissa (ohjelman sääntöjä käyttäen) todeksi? Toisin sanoen, imperatiivisen ohjelman tavoin, logiikkaohjelma tuottaa (IOsivuvaikutukset unohtaen) muuttujien arvon lopputuloksenaan; mutta toisin kuin imperatiivinen ohjelma, se voi tuottaa useampia kuin yhden vastauksen, tai vastauksen puutteen (tavoitetta ei ole osoitettavissa todeksi millään muuttujien arvoilla). Tarkastellaan seuraavassa logiikkaohjelmointikielen toimintaperiaatteita. 11.2 Korvaukset Seuraavassa rajoitutaan tarkastelemaan vain termejä, koska jokainen predikaatti on muodollisesti myös termi. Logiikkaohjelmointikielen toiminnan määrittelyssä tarvitaan aiemmista kielistä tuttu vekotin, osittaisfunktio muuttujien nimiltä arvoille. Toisi kuin aiemmissa kielissä, arvot ovat termejä. Myös toisin kuin aiemmissa kielissä, näillä osittaisfunktoilla on uusi kollektiivinimi: korvaus (engl. substitution). Tämän nimen taustalla on se ajatus, että korvaus on normaalin muuttujankorvausoperaattorin yleistys, eräänlainen rinnakkainen muuttujankorvaus. Korvauksen soveltaminen termiin ja predikaattiin on varsin helppo määritellä (huomaa, että tσ tarkoittaa korvauksen σ soveltamista ter-
11.3. SAMASTUS 115 miin t): Xσ = { X σ(x) jos σ(x) muuten (11.1) aσ = a (11.2) f ((t i ) n i=1 )σ = f ((t iσ) n i=1 ) (11.3) Kahden korvauksen σ ja θ yhdistäminen, jota merkitään σθ, määritellään siten, että seuraava yhtälö pätee kaikilla termeillä t: t(σθ) = (tσ)θ (11.4) Toisin sanoen: { θ(x) (σθ)(x) = θ(σ(x)) jos σ(x) jos θ(σ(x)) = X Huomaa järjestyksen vaihtuminen! Yksinkertaisuuden vuoksi sovitaan, että millekään korvaukselle σ ja millekään muuttujalle X ei päde σ(x) = X, koska kaikille termeille t pätee tσ = tσ, missä σ = σ dom σ\{x}. Korvauksille voidaan määritellä esijärjestys 1 seuraavasti: σ θ ( σ on yleisempi kuin θ ) pätee, jos on olemassa korvaus η, jolle pätee ση = θ. Se ei ole osittaisjärjestys, sillä voi olla σ θ ja θ σ ja silti σ = θ, esimerkiksi jos σ = {(X, Y)} ja θ = {(Y, X)}. Niinpä määritelläänkin uusi ekvivalenssirelaatio seuraavasti: Jos σ θ ja θ σ, pätevät niin silloin σ θ ( σ ja θ ovat muuttujanvaihtoa vaille samat ) pätee. 11.3 Samastus Baader ja Snyder (2001) ovat laatineet kattavan esityksen samastuksesta. Määritelmä 4. Olkoon T joukko termejä. Korvaus σ on näiden termien samastin (engl. unifier), jos 1. dom σ t T FV(t) ja 2. jokaiselle t, u T pätee tσ = uσ. 1 Esijärjestys (engl. preorder) on refleksiivinen ja transitiivinen relaatio, joka ei välttämättä täytä symmetrisyys- tai antisymmetrisyysvaatimusta.
116 LUKU 11. LOGIIKKAOHJELMOINNIN IDEA Ensimmäinen ehto ei ole oikeastaan tarpeen, mutta sillä rajataan pois epäolennaisuuksia. Määritelmän olennainen sisältö on toisessa ehdossa. Esimerkiksi termien f (2, X) ja f (X, 2) (ainoa) samastin on {(X, 2)} ja termien f (2, g(x)) ja f (X, Y) (ainoat) samastimet ovat {(X, 2), (Y, g(2))} ja {(X, 2), (Y, g(x))}. Termien f (X) ja f (Y) samastimia ovat muiden muassa {(X, Y)}, {(Y, X)} ja {(X, g(3)), (Y, g(3))}. Määritelmä 5. Termijoukon samastin σ on sen yleisin samastin (engl. most general unifier), jos kaikille kyseisen termijoukon samastimille θ pätee σ θ. Termijoukon T yleisintä samastinta, jos se on olemassa, merkitään mgu T. Yleisin samastin on muuttujanvaihtoa vaille yksikäsitteinen. Esimerkiksi termien f (X) ja f (Y) yleisimpiä samastimia ovat {(X, Y)}, {(Y, X)} ja {(X, Z), (Y, Z)}; ne ovat kaikki muuttujanvaihtoa vaille samoja. Samastusongelmaksi (engl. unification problem) sanotaan seuraavaa ohjelmointitehtävää: On annettu (äärellinen) joukko termejä. Selvitä, onko tällä joukolla yleisintä samastinta, ja jos on, laske sellainen. Algoritmia, joka ratkaisee samastusongelman, sanotaan samastusalgoritmiksi (engl. unification algorithm). Eräs yksinkertainen kahden termin samastusalgoritmi toimii seuraavasti: function mgu(t,u ): local σ := local procedure unify(t,u): if ISVAR(t): t := tσ if ISVAR(u): u := uσ if ISVAR(t) t = u: if t FV(u): throw occurs check failure σ := σ{(t, u)} else if ISVAR(u) t = u: if u FV(t): throw occurs check failure σ := σ{(u, t)} else if HEAD(t) = HEAD(u) ARITY(t) = ARITY(u): for i = 1 to ARITY(t):
11.4. LOGIIKKAOHJELMAN SUORITUS 117 unify(subterm(t, i),subterm(u, i)) else: throw symbol mismatch unify(t,u ) return σ Algoritmissa käytetään seuraavia apufunktioita: ISVAR(t) = (t VariableNames) { f jos (u) + : t = f ((u) + ) HEAD(t) = t muuten { n jos f, (t) + : t = f ((t ARITY(t) = i ) i=1 n ) 0 muuten SUBTERM(t, k) = t k jos n k, (u i ) n i=1 : t = f ((t i) n i=1 ) Algoritmin idea on varsin yksinkertainen: käydään termejä rinnakkain rekursiivisesti läpi. Jos ne jossain kohtaa eroavat muutoin kuin muuttujan kohdalla, algoritmi epäonnistuu ( symbol mismatch ). Muuttujan kohdalla samastinehdokasta σ täydennetään. Ainoa hämmennystä ehkä aiheuttava kohta on esiintymistarkastus eli occurs check. Jos sitä ei olisi, algoritmi väittäisi, että termien X ja f (X) yleisin samastin olisi σ = {(X, f (X))}. Se ei kuitenkaan ole näiden termien samastin: Xσ = f (X) = f ( f (X)) = f (X)σ. Näillä termeillä ei ole samastinta lainkaan: ainoa mahdollisuus olisi σ = {(X, f ( f ( )))}, mutta äärettömän laajat termit eivät ole sallittuja. 11.4 Logiikkaohjelman suoritus Oletetaan aluksi, että eri säännöissä on eri muuttujat ja että tavoitteen muuttujat eivät esiinny säännöissä. Koska kukin sääntö sitoo muuttujansa, tämä on mahdollista varmistaa vaihtamalla säännöissä muuttujien nimiä tarpeen mukaan. Logiikkaohjelman suorituksen ydin on kulloisenkin tavoitteen q todistusyritys. Ohjelman säännöt käydään yksi kerrallaan läpi, kunnes löytyy jokin sellainen sääntö (p i ) i=1 n p, jolle mgu{p, q} on määritelty, toisin sanoen jolle p ja q samastuvat (engl. unify). Tällöin kyseinen sääntö tulee ikään kuin aliohjelman tavoin suoritettavaksi; jos n = 0, tämä tehtävä on
118 LUKU 11. LOGIIKKAOHJELMOINNIN IDEA triviaali. Koko säännöllä on oltava yhteinen samastin: ensimmäisen premissin samastimen tulee tarkentaa johtopäätöksen samastinta, ja toisen premissin samastimen tulee tarkentaa ensimmäisen premissin samastinta; näin siksi, että sama muuttuja on koko säännön ajan sama muuttuja, ja sillä pitää olla koko säännön ajan sama tulkinta. Mikäli jonkin tavoitteen kohdalla päästään koko sääntölistan loppuun asti, katsotaan tavoitteen todistus epäonnistuneeksi. Mikäli kyse oli alkuperäisestä koko ohjelman tavoitteesta, ohjelma päättyy. Muuten kyse oli tavoitteesta, joka oli jonkin säännön premissi, ja sen suorituksen syynä oli jokin toinen tavoite q, joka samastui säännön johtopäätökseen. Tällöin suoritus peruuttaa (engl. backtrack): kyseine sääntö hylätään ikään kuin samastus olisi epäonnistunut ja suoritus jatkaa tavoitteen q todistusta alkaen seuraavasta vielä soveltamattomasta säännöstä. Peruutusta logiikkaohjelmassa käytetään myös onnistuneen todistuksen jälkeen, jos tarkoituksena on selvittää kaikki mahdolliset alkuperäisessä tavoitteessa esiintyvien muuttujien onnistumisen aiheuttavat arvot. Esimerkkinä tarkastellaan Salminen ja Väänänen (1997) innoittamana kaupunkiverkkoja. Olkoon meillä seuraava säännöstö: yhteys(lontoo, lontoo), (11.5) yhteys(lontoo, pariisi), (11.6) yhteys(lontoo, praha), (11.7) yhteys(lontoo, rooma), (11.8) yhteys(pariisi, lontoo), (11.9) yhteys(pariisi, pariisi), (11.10) yhteys(pariisi, praha), (11.11) yhteys(pariisi, rooma), (11.12) yhteys(praha, lontoo), (11.13) yhteys(praha, pariisi), (11.14) yhteys(praha, praha), (11.15) yhteys(praha, rooma), (11.16) yhteys(rooma, lontoo), (11.17) yhteys(rooma, pariisi), (11.18) yhteys(rooma, praha), (11.19) yhteys(rooma, rooma) (11.20)
11.5. SAMASTUS PARAMETRINVÄLITYSMEKANISMINA 119 Nyt kysely yhteys(pariisi, X) käy sääntölistaa läpi yksi kerrallaan kunnes löytää säännön, jonka johtopäätös samastuu kyselyn kanssa; se on sääntö (11.9), ja samastin on {(X, lontoo)}. Koska säännöllä ei ole premissejä, säännön suoritus onnistuu heti, ja käyttäjälle tulostetaan X = lontoo. Sen jälkeen peruutetaan ja etsitään seuraava sopiva sääntö, joka on (11.10); samastin on {(X, pariisi)} ja käyttäjälle tulostetaan X = pariisi. Näin jatkaen käyttäjälle tulostetaan myös X = praha ja X = rooma, minkä jälkeen ei enää samastuvia sääntöjä löydy ja ohjelman suoritus päättyy. Lisätään nyt kaksi sääntöä: yhteys(lontoo, glasgow), (11.21) yhteys(a, B), yhteys(b, C) yhteys(a, C) (11.22) Nyt kyselyn yhteys(x, glasgow) suoritus löytää sopivan säännön vasta säännöstä (11.22), ja (eräs) samastin on {(A, X), (C, glasgow)}. Tällä säännöllä on kaksi premissiä, joten suoritus joutuu tarkastelemaan ensiksi tavoitetta yhteys(x, B). Jo sääntö (11.5) tuottaa sen kohdalla tuloksen, samastimena {(X, lontoo), (B, lontoo)}. Koska tuolla säännöllä ei ole premissejä, päästään kokeilemaan, toimiiko samastus myös toisen premissin kohdalla: yhteys(lontoo, glasgow); ja sääntö (11.21) sanoo, että tämä onnistuu. Niinpä käyttäjälle tulostetaan X = lontoo. Seuraavaksi, peruutuksen jälkeen, kokeillaan sääntöä (11.6) tavoitteeseen yhteys(x, B), jolloin löytyy samastin {(X, lontoo), (B, pariisi)}. Tällöin säännön (11.22) toinen premissi muuttuu muotoon yhteys(pariisi, glasgow). Tämän premissin todistus joutuu kulkemaan vielä sääntöjen (11.22) ja (11.9) kautta, mutta lopulta todistus onnistuu, ja käyttäjälle tulostuu jälleen X = lontoo. Vastaavasti käy kaikkien muidenkin kaupunkien kohdalla, ja lopulta X =... -luettelo kattaa kaikki ohjelman tuntemat kaupungit itse Glasgowta lukuunottamatta (koska ohjelma ei tiedä mitään reittiä Glasgowsta pois). 11.5 Samastus parametrinvälitysmekanismina Kuten edellä todettiin, logiikkaohjelman sääntöä voidaan pitää eräänlaisena aliohjelmana. Tällöin kuitenkin samastus johtaa varsin erikoiseen parametrinvälitysmekanismiin. Tarkastellan sääntöä f (A, B). Sitä voidaan kutsua eri tavoin: Kutsu f (a, b) haluaa tietää, voiko säännössä A olla a ja B olla b. Tällöin parametrinvälitys käyttäytyy arvovälitteisyyden tavoin, ja kutsu palauttaa totuusarvon.
120 LUKU 11. LOGIIKKAOHJELMOINNIN IDEA Kutsu f (a, X) haluaa tietää kaikki ne B:t, jotka tulevat kyseisen säännön kohdalla kyseeseen silloin, kun A on a. Kutsu toimii proseduurikutsun tavoin, mutta a välittyy arvovälitteisesti ja X välittyy tulosvälitteisesti. Lisäksi kutsu on (hyväntahtoisesti) epädeterministinen. Kutsu f (X, b) toimii, mutatis mutandis, kuten edellinen. Kutsu f (X, Y) haluaa tietää kaikki ne A:n ja B:n arvot, jotka tulevat säännössä kyseeseen. Kutsu on proseduurimainen ja hyväntahtoisesti epädeterministinen, ja molemmat parametrit välittyvät tulosvälitteiseen tapaan. Ja nämä ovat vain ne helpot tapaukset. Yksi esimerkki hurjemmasta kutsusta on f (h(a, X), Y).