Tässä materiaalissa käydään läpi Prologiin (erityisesti kurssilla käytettävään GNU Prologiin) liittyviä käytännön seikkoja. Tarkoituksena on käsitellä erityisesti sellaisia asioita, jotka tuottavat ongelmia imperatiivisiin kieliin tottuneille ohjelmoijille. Lisäksi esitellään yleisimpiä gprologista löytyviä valmiita predikaatteja ja niiden toimintaa. Opiskelijalla oletetaan olevan perustiedot logiikasta ja jonkinlainen ymmärrys Prologin toiminnasta yleisellä tasolla.

Aloitus

Kurssilla käytetään Lintulaan asennettua GNU Prologia, joka löytyy hakemiston /usr/local/lang/prolog/ alta. Kun tämä hakemisto on lisätty hakupolkuun, käynnistyy prolog-tulkki komennolla gprolog.

Aloitetaan Prologiin tutustuminen käynnistämällä tulkki ja antamalla ohjelmat sille interaktiivisina kyselyinä (query). Kukin kysely koostuu yhdestä tai useammasta pilkulla tai puolipisteellä erotetusta predikaatista ja loppuu pisteeseen. Kukin predikaatti joko onnistuu tai epäonnistuu, ja koko kysely onnistuu, jos sen muodostama logiikan lause on tosi. Kyselystä muodostuu logiikan lause siten, että pilkku on looginen "ja" ja puolipiste "tai". Muuttujanimet aloitetaan aina isolla kirjaimella ja vakiot pienellä.

Gprologista löytyy runsaasti valmiita järjestelmäpredikaatteja (builtin predicate), mm. peruslaskuoperaatiot, operaattoreita vertailua ja samaistusta (unification) varten, predikaatteja listan käsittelyyn sekä IO-toimintoihin:


% gprolog
GNU Prolog 1.2.13
By Daniel Diaz
Copyright (C) 1999-2002 Daniel Diaz
| ?- X=42, write(X), Y=X+1, write(X).
4242

X = 42
Y = 42+1

yes

Eli sidotaan muuttuja X arvoon 42, kirjoitetaan se ruudulle ja sidotaan muuttuja Y arvoon X + 1. On ehkä yllättävää, että tämän jälkeen Y ei olekaan 43 vaan 42+1. Mikäli haluamme, että oikeanpuolisen termin arvo lasketaan auki ennen samaistamista, on käytettävä predikaattia is:


| ?- X=42, write(X), Y is X+1, write(X).
4242

X = 42
Y = 43

yes

On tärkeää muistaa, että samaistaminen ei ole sama asia kuin imperatiivisten ohjelmointikielten sijoittaminen, vaikka se siltä äkkiseltään näyttääkin. Käyttäjän kannalta olennaisin ero lienee se, että samaistamista ei voi suorittaa milloin tahansa: Kun muuttuja on sidottu johonkin vakioon, ei se voi enää saada muita arvoja muuta kuin hakualgoritmin peruuttaessa. Jos uudelleen sitomista yritetään, se yleensä epäonnistuu kun alkioita ei voida unifioida keskenään. Vain sitomattoman muuttujan voi unifioida mihin tahansa, sidotut muuttujat ainoastaan muuttujaan tai vakioon, jolla on sama arvo.


| ?- X=4, Y=3, X=Y.

no
| ?- X=3, Y=3, X=Y.

X = 3
Y = 3

yes
| ?- X + 3 = 5 + Y + 3.

X = 5+Y

(10 ms) yes

Prolog on tyypittämätön kieli, sen muuttujat voidaan sitoa mielivaltaisiin termeihin, jotka on rakennettu luvuista, merkkijonoista, muuttujista ja matemaattisista operaattoreista. Termeissä voi myös esiintyä ohjelmoijan itse keksimiä vakio- ja funktionimiä, joita kutsutaan atomeiksi. Prologilla voi kirjoitella lauseita, joissa kissoja ja koiria laskeskellaan yhteen ja kaikki sujuu onnellisesti kunnes arvo yritetään laskea auki:


| ?- X = kissa + koira, Y = X+13. 

X = kissa+koira
Y = kissa+koira+13

yes
| ?- X = kissa + koira, Y is X.
uncaught exception: error(type_error(evaluable,kissa/0),(is)/2)

Prologissa on varsin paljon tällaisia predikaatteja, jotka odottavat, että niiden saamilla parametreilla on tiettyjä ominaisuuksia. Kannattaa tarkistaa asia manuaalista ennen kuin käyttää uutta predikaattia.

Monet järjestelmäpredikaateista myös olettavat, että osa tai kaikki parametreista ovat sidottuja. Otetaan tästä esimerkiksi predikaatti for(X,Alku,Loppu), joka ei yhdennäköisyydestä huolimatta ole silmukkalause kuten C++:n for. Predikaatti for onnistuu, mikäli X on integer välillä [Alku...Loppu] ja Loppu on suurempi tai yhtä suuri kuin Alku. Sitä voi kutsua siten, että kaikki parametrit ovat tunnettuja, tai siten että X on tuntematon, jolloin se generoi hakupuuhun X:n arvolle kaikki vaihtoehdot väliltä [Alku...Loppu]. Sitä ei voi - eikä olisi tietenkään mielekästäkään - kutsua siten, että Alku tai Loppu ovat tuntemattomia, silloinhan se onnistuisi äärettömän suurella joukolla lukuja, eikä vaihtoehtojen generointi ikinä loppuisi.


| ?- for(5,2,8).

yes
| ?- for(10,2,8).

no
| ?- for(10,12,8).

