Prolog: De fyra värsta felen du kan göra
När logikprogrammering går snett: De fyra vanligaste fällorna i Prolog
Prolog har ett slags charm som få andra språk kan matcha. Medan de flesta utvecklare arbetar med imperativa eller objektorienterade metoder, väljer många som använder Prolog en helt annan väg – deklarativ logik. Det kan kännas både kraftfullt och elegant, men det finns fallgropar som lätt förstör fördelarna.
Några enkla principer avgör om koden blir stabil eller börjar krångla i produktion. Bryter du mot dem riskerar du att få program som ger felaktiga svar, missar lösningar eller blir svåra att underhålla. Här är de fyra vanligaste misstagen som dyker upp hos både nybörjare och erfarna utvecklare.
När lösningar tyst försvinner
En vanlig situation är att en predikat fungerar perfekt när du testar med specifika värden. Men så snart någon ställer en mer generell fråga, slutar den leverera de resultat som borde finnas.
Detta händer ofta när man använder "orena" konstruktioner som cut (!), if-then-else eller typkontroller som var/1. De känns bekväma när man tänker proceduellt, men de bryter mot Prologs deklarativa natur. Koden blir beroende av exakt hur den anropas.
% Felaktigt: cut används för att "optimera"
factorial(0, 1) :- !.
factorial(N, F) :-
N > 0,
N1 is N - 1,
factorial(N1, F1),
F is N * F1.
När man frågar efter alla giltiga lösningar ger den bara ett enda resultat och stannar. Den deklarativa lösningen är att använda dif/2 och andra konstraint-predikat tillsammans med högre ordningens meta-predikat. Koden blir både mer generaliserad och lättare att testa.
När databasen blir en fälla
Många upptäcker assertz/1 och retract/1 och ser en ny värld av flexibilitet. Men dessa funktioner som ändrar globalt tillstånd är också en väg till brittla code.
Om man ändrar databasen under körning skapar man implicit beroenden. Sammanhanget försvinner från koden och blir istället något som "man måste veta". Det gör både testing och debugging mycket svårare. Består oförutsedda förändringar från tidigare runs kvar i databasen, får man ofta mystiska resultat.
En bättre approach är att alltid skicka tillstånd vidare som argument i predikaten. Så behåller man klarhet och tillförlitlighet.
När utskrifter tar över
En till fälla är när logic blandas med side effects:
% Felaktigt: logic och utskrifter blandade
solve_and_print :-
solution(S),
format("The solution is: ~q~n", [S]).
Detta gör det omöjligt att testa predikatet, eftersom det inte kan behandlas som en ren relation. Resultatet finns bara på skärmen och inte som en Prolog-term som kan användas vidare.
En enkel fix är att hålla logic ren och låta presentationen skötas av top-level. Om man behöver specialformat kan DCG-notation användas. Det gör koden återanvändbar och testbar.
När gamla konstruktioner står i vägen
Prolog har utvecklat sig mycket. CLP(FD) har funnits i över tjugo år och ger modernare och clearer abstractions.
Men många använder fortfarande äldre versionen av is, =:= och > eftersom de "alltid har fungerat". Det gör koden svårare att förstå och lära sig. Modernare constraint-baserade verktyg gör istället att den deklarativa meningen blir tydlig – koden läses som en specifikation snarare än en procedur.
Ett exempel som visar allt
En kombination av alla misstag ser ungefär så här ut:
% Den problematiska versionen
horror_factorial(0, 1) :- !.
horror_factorial(N, F) :-
N > 0,
N1 is N - 1,
horror_factorial(N1, F1),
F is N * F1.
Detta brötter tre principer samtidigt – cut, äldre arithmetic och bristande generality. När man frågar generellt efter alla lösningar ger den bara ett resultat och stannar.
Hur man skriver bättre Prolog-kod
För att undvika dessa fällor gäller det att:
- Använda deklarativa konstruktioner i stället för cut och typkontroller.
- Hålla tillstånd synligt – skicka det vidare som argument.
- Separera logic och presentation – låt top-level hantera utskrifter.
- Använda modern CLP(FD) och liknande verktyg för att koden ska bli klarare.
Koden ska helst fungera även när man ställer den mest generella frågan. Det är då Prolog verkligen visar sin styrka.