Prolog – seznamy Jan Hric, 1997 – 2010b KTIML MFF UK URL:
není rozlišován vstupní vs. výst. argument + vstupní argument - výstupní argument +- arg. obsahuje volné proměnné ? cokoli mod predikátu a volání př: faktorial(+,?), rodic(-,-) - systém to nekontroluje, konvence programátora Tok dat
Tok dat - příklad definice rodic/2 predek(X,X). % 1. varianta predek(X,Y):- rodic(X,Z), predek(Z,Y). predek(X,X). % 2. varianta predek(X,Y):- rodic(Z,Y), predek(X,Z). volání p(+,+), p(+,-), p(-,+), p(-,-) stejné 1. lepší 2. lepší ?- p(adam,X) vs. ?- p(X,cyril) vhodnost závisí na zamýšleném (příp. obvyklém) použití jiné varianty (přehození cílů, klauzulí) jsou horší
Operace na seznamech zjistit, zda X je prvkem seznamu L % member(?X,+L) member(X,[X|_]). % X je hlavou member(X,[_|L]):- member(X,L). % X je v těle ?- member(b,[a,b,c]). % m(+,+) yes ?- member(X,[a,b]). % m(-,+) X=a; % generovani X=b; no
Zpracování rekurzívních dat. struktur - rekurzívní d.s. se zpracovávají rek. programy - koncové případy jsou ošetřeny fakty anebo nerekurzívní klauzulí - aspoň jeden fakt anebo nerek. klauzule ! - každý možný tvar d.s. (konstruktor) má být ošetřen - jinak neúspěch - např. seznamy: []/0,./2 stromy: void/0, t/3 - u member/2 ošetření [] není nutné (ekv. fail) - není to obvyklé
Member/2 - pokrač. - příklad podspecifikovaného cíle ?- member(a,L). % m(+,-) L = [a|_1]; L = [_1,a|_2]; L = [_1,_2,a|_3];... atd. - vydává se “nekonečně” mnoho řešení backtrackingem - _číslo je výpis volné proměnné
member – pro asociativní paměť Položky seznamu: klíč-hodnota % -(k,h) ?- Tab= [k1-h1,k2-h2,k2-h2b], % generovat member(k2-HN,Tab).% ke kN najde HN HN = h2; HN = h2b; No - Zde využíváme backtracking, ale lze vracet i všechny hodnoty najednou, v seznamu % lookup(Klic, Pamet, Hodnota) : interface pro vyhledávání v nějaké datové struktuře, nejen seznamu (BVS) lookup(K, Tab, H):- member(K-H, Tab). DC: lookup2D/3: vyhledávání pro dvousložkový klíč, v rovině
První a poslední prvek seznamu % first(?X,+L) :- X je první v L a) first(X,[X|_]). % dobrý styl b) first(X,L):- L=[X|_]. % last(?X,+L) :- X je poslední v L last(X,[X]). last(X,[_|L]):- last(X,L). %nesymetrický přístup: O(1) na první prvek vs. O(n) na poslední
Spojení seznamů - append(L1,L2,L3) spojí L1 a L2 do L3 append([],L,L). append([X|L1],L2,[X|L3]):- append(L1,L2,L3). ?- append([a,b],[c,d],[a,b,c,d]). %(+,+,+) yes ?- append([a,b],[c,d],L). % mod (+,+,-) L=[a,b,c,d]; no - programátorské idiomy: strukturální rekurze podle prvního arg., tvorba výsledku - skládáním substitucí -Pozn.: polymorfizmus – analyzuje a využívá se pouze „horní“ struktura
?- X=[a,b], Y=[c,d], append(X,Y,Z). Z=[a,b,c,d] tvorba Z - skládáním substitucí Z = _L3´ Z = [a|_L3´´] Z = [a,b|_L3´´´] Z = [a,b,c,d] X Z Y.. \. / \. / \.. a / \ c / \ [] [] b d
append/3 jako (nederministický) generátor ?- append(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=[]; no
concat/2 – Spojení seznamů - převod (řádkové) matice na vektor % concat(+L,-K) – seznam seznamů L spojí do jednoúrovňového seznamu K concat([], []). concat([Xs|L1],L0) :- append(Xs,L2,L0), concat(L1,L2). ?- concat([[1,2],[5],[],[3,4]],Lout). Lout=[1,2,5,3,4] -Počas výpočtu je L0 seznam s volným koncem -L0=[1,2|L2’] ~> [1,2,5|L2’’] ~> [1,2,5,3,4|L2’’’] ~> [1,2,5,3,4] -Nelze použít concat(-,+) pro rozdělení; generuje Xs=[]
Obracení seznamu % reverse(L,R):- R je L pozpátku % mod (+,-) reverse([],[]). reverse([X|L],R):- reverse(L,R1), append(R1,[X],R). - nepříjemnost: složitost O(n^2) - typická struktura rekurzívních klauzulí pred :- předvýpočet, % analýza d.s. pred, % rek. výpočet mezivýsl. postvýpočet. % použití -”-
Obracení seznamu II - lineárně reverse(L,R):- rev1(L,[],R). rev1([],R,R). % mod (+,+,-) rev1([X|L],A,R):- rev1(L,[X|A],R). - programátorská technika - akumulátor: “A” - inicializace - samostatná klauzule/predikát - odstínění uživatele od technických detailů - předávání (volné) výstupní prom. v rek.: “R” - obvykle samostatný argument - ukončení - předání: “R:=A” %3.arg := 2.arg x skládání substitucí - přístup na akum. počas výpočtu
Vypouštění prvku % delete(X,I,O):- vypuštěním X ze seznamu I je seznam O; X musí být v I, vypouštíme jeden (lib.) výskyt % mody vypouštění (?,+,-), vkládání (?,-,+); presneji (?,[?],-) delete(X,[X|I],I).%1 X musí být v arg2 delete(X,[Y|I],[Y|O]):-%2 delete(X,I,O). ?- delete(X,[1,2,3],O). % 3 výsledky ?- delete(a,I,[b,c]). %vkládání,3x Varianta: delete1/3: vypustí prvek a vždy uspěje delete1(X,[],[]).%3+^1+^2: falešné matche! Nekorektní další výsledky; správnost všech v.! DC: deleteAll(X,Lin,Lout);
Permutace % perm(I,O):- O je permutací I, (+,-) perm([],[]). perm(I,[X|O]):- delete(X,I,I1), perm(I1,O). ?- perm([1,2,3,4],O). - skládání substitucí - rekurze podle výstupu - nederminizmus: řešení se vrací postupně - jiná (těžší) možnost: vrátit najednou seznam všech permutací - DC: permutace rekurzí podle vstupu. V jakém pořadí budou vydávány?
Použití append/3 last(X,L):- append(_,[X],L). - stejná asymptotická složitost - horší konkrétní složitost, ošetřujeme v append 1.arg. zbytečně delete(X,I,O):- append(L1,[X|L2],I), append(L1, L2, O). - L1 se prochází 2x - nejde použít v modu (?,-,+) - muselo by se napsat jinak: prohodit cíle v těle prefix/2, suffix/2 - zdarma (bez rekurze) prefix(P,L):- append(P,_,L). Pozn. SwIng: budování konkrétních predikátů zvrchu vs. obecné predikáty zespodu (do knihoven) a příp. interface. Při (častých) změnách méně výkonného kódu ~> méně úprav a chyb
Seznam výsledků % suffix(+L,-S) – S je přípona L, backtrackingem suffix(S,S).% zahrnuje suffix([],[]). suffix([_|L],S):- suffix(L,S). ?- suffix([1,2,3],L). L=[1,2,3] ; L=[2,3] ; L=[3] ; L=[] ; no % suffixy(+L,-Ss) – Ss jsou všechny prípony L v seznamu suffixy([],[[]]). suffixy([X|Xs],[[X|Xs]|Ss]):- suffixy(Xs,Ss). ?- suffixy([1,2,3],Ss). Ss=[[1,2,3],[2,3],[3],[]]% typicky vhodnější
Prefixy % prefixy(+Xs,-Ps)- Ps je seznam všech předpon Xs prefixy([],[[]]). prefixy([X|Xs],[[]|P0]) :- prefixy(Xs,P1), map_pridejH(X,P1,P0).% (1) prefixy v P1 upravíme % přidáva hlavu ke každému seznamu v P1 map_pridejH(_X,[],[]).% jednoúčelové map map_pridejH( X,[P|Ps],[[X|P]|Ps0]):- map_pridejH(X,Ps,Ps0). ?- prefixy([1,2,3],Ps). Ps=[[],[1],[1,2],[1,2,3]] - Průběžně: Ps’=[[],[2],[2,3]], Ps’’=[[],[3]], Ps’’’=[[]] - (1) obecné map:..,map(pridejH(X),P1,P0),… -pridejH(X,L,[X|L]). % mimo logiku 1. řádu
Seznam prvků ve stromu tree2list(Strom,Seznam) :- do Seznamu pozbírá prvky Stromu (zleva doprava) tree2list(void,[]). tree2list(t(L,X,R),O):- tree2list(L,OL), %rekurze doleva tree2list(R,OR), %rekurze doprava append(OL,[X|OR],O). % spojení inorder - seznam jako výsledek -pre-,post-,inorder (zleva): rozdíl toku programu a “toku” dat -append/3 na stejném místě, ale s jinak předávanými parametry - pro generování stromů (mod (-,+) ) nutno prohodit cíle - pro repr. stromů použít funkční symbol t/3, a void/0 - nevhodný je (zde trojprvkový) seznam, pokud je počet položek pevný ?- TestData = t( t(void,1,void), 2, t(void,3,void)), tree2list(TestData,V).
Výroba vyváženého stromu - z (uspořádaného) seznamu L chci vyvážený strom T % vyvazBS(+L,-T). vyvazBS([],void). %vyvazBS([X],t(void,X,void)). %konc.podm.není nutná vyvazBS(L,t(TL,X,TR)):- rozdel(L,L1,[X|L2]),% rozdělí na poloviny % pro uspořádaný seznam L: X je medián vyvazBS(L1,TL), vyvazBS(L2,TR). - vyvazBS/2 lze použít v heapsortu pro vybudování správné struktury „haldy“ (bez uspořádání)
Rozdel/3 -rozdel(L,L1,L2) – rozdělí L na první a druhou polovinu, |L1|<=|L2|<=|L1|+1 -Použije v rozdel1/4 druhý arg. jako čítač rozdel(L,L1,L2):- rozdel1(L,L,L1,L2). % interface rozdel1(L2,[],[],L2).% sudá délka rozdel1(L2,[_],[],L2).% lichá délka rozdel1([X|L],[_,_|Acc],[X|L1],L2):- rozdel1(L,Acc,L1,L2).
Množinové výrazy -Výraz obsahuje +/2 pro sjednocení, */2 pro průnik, -/2 pro rozdíl, seznam pro množinu a i(D,H) pro celočíselný interval. - evalS: interpret množinových výrazů, term je program porovnejte s Aho-Corasick: vzorky (1) jsou program (v neplnohodnotném jazyce, doménově specifický jazyk - DSL), který dokonce dokážeme přeložit ad(1): vzorky jsou seznam seznamů (reprezentací) znaků, např. [[h,e],[s,h,e],[h,e,r]] ?- evalS(-(+([3,1,3],i(2,4)),[2,3]), S). S = [1,4] % synt. cukr: zápis s operátory ?-evalS(([3,1,3]+ i(2,4))–[2,3], S). evalS(L,S):- isList(L), list2set(L,S). % převod na (usp.) množinu evalS(i(D,H),S) :- genInterval(D,H,S). evalS(+(E1,E2),S):-evalS(E1,S1), evalS(E2,S2),union(S1,S2,S). evalS(-(E1,E2),S):-evalS(E1,S1), evalS(E2,S2), rozdilS(S1,S2,S). evalS(*(E1,E2),S):-evalS(E1,S1), evalS(E2,S2),prunik(S1,S2,S). -lze použít pro uspořádané i neuspořádané množiny na výst. -Impl. se liší ve výkonných pred. union/3, …, list2set/2, genInterval/2. -DC: interpret multimnožinových výrazů
Shrnutí - tok dat, mody - dvojí použití predikátů: výpočet, test - problémy s podspecifikovanými argumenty v cíli - nedeterminizmus - zpracování rek. struktur - strukturální rekurze - tvorba výsledků - skládáním substitucí - akumulátor -použít interface: pokud rekurzi nejde použít přímo (např. je potřebný „lokální“ argument) - dobrý styl: nezatěžovat uživatele „technickými“ zbytečnostmi - seznam výsledků, místo vracení backtrackingem - o rekurzi víte vše, dále vestavěné predikáty, programátorské techniky a příklady
Autotest naprogramujte permutace pomocí vkládání prvku a akumulátoru návod: použijte akumulátor na permutaci dosud zpracovaného prefixu vstupního seznamu napište rozdělení prvků seznamu na sudé a liché (podle pořadí) - v jednom průchodu seznamem - správně ošetřete seznam liché délky - použití v “rozděl a panuj”: mergesort ? je append/3 použitelný v modu (+,-,-) ? jak se bude chovat volání: append([a,b],X,Y) - pokud má otázka smysl