no 
| ?- for(2,X,5).
uncaught exception: error(instantiation_error,for/3)
| ?- for(X,2,5), write(X), nl, fail.
2
3
4
5

no

Predikaatti nl onnistuu aina ja tulostaa ruudulle rivinvaihdon. Predikaatti fail taas epäonnistuu aina. Kun kysely epäonnistuu aina nl:n jälkeen, peruutetaan edelliseen haarautumiskohtaan eli for:iin niin kauan kun uusia vaihtoehtoja löytyy. Kun kaikki vaihtoehdot on kokeiltu, predikaatti epäonnistuu.

Prologin perustietorakenne on lista, joka on termi siinä missä kokonaisluvutkin. Lista koostuu päästä (head) ja hännästä (tail) ja se esitetään yleensä muodossa [head|tail] tai vaihtoehtoisesti alkiot hakasulkujen sisällä pilkuilla eroteltuina. Listan pää voi olla mikä tahansa termi ja sen häntä on aina lista. Myös tyhjä lista on lista, se esitetään tyhjillä hakasuluilla []. Gprologissa on runsaasti valmiita predikaatteja listojen käsittelyyn, mm. member(X,L), joka onnistuu kun alkio X kuuluu listaan L, sum_list(L,X), joka onnistuu kun X on listan L elementtien summa ja min_list(L,X), joka onnistuu kun X on listan L pienin elementti.
| ?- [2|L] = [X|[3,2]].

L = [3,2]
X = 2

yes
| ?- member(X,[1,3,5]).

X = 1 ? ;

X = 3 ? ;

X = 5 ? ;

no
| ?- min_list(L,3).

L = [3] ? ;
uncaught exception: error(instantiation_error,(>=)/2)
| ?- sum_list([4,6,2],Y).

Y = 12

yes
| ?- sum_list([1,3,[3,4]],X).
uncaught exception: error(type_error(evaluable,'.'/2),(is)/2)

Min_list ja sum_list vaativat, että listan alkiot ovat aritmeettisesti evaluoitavissa. Mikäli näin ei ole, syntyy virhetilanne.

Jos kyselyyn on useampia vastauksia, ei ohjelma tulosta "yes", vaan odottaa käyttäjän kommenttia. Puolipisteellä tulkki tulostaa seuraavan ratkaisun ja enterillä se lopettaa haun.

Tulkin tapaa suorittaa ohjelmaa voi seurata järjestelmäpredikaatin trace avulla. Tracen suorittamisen jälkeen tulkki tulostaa konsolille tiedon siitä, mitä predikaattia ollaan suorittamassa, ja jää jokaisen askeleen jälkeen odottamaan käyttäjän enterin painallusta.

| ?- trace.
The debugger will first creep -- showing everything (trace)

yes
{trace}
| ?- member(Y,[1,3,6]), sum_list([1,2,3],Y).
      1    1  Call: member(_16,[1,3,6]) ? 
      1    1  Exit: member(1,[1,3,6]) ? 
      2    1  Call: sum_list([1,2,3],1) ? 
      2    1  Fail: sum_list([1,2,3],1) ? 
      1    1  Redo: member(1,[1,3,6]) ? 
      1    1  Exit: member(3,[1,3,6]) ? 
      2    1  Call: sum_list([1,2,3],3) ? 
      2    1  Fail: sum_list([1,2,3],3) ? 
      1    1  Redo: member(3,[1,3,6]) ? 
      1    1  Exit: member(6,[1,3,6]) ? 
      2    1  Call: sum_list([1,2,3],6) ? 
      2    1  Exit: sum_list([1,2,3],6) ? 

Y = 6 ? ;
      1    1  Redo: member(6,[1,3,6]) ? 
      1    1  Fail: member(_16,[1,3,6]) ? 

(10 ms) no
{trace}

Prolog-ohjelman kirjoittaminen

Prolog-ohjelmatiedostot nimetään nimi.pl. Ohjelma koostuu predikaateista, jotka konsultoidaan prologtulkkiin komennolla [nimi]. Tämän jälkeen ohjelmatiedostossa määriteltyjä predikaatteja pystyy käynnistämään interaktiivisesti samalla tavalla kuin Prologin valmiita järjestelmäpredikaatteja.

Prolog-ohjelma koostuu joukosta predikaatteja, jotka on määritelty kaavoina (clause). Samalle predikaatille voidaan kirjoittaa useita sen määritteleviä kaavoja, jotka Prolog suorittaa järjestyksessä yksi kerrallaan alkaen ylimmästä. Samalle predikaatille voi myös kirjoittaa rinnakkaisia versioita, joiden ariteetit eli niiden saamien parametrien määrä poikkeavat toisistaan (vrt. metodien kuormittaminen C++:ssa). Yhden predikaatin määrittelevät kaavat kirjoitetaan peräkkäin samaan ohjelmatiedostoon, muussa tapauksessa tulkki antaa varoituksen konsultoinnin aikana. Kukin kaava koostuu päästä ja vartalosta, joka puolestaan on sarja predikaatteja (goal). Pää ja vartalo erotetaan toisistaan symbolilla ":-" ja jokainen kaava lopetetaan pisteellä. Kaavaa, jonka vartalo on tyhjä, kutsutaan faktaksi (fact). Yksittäisen predikaatin määrittävät kaavat muodostavat disjunktion (looginen tai), ja kaavan määrittelevät predikaatit muodostavat konjunktion (looginen ja). Toisin sanoen predikaatti onnistuu, mikäli yksi sen kaavoista onnistuu, ja kaava onnistuu, mikäli kaikki sen predikaatit onnistuvat.

Ensimmäinen esimerkkiohjelma