330 likes | 461 Views
ackumulatorer. CM 3.7 AOP 8.3 (BBS 5.3, 6.2.2). dagens föreläsning. rekursion iteration ackumulatorer för effektivitet exempel. 1. summera (repetition). summerar alla tal mellan N och M: summera(N, N, N) . summera(N, M, Summa) :- N < M , N1 is N+1 , summera(N1, M, Delsumma) ,
E N D
ackumulatorer CM 3.7 AOP 8.3 (BBS 5.3, 6.2.2)
dagens föreläsning • rekursion • iteration • ackumulatorer för effektivitet • exempel
1. summera (repetition) • summerar alla tal mellan N och M: summera(N, N, N) . summera(N, M, Summa) :- N < M , N1 is N+1 , summera(N1, M, Delsumma) , Summa is N + Delsumma .
körning av summera ?- summera(1,5,X). 1 1 Call: summera(1,5,_454) ? 2 2 Call: 1<5 ? 2 2 Exit: 1<5 ? 3 2 Call: _981 is 1+1 ? ... 4 2 Call: summera(2,5,_973) ? 7 3 Call: summera(3,5,_3556) ? 10 4 Call: summera(4,5,_6139) ? 13 5 Call: summera(5,5,_8722) ? … 17 2 Exit: 15 is 1+14 ? 1 1 Exit: summera(1,5,15) ? X = 15 ?
effektivitet… antag följande körning: ?- summera(1, 1000, X1). prolog måste då lösa: summera(2, 1000, X2), X1 is 1+X2 summera(3, 1000, X3), X2 is 2+X3, X1 is 1+X2 o.s.v. tills: summera(999, 1000, X999), X998 is 998+X999, …, X2 is 2+X3, X1 is 1+X2
effektivitet… • detta ger en ganska lång ”att-göra-lista”! • när det tusende rekursiva anropet har nåtts, måste prolog ”komma ihåg” att göra 1000 additioner… • istället kan vi använda ackumulatorer genom att: • lägga till ett extra argument som representerar den delsumma vi fått hittills.
naturligt? • vi tar det första talet (t ex 1) • och lägger till det andra talet (2+1) • sedan lägger vi till 3 till summan av föregående steg (3+3) • osv: 4+6, 5+10, 6+15, …
summera med ackumulator accSummera(N, N, Acc, Summa) :- Summa is N + Acc . accSummera(N, M, Acc, Summa) :- N < M , N1 is N+1 , Acc1 is N + Acc , accSummera(N1, M, Acc1, Summa) .
körning av summera (igen) ?- summera(1,5,X). 1 1 Call: summera(1,5,_454) ? 2 2 Call: 1<5 ? 2 2 Exit: 1<5 ? 3 2 Call: _981 is 1+1 ? ... 4 2 Call: summera(2,5,_973) ? 7 3 Call: summera(3,5,_3556) ? 10 4 Call: summera(4,5,_6139) ? 13 5 Call: summera(5,5,_8722) ? … 17 2 Exit: 15 is 1+14 ? 1 1 Exit: summera(1,5,15) ? X = 15 ?
fix • summera2 kallar accSummera med ackumulatorn satt till 0 • mer om detta snart!
körning av accSummera ?- summera2(1,5,X). 1 1 Call: summera2(1,5,_454) ? 2 2 Call: accSummera(1,5,0, _454) ? 3 3 Call: 1<5 ? 3 3 Exit: 1<5 ? … 6 3 Call: accSummera(2,5,1, _454) ? … 10 4 Call: accSummera(3,5,3, _454) ? 14 5 Call: accSummera(4,5,6, _454) ? 18 6 Call: accSummera(5,5,10, _454) ? 1 1 Exit: summera2(1,5,15) ? X = 15 ?
slutsatser • lika många steg • men: minnesutrymmet konstant i ackumulatorvarianten (oberoende av antalet iterationer)
imperativa språk… int räknare = 0; while (räknare < 10) { räknare = räknare + 1; } return räknare; • prolog har inga ’storage variables’ (som t.ex. ’räknare’ ovan) • därför måste vi använda rekursion i prolog
2. length (repetition) • längden av en lista kan ges av predikatet length/2: length([],0) . length([_|Tail], N) :- length(Tail, N1), N is 1 + N1 .
length med ackumulator accLength([_|Tail],Acc,L) :- Acc1 is A+1, accLength(Tail,Acc1,L) . accLength([],A,A) .
sammanfattning • prolog tillhandahåller inte iteration • vi itererar med hjälp av den mer generella rekursionen • fördelen med ”sann” iteration är effektivitet eftersom: • för varje rekursivt anrop som inte terminerats måste en struktur (’stack frame’) hållas i minnet
sammanfattning (forts.) • alltså: ett rekursivt program växer linjärt. • en iterativ procedur använder en konstant mängd utrymme (oberoende av antal iterationer) • ett sätt att göra rekursiva procedurer iterativa är att använda ackumulatorer
ytterligare effektivitet • kan vi med hjälp av ackumulatorer ytterligare effektivisera programmeringen? • japp, i vissa fall:
3. reverse • [a,b,c,d] ska reverseras till [d,c,b,a] • två versioner: • naiveRev/2 • accRev/3 (rev/2)
naiveRev • basfall: • om vi reverserar [], så får vi tomma listan ([]) • rekursiva fallet: • om vi reverserar [H|Tail], så slutar vi med listan vi får av att reversera Tail och sätta ihop den med H.
naiveRev/2 • alltså: naiveRev([],[]). naiveRev([H|Tail],R) :- naiveRev(Tail,RevT), append(RevT,[H],R).
trace: naiveRev ?- naiveRev([a,b,c],R). 1 1 Call: naiveRev([a,b,c],_227) ? 2 2 Call: naiveRev([b,c],_699) ? 3 3 Call: naiveRev([c],_1069) ? 4 4 Call: naiveRev([],_1438) ? 4 4 Exit: naiveRev([],[]) ? 5 4 Call: append([],[c],_1069) ? 5 4 Exit: append([],[c],[c]) ? 3 3 Exit: naiveRev([c],[c]) ? 6 3 Call: append([c],[b],_699) ? 7 4 Call: append([],[b],_3515) ? 7 4 Exit: append([],[b],[b]) ? 6 3 Exit: append([c],[b],[c,b]) ? 2 2 Exit: naiveRev([b,c],[c,b]) ? 8 2 Call: append([c,b],[a],_227) ? 9 3 Call: append([b],[a],_5245) ? 10 4 Call: append([],[a],_5609) ? 10 4 Exit: append([],[a],[a]) ? 9 3 Exit: append([b],[a],[b,a]) ? 8 2 Exit: append([c,b],[a],[c,b,a]) ? 1 1 Exit: naiveRev([a,b,c],[c,b,a]) ? R = [c,b,a]
resultat 20 steg i tracen
accRev/3 accRev([H|Tail],A,R) :- accRev(Tail,[H|A],R). accRev([],A,A).
trace: accRev ?- accRev([a,b,c],[],R). 1 1 Call: accRev([a,b,c],[],_491) ? 2 2 Call: accRev([b,c],[a],_491) ? 3 3 Call: accRev([c],[b,a],_491) ? 4 4 Call: accRev([],[c,b,a],_491) ? 4 4 Exit: accRev([],[c,b,a],[c,b,a]) ? 3 3 Exit: accRev([c],[b,a],[c,b,a]) ? 2 2 Exit: accRev([b,c],[a],[c,b,a]) ? 1 1 Exit: accRev([a,b,c],[],[c,b,a]) ? R = [c,b,a]
resultat 8 steg i tracen
accRev lista ackumulator [a,b,c] [] [b,c] [a], dvs: [a|[]] [c] [b,a] [] [c,b,a] • ackumulatorn kopieras sedan till accRev/3 tredje argument • accRev([],A,A). • vi effektiviserar alltså bort ’append’
ett vanligt ”fix” % dölj ackumulatorn vid användning rev(L,R) :- accRev(L,[],R). • rev/2 anropas alltså med samma antal argument som vid naiveRev/2
jämförelse • naiveRev (3 element): 20 instruktioner • accRev (3 element): 8 instruktioner • naiveRev (8 element): 90 instruktioner • accRev (8 element): 18 instruktioner • och som sagt: minnesutrymmet växer linjärt i naiveRev, • men är konstant i accRev
har vi sett det här förut? • accRev igen: accRev([H|Tail],A,R) :- accRev(Tail,[H|A],R). • skriv om som: accRev([H|Tail]) --> accRev(Tail),[H].
körning… ?- accRev([a,b,c],X,[]). 1 1 Call: accRev([a,b,c],_476,[]) ? 2 2 Call: accRev([b,c],_476,_1034) ? 3 3 Call: accRev([c],_476,_1538) ? 4 4 Call: accRev([],_476,_2042) ? 4 4 Exit: accRev([],_476,_476) ? 5 4 Call: 'C'(_476,c,_1538) ? 5 4 Exit: 'C'([c|_1538],c,_1538) ? 3 3 Exit: accRev([c],[c|_1538],_1538) ? 6 3 Call: 'C'(_1538,b,_1034) ? 6 3 Exit: 'C'([b|_1034],b,_1034) ? 2 2 Exit: accRev([b,c],[c,b|_1034],_1034) ? 7 2 Call: 'C'(_1034,a,[]) ? 7 2 Exit: 'C'([a],a,[]) ? 1 1 Exit: accRev([a,b,c],[c,b,a],[]) ? X = [c,b,a]
sammanfattning • ackumulatorer är ett generellt sätt att med hjälp av rekursion implementera iteration • iteration är utrymmeseffektivt (konstant under iterationerna) medan rekursion växer linjärt med antalet iterationer • ibland är också iteration processeffektivt (t ex vid reversering av listor)
frågor etc: ponjo@ida.liu.se