Recursie 2

Creative Commons License
This work is licensed under a
Creative Commons Attribution-NonCommercial-ShareAlike 4.0
International License
.

Editor:

John Val

Bron: Learn Prolog Now!

Vragen

Wat leer je in dit hoofdstuk:

De volgorde van regels, de volgorde van doelen, en beëindiging

Prolog was de eerste redelijk succesvolle poging om een programmeertaal te maken waarin logische regels de code vormen. De onderliggende simpele ( en verleidelijke ) visie voor deze programmeertaal is: de taak van een programmeur is het beschrijven van een probleem. De programmeur hoeft alleen maar ( in de taal van de logica ) een declaratieve beschrijving te geven van de situatie rond het probleem. De programmeur zou verder geen instructies hoeven te geven hoe de computer dat de oplossing van het probleem moet bereiken. Om de oplossing van het probleem te krijgen hoeft de programmeur slechts vragen te stellen. Het systeem, onderliggend aan de taal, moet zelf maar uitvinden hoe het tot het antwoord komt.

Dat is het idee en Prolog heeft al flinke stappen genomen om dit te realiseren. Maar Prolog is niet, en ik herhaal niet, een volledig logica programma.. Als je alleen maar declaratief kan denken over een Prolog programma dan ga je moeilijke tijden tegemoet. In de vorige twee lessen hebben we geleerd dat Prolog een specifieke manier heeft om een vraag te beantwoorden: Prolog doorzoekt de kennisbank van boven naar beneden en samengestelde vragen van links naar rechts en gebruikt backtracking om terug te keren van verkeerde keuzes. Deze procedurele kanten hebben een belangrijke invloed op wat er precies gebeurt als een vraag wordt opgelost. We hebben al een groot probleem gevonden met de kennisbank

p:- p.
We zullen nu gaan ontdekken dat het makkelijk is om kennisbanken te schrijven die dezelfde logsiche betekenis hebben, maar die bij ondervraging verschillend gedrag vertonen. We gaan hier eens over uitweiden.

We bekijken nogmaals (een deel van) onze oorspronkelijke afstammeling kennisbank (laten we dit afstammeling1.pl noemen):

kindVan(geert,harry). kindVan(harry,johan). kindVan(johan,henk). kindVan(henk,carola). is_afstammeling_van(X,Y):- kindVan(X,Y). is_afstammeling_van(X,Y):- kindVan(X,Z), is_afstammeling_van(Z,Y).
We maken hier slechts één verandering en noemen dit programma afstammeling2.pl
kindVan(geert,harry). kindVan(harry,johan). kindVan(johan,henk). kindVan(henk,carola). is_afstammeling_van(X,Y):- kindVan(X,Z), is_afstammeling_van(Z,Y). is_afstammeling_van(X,Y):- kindVan(X,Y).
Het enige dat we hebben veranderd is de volgorde van de clausules is_afstammeling_van/2. Dus logisch (declaratief) hebben we niets veranderd, maar zijn er verschillen in de procedure van het oplossen van vragen? Het antwoord is ja, maar niet in het aantal oplossingen. Stellen we de vraag ?- is_afstammeling_van(X,Y)., dan geeft afstammeling1.pl als eerste het antwoord:
X = geert, Y = harry
en afstammeling2.pl komt als eerste terug met
X = geert, Y = carola
. Als je alle antwoorden laat opzoeken dan krijg je precies dezelfde mogelijkheden, maar de volgorde is anders. Ruwweg (een voorbehoud komt later) komt het er op neer dat de volgorde van clausules in een programma het gedrag (behalve de volgorde van de uitkomsten) van het programma niet verandert

Laten we verder gaan met het aanbrengen van veranderingen. In het volgende programma (afstammeling3.pl)

kindVan(geert,harry). kindVan(harry,johan). kindVan(johan,henk). kindVan(henk,carola). is_afstammeling_van(X,Y):- is_afstammeling_van(Z,Y). kindVan(X,Z). is_afstammeling_van(X,Y):- kindVan(X,Y).
is de samengestelde vraag in de body van de eerste clausule omgekeerd, ofwel we hebben de volgorde van de te bewijzen doelen veranderd. Aan beide doelen moet worden voldaan, dus waarom niet gewoon omdraaien? Wel het blijkt dat dit grote gevolgen kan hebben. Daar waar de eerste twee programma's op de vraag ?- is_afstammeling_van(geert,carola). snel het antwoord true geven komt Prolog bij afstammeling3 met een foutmelding Stack limit (0.2Gb) exceeded of iets dergelijks. Prolog zit in een oneindige lus. Waarom? Dit zit zo: Prolog gebruikt om de vraag ?- is_afstammeling_van(geert,carola). te beantwoorden de eerste clausule en ( vanwege de van links naar rechts afhandeling van samengestelde doelen ) gaat dan de vraag is_afstammeling_van(_W1,Y) proberen op te lossen met _W1 een nog onbepaalde variabele. Maar ook voor dit nieuwe doel wordt weer de eerste clausule gebruikt en wordt het nieuwe doel is_afstammeling_van(_W2,Y) met _W2 een nog onbepaalde variabele. Je ziet het al gebeuren, dit gaat maar door en door.

