Capítulo 8: Programación Lógica Prolog data de 1972 y da origen al paradigma lógico. Se origina a partir de las gramáticas W, que ayudan a describir Algol 68. La programación lógica se fundamenta en las relaciones y no las funciones. Las relaciones son más flexibles que las funciones porque en las relaciones los argumentos y los resultados se tratan en forma uniforme. Relaciones ********** Una relación es una tabla con n (>=0) columnas y número ilimitado de filas (posiblemente infinito). Una tupla (a1, a2, ..., an) está en la relación r si existe alguna fila con ese contenido. Un típico ejemplo es la relación ser padre, abuelo o hijo entre personas. padre: pedro juan pedro es padre de juan juan diego pedro ana Esto en prolog se anota: padre(pedro,juan). (1) padre(juan,diego). (2) padre(pedro,ana). (3) Podríamos especificar de igual manera una relación para hijo, pero lógicamente se puede describir en términos de la relación padre: hijo(X,Y):- padre(Y,X). (4) Esto se lee: X es hijo de Y si se puede demostrar que Y es padre de X. También podemos especificar la relación abuelo como: abuelo(X,Z):- padre(X,Y), padre(Y,Z). (5) Esto se lee: X es abuelo de Z si se puede demostrar que existe Y tq. se demuestra que X es padre de Y e Y es padre de Z. - (1) a (5) se denominan cláusulas de Horn. - padre(pedro,juan) e hijo(X,Y) se denominan predicados, un caso particular de término compuesto. - pedro, juan son términos simples o átomos (aparecen en minúsculas) - X, Y son variables (porque aparecen en mayúsculas). Un programa en Prolog se puede describir como una base de datos (i.e. un conjunto de relaciones) expresada como cláusulas simples: (1), (2) y (3), y además cláusulas complejas como (4) y (5) que pérmiten inferir relaciones a partir de las existentes. Consultas ********* La ejecución de un programa consiste en hacer una consulta (query) para determinar si existe una tupla en alguna relación. Por ejemplo: ?- padre(juan,diego). yes significa que la tupla (juan,diego) está en al relación padre. También se puede consultar: ?- padre(X,juan). X=pedro Es decir quién es el padre de Juan? O bien: ?- padre(pedro,X). o equivalentemente ?- hijo(X, pedro). X=juan; X=ana Es decir cuáles son los hijos pedro? Por esto las relaciones son más poderosas que las funciones, porque según en que lugar se ponga la incógnita se obtienen funcionalidades distintas como "quién es padre de" o "de quién es padre". Y por último quién es nieto de pedro: ?- abuelo(pedro, Y). Y=diego Prolog incorpora un motor de búsqueda que recorre las cláusulas de Horn para inferir relaciones. Este motor de búsqueda corresponde matemáticamente a un demostrador de teoremas. Clausulas de Horn ***************** Una cláusula de Horn es un hecho universal: su veracidad no se discute. La sintaxis es: ::= . ::= :- . ::= | , ::= | empieza con mayúscula | empieza con minúscula | ( ) término compuesto ::= . Unificación *********** Cuando Prolog resuelve una consulta como padre(pedro,X), el motor busca en las tuplas conocidas de la relación padre: (pedro,juan), (juan,diego) y (pedro,ana), tratando de hacer una correspondencia con (pedro,X). Esto se llama unificación. - Unificación de (pedro,X) con (pedro,juan): sí, es válida. pedro se unifica trivialmente con pedro, pero para que X se unifique con juan, se requiere que X=juan. - Unificación de (pedro,X) con (juan,diego): no. pedro no puede unificarse con juan. - Unificación de (pedro,X) con (pedro,ana): sí. Pero con la restricción que X=ana. Ejemplo: descendientes desciende(X,Y):- padre(Y,X). (1) desciende(X,Z):- padre(Z,Y),desciende(X,Y). (2) Las variables son *siempre* locales a la reglas en donde aparecen. La variable X de la regla (1) no tiene nada que ver con la variable X de (2). Sin embargo, la variable X en padre(Y,X) de la regla (1) se refiere a la misma variable X de desciende(X,Y) en la regla (1). ?- desciende(X,pedro). X=juan; X=diego; X=ana; ?- desciende(ana,X),desciende(diego,X). X=juan Cuidado: el orden en que se especifican los predicados es relevante para que el motor de búsqueda de prolog funcione adecuadamente. Por ejemplo: desciende(X,Z):- desciende(X,Y),padre(Z,Y). Nunca termina, pues cuando Prolog intenta resolver desciende(X,Z) se encuentra que la complejidad de desciende(X,Y) es equivalente! Listas ****** En prolog las listas se denotan: - [] : la lista vacía - [a] : la lista que contiene el átomo a - [1,2,3] : la lista que contiene 1 2 y 3. - [n | X] : una lista cuyo primer elemento es el átomo n y el resto de los elementos es una variable X. Prolog incorpora relaciones predefinidas para las listas. Son el equivalente de las funciones predefinidas en los lenguajes tradicionales. Append - Cuál es el resultado de concatenar [a] con [b,c]? ?- append([a],[b,c],X). X=[a,b,c] - Qué listas se requiere concatenar para que el resultado sea [a,b,c]? ?- append(X,Y,[a,b,c]). X=[] Y=[a,b,c]; X=[a] Y=[b,c]; etc. Para ir obteniendo las respuestas presione n. Si presione enter o y, la búsqueda se detiene. El predicado append está definido en Prolog de la siguiente manera: append([],L,L). append([X|L1],L2,[X|L3]) :- append(L1,L2,L3). Variables anónimas ****************** Member member(X,[X|_]). (A) member(X,[_|L]) :- member(X,L). (B) equivale a member(X,[G1|L]):-... . La variable _ es anónima, lo que significa que es su única aparición y no se usa en otro contexto de la misma regla. Todas las apariciones de _ se refieren a variables distintas. - Cuales son los elementos de la lista [1,2,3]? ?- member(X,[5,-3,10]). X=5; X=-3; X=10 - ¿De qué listas de tamaño 3 es miembro a? ?- member(a,[X,Y,Z]). X = a X = _G290 X = _G290 Y = _G293 Y = a Y = _G293 Z = _G296 ; Z = _G296 ; Z = a ; Este es un caso de respuesta *parcialmente especificada*. _Gxxx son *variables*, lo que significa que se pueden reemplazar por cualquier valor, i.e. corresponde un "para todo". Algoritmo de búsqueda ******************** Un query consise en una conjunción de predicados que Prolog debe demostrar que es verdadero, mostrando además las restricciones bajo las cuales se cumple. Ejemplo 1: 1.- member(2,[1,2,3]). Se considera (A): no unifica Se considera (B): unifica con X=2, L=[2,3] Se insertan las condiciones de (B) en la lista de metas reemplazando la variables pro las restricciones. 2.- member(2,[2,3]). Se considera (A): unifica con X=2, L=[3]. Como no hay condiciones => exito. El interprete arroja yes. Ejemplo 2: 1.- member(a,[G1,G2,G3]). X=G1,Y=G2,Z=G3 (A) member(X,[X|L]).: unifica con X=G1=a, L=[G2,G3] exito X=a, Y=G2, Z=G3 (esperar respuesta y o n del usuario. Si y, detener la búsqueda, si n, retomar la búsqueda como si hubiese fracasado). ... usuario ingresa n (B) member(X,[_|L]) :- member(X,L). unifica con X=a, L=[G2,G3] Insertar la condición. 2.- member(a,[G2,G3]). X=G1,Y=G2,Z=G3 (A) member(X,[X|L]).: unifica con X=G2=a , L=[G3] exito X=G1, Y=a, Z=G3 ... usuario ingresa n (B) member(X,[_|L]) :- member(X,L). unifica con X=a, L= [G3] Insertar condición. 3.- member(a,[G3]). (A) member(X,[X|L]).: unifica con X=G3=a , L=[] exito X=G1, Y= G2, Z= a ... usuario ingresa n (B) member(X,[_|L]) :- member(X,L). unifica con X=a,L=[] 4.- member(a,[]). (A) no unifica (B) no unifica no Estrategia de solución de problemas: adivinar y verificar ********************************************************* La idea es usar un predicado con variables desconocidas y que Prolog asignará valores de acuerdo a su motor de búsqueda de soluciones. Un segundo predicado verifica si el valor asignado es satisfactorio o no. Ejemplo: overlap(L1,L2) indica si dos listas poseen un elemento en común. overlap(L1,L2):-member(X,L1),member(X,L2). (C) ?- overlap([1,2,3], [6,2,8]). El primer predicado member(X,[1,2,3]) adivina posibles soluciones, que serían X=1, X=2, X=3. Luego, el segundo predicado verifica si estas soluciones cumplen con member(X,[6,2,8]), existiendo una solución con X=2. Luego las dos listas sí poseen un elemento en común. 1.- overlap([1,2,3], [6,2]). (C) overlap(L1,L2):-member(X,L1),member(X,L2). unifica L1=[1,2,3] y L2=[6,2], X=G1 2.- member(G1,[1,2,3]),member(G1,[6,2]). (A) member(X,[X|L]). unifica con X=G1=1,L=[2,3] No hay condiciones: se elimina la meta de la lista, pero reemplazando las variables resueltas en las metas que quedan. 3.- member(1,[6,2]). (A) member(X,[X|L]). no unifica (B) member(X,[_|L]) :- member(X,L). unifica X=1,L=[2] 4.- member(1,[2]). 5.- member(1,[]). => fracaso -> backtrack (B) member(X,[_|L]) :- member(X,L). unifica X=G1,L=[2,3] 3.- member(G1,[2,3]),member(G1,[6,2]). (A) member(X,[X|L]). unifica con X=G1=2 y L=3. 4.- member(2,[6,2]). (A) member(X,[X|L]). no unifica (B) member(X,[_|L]) :- member(X,L). unifica con X=2,L=[2] 5.- member(2,[2]). (A) member(X,[X|L]). unifica exito Términos compuestos ******************* Un término compuesto es de la forma (). Se pueden usar para representar estructuras de datos recursivas como los árboles. Ejemplo: - árbol no ordenado node(leaf(6),node(leaf(8),leaf(20))) - árbol de búsqueda binaria node( node(5, node(2,leaf,leaf), leaf), node(7,leaf,leaf)) Estos son datos, no predicados. Se pueden hacer reglas para buscar elementos en estos árboles: - no ordenada: belongs(X,leaf(X)). belongs(X,node(L,R)):-belongs(X,L). belongs(X,node(L,R)):-belongs(X,R). - ordenada: belongs(X,node(X,L,R)). belongs(X,node(Y,L,R)):-XY,belongs(X,R). Predicados no invertibles ************************* Los operadores relacionales en Prolog son no invertibles. Esto quiere decir que cuando Prolog trata de evaluarlos, todas las variables tienen que estar *resueltas*. ?- belongs(1,node(5,node(1,leaf,leaf)),leaf). yes. ?- belongs(X,node(5,node(1,leaf,leaf)),leaf). X = 5 ; ERROR: Arguments are not sufficiently instantiated Ejemplo: inserción en un abb. insert(X,T,U) significa que U es el resultado de insertar X en T. ?- insert(5,node(4,leaf,leaf),X). X=node(4,leaf,node(5,leaf,leaf)) Solución: insert(X,leaf,node(X,leaf,leaf)). insert(X,node(X,L,R),node(X,L,R)). insert(X,node(Y,L,R), node(Y,L2,R)) :- XY,insert(X,R,R2). La programación lógica se reduce a la programación funcional cuando sistemáticamente la única variable se encuentra en el último argumento, y además solo una de las reglas es aplicable. Excepciones a la lógica *********************** Motivación: Se tiene una base de reglas que indica entre qué ciudades hay caminos. Interesa conocer la ruta que se debe seguir para llegar de una ciudad a otra. Solución errada: (B1) camino(santiago,sanantonio). (B2) camino(sanantonio,valparaiso). (B3) camino(rancagua,santiago). ... (R1) ruta(X,Y) :- camino(X,Y). (R2) ruta(X,Y) :- camino(X,Z),ruta(Z,Y). Ejemplos: ?- ruta(santiago,X). X= sanantonio ; X= valparaiso No dice que también se puede llegar a rancagua porque no se ha dicho que la relación camino es simétrica. Para indicar la simetría, la lógica indicaría que agregar la siguiente regla tiene sentido: (R3) camino(X,Y) :- camino(Y,X). Pero el problema es que agregamos ciclos: ?- ruta(santiago,X). X = sanantonio ; X = rancagua ; X = sanantonio ; X = rancagua ... etc ... 1.- ruta(santiago,G1). unifica con (R1) X=santiago, Y=G1 2.- camino(santiago,G1). unifica con (B1) G1=sanantonio => respuesta 1 X=sanantonio backtrack unifica con (R3) X=santiago Y=G1 3.- camino(G1,santiago). unifica con (B3) G1=rancagua => respuesta 2 X=G1=rancagua unifica con (R3) X=G1,Y=santiago 4.- camino(santiago,G1) ... etc.: ciclo ... Algo parecido ocurre cuando se incorpora la reflexión en la ruta: (R3) ruta(X,Y) :- camino(Z,X),ruta(Z,Y). Este problema se resuelve cambiando el enfoque: se introduce un predicado adicional. ruta2(X,Y,L): significa que existe un ruta entre X e Y sin pasar por ninguna de las ciudades de L. Entonces: ruta(X,Y) :- ruta2(X,Y,[X]). El problema es cómo expresar en prolog que X no pertenezca a una lista L. Negación en Prolog ****************** En Prolog se puede establecer como condición que un predicado no se puede desmotrar con not. ?- not(member(santiago,[valparaiso,rancagua])). yes ?- not(member(santiago,L)). no. El predicado not termina exitosamente si no encuentra ninguna solución para el predicado que recibe como argumento. Fracasa cuando al menos encontrar una solución. En el segundo caso, L=[santiago|G20] es una solución y por lo tanto not fracasa. Cuando use not, asegúrese que todas las variables estén instanciadas. El not de Prolog no es el not de la lógica porque en lógica si algo no se puede demostrar no significa que sea falso. A pesar de todo, esta acepción adoptada por Prolog es útil al momento de programar aplicaciones. Solución: (R1) ruta(X,Y) :- ruta2(X,Y,[X]). (R2) ruta2(X,X,_). (R3) ruta2(X,Y,L) :- camino(X,Z),not(member(Z,L)),ruta2(Z,Y,[Z|L]). (R4) ruta2(X,Y,L) :- camino(Z,X),not(member(Z,L)),ruta2(Z,Y,[Z|L]). Observación: acá not funciona porque cuando se trata de probar member(Z,L1), tanto Z como L1 estarán completamente instanciadas (sin referencias a otras variables, como L1=[a|G20]). ?- ruta(santiago,X). 1.- ruta(santiago,G1). unifica con (R1) X=santiago Y=G1 2.- ruta2(santiago,G1,[santiago]). unifica con (R2) X=santiago, G1=santiago ==> respuesta 1: X= santiago (no deseada) backtrack unifica con (R3) X=santiago Y=G1 L=[santiago] 3.- camino(santiago,Z),not(member(Z,[santiago])),ruta2(Z,G1,[Z|santiago]). unifica con (B1) Z=sanantonio 4.- not(member(sanantonio,[santiago])), ruta2(sanantonio,G1,[sanantonio,santiago]). not(...) exitoso 5.- ruta2(sanantonio,G1,[sanantonio,santiago]). unifica con (R2) G1=sanantonio ==> respuesta 2: X=sanantonio backtrack unifica con (R3) X=sanantonio Y=G1 L=[sanantonio,santiago] 6.- camino(sanantonio,Z),not(member(Z,[sanantonio,santiago])), ruta2(Z,G1,[Z,sanantonio,santiago]). unifica con (B2) Z=valparaiso 7.-not(member(valparaiso,[sanantonio,santiago])), ruta2(valparaiso,G1,[valparaiso,sanantonio,santiago]). not(...) exitoso 8.- ruta2(valparaiso,G1,[valparaiso,sanantonio,santiago]). unifica con (R2) G1= valparaiso ==> respuesta 3_ X=valparaiso ... etc. ... Ejercicios: - modifique la solución de manera que X=santiago no sea una respuesta - modifique la solución de manera que ahora se entregue la lista de ciudades en la ruta. ?- ruta(santiago,valparaiso,L). L=[santiago,sanantonio,valparaiso]. Cortes (cuts) ************* - Se utilizan para eliminar para podar el árbol de búsqueda de soluciones. - Permiten optimizar los programas en Prolog. - Se salen del paradigma lógico pero son indispensables desde un punto de vista pragmático. - Hacen posible la implementacion de not. Uso: B :- C_1, ..., C_(i-1), !, C_(i+1), ...,C_k B :- ... Esto significa que cuando se pretende probar el predicado B, después de encontrar variables que prueban C_1, ..., C_(i-1), el ! (cut) indica que se debe descartar cualquier otra solución para B. Por lo tanto, se intentará probar C_(i+1), ...,C_k y si no se encuentra una solución, se determina que B es falso. Si se encuentra la solución esa será la única solución de B. Si no se logra probar C_1, ..., C_(i-1), se intenta con B. Si no hay un cut, se determinarán todas las soluciones posibles. Ejemplo 1: p(a). p(b):- p(c), !, p(d). p(c). p(d). ?- p(a), p(b), p(c), p(d). yes ?- p(X). X=a; X=b X=c y X=d no aparecen como soluciones de p(X) porque el cut las eliminó. Al hacer el query p(c), como no unifica con p(b), nunca se ejecutará el cut. Ejemplo 2: belongs(X,leaf(X)). belongs(X,node(L,R)):-belongs(X,L),!. belongs(X,node(L,R)):-belongs(X,R). ?- belongs(pedro, node(leaf(juan),leaf(pedro))). yes. ?.- belongs(X, node(leaf(juan),leaf(pedro))). X= juan; no. Cuidado: el cut elimina la invertibilidad de los predicados. Ejemplo 3: Listas de asociación. put(K,V, LA, LAR) :- El resultado de agregar (K,V) a la lista de asociación LA es LAR. put(K,V,[],[pair(K,V)]). put(K,V,[pair(K,W)|L],[pair(K,V)|L]) :- !. put(K,V,[P|L],[P|R]) :- put(K,V,L,R). get(K,V,L) :- put(K,V,L2,L). ?- put(a,2,[pair(a,1)],L). L=[pair(a,2)] si no se coloca el cut, hay una solución adicional: L=[pair(a,1),pair(a,2)]. ?- get(b,V, [pair(a,1),pair(b,2),pair(c,3)]). V=2 no Ejemplo 4: La negación not(X) :- X, !, fail. not(_). Extensiones imperativas: ************************ Se usan predicados que siempre tienen éxito: - Escribir en pantalla valores: ?- write(1), write(hola), write([1,que]). 1hola[1, que] yes - strings en forma de atomos: ?- write('hola') <=> write(hola). hola yes ?- write('hola que tal !"$%&/()'). hola que tal !"$%&/()' yes - strings como listas de caracteres. ?- write("hola"). [104, 111, 108, 97] Yes Esto permite manipular los caracteres. - Escribir en pantalla con formato: ?- writef("%s\n%d\n",["hola", 123]). hola 123 Yes - Aritmética con los operadores: + - * / mod ?- X=5, Y is X+1 X=5 Y=6 is es el operador de asignación Ojo: ?- 1 is 1+1 no. - Cuidado: = no es la asignación. ? X=5, Y=X+1 X=5 Y=5+1 = es el operador de unificación, no de asignación - Comparación: > < =< >= =/= =:= ?- 5+1 > 5 Yes ?- 6+1=:= 7 Yes ?- 6+1=7 no - Encontrar todas las soluciones de un predicado: ?- findall(X,append(X,_,[1,2]),L) X=_G1 L=[[], [1], [1,2]] - Ciclos: ?- between(1,3,X), Y is X*2, write(Y), fail. 246 for(X, INI, FIN, P) :- between(INI,FIN,X), P, fail. mcd(X, X, X). mcd(X, Y, R) :- X>Y, writef("%d>%d\n",[X,Y]), Z is X-Y, mcd(Z,Y,R). mcd(X, Y, R) :- X X= 1 eval([1, +, 1], X) => X= 2 eval([1, +, 3, *, 2], X) => X= 7 eval([[1, +, 3], *, 2], X) => X= 8