IX. Programozás Prologban

Elsõ nekifutás: a Prolog elemei

A Prolog mesterséges intelligencia feladatok megoldására alkotott nyelv. A mesterséges intelligencia programok bizonyos típusú problémákat oldanak meg, melyek elsõ pillantásra emberi intuíciót igényelnek, bár némelyiküknek már létezik pontosan algoritmizálható megoldása is. Sok mesterséges intelligencia problémának azonban ma még nem ismert a pontos megoldása, és a programokban nagy szerepet játszanak az emberi tapasztalatokból leszûrt, nem bizonyított (heurisztikus) szabályok.

A Prolog nyelvre jellemzõ, hogy a programozónak nem megoldási algoritmust kell adnia, hanem a feladat megoldásához szükséges tényeket és szabályokat kell betáplálnia, melyek segítségével a Prolog önállóan jut el a végkövetkeztetésig.

A Prolog nyelvnek sok változata létezik, ebben a jegyzetben a Turbo Prolog szintaktikáját fogom használni. A tények és szabályok leírásában természetesen minden Prolog megegyezik.

A Prolog a matematikai logikára épül. A tények és szabályok megadása után a rendszernek feltehetõ egy eldöntendõ kérdés. A kérdés állítás formájában fogalmazandó meg, és a Prolog megadja, hogy az állítás igaz-e vagy hamis, illetve, hogy milyen esetekre igaz. Ha a betáplált tények és szabályok ismeretében ez nem dönthetõ el, a Prolog az állítást hamisnak veszi (ez egyáltalán nem nyilvánvaló! Pl. „A Szíriusz kis zöld emberkéi szeretik a tejbegrízt." igaz-e vagy hamis?).

A Prologban a szövegeket nem szükséges idézõjelbe tenni, de akkor nem kezdõdhetnek nagybetûvel és nem tartalmazhatnak szóközt. A nagybetûs szövegek a Prolog paramétereit jelzik.

A Turbo Prolog program szerkezete:

DOMAINS {típusdeklarációk}
   név=típus
   ...
PREDICATES
{a szabályok paraméterei típusának megadása}
   szabály(paramétertípusok)
   ...
CLAUSES
{a szabályok, végükön ponttal}
   szabály.
   ...
[GOAL]
{nem interaktív mód esetén az eldöntendõ állítás}
   ...

Nézzünk egy Prolog programot:

DOMAINS
   s=symbol
PREDICATES
   hal(s)
   vanneki(s,s)
CLAUSES
   hal(ponty).
   hal(csuka).
   vanneki(ponty,pikkely).

A symbol típus tetszõleges kisbetûvel kezdõdõ, szóközmentes szöveget jelent. Egyéb típusok: integer, real, char, string. Ha a GOAL szekció hiányzik, a Prolog interaktív módban vár a kérdés beírására. A program lényegi része a CLAUSES azonosító után következik, a továbbiakban ezzel foglalkozuk.

A szabály neve után zárójelben adtuk meg a szabály paramétereit. Minden sor azt adja meg, hogy egy szabály milyen esetben teljesül, vagyis pl. a hal(valami) állítás mikor igaz. Ezen szabályok alapján feltehetjük a kérdést (?-lel jelölöm a kérdés promptját, ez Turbo Prologban Goal:):

Az elsõ kérdésre a válasz No, a másodikra Yes, a harmadikra X=ponty, X=csuka. A válasz a következõképpen született: a Prolog az elsõ esetben megvizsgált minden hal(...) szabályt. Sehol nem talált egyezést, így a válasz hamis. A második esetben talált egyezést,a válasz igaz. A harmadik esetben a kérdés szabad paramétert tartalmazott (olyan paraméter, amely még nem kapott értéket). A hal(...) szabályok vizsgálatánál a Prolog meghatározta, hogy X mely értékeire teljesülhet az állítás. Ez a Prolog mûködésének egyik kulcsa, a mintaillesztés. Ennek során a szabad paraméterek értéket kapnak.

1. feladat: mi lesz a válasz a következõ kérdésekre:
Vanneki(ponty,A), Vanneki(A,pikkely), Vanneki(A,szárny), Vanneki(X,Y)?

A _ (aláhúzás) olyan érték, amely mindenre illeszthetõ („akármi"). A hal(_) értéke Yes.

A tényeken kívül megadhatunk összetettebb szabályokat. Pl. az utolsó szabály helyett:

vanneki(X,pikkely) :- hal(X).

:- helyett használható if is. Ez a szabály azt mondja ki, hogy valaminek akkor van pikkelye, ha arra a valamire teljesül, hogy hal. Vanneki szabály kiértékeléséhez a Prolog megvizsgálja a hal szabályokat, és mintaillesztés segítségével egyezést keres.

Vanneki(csiga,pikkely) kiértékelésekor vanneki(X,pikkely) szabálynál illesztési lehetõséget vesz észre, elvégzi X=csiga illesztést, majd megvizsgálja hal(X) feltételt. X most már nem szabad paraméter, tehát a Prolog a hal(csiga) állítást értékeli ki. Nem talál egyezést, így a válasz No.

Vanneki(A,pikkely) kiértékelésekor vanneki(X,pikkely) ismét egyezést mutat, X=A illesztéssel. Így X továbbra is szabad paraméter marad (mivel A szabad paraméter), és hal(A) vizsgálata következik. Két megoldást kapunk A-ra.

= operátor kétféleképpen mûködik. érték = érték esetben megvizsgálja a két érték egyezését, és igaz vagy hamis eredményt ad. szabad paraméter = érték esetben mintaillesztõként mûködik, és az értéket értékül adja a szabad paraméternek, mely ettõl fogva nem lesz szabad, értékként mûködik. szabad paraméter = szabad paraméter esetben a baloldali paraméternek értékül adja a jobboldalit, mégpedig úgy, hogy a továbbiakban a baloldali helyett a jobboldalit használja (név szerinti paraméterátadás), ld. X=A illesztést elõbb. Az = operátor mûködése nem szimmetrikus, és nem használható érték = paraméter formában, ugyanis mindig a baloldalnak adja értékül a jobboldalt.

Egészítsük ki a szabályokat hal(angolna). sorral. Mivel az angolnának nincs pikkelye, a Vanneki szabály módosul. Valaminek van pikkelye, ha hal, és nem angolna. Ebben segít a not(érték) szabály, mely a benne lévõ állítás igazság-értékét fordítja meg:

vanneki(X,pikkely):- hal(X), not(X=angolna).

A not-on belül már nem mûködik a mintaillesztés, csak helyben kiértékelhetõ állításokat tartalmazhat, vagyis nem tartalmazhat szabad paramétereket. Ezért a két feltétel sorrendje nem cserélhetõ fel, mire a kiértékelés a not-ra kerül, X már értéket kapott.

A , (vesszõ) helyett and is használható, logikai és kapcsolatot jelent. Egy szabályon belül több, vesszõvel elválasztott rész-állítás lehet. A Prolog a következõképpen dolgozza fel az ilyen szabályokat: balról jobbra haladva mindegyik állításra keres megoldást. Példánkban tegyük fel a Vanneki(X, pikkely) kérdést. hal(X)-re talált X=ponty megoldást. Ezután a következõ állításra lép: not(X=angolna). Mivel a ponty=angolna tényleg hamis, ez az állítás is teljesül. A szabálynak vége, X=ponty kiírható. Ezután a Prolog visszalép, és keresi a következõ jó megoldást. X=csuka is megfelel. Végül X=angolna helyettesítéssel lép tovább, not(angolna=angolna) most nem teljesül. Ezért a Prolog visszalép eggyel, és újabb megoldást keres hal(X)-re. Nincs több, így leáll.

Látható, hogy az összes jó megoldás meghatározásához a Prolog a backtrack algoritmusát használta: ha egy lépés jó, jobbra lép a következõ feltételhez, ha nem, visszalép eggyel, és keresi a következõ jó megoldást (ld. V. fejezet).

Az egymás után következõ, azonos nevû szabályok egymással vagy kapcsolatban állnak. Ha egy szabály kiértékelését a Prolog backtrack kereséssel befejezte, a következõ szabályra lép (hogyan teljesülhet még az állítás).

2. feladat: Milyen megoldásokat ír ki a Prolog, és milyen sorrendben?
? hal(X), hal(Y), not(X=Y)

3. feladat: miért hibás az abszolútérték következõ megvalósítása, és hogyan kellene helyesen leírni?
   abs(X,Y):- X>=0, Y=X.
   abs(X,Y):- Y=-X.

Ha nem akarjuk, hogy a Prolog visszalépjen, hogy a többi lehetõséget megvizsgálja, használhatjuk a ! jelet, mely visszalépés esetén leállítja a kiértékelést. A fenti program lehetséges kijavítása:
   abs(X,Y):- X>=0, !, Y=X.
   abs(X,Y):- Y=-X.
Így ha X>=0, de Y=X nem teljesül, nem lép vissza, hogy a következõ sorban rossz megoldást találjon.

A rekurzió természetes velejárója a Prolog programozásának, hiszen ciklus egyáltalán nincs benne. (ld. a III. fejezetet!) Nézzük meg a faktoriális megvalósítását:

A Prolog csak logikai (igaz/hamis végeredményû) függvényeket ismer. A faktoriális-függvényt úgy kell megfogalmaznunk, hogy „mikor teljesül, hogy A faktoriálisa B?" Ha a Prolog már talált egy megoldást, meg kell akadályoznunk, hogy Z=A-1 helyettesítést újból végrehajtva a végtelen visszalépés hibát okozzon, ez a szerepe !-nek. Továbbá, a két szabály sorrendje sem mindegy: ha a második szabályt tettük volna elõre, az, mivel minden esetben illeszthetõ, szintén végtelen rekurziót okozott volna.

4. feladat: készítsd el Prologban a hatványozás függvényét (természetesen csak egész kitevõkre, az alap lehet real)!

Egy leszármazási sort írnak le a következõ típusú tények. A gyereket mindig az apánál jelezzük (feltételezzük, hogy minden gyerek boldog házasságban jött világra):
   gyereke(apa,gyerek)
   felesege(apa,anya)

A közvetlen leszármazott definíciója:
   leszarm(F,GY):- gyereke(F,GY).
   leszarm(F,GY):- felesege(A,F), gyereke(A,GY).

5. feladat: írd meg az Utod(F,U) függvényt (F-nek U közvetlen vagy nem közvetlen leszármazottja). Töltsd föl tényekkel az adatbázist, és teszteld!

6. feladat: írd meg és teszteld a következõ függvényeket!
   Anyja(F,GY) (F anyja GY-nek)
   Anya(F) (F valakinek (_) az anyja)
   Rokon(X,Y) (X-nek és Y-nak van közös õse)
   Testver(X,Y) (X és Y szülei közösek)
   Unoka(F,GY) (F-nek GY unokája)
   Utod2(F,GY,N) (F-nek GY N-edik generációs utódja)

Második nekifutás: mûveletek listákkal

A Prolog jellemzõ adatszerkezete a lista. A DOMAINS szekcióban a lista típusa alaptípus*-gal jelölt (pl. symbol*). Lista kétféleképpen adható meg: [elem, elem,...], vagy [elem, elem... | maradéklista] (ld. a III. fejezet LOGO-beli listáit). Mintaillesztésnél a lista egyes komponenseire külön-külön lehet illeszteni, pl. [a,b,c] listára [E|L] illesztésével E=a, L=[b,c] lesz az eredmény. Példa lista elemszámára:

DOMAINS
   l=symbol*
PREDICATES
   elemszam(l,integer)
CLAUSES
   elemszam([],0).
   elemszam([_|L],N):- elemszam(L,X), N=X+1.

Figyeljük meg _ használatát paraméter helyett. Szabad paraméter használata felesleges lett volna, mert az illesztett értéket sehol nem használnánk fel. ! használatára viszont nem volt szükség az ismételt rekurzió elkerülésére, mert az üres lista nem illeszthetõ a második szabályra.

1. feladat: írd meg eleme(elem,lista) függvényt, mely eldönti, a lista tartalmaz-e egy bizonyos elemet!

Vizsgáljuk meg, mi a hiba a következõ szabályban (hanyszor(elem,lista,szám), a lista szám-szor tartalmaz egy elemet):
   hanyszor(_,[],0).
   hanyszor(S,[E|L],N):- S=E, hanyszor(S,L,X), N=X+1.
   hanyszor(S,[_|L],N):- hanyszor(S,L,N).

Ha egy paraméter illeszkedik a 2. szabályra, az illeszkedni fog a harmadikra is. A jó megoldás után ezért a Prolog rossz megoldásokat is fog találni, kisebb számokkal, egészen 0-ig. Ez elkerülhetõ, ha a 3. szabályra csak akkor kerül sor, ha a 2. nem teljesült. Tehát a 3. szabály helyesen:
   hanyszor(S,[E|L],N):- not(E=S), hanyszor(S,L,N).
De ehelyett használhatjuk a !-et a 2. szabály végén (így ha a 2. szabály teljesül, több szabályt nem keres a Prolog). A szabályok feldolgozása gyorsítható, ha a 2. szabályban az E=S feltételt már a mintaillesztésnél feldolgozzuk. Így a 2. szabály:
   hanyszor(S,[S|L],N):- hanyszor(S,L,X), N=X+1.

2. feladat: írj függvényt, mellyel megadható egy lista valahányadik eleme! Ugyanezzel a függvénnyel megadható az is, hogy egy elem hányadikként szerepel a listában.
hanyadik(lista,elem,sorszám)

3. feladat: készíts függvényt, mellyel meghatározható egy számokat tartalmazó lista elemeinek összege!

Nézzük meg, hogyan lehet lista valahányadik elemét kivenni a listából:
   kivesz([_|L],1,L).
   kivesz([E|L],N,[E|L2]):- M=N-1, kivesz(L,M,L2).

4. feladat: készíts függvényt, mely két listát egy harmadikká kapcsol össze!

Látható, hogy a lista-mûveletek lényege (rekurzió, visszavezetés maradéklistára) azonos a LOGO-nál látottakkal, csupán logikai függvényként kell megfogalmazni.

Nézzünk most egy összetettebb példát: a repulo(varos,varos) tények megadják, hogy mely városok között van közvetlen repülõgép-összeköttetés. Ha A és B össze van kötve, a tények között nem szerepel repulo(B,A), de azért azt is élnek kell venni. Ezért definiáljuk a közvetlen járatra vonatkozó szabályt:
   jarat(A,B):- repulo(A,B).
   jarat(A,B):- repulo(B,A).
Ezután megfogalmazzuk, hogy mikor lehet eljutni egyik városból a másikba: ha van köztük közvetlen járat, vagy az egyikbõl el lehet jutni egy olyan közbensõ városba, amelybõl el lehet jutni a másikba:
   eljut(A,B):- jarat(A,B).
   eljut(A,B):- jarat(A,X), eljut(X,B)
.
Vigyázat: a leszármazási fánál ez a módszer mûködött. Ez azonban általános gráf bejárása, és mindenképpen el kell kerülnünk a köröket, különben végtelen rekurzió állhat elõ. Tehát a klasszikus gráfbejáró algoritmushoz hasonlóan itt is tárolnunk kell, hogy hol jártunk. Ez listában történik. A program mélységi keresést végez.
   eljut(A,B,L):- jarat(A,B), not eleme(B,L).
   eljut(A,B,L):- jarat(A,X), not eleme(X,L), L2=[X|L], eljut(A,B,L2).

A függvény hívása eljut(város,város,[]) formában történik.

Harmadik nekifutás: barátságosabb programok

A Prolog programban szerepelhet egy GOAL szekció is, mely a kiértékelendõ kifejezést tartalmazza. Ekkor a Prolog nem interaktív módban fut, vagyis nem tesz fel kérdést. A Turbo Prolog ebben az üzemmódjában csak egy jó megoldást keres, és azután nem lép vissza. A felhasználói felülethez a Prolog tartalmaz olyan függvényeket, melyeken a kiértékelés „átlép", és mellékhatásuk miatt lényegesek (pl. kiírnak valamit). Ilyenek:

Klasszikus példa: a program számokat kér be, kiírja a reciprokukat, kivéve a 0-t, mert akkor leáll. A végtelen ciklust végtelen rekurzióval oldottuk meg.

PREDICATES
   megold
CLAUSES
   megold:- readint(X), not(X=0), Z=1/X, write(Z), nl, megold.
GOAL
   megold

Végül nézzük meg, hogyan lehet az összes megoldást kiíratni GOAL üzemmódban.

GOAL
   eleme(E,[a,b,c,d,e]), write(E), nl

Ekkor a program csak a lista elsõ elemét írja ki, mert az elsõ megoldásnál leáll. A Prolog FAIL függvénye mindig No-ra értékelõdik ki, ezt használhatjuk arra, hogy attól a ponttól visszalépjen, mintha nem talált volna megoldást. A következõ javítással a program kiírja a lista összes elemét:
   eleme(E,[a,b,c,d,e]), write(E), nl, Fail

A vagy kapcsolatot (or, vagy pontosvesszõ) arra használhatjuk, hogy megtakarítsuk vele az azonos függvényfejre való ismételt mintaillesztést. Az elõzõ rész Jarat függvénye ennek felhasználásával:
   jarat(A,B):- repulo(A,B); repulo(B,A).

Ez pontosan úgy mûködik, mint az elõzõ oldali változat.

Elõzõ fejezet
Tartalomjegyzék
Honlap