De op het oog kleine verandering van afstammeling2.pl naar afstammeling3.pl heeft dus procedureel rampzalige gevolgen. In Prolog terminologie hebben we hier een klassiek voorbeeld van een links recursieve clausule, ofwel een clausule waarin het meest linkse doel in de body gelijk is ( uitgezonderd de namen van de argumenten ) aan de head van de clausule. Ons voorbeeld heeft laten zien dat zulke links recursieve clausules makkelijk tot niet eindigende (non-terminating) berekeningen leiden. De volgorde van de doelen, in het bijzonder links recursief, kan de bron zijn van alle ellende wanneer er sprake is van non-termination.

We zijn er nog niet helemaal. Eerder hebben we opgemerkt dat we een klein voorbehoud moeten maken bij de volgorden van clausules. We zeiden dat de volgorde van clausules alleen de volgorde van de oplossingen verandert, maar gaat niet altijd op wanneer we met niet-eindigende clausules werken. Laten we eens naar het vierde ( en tevens laatste ) programma kijken (afstammeling4.pl).

kindVan(geert,harry). kindVan(harry,johan). kindVan(johan,henk). kindVan(henk,carola). is_afstammeling_van(X,Y):- kindVan(X,Y). is_afstammeling_van(X,Y):- is_afstammeling_van(Z,Y), kindVan(X,Z).
We hebben hier eigenlijk alleen de clausule volgorde in afstammeling3.pl veranderd. Declaratief weer precies dezelfde betekenis. Stellen we weer de vraag ?- is_afstammeling_van(geert,carola). dan krijgen nu (in SWISH) weer het juiste antwoord. (andere Prolog versies kunnen hier al het gedrag van afstammeling3.pl vertonen) Maar we hebben hier toch weer de links recursieve clausule? Waarom gaat het nu wel goed? Ten opzichte van afstammeling3.pl hebben we alleen de volgorde van de clausles veranderd, dus zou er volgens onze eerdere bewering geen verschil moeten zijn in de uitkomst. Vraag je om een tweede antwoord dan schiet ook afstammeling4.pl de non-terminating stand. Ook als we de vraag ?- is_afstammeling_van(geert,harry). vragen dan gaat het ( alweer direct oneindig ) mis bij afstammeling3.pl terwijl afstammeling4.pl als eerste de vraag vertaald naar het doel van de eerste (niet recursieve) clausule ?-kindVan(geert,harry). En dit is waar.

Dus in het geval van niet-eindigende clausules kan de volgorde van de clausules tot antwoorden leiden. Alhoewel het hier toch ook de volgorde van de doelen is die echt bepalend is voor het verschil. De volgorde van de clausules blijft minder belangrijk. Om beëindiging te garanderen moeten we dus vooral aandacht geven aan de volgorde van de doelen in de body van de clausules.

Wat hebben we nu gezien? We hebben vier verschillende programma's gezien die precies de zelfde logica beschrijven, maar die door de procedurele stappen die Prolog uitvoert om vragen te beantwoorden heel verschillend kunnen werken. Het veranderen van de volgorde van clausules in de kennisbank heeft geen heel grote gevolgen (afstammeling1 en afstammeling2). Echter het veranderen van de volgorde van doelen in de body in een recursieve clausule kan desastreus aflopen (afstammeling3 en afstammeling4), bij het beantwoorden van een vraag kan Prolog in een oneindigende aanroep van de recursieve clausule komen.

Wat zijn de consequenties van onze discussie het produceren van werkende Prolog-programma's? Het is waarschijnlijk het beste om het volgende te zeggen. Vaak kunt je het algemene idee (het grote beeld) krijgen van hoe je het programma moet schrijven door declaratief te denken, dat wil zeggen door te denken in termen die het probleem nauwkeurig beschrijven. Dit is een uitstekende manier om problemen te benaderen, en zeker een manier die het meest in overeenstemming is met de geest van logisch programmeren. Maar als je dat eenmaal hebt gedaan, moet je nadenken over hoe Prolog procedureel gaat werken met kennisbanken die je hebt geschreven. In het bijzonder, om te zorgen voor beëindiging, moet je controleren of de door jou gegeven doelvolgorde verstandig is. De basisvuistregel is dat je nooit als het meest linkse doel van de body niet dezelfde clausule neemt als die in de head van de clausule. Plaats dergelijke recursieve doelen juist zo ver mogelijk rechts in de rij van doelen in de samengestelde head. Dat wil zeggen, plaats ze achter de doelen die testen voor de verschillende (niet-recursieve) beëindigingsvoorwaarden.