Unifikace -=-=-=-=-=- Computers are useless. They can only give you answers. -- Pablo Picasso Možná vás zajímá, jak prolog vlastně najde výsledek. Postup není tak přímočarý, jako u procedurálních jazyků, ale přesto celkem jednoduchý. Jeho základem je unifikace. Pokud zadáte dotaz predikát(parametry), prolog začne prohledávat databázi a hledat definice predikátů, dokud nenajde odpovídající jméno a počet parametrů. Potom se pokusí predikát zunifikovat z vaším dotazem. Postupuje tak, že nejprve se pokusí zunifikovat jméno predikátu a potom jednotlivé dotazy v jeho těle (postupně). Unifikace probíhá podle následujících pravidel: Dva termy jsou unifikovatelné kdyz: 1) jsou shodné 2) poměné v obou termech můžu substituovat tak, aby platilo 1) Rozhodnutí zda S a T jsou unifikovatelné probíhá tedy takto: 1) Pokud S i T jsou stejné konstanty, potom jsou unifikovatelné 2) Pokud S je proměná a T je term, potom jsou unifikovatelné a provede se substituce S=T (a obráceně) 3) Pokud S a T jsou struktury potom jsou unifikovatelné pokud a) mají shodný funktor a počet parametrů b) odpovídající dvojice parametrů jsou unifikovatelné Základní trik je v tom, že v případě, že prolog neuspěje a nějaké dva termy uprostřed těla predikátu se nepodaří vyplnit, vrátí se zpět až k poslední volné proměné a zkusí udělat jinou substituci, pokud ani to neuspěje, zkusí další atd. až vyčerpá všechny možnosti. Potom se vrátí ještě o proměnou zpět. V případě že se vrátí úplně na začátek, pokusí se najít další definici predikátu a teprve v případě, že už další neexistuje, neuspěje. Postupuje tedy backtracingem. Můžu napsat následující rekurzivní definici potomka: r(a,b). % (r1) rodic(anna, bohous). r(b,c). % (r2) rodic(bohous,cyril). r(c,d). % (r3) rodic(cyril,dana). p(X,Y) :- r(X,Y). % (p1) potomek(X,Y) :- r(X,Y). p(X,Y) :- r(X,Z), p(Z,Y). % (p2) potomek(X,Y) :- r(X,Z), potomek(Z,Y). Tady je poprvé, co jsme použili dvou podmínek pro definici potomka. Prolog se napřed pokusí naplnit první - tedy X je potomkem Y pokud Y je rodič X. A teprve v případě neúspěchu vezme rodiče X (Z) a zkusí zjistit stejným postupem, jestli Z je potomek X. Tedy pokud se budu ptát ob tři generace, třikrát se provede druhá podmínka, než se podaří unifikovat první. Takto bude probíhat výpočet: (novy cíl, podle které klauzule, lokální substituce) ?- p(a,X). %vyhledej všechny potomky anny r(a,X) (p1) {X1/a,Y1/X} úspěch,"X=b" (r1) {X/b} r(a,Z1),p(Z1,X) (p2) {X1/a,Y1/X} p(b,X) (r1) {Z1/b} r(b,X) (p1) {X2/b, Y2/X} úspěch,"X=c" (r2) {X/c} r(b,Z2),p(Z2,X) (p2) {X2/b,Y2/X} p(c,X) (r2) {Z2/c} r(c,X) (p1) {X3/c, Y3/X} úspěch,"X=d" (r3) {X/d} r(c,Z3),p(Z3,X) (p2) {X3/c,Y3/X} p(d,X) (r3) {Z3/d} r(d,X) (p1) {X4/d,Y4/X} neúspěch ... "no" Znalosti tohoto postupu lze zneužívat pro různé účely. Můžu programy různě optimalizovat. Například můžu napsat i následující definici potomka: p(X,Y) :- r(X,Z), p(Z,Y). % (p2) potomek(X,Y) :- r(X,Z), potomek(Z,Y). p(X,Y) :- r(X,Y). % (p1) potomek(X,Y) :- r(X,Y). (pravidla jsou prohozena) Tato definice je samozřejmě také správně. Ale v případě, že se budu ptát, jestli Cyryl je potomek Bohouše, první podmínka napřed dojde až na začátek rodokmenu, tedy k Anně a pak se bude postupně vracet. Provádění tedy bude pomalejší. Můžu napsat ještě jednu definici: p(X,Y) :- p(Z,Y), r(X,Y). % (p2) potomek(X,Y) :- potomek(Z,Y), r(X,Z). p(X,Y) :- r(X,Y). % (p1) potomek(X,Y) :- r(X,Y). I tato definice je správně, ale nebude fungovat, protože prolog na to, aby zjistil, jestli X je potomek Y, napřed se pokusí vyhledat všechny potomky X, ale to se mu nepodaří, protože rekurze nikdy nezkončí - Poprvé se za Z v p(Z,Y) dosadí volná proměná. Potom se ale bude hledat potomek kohokoliv a teprve později zjištovat, jestli je opravdu potomkem X, zůstane tedy volná proměná a znova se bude hledat potomek kohokoliv. Z toho vyplívá, že je dobré vždy psát konkrétnější definice napřed - Prolog má potom méně možností. Také je dobry vždy psát ukončující podmínku rekurze před její definicí. Unifikaci můžu zneužít i pro jiné věci. Například pro emulaci smyčky. Pokud chci v bludišti od textovky vypsat všechny cesty vedoucí ven z místnosti, můžu napsat například: connect(zachod,zahrada). connect(zachod,chodba). zachod<--->chodba<--->zahrada connect(chodba,zachod). | ^ connect(zahrada,chodba). +----------------------+ connect(chodba,zahrada). cesty(L) :- %vypíše všechny cesty s místnosti connect(L,X), %zjistí cestu z L a do X uloží cil-tady se dela backtracin write(X), %vypíše cílovou místnost write(' '), %oddělí slova fail. %selže,aby se systém pokusil najít další cestu a také ji v cesty(_) :- %tohle se vyvolá,když už není žádná cesta k vypsání nl. %nový řadek-konec výpisu Predikát cesta používá několik vestavěných predikátů v prologu. Write slouží k vypsání daného termu, nl vypíše novou řádku a fail je predikát, který je vždy "no". V tom je právě ten fígl. Predikát cesty se napřed najde první cestu z L do X - connect(L,X). Potom write tuto cestu vypíše a nakonec se najede na fail. Prolog se tedy vrátí na nejbližší rozcestí - connect(L,X) a zkusí jinou cestu, tu také vypíše a vrátí se zpět atd. až nakonec už žádná další cesta není a prolog tedy najde další definici, ověří platnost predikátů nl, ten vypíše novou řádku a uspěje. Výpočet je tedy hotov. výheň