INF4170 - Logikk og analysemetoder Forslag til løsning på oppgave 3.2.1 fra læreboken Joakim Hjertås, joakimh@ifi.uio.no 7. mars 2004 Sammendrag Disse sidene kommer med forslag til løsning på oppgave 3.2.1 fra læreboken "First-Order Logic and Automated Theorem Proving" av Melvin Fitting. Det tas utgangspunkt i at «Dual Clause Form»-programmet fra kapittel 2.9 er utvidet som beskrevet i kapittel 3.2 og alle endringer som foretas etter det beskrives. Jeg er en nybegynner i Prolog og skriver dette notatet hovedsaklig for egen læring. Om det i dette tilfellet ikke er forfatteren selv som leser, håper jeg at du får noe ut av lesingen. Jeg tror løsningen skal fungere godt og er rimelig oversiktlig og håper derfor at du tar kontakt om du finner ut at den ikke er en av delene. Har du noen andre tilbakemeldinger; forslag til forbedringer, funnet noe feil, andre forslag til løsninger eller noe annet så håper jeg også å høre fra deg. Teksten er på norsk, mens kode og kommentarer i kode er skrevet på engelsk. Introduksjon Programmet baserer seg på «Dual Clause Form»-programmet fra kapittel 2.9 og utvidelsen av dette som gjøres i kapittel 3.2. Programmet består da av «Dual Clause Form»-programmet (eventuelt fjernet dualcloseform og expand da disse ikke er nødvendige, men man kan godt la de være) og reglene closed, if_then_else, yes, no, expand_and_close og test hvor den siste versjonen (side 49-50) av de to sistnevnte benyttes. Kort fortalt fungerer da programmet slik at test bruker if_then_else med expand_and_close som betingelse slik at hvis denne lykkes kalles yes, ellers kalles no. expand_and_close sjekker ved hjelp av closed og «Dual Clause Form»- programmet om tablået kan lukkes. Mål og strategi For meg er det et poeng å benytte «Dual Close Form»-programmet uten endringer, for således å holde meg til å endre utvidelsen fra kapittel 3.2 av programmet. 1
En annen fremgangsmåte ville være å gjøre endringer i singlestep siden alle appliseringer av tablåekspansjonsregler skjer der. Strategien er at etter hver applisering av en tablåekspansjonsregel skal grener som kan lukkes (la oss kalle slike grener lukkbare) fjernes. Om man etter å ha gjentatt dette så mange ganger som mulig ender opp med et tablå som ikke har noen grener, så er dette et bevis siden kun grener som kan lukkes fjernes. Alternativt kommer man til et punkt hvor tablået har grener igjen, men ingen tablåekspansjonsregel kan appliseres og man kan da konkludere at tablået ikke kan lukkes. Fjerning av lukkede grener: close Vi lager først en regel som fjerner lukkbare grener og kaller den close. Denne regelen tar to argumenter hvor begge er tablåer og det andre er det første hvor lukkbare grener er fjernet. La oss begynne med en kommentar som forklarer close: /* close(oldtableau, NewTableau) : - NewTableau is the result of removing all branches which are possible to close from OldTableau. */ Det enkleste er å fjerne grener fra et tomt tablå, resultatet er et tomt tablå: /* The closed version of an empty tableau is an empty tableau. */ close([], []) :- /* No other rules are meaningful to try. */!. Om det første tablået ikke er tomt, rekurserer close over grenene i det. Vi har to tilfeller: 1. Den første grenen i det første tablået er lukkbar: Grenen fjernes ved at det andre tablået settes lik resten av det første tablået hvor alle lukkbare grener er fjernet. 2. Den første grenen i det første tablået er ikke lukkbar: Grenen beholdes ved at det andre tablået settes til å være denne grenen sammen med resten av det første tablået hvor alle lukkbare grener er fjernet. Først to tilfeller hvor den første grenen er lukkbar. Den første grenen er lukkbar hvis den inneholder false: /* Removes the first branch if it contains false. */ /* Checks whether false is a member of the first branch. */ member(false, FirstBranch), /* If the above holds, do not backtrack beyond this point. */ /* Recursive call with FirstBranch removed. */ close(rest, NewTableau). 2
eller hvis den inneholder Z og Z: /* Removes the first branch if it contains both Z and neg Z. */ /* Succeeds if both Z and neg Z are in the first branch. */ member(z, FirstBranch), member(neg Z, FirstBranch), /* If the two calls above succeed, no later rules should be used. */ /* Recursive call with FirstBranch removed. */ close(rest, NewTableau). Siden reglene over inneholder cut, vil Prolog ikke prøve senere regler ved backtracking, således vet vi når vi er kommet ned hit at ingen av reglene over passet og grenen kan uten videre beholdes siden den ikke er lukkbar: /* Keeps the first branch. */ /* Closes the rest of the first tableau and puts the result into a fresh variable. */ close(rest, RestNewTableau), /* NewTableau is the list consisting of the first branch of the first tableau followed by the closed version of the rest of the first tableau. */ NewTableau = [FirstBranch RestNewTableau]. Merk forøvrig at closed fra lærebokens kapittel 3.2 brukes for å sjekke om et tablå kan lukkes uten å gjøre noe mer med det, mens close brukt med en fri variabel som andre argument alltid vil komme opp med et forslag til verdi som variabelen kan anta. Således vil close(t, []). gi samme svar som closed(t). hvor T er et tablå. Cut i den første regelen er i og for seg unødvendig, men siden ingen av de andre reglene passer kan man godt stoppe eventuell backtracking eksplisitt der. Endring av expand_and_close Vi vil nå at expand_and_close skal bruke vår nye regel close etter hver applisering av en tablåekspansjonsregel. I så fall trenger vi ikke lenger sjekke med closed om tablået kan lukkes, det holder å sjekke om tablået er tomt. Regelen closed kan egentlig bare slettes da vi ikke bruker den lenger etter denne endringen. /* expand_and_close(tableau) :- Some expansion of Tableau closes. */ /* If the tableau is empty, it is closed. */ expand_and_close([]). /* Tableau is not empty. */ 3
expand_and_close(tableau) :- /* Try to apply a tableau expansion rule. Note that singlestep fails if it is unable to apply a rule, and then this rule will fail as well. */ singlestep(tableau, ExpandedTableau), /* Should try only one possible expansion. */ /* Remove branches which can be closed. */ close(expandedtableau, CTableau), /* Recursive call with the argument being ExpandedTableau with branches possible to close removed. */ expand_and_close(ctableau). Denne vil altså uansett lykkes når tablået er tomt. Hvis det derimot ikke er tomt vil om mulig en tablåekspansjonsregel appliseres, lukkbare grener fjernes og så rekursere; om det ikke er mulig å applisere en regel vil singlestep feile og dermed også hele denne regelen feile. Hvorfor cut er så viktig i expand_and_close Denne seksjonen er nok noe slitsom og anbefales kun lest av spesielt interesserte. dualclause fra «Dual Clause Form»-programmet gir i noen tilfeller uendelig mange mulige svar, prøv f.eks. spørringen dualcloseform(p and q, D). og trykk semikolon for å få så mange svar du måtte ønske. Grunnen til dette ligger i singlestep (som igjen får det fra remove, se notatet «Cut i Prolog» av Christian Mahesh Hansen) som kan gi to mulige svar når Prolog backtracker. La oss se på en eksempelkjøring:?- singlestep([[p and q]], D). D = [[p,q]]? ; D = [[p,q,p and q]]? ; no Legg merke til at det vi brukte i argumentet i spørringen kommer tilbake som en del av det andre svaret. La oss nå anta at expand_and_close lages som over bare uten cut og se på kjøringen av spørringen expand_and_close([[p and q]]).: Argumentet er ikke en tom liste, så den andre delen av regelen vil brukes. singlestep([[p and q]], ExpandedTableau) gir ExpandedTableau=[[p,q]] som første svar. close([[p,q]]), CTableau klarer ikke å lukke grenen og gir oss CTableau=[[p,q]] som svar. Så kommer det rekursive kallet med dette som argument expand_and_close([[p, q]]). 4
Dette er heller ikke en tom liste, så den andre delen av regelen brukes igjen. Denne gangen feiler singlestep([[p,q]], ExpandedTableau) siden ingen tablåekspansjonsregler kan appliseres. Dermed feiler også kallet expand_and_close([[p, q]]) som vi er inne i nå. Dette fører til at Prolog backtracker for å prøve andre alternativer. Kallet på close har ingen alternative løsninger så backtrackingen fortsetter. Kallet singlestep([[p and q]], ExpandedTableau) som i stad ga oss ExpandedTableau=[[p],[q]] gir oss nå sitt andre svar ExpandedTableau=[[p,q,p and q]]. close([[p,q,p and q]]), CTableau klarer naturlignok ikke å lukke denne grenen heller, så den gir oss CTableau=[[p,q,p and q]] som svar. Så får vi neste rekursive kall som nå blir expand_and_close([[p,q,p and q]]). De som fremdeles henger med og er våkne vil nok gjette seg til det helt riktige at om kallet expand_and_close([[p and q]]) går til kallet expand_and_close([[p,q,p and q]]) så vil også dette gå videre uten å stoppe. Vi har en form for uendelig rekursjon («rekursjonsbrønn») som vi vel kan kalle en «backtrackingsbrønn»? Det er uendelig mange muligheter å backtracke over, men ingen av dem får expand_and_close til å lykkes. Hele poenget er at om expand_and_close inneholder cut som foreslått tidligere, vil Prologs backtracking ikke gå så langt tilbake som den gjorde i kjøreeksempelet over, den vil stoppe ved cut og altså aldri komme til det andre svaret fra singlestep. 5