Ezúttal csak egy témánk van, de az nagyon fontos, és lesz hozzá sok feladat :)
Ahogy a múltkor láttuk, egy Prolog kifejezés vagy egyszerű (konstans vagy változó), vagy egy fix aritású összetett struktúra. Gyakran előfordul azonban, hogy nem tudjuk előre, hogy hány adattal akarunk foglalkozni. Hogyan tudnánk dolgoknak egy listáját egy struktúrával kifejezni? Természetesen rekurzívan! Kezdjük először csak két dologgal:
lista(a, b)
Ha hármat akarunk beletenni egy listába, akkor megtehetnénk, hogy eggyel több argumentumot veszünk bele:
lista(a, b, c)
… de ez több okból sem igazán jó megoldás. Egyrészt ez így egy másik funktor lesz (hiszen más az aritása), másrészt ezzel még mindig csak ismert hosszúságú listákat tudnánk készíteni. Helyette csinálhatjuk viszont a következőt:
lista(a, lista(b, c))
Ezt tetszőlegesen lehet folytatni, pl.
lista(a, lista(b, lista(c, lista(d, e))))
Ez egyrészt azért jó, mert így mindig egy 2-aritású lista funktorunk van, másrészt ezzel tudunk olyat írni, hogy
lista(a, Maradék)
ami egy tetszőleges lista, aminek az első eleme a, vagy
lista(a, lista(b, Maradék))
ami egy olyan tetszőleges lista, aminek az első két eleme a és b.
Ennek így egy szépséghibája van: az utolsó elem kezelése kicsit más, mint a többié, és emiatt pl. az előző példákban a “tetszőleges” lista mégsem teljesen tetszőleges, mert nem lehet egy- ill. kételemű. Ezt azzal tudjuk megoldani, hogy bevezetünk egy olyan konstanst, amivel a lista végét jelöljük. Pl. az a, b és c elemeket tartalmazó lista ekkor így néz ki:
lista(a, lista(b, lista(c, vége)))
A szokásos Prolog jelölés a lista helyett a pont (.) funktor, a vége helyett pedig a szögletes zárójelpár ([]) atom. Az SWI-Prolog azonban a pontot lecserélte a kicsit furcsán kinéző '[|]' funktorra. Az előző listát tehát erre átírva ilyet kapunk:
'[|]'(a, '[|]'(b, '[|]'(c, [])))
Hát ez elég szörnyen néz ki. Szerencsére van rá egy egyszerűsített jelölés (ami kicsit magyarázza a funktor névválasztását):
?- '[|]'(X, Y) = [X|Y].
trueSőt, nem csak
?- '[|]'(a, '[|]'(b, '[|]'(c, []))) = [a | [b | [c | []]]].
trueteljesül, hanem az egymásba ágyazást vesszővel lehet helyettesíteni:
?- [a | [b | [c | []]]] = [a, b, c | []].
trueÉs végül van még egy utolsó “egyszerűsítés” (a szakszó erre a syntactic sugar avagy szintaktikus cukor): ha a függőleges vonal jobboldalán a [] van, akkor ezt el lehet hagyni:
?- [a, b, c | []] = [a, b, c].
trueEz tehát a Prolog láncolt lista struktúrája; a [] neve “üres lista”, és egy [X|Y] listában az X a lista eleje (angolul head, fej), az Y pedig a lista maradéka (angolul tail, farok). A maradék mindig vagy egy lista, vagy az üres lista atom.
A gyakorlatban mindig ezt az egyszerű formát használjuk, de fontos érteni, hogy ez valójában egymásba ágyazott funktorokból áll, és ezért pl. [a, b, c] = [a | [b, c]] = [a, b | [c]] = [a, b, c | []].
Az egyik legfontosabb kérdés, amit listákkal kapcsolatban feltehetünk, az az, hogy valami benne van-e:
tartalmaz(X, [X|_]).
tartalmaz(X, [_|Maradék]) :- tartalmaz(X, Maradék).Szavakban megfogalmazva, egy lista akkor tartalmaz valamit, (i) ha az az eleje, vagy (ii) ha a maradéka tartalmazza azt. Ezzel
?- tartalmaz(b, [a, b, c]).
true
?- tartalmaz(b, [a, [b, c]]).
false
?- tartalmaz([b, c], [a, [b, c]]).
trueEmellett a
?- tartalmaz(X, [a, b, c]).kérdésre megkapjuk az X = a, X = b és X = c megoldásokat, sőt, meg is fordíthatjuk, és feltehetjük a kérdést, hogy “Milyen listák tartalmazzák a-t?”
?- tartalmaz(a, L).Erre a következő (jellegű) megoldásokat kapjuk:
L = [a | Maradék]
L = [X, a | Maradék]
L = [X1, X2, a | Maradék]
L = [X1, X2, X3, a | Maradék]
...
Az első egy tetszőleges lista, aminek az első eleme a; a második egy olyan, aminek a második eleme a és így tovább.
Feltehetünk összetett kérdéseket is, pl. milyen háromelemű listák vannak, amelyek tartalmazzák az a, b és c elemeket?
?- L = [_, _, _], tartalmaz(a, L), tartalmaz(b, L), tartalmaz(c, L).
L = [a, b, c]
L = [a, c, b]
L = [b, a, c]
L = [c, a, b]
L = [b, c, a]
L = [c, b, a]Két listát össze is tudunk csatolni. Legyen hozzáfűz(L1, L2, L3) igaz akkor, ha L1 és L2 egymás után rakva L3-at adja, pl.
?- hozzáfűz([a, b, c], [d, e], [a, b, c, d, e]).
true
Ha L1 üres, akkor L2 és L3 megegyezik:
hozzáfűz([], L2, L2).Egyébként L1 első eleme az L3 első eleme lesz; az L3 maradéka pedig az L1 maradékának és az L2-nek az összefűzéséből adódik:
hozzáfűz([X|M1], L2, [X|M3]) :- hozzáfűz(M1, L2, M3).Annak ellenére, hogy onnan indultunk, hogy hogyan lehet két listát összefűzni, a kérdés ismét megfordítható, pl. “Hogyan lehet egy listát két listára osztani?”
?- hozzáfűz(L1, L2, [a, b, c]).
L1 = [],
L2 = [a, b, c]
L1 = [a],
L2 = [b, c]
L1 = [a, b],
L2 = [c]
L1 = [a, b, c],
L2 = []Vagy: “Igaz-e, hogy az [a, b] listával kezdődik az [a, b, c] lista?”
?- hozzáfűz([a, b], _, [a, b, c]).
trueMegkereshetjük vele egy listában az előző és következő elemet:
?- hozzáfűz(_, [Előző, már, Következő | _],
[jan, feb, már, ápr, máj, jún, júl, aug, szep, okt, nov, dec]).
Előző = feb,
Következő = áprA tartalmaz szabályt is leírhatjuk a segítségével:
tartalmaz(X, L) :- hozzáfűz(_, [X|_], L).Szavakban: egy L lista akkor tartalmaz egy X elemet, ha szétválasztható két listára, amiből a másodiknak az első eleme X. (Az első lista ill. a második maradéka is lehet üres.)
Új elemet egy lista elejéhez olyan könnyű hozzáadni, hogy erre nem is szokás külön szabályt írni, de ha akarunk, itt van:
hozzáad(X, L, [X|L]).A listák végére (hatékonyan!) beszúrni kicsit bonyolultabb, majd később lesz róla szó.
Hogyan kell törölni egy elem egy előfordulását egy listából?
töröl(X, [X|M], M).
töröl(X, [Y|M], [Y|M1]) :- töröl(X, M, M1).Tehát: ha X a lista első eleme, akkor a törlés után a lista maradékát kapjuk. Egyébként a lista első eleme és az eredmény első eleme megegyezik, és az eredmény maradéka pedig ugyanaz, mint az eredeti lista maradéka, amiből kitöröltük az X-et.
Egy példa a használatára:
?- töröl(a, [a, b, a, a], L).
L = [b, a, a]
L = [a, b, a]
L = [a, b, a]Itt a második és harmadik megoldás azonosnak tűnik, de valójában az egyik az eredeti listából az a elem második, a másik pedig a harmadik előfordulását törölte.
Mi történik, ha az “eredeti” listát vesszük ismeretlennek?
?- töröl(a, L, [1, 2, 3]).
L = [a, 1, 2, 3]
L = [1, a, 2, 3]
L = [1, 2, a, 3]
L = [1, 2, 3, a]Ahogy látszik, a “Mi az a lista, amiből ha kitörlünk egy a-t, akkor [1, 2, 3]-at kapunk?” kérdés egyszerűbben úgy fogalmazható meg, hogy “Mit kapunk, ha az [1, 2, 3] listába beleteszünk egy a-t?”
Ez van annyira hasznos, hogy adhatunk neki egy új nevet:
betesz(X, L, L1) :- töröl(X, L1, L).A törlés használható kiválasztásra is:
?- töröl(X, [a, b, c], _).
X = a
X = b
X = cHa a törölni kívánt elem nem szerepel a listában, az eredmény false lesz. Ezt kihasználva ezzel is definiálhatjuk a tartalmaz szabályt:
tartalmaz(X, L) :- töröl(X, L, _).Következőnek nézzük meg, hogy mikor része egy lista egy másiknak:
?- részlista([c, d, e], [a, b, c, d, e, f]).
true
?- részlista([c, e], [a, b, c, d, e, f]).
falseAhogy a második példából látszik, itt a “részét” nem úgy értelmezzük, hogy az első lista minden elemét tartalmazza a második (mint egy halmaznál), hanem hogy pontosan ugyanolyan sorrendben, más elemek közbeékelődése nélkül szerepelnek.
Ez a szabály könnyen megadható a hozzáfűz segítségével:
részlista(R, L) :- hozzáfűz(_, L1, L), hozzáfűz(R, _, L1).Tehát R akkor része L-nek, ha valamilyen listát elé- és utánafűzve megkapjuk L-et. A definíció ezt két részletben írja le: az első tag azt mondja, hogy az L az L1 listára végződik; a második pedig azt, hogy ez az L1 lista az R-el kezdődik. A vég kezdete pedig épp azt jelenti, hogy R valahol belül van L-ben (vagy valamelyik szélén, ha az itt alsóvonással jelölt listák egyike az üres lista).
Szokás szerint nézzük meg, mit kapunk a “fordított” felhasználásban:
?- részlista(R, [a, b, c]).
R = []
R = [a]
R = [a, b]
R = [a, b, c]
R = []
R = [b]
R = [b, c]
R = []
R = [c]
R = []Ahogy várható volt, ezzel megkapjuk az [a, b, c] lista összes részlistáját - az üreset többször is (miért? Tipp: nézzétek meg a hozzáfűz(X, _, [a, b, c]) kimenetét).
Két lista akkor permutációja vagy átrendezése egymásnak, ha ugyanazokat az elemeket tartalmazzák. Az üres listának csak az üres lista a permutációja:
permutáció([], []).Ha az első lista nem üres, akkor visszavezethetjük a feladatot az eggyel rövidebb listákra:
permutáció([X|M], P) :- permutáció(M, L), betesz(X, L, P).Tehát úgy kapjuk az első lista permutációját, hogy vesszük a maradék (M) egy permutációját (L), és ebbe betesszük az X-et.
Egy másik logika az lehet, hogy kiválasztunk (kitörlünk) egy elemet, a maradéknak vesszük egy permutációját, és az elejáre beszúrjuk a kitörölt elemet:
permutáció(L, [X|P]) :- töröl(X, L, M), permutáció(M, P).Ellenőrizzük, hogy működik-e! A
?- permutáció([piros, zöld, kék], P).kérdésre mindkettő visszaadja mind a 6 jó megoldást (bár különböző sorrendben) és közli, hogy nincs több. Viszont a fordított
?- permutáció(P, [piros, zöld, kék]).esetben az első verzió a 6 megoldás után végtelen rekurzióba kerül, a másodiknál pedig már az első után beragad. Meg lehet oldani, hogy mindig jó legyen, csak kicsit ki kell egészíteni, de mivel szimmetrikus, nincs rá igazán szükség.
Írjatok egy szabályt, amivel ki lehet venni egy listából az utolsó 3 elemet!
?- kivesz3([a, b, c, d, e], [a, b]).
trueÍrjatok egy szabályt, amivel ki lehet venni egy listából az első és utolsó 3 elemet!
?- kivesz33([a, b, c, d, e, f, g], [d]).
trueÍrjatok egy szabályt, amivel megkaphatjuk egy lista utolsó elemét! Készítsetek két verziót, egyet hozzáfűz-zel, egyet anélkül!
?- utolsó([a, b, c], c).
trueÍrjátok meg a páros_hosszú(L) és páratlan_hosszú(L) szabályokat! Ezek akkor igazak, amikor az L lista páros- ill. páratlan számú elemből áll (nincs szükség számokra hozzá).
Írjatok egy szabályt, ami megállapítja, hogy két lista megfordítottja-e egymásnak!
?- fordított([a, b, c], X).
X = [c, b, a]Írjatok egy szabályt, ami megállapítja, hogy egy szó palindróma-e, azaz visszafele is ugyanaz-e!
?- palindróma([g,ö,r,ö,g]).
trueÍrjatok egy szabályt, amivel egy listát eggyel “elforgathatunk” úgy, hogy az első lista első eleme a második végére kerül!
?- forgat([a, b, c, d], [b, c, d, a]).
trueÍrjatok egy szabályt, ami a részhalmazságot vizsgálja! Le is lehessen vele generálni az összes részhalmazt! (A halmaz itt egy olyan rendezett lista, amiben minden elem egyszer fordul elő.)
?- részhalmaz([a, b, c], R). % az eredmény lehet más sorrendben
R = [a, b, c]
R = [a, b]
R = [a, c]
R = [a]
R = [b, c]
R = [b]
R = [c]
R = []Írjatok egy szabályt, amivel ellenőrizhető, hogy két lista ugyanolyan hosszú!
?- ugyanolyan_hosszú([1, 2, 3], [a, b, c]).
trueÍrjatok egy szabályt, amivel ki lehet “lapítani” egy listát, tehát a belső listák elemeit kiviszi a legkülső szintre:
?- lapít([a, b, [c, d], [], [[[e]]], f], L).
L = [a, b, c, d, e, f]Ez a dokumentum az alábbi könyv 3.1-3.2 fejezete alapján készült:
I. Bratko: Prolog Programming for Artificial Intelligence, 4th Ed., Pearson, 2011.
A Prolog beépítve tartalmaz néhány hasznos szabályt, amit most elkészítettünk:
tartalmaz → memberhozzáfűz → append (és prefix(P, L) = append(P, _, L))töröl → selectpermutáció → permutation (de ez mindig jól működik)utolsó → lastfordított → reverseugyanolyan_hosszú → same_length˛lapít → flatten