Programación Avanzada (PAV) Facultad de Informática - 5A Tema 1: Introducción a los lenguajes de programación lógica Germán Vidal [email protected] Despacho D-242 Edificio DSIC (2o piso) Curso 2003/2004 1 Bibliografı́a Lógica Algoritmica (UPM) http://www.clip.dia.fi.upm.es/~logalg/ K. Apt. From Logic Programming to Prolog. Prentice-Hall, 1997. J.M. Spivey. Logic Programming: The Essence of Prolog. Prentice-Hall, 1996. L. Sterling and E. Shapiro. The Art of Prolog: Advanced Programming Techniques. The MIT Press, Cambridge, MA, 1986. Archivo LP http://www.comlab.ox.ac.uk/archive/logic-prog.html Introducción a la programación lógica Sintaxis: variables, constantes y estructuras (usando notación Prolog) Variables: comienzan con una letra mayúscula (o “_”), y puede incluir caracteres, números y “_” X, Im4u, Mi_coche, _, _x, _22 Constantes: comienzan con una letra minúscula, y puede incluir caracteres, números y “_” (entre comillas simples, cualquier cosa) a, juan, juan_luis, 66, ’Juan Luis’ Estructuras: encabezadas por un functor (como el nombre de una constante) seguidas de un número fijo de argumentos entre paréntesis fecha(miercoles, Mes, 1996) (los argumentos pueden ser a su vez variables, constantes o estructuras) El número de argumentos de una estructura es su aridad Los functores se suelen representar por nombre/aridad. Una constante se puede ver como un functor de aridad cero 3 Las variables, constantes y estructuras se denominan en conjunto términos (de un lenguaje de primer orden). Son las estructuras de datos de un programa lógico Ejemplos Término pedro hora(minuto,segundo) par(Calvin,tigre(Hobbes)) Par(uno,dos) Par Tipo constante estructura estructura ilegal variable 4 Functor principal pedro/0 hora/2 par/2 — — Sintaxis: átomos, literales y hechos Atomos: un átomo es una expresión de la forma p(t1, t2, . . . , tn) donde p es el sı́mbolo de predicado del átomo (mismas convenciones que los functores), n es su aridad y t1, t2, . . . , tn son términos El sı́mbolo de predicado de un átomo se denota por p/n Literales: un literal es un átomo positivo o negativo Los literales no pueden aparecer dentro de un término Hechos: un hecho es una expresión de la forma p(t1, t2, . . . , tn). donde p(t1, t2, . . . , tn) es un átomo Ejemplos perro(nombre(ricky), color(negro)). amigos(’Ana’, ’Juan’). Los átomos y términos se distinguen por el contexto: perro(nombre(ricky), color(negro)) es un átomo y color(negro) es un término 5 Sintaxis: reglas, cláusulas y predicados Reglas: una regla es una expresión de la forma p0(t1, t2, . . . , tn0 ) ← p1(t11, t12, . . . , t1n1 ), ... m m pm(tm 1 , t2 , . . . , tnm ). La expresión a la izquierda de la flecha debe ser un átomo (no negado) y se llama la cabeza de la regla Las expresiones a la derecha son literales y forman el cuerpo de la regla Los literales del cuerpo se llaman también llamadas a procedimiento Ejemplo comida(Primero, Segundo, Tercero) <aperitivo(Primero), plato_principal(Segundo), postre(Tercero). Ambos reglas y hechos se denominan cláusulas 6 Predicados: todas las cláusulas del programa cuyas cabezas tienen el mismo nombre y aridad forman la definición del predicado Ejemplo mascota(ricky). mascota(X) <- animal(X), ladra(X). mascota(X) <- animal(X), vuela(X). El predicado mascota/1 tiene 3 cláusulas (un hecho y dos reglas) 7 Significado declarativo de hechos y reglas El significado declarativo es el correspondiente en la lógica de primer orden, de acuerdo a ciertas convenciones: Hechos: establecen cosas que son ciertas Ejemplo: el hecho animal(ricky). puede leerse como “ricky es un animal” Reglas: las comas en el cuerpo denotan la conjunción: p ← p1 , . . . , pm . se debe leer como p ← p1 ∧ . . . ∧ p m . Una regla p ← p1, . . . , pm. tiene el significado “si p1 y . . . y pm son ciertos, entonces p es cierto” (“←” denota la implicación lógica) Ejemplo: la regla mascota(X) <- animal(X), ladra(X). se puede leer como “X es una mascota si es un animal y ladra” Observad que un hecho p. se puede escribir también de la forma p <- true. 8 Significado declarativo de los predicados Predicados: las cláusulas en el mismo predicado p ← p1 , . . . , pn . p ← q1, . . . , qm. ... definen alternativas diferentes (para p), i.e., se pueden leer como “para demostrar p, demostrar p1 ∧ . . . ∧ pn, o bien demostrar q1 ∧ . . . ∧ qm, o bien. . . ” Ejemplo: las reglas mascota(X) <- animal(X), ladra(X). mascota(X) <- animal(X), vuela(X). expresan dos formas en las que X puede ser una mascota Observad que las variables X en las dos cláusulas anteriores son diferentes (pese a que tienen el mismo nombre): las variables son locales a las cláusulas (y se renombran cada vez que se usa una cláusula) 9 Programas, objetivos y ejecución Programa lógico: un conjunto de definiciones de predicados (o procedimientos) Nota: en ocasiones el objetivo (definido a continuación) se considera también parte del programa Ejemplo mascota(X) <- animal(X), ladra(X). mascota(X) <- animal(X), vuela(X). animal(ricky). animal(piolin). animal(hobbes). ladra(ricky). vuela(piolin). ruge(hobbes). Objetivo: una expresión de la forma ← p1(t1, . . .), . . . , pn(tn, . . .). (i.e., una cláusula sin cabeza) Ejemplo: <- mascota(X). Los objetivos representan preguntas al programa 10 Ejecución: dado un programa y un objetivo, un sistema de programación lógica intentará encontrar una “respuesta” al objetivo Observad que la semántica declarativa no especifica cómo hacer esto ⇒ este es el papel de la semántica operacional Ejemplo: en el programa anterior, el sistema intentará encontrar una “sustitución” para X que haga mascota(X) cierto. Intuitivamente, tenemos dos posibles respuestas: ricky y piolin 11 Significado operacional Un programa lógico tiene el significado operacional [Kowalski]: Una cláusula p ← p1, . . . , pm. expresa que “para demostrar p, hay que demostrar primero p1 y . . . y pm” En principio, el orden en el que los literales del cuerpo se resuelven no importa, aunque, dependiendo del sistema, el orden puede estar fijado La presencia de varias cláusulas aplicables para un literal dado del cuerpo significa que existen varios caminos posibles a una solución, y todos ellos deben ser explorados En principio, el orden en que estos caminos son explorados no importa, aunque, dependiendo del sistema, esto también puede estar fijado 12 El árbol de búsqueda Arbol de búsqueda: un objetivo + un programa lógico determinan un árbol de búsqueda Los detalles de la semántica operacional explican como se debe explorar este árbol de búsqueda en ejecución —de hecho, semánticas operacionales diferentes dan lugar a árboles diferentes Ejemplo: el objetivo ← mascota(X) con el programa anterior genera el árbol de búsqueda: (en las cajas: bloques “and”) Observad que objetivo diferente ⇒ árbol diferente 13 Sustituciones Una sustitución es una aplicación finita de variables a términos, denotada por θ = {X1/t1, . . . , Xn/tn}, donde • las variables X1, . . . , Xn son diferentes • para i = 1, . . . , n, Xi 6≡ ti Un par Xi/ti se llama un enlace (binding) Las sustituciones operan sobre expresiones (denotadas por E), i.e., un término, una secuencia de literales, o una cláusula La aplicación de θ a E (denotado por Eθ) se obtiene reemplazando simultaneamente cada ocurrencia de Xi en E por ti, donde Xi/ti ∈ θ La expresión resultante Eθ se denomina una instancia de E Ejemplo Sea θ = {X/f (Y ), Y /h(a)} y E ≡ p(X, Y, c) Entonces, Eθ ≡ p(f (Y ), h(a), c) 14 Dadas θ = {X1/t1, . . . , Xn/tn} y σ = {Y1/s1, . . . , Ym/sm}, su composición θσ se define a partir del conjunto: {X1/t1σ, . . . , Xn/tnσ, Y1/s1, . . . , Ym/sm} eliminando aquellos pares Xi/tiσ tales que Xi ≡ tiσ, ası́ como aquellos pares Yi/si tales que Yi ∈ {X1, . . . , Xn} Ejemplo: si θ = {X/3, Y /f (X, 1)} y σ = {X/4}, entonces θσ = {X/3, Y /f (4, 1)} Una sustitución θ es más general que σ si existe algún γ tal que θγ = σ Ejemplo: θ = {X/f (Y )} es más general que σ = {X/f (h(Z)), Y /h(Z)} 15 Unificación Unificar dos términos A y B consiste en encontrar la menor sustitución para sus variables que los hace idénticos Ejemplo A unifica vuela(piolin) X X f (X, g(t)) f (X, g(t)) f (X, X) con B vuela(piolin) Y a f (m(h), g(M )) f (m(h), t(M )) f (Y, h(Y )) usando θ {} {X/Y } {X/a} {X/m(h), M/t} imposible (1) imposible (2) 1. dos términos con diferente nombre o aridad no se pueden unificar (sólo podemos asignar valores a las variables) 2. una variable no se puede enlazar a un término que contenga dicha variable (crearı́amos un término infinito). Esto se conoce como “occur check” Si Aθ ≡ Bθ, entonces • θ es un unificador de A y B • A y B son unificables Dados dos términos, si son unificables, entonces siempre existe un unificador más general (mgu) único para ellos 16 Algoritmo de unificación Sean A y B dos términos: 1. θ = ǫ (≡ { }) 2. while Aθ 6≡ Bθ do a) buscamos el sı́mbolo más a la izquierda de Aθ tal que el correspondiente sı́mbolo de Bθ sea distinto b) sean tA y tB los términos de Aθ y Bθ encabezados por dichos sı́mbolos 1) si ninguno de los dos es una variable, o uno es una variable que aparece en el otro ⇒ parar con fallo 2) en otro caso, sea tA una variable ⇒ la nueva θ es el resultado de θ{tA/tB } 3. devolver θ como el mgu de A y B Ejemplo A ≡ p(X, X), B ≡ p(f (A), f (B)) θ ǫ {X/f (A)} {X/f (B), A/B} Aθ p(X, X) p(f (A), f (A)) p(f (B), f (B)) Bθ p(f (A), f (B)) p(f (A), f (B)) p(f (B), f (B)) Enlace {X/f (A)} {A/B} fin Ejemplo A ≡ p(X, f (Y )), B ≡ p(Z, X) θ ǫ {X/Z} {X/f (Y ), Z/f (Y )} Aθ p(X, f (Y )) p(Z, f (Y )) p(f (Y ), f (Y )) 17 Bθ p(Z, X) p(Z, Z) p(f (Y ), f (Y )) Enlace {X/Z} {Z/f (Y )} fin Un intérprete (elemental) de programas lógicos Entrada: un programa lógico P y un objetivo Q Salida: Qθ (sustitución respuesta) si Q es demostrable en P , f allo en otro caso Algoritmo: 1. Inicializar el “resolvente” R a {Q} 2. while R no sea vacı́o do a) elegir un literal A de R b) elegir una cláusula (renombrada) A′ ← B1, . . . , Bn de P c) unificar A y A′ ⇒ θ (si no hay ninguna, terminar con fallo) d ) eliminar A de R y añadir B1, . . . , Bn a R e) aplicar θ a R y Q 3. devolver Q Los pasos (a) y (b) son indeterministas Regla de computación: decide qué literal (a) Regla de búsqueda: decide que cláusula (b) La elección de una cláusula distinta (b) puede llevar a una solución distinta En general, hay que intentar varias alternativas antes de encontrar la solución (o las soluciones) 18 Comportamiento operacional Dado el programa: C1: C2: mascota(X) <- animal(X), ladra(X). mascota(X) <- animal(X), vuela(X). C3: C4: C5: animal(ricky). animal(piolin). animal(hobbes). C6: C7: C8: ladra(ricky). vuela(piolin). ruge(hobbes). Resolver <- mascota(X).: Q mascota(X) mascota(X1) mascota(piolin) mascota(piolin) R mascota(X) animal(X1), vuela(X1) animal(piolin) — Cláusula C2 C7 C4 — θ {X/X1} {X1/piolin} { } — Resolver <- mascota(X). (estrategia diferente): Q mascota(X) mascota(X1) mascota(hobbes) R mascota(X) animal(X1), ladra(X1) ladra(hobbes) 19 Cláusula C1 C5 ??? θ {X/X1} {X1/hobbes} fallo Programación de bases de datos Una base de datos lógica es un conjunto de hechos y reglas, i.e., un programa lógico: padre_de(juan, pedro). padre_de(juan, maria). padre_de(pedro, miguel). madre_de(maria, david). abuelo_de(L, M) <- padre_de(L, N), padre_de(N, M). abuelo_de(X, Y) <- padre_de(X, Z), madre_de(Z, Y). Dada esta BD, un sistema de programación lógica puede responder a preguntas como: <- padre_de(juan, pedro). Yes <- padre_de(juan, david). No <- padre_de(juan, X). {X = pedro} {X = maria} <- abuelo_de(X, miguel). {X = juan} <- abuelo_de(X,Y). {X = juan, Y = miguel} {X = juan, Y = david} <- abuelo_de(X,X). No Ejercicio: Reglas para abuela_de(X,Y)? 20 Programas lógicos y el modelo relacional de BD Tradicional ⇒ Modelo relacional Fichero Relación Tabla Registro Tupla Fila Campo Atributo Columna Ejemplo: Nombre Alonso Perez Borja Edad 20 36 26 Sexo H M M Nombre Alonso Alonso Perez Borja Borja persona Ciudad Londres Madrid Lisboa Paris Valencia residió en El orden de las filas no es importante No se permiten filas duplicadas 21 Años 3 7 2 1 6 Programas lógicos y el modelo relacional de BD (cont.) BD relacionales ⇒ Programación lógica Nombre de relación sı́mbolo de predicado Relación Predicado consistente en hechos “básicos” (hechos sin variables) Tupla Hecho básico Atributo Argumento de un predicado Ejemplo: persona(alonso, 20, hombre). persona(perez, 36, mujer). persona(borja, 26, mujer). Ejemplo: residio_en(alonso, londres, 3). residio_en(alonso, madrid, 7). residio_en(perez, lisboa, 2). residio_en(borja, paris, 1). residio_en(borja, valencia, 6). 22 Programas lógicos y el modelo relacional de BD (cont.) Las operaciones del modelo relacional se pueden implementar fácilmente como reglas: • Unión: r_union_s(X1,...,Xn) <- r(X1,...,Xn). r_union_s(X1,...,Xn) <- s(X1,...,Xn). • Diferencia de conjuntos: r_dif_s(X1,...,Xn) <- r(X1,...,Xn), not s(X1,...Xn). (hablaremos de la negación más adelante. . . ) • Producto cartesiano: r_X_s(X1,...,Xn,Xn+1,...Xn+m) <- r(X1,...,Xn), s(Xn+1,...Xn+m). • Proyección: r13(X1,X3) <- r(X1,X2,X3). • Selección: r_sel(X1,X2,X3) <- r(X1,X2,X3), <(X2,X3). 23 Programas lógicos y el modelo relacional de BD (cont.) Otras operaciones derivadas también pueden expresarse más fácilmente en programación lógica: • Intersección: r_inter_s(X1,...,Xn) <- r(X1,...,Xn), s(X1,...,Xn). • “Join”: r_joinX2_s(X1,...,Xn) <- r(X1,X2,X3,..,Xn), s(Y1,X2,Y3,...Yn). Generación de duplicados ⇒ ver setof en Prolog Las bases de datos deductivas usan estas ideas para desarrollar bases de datos basadas en la lógica • a menudo se usa un subconjunto de los programas lógicos, e.g., “Datalog”, donde no hay functores • se suele utilizar alguna variante del mecanismo “bottomup” como estrategia de ejecución, e.g., el operador TP (se verá más adelante) modificado para que tenga en cuenta el objetivo 24 Programación recursiva Ejemplo: antepasado parent(X, Y) <- father(X, Y). parent(X, Y) <- mother(X, Y). ancestor(X,Y) <- parent(X, Y). ancestor(X,Y) <- parent(X, Z), parent(Z, Y). ancestor(X,Y) <- parent(X, Z), parent(Z, W), parent(W, Y). ancestor(X,Y) <- parent(X, parent(Z, parent(W, parent(K, ... Z), W), K), Y). Podemos dar una definición recursiva: parent(X, Y) <- father(X, Y). parent(X, Y) <- mother(X, Y). ancestor(X,Y) <- parent(X, Y). ancestor(X,Y) <- parent(X, Z), ancestor(Z, Y). 25 Programación recursiva: Aritmética Los términos se pueden clasificar en tipos, cada uno de ellos conteniendo un conjunto (posiblemente infinito) de términos Un programa que defina un tipo se denomina una definición de tipo Tipos recursivos: se definen mediante programas lógicos recursivos El tipo recursivo más simple: números naturales: 0, s(0), s(s(0)), ... definido por el programa lógico: numero_natural(0). numero_natural(s(X)) <- numero_natural(X). Los números naturales se pueden ordenar por “<=”: <=(0, X) <- numero_natural(X). <=(s(X), s(Y)) <- <=(X, Y). Diferentes usos: <=(X,s(s(0))), <=(s(s(0)),Y), . . . 26 Operación suma: suma(0, X, X) <- numero_natural(X). suma(s(X), Y, s(Z)) <- suma(X, Y, Z). Diferentes usos: suma(s(s(0)),s(0),Z), suma(s(s(0)),Y,s(s(s(0)))),. . . Varias soluciones: suma(X, Y, s(s(s(0)))), . . . Otra posible definición para la suma: suma(X, 0, X) <- numero_natural(X). suma(X, s(Y), s(Z)) <- suma(X, Y, Z). El significado de suma es el mismo si se unen las dos definiciones Pero, no es recomendable: existen varios árboles de prueba para el mismo objetivo ⇒ no es eficiente, no es conciso. El objetivo de la programación lógica: encontrar formulaciones compactas y computacionalmente eficientes 27 Ejercicio: definir producto(X, Y, Z) (Z = X∗Y ), exp(N,X,Y) (Y = X N ), fact(N,F) (F = N !), minimo(N1,N2,Min) Ejemplo: La función mod(X,Y,Z) (∃Q. X = Y ∗ Q + Z y Z < Y ) se puede definir: mod(X,Y,Z) <- producto(Y,Q,W), suma(W,Z,X), Z < Y. Otra posible definición: mod(X, Y, X) <- X < Y. mod(X, Y, Z) <- suma(X1, Y, X), mod(X1, Y, Z). La segunda es mucho más eficiente que la primera (comparad el tamaño de los árboles de búsqueda) La función de Ackermann: ackermann(0,N) = N+1 ackermann(M,0) = ackermann(M-1, 1) ackermann(M,N) = ackermann(M-1, ackermann(M,N-1)) se puede definir ası́: ackermann(0,N,s(N)). ackermann(s(M),0,Val) <ackermann(M,s(0),Val). ackermann(s(M),s(N),Val) <ackermann(s(M),N,Val1), ackermann(M,Val1,Val). 28 Programación recursiva: Listas Se trata de una estructura binaria: el primer argumento es un elemento, el segundo argumento es el resto de la lista Necesitamos: • un sı́mbolo constante: la lista vacı́a, denotada por [ ] • un functor de aridad 2: tradicionalmente el punto “.” Notación alternativa: .(X,Y) ⇒ [X|Y] (X es la cabeza, Y es la cola) Ejemplo Objeto formal .(a,[]) .(a,.(b,[])) .(a,.(b,.(c,[]))) .(a,X) .(a,.(b,X)) Notación cabeza/cola [a|[]] [a|[b|[]]] [a|[b|[c|[]]]] [a|X] [a|[b|X]] Notación por enumeración [a] [a,b] [a,b,c] [a|X] [a,b|X] Observad que: [a,b] y [a|X] unifican usando {X/[b]} [a] y [a|X] unifican usando {X/[]} [a] y [a,b|X] no unifican [] y [X] no unifican Definición del tipo lista: lista([]). lista([X|Y]) <- lista(Y). (lista de números naturales, etc.) 29 Programación recursiva: Listas (cont.) X es miembro de la lista Y : member(a,[a]). member(b,[b]). etc. ⇒ member(X,[X]). member(a,[a,c]). member(b,[b,d]). etc. ⇒ member(X,[X,Y]). member(a,[a,c,d]). member(b,[b,d,l]). etc. ⇒ member(X,[X,Y,Z]). ⇒ member(X,[X|Y]) <- lista(Y). member(a,[c,a]). member(b,[d,b]). etc. ⇒ member(X,[Y,X]). member(a,[c,d,a]). member(b,[s,t,b]). etc. ⇒ member(X,[Z,Y,X]). ⇒ member(X,[Y|Z]) <- member(X,Z). Usos de member: • comprobar si un elemento pertenece a una lista (member(b, [a,b,c])) • buscar un elemento de una lista (member(X,[a,b,c])) • buscar una lista que contenga un cierto elemento (member(a,L)) Ejercicio: definir prefijo(X,Y) (la lista X es un prefijo de la lista Y), e.g., prefijo([a,b], [a,b,c,d]) Ejercicio: definir sufijo(X,Y), sublista(X,Y) 30 Programación recursiva: Listas (cont.) Operación básica: concatenación de listas append([],[a],[a]). append([],[a,b],[a,b]). ⇒ append([],Y,Y) <- lista(Y). append([a],[b],[a,b]). append([a],[b,c],[a,b,c]). etc. ⇒ append([X],Y,[X|Y]) <- lista(Y). append([a,b],[c],[a,b,c]). append([a,b],[c,d],[a,b,c,d]). etc. ⇒ append([X,Z],Y,[X,Z|Y]) <- lista(Y). etc. Obtendrı́amos infinitas cláusulas ⇒ buscamos otra solución: append([a,b],Y,[a|[b|Y]]). ≡ append([a,b],Y,[a|Z]) donde Z = append([a,b,c],Y,[a|[b|[c|Y]]]). append([a,b,c],Y,[a|Z]) donde Z ⇒ append([X|Xs],Ys,[X|Zs]) <- [b|Y] ≡ = [b|W], W = [c|Y] append(Xs,Ys,Zs). Ası́, tenemos: append([],Ys,Ys) <- lista(Ys). append([X|Xs],Ys,[X|Zs]) <- append(Xs,Ys,Zs). Usos de append: • concatenar 2 listas dadas: append([a,b],[c],Z) • calcular la resta de listas: append(X,[c],[a,b,c]) • dividir una lista: append(X,Y,[a,b,c]) Ejercicio: definir length(Xs,N) (N es la longitud de Xs) 31 Programación recursiva: Listas (cont.) reverse(Xs,Ys): Ys es la lista resultante de invertir los elementos de la lista Xs para cada elemento X de Xs, debemos poner X al final de la lista resultante de invertir el resto de Xs: reverse([X|Xs],Ys) <- reverse(Xs,Zs), append(Zs,[X],Ys). ¿Cómo podemos parar? reverse([],[]). Definido de esta forma reverse es muy ineficiente! Otra posible definición es: reverse(Xs,Ys) <- reverse(Xs,[],Ys). reverse([],Ys,Ys). reverse([X|Xs],Acc,Ys) <- reverse(Xs,[X|Acc],Ys). Ejercicio: encontrar las diferencias en términos de eficiencia entre esta definición y la anterior 32 Aprendiendo a construir programas recursivos Hasta cierto punto, es una cuestión de práctica. . . Se pueden obtener por inducción (como en los ejemplos previos): elegante, pero generalmente difı́cil (no es la forma en la que la mayor parte de la gente lo hace) Más usualmente: establecer el/los caso/s base/s, y pensar entonces en el/los caso/s recursivo/s A menudo, puede servir de ayuda al construir el programa tener en mente una posible ejecución del programa, asegurandose que es “declarativamente” correcto (i.e., considerar si los usos alternativos tienen sentido desde el punto de vista declarativo) Aproximación global “top-down”: • enunciar el problema general • descomponerlo en subproblemas • resolver los subproblemas De nuevo, la mejor aproximación: práctica. . . 33 Programación recursiva: árboles binarios Se representan con un functor ternario tree(Element,Left,Right) El árbol vacı́o se denota por void Definición: binary_tree(void). binary_tree(tree(Element,Left,Right) <binary_tree(Left), binary_tree(Right). Definición de tree_member(Element,Tree): tree_member(X,tree(X,Left,Right)). tree_member(X,tree(Y,Left,Right)) <tree_member(X,Left). tree_member(X,tree(Y,Left,Right)) <tree_member(X,Right). Definición de pre_order(Tree,Order): pre_order(void,[]). pre_order(tree(X,Left,Right),Order) <pre_order(Left,OrderLeft), pre_order(Right,OrderRight), append([X|OrderLeft],OrderRight,Order). Ejercicio: definir in_order(Tree,Order) y post_order(Tree,Order) 34 Prog. rec.: manipulación de expresiones simbólicas Torres de Hanoi: mover una torre de n discos de un palo a otro, con la ayuda de un tercero auxiliar Reglas: • sólo se puede mover un disco cada vez • un disco nunca puede estar sobre otro disco más pequeño El predicado es hanoi(N,A,B,C,Moves) donde Moves es la secuencia de movimientos para llevar N discos de A a B usando C como auxiliar Cada movimiento move(A,B) representa el desplazamiento del disco superior del palo A al palo B hanoi(s(0),A,B,C,[move(A,B)]). hanoi(s(s(N)),A,B,C,Moves) <hanoi(s(N),A,C,B,Moves1), hanoi(s(N),C,B,A,Moves2), append(Moves1, [move(A,B)|Moves2], Moves). 35 Prog. rec.: manipulación de exp. simbólicas (cont.) Reconocimiento de la secuencia de caracteres aceptados por el siguiente autómata finito indeterminista: donde q0 es el estado inicial y final Representamos los strings como listas de constantes initial(q0). final(q0). delta(q0,a,q1). delta(q1,b,q0). delta(q1,b,q1). accept(S) <- initial(Q), accept(Q,S). accept(Q,[]) <- final(Q). accept(Q,[X|Xs]) <- delta(Q,X,NewQ), accept(NewQ,Xs). 36 El lenguaje (SICStus) Prolog Prolog Se trata de un lenguaje lógico práctico basado en el paradigma de programación lógica Las principales diferencias con la programación lógica “pura” son: • más control en el flujo de ejecución de los programas • regla de búsqueda en profundidad • regla de computación “left-to-right” • algunos predicados predefinidos no son declarativos • facilidades meta-lógicas • no se realiza el “occur check” en la unificación Ventajas: • se puede compilar a código eficiente • tiene un mayor poder expresivo Inconvenientes: incompletitud (debido a la regla de búsqueda) 38 Sintaxis Prolog y terminologı́a La flecha ← se reemplaza por :e.g., a ← b. se convierte en a :- b. En Prolog, el término “átomo” se suele aplicar a las constantes del programa Las variables y las constantes siguen las normas anteriores • Variables: X, Valor, A, A1, _3, _resultado • Constantes (“átomos”): x, =, [], ’Algol-3’ Números: 0, 999, -77, 5.23, 0.23e-5, 0.23E-5 String: "Prolog" ≡ [80,114,111,108,111,103] (lista de códigos ASCII) 39 Sintaxis Prolog: Operadores Ciertos functores y sı́mbolos de predicado están predefinidos como infijos (o postfijos), aparte de la notación estándar Util para hacer los programas más legibles Notación estándar +(a,/(b,c)) is(X, mod(34,7)) <(+(3,4),8) =(X,f(Y)) -(3) spy(/foo(,3)) :-(p(X),q(Y)) :-(p(X), ’,’,(q(Y),r(Z))) Notación Prolog a+b/c X is 34 mod 7 3 + 4 < 8 X = f(Y) -3 spy foo/3 p(X) :- q(Y) p(X) :- q(Y),r(Z) Observad que con esta notación, las cláusulas en Prolog se puede ver también como términos Prolog 40 El mecanismo de ejecución de Prolog Selecciona siempre el objetivo más a la izquierda del resolvente (regla de computación) Explora el árbol de búsqueda en profundidad considerando las cláusulas alternativas en el orden de aparición en el texto del programa (regla de búsqueda) Alternativas ⇒ backtracking grandparent(C,G) :- parent(C,P), parent(P,G). parent(C,P) :- father(C,P). parent(C,P) :- mother(C,P). father(carlos, felipe). father(ana, jorge). mother(carlos, ana). 41 Anotaciones de control en Prolog El programador dispone de 3 formas para controlar la ejecución: El orden de los objetivos en el cuerpo de las cláusulas: • profundo efecto en el tamaño de la computación (en el lı́mite, sobre la terminación) p(X) :- X = 4. p(X) :- X = 5. q(X,Y) q(X,Y) q(X,Y) q(X,Y) q(X,Y) :::::- X X X X X = = = = = 1, 2, 3, 4, 5, Y Y Y Y Y = = = = = a. b. c. d. e. Comparad p(X),q(X,Y) con q(X,Y),p(X) • el orden óptimo depende del uso (del objetivo) El orden de las cláusulas en un predicado: • afecta al orden en que se generan las soluciones en el ejemplo anterior, {X/4,Y/d} y {X/5,Y/e} o bien {X/5,y/d} y {X/4,Y/d} • si sólo se requiere una solución: relevante para el tamaño de la computación y para la terminación El uso del operador de corte: “!” (se verá más adelante) 42 Interfaz de usuario del (Edimburgh) Prolog El usuario interactúa con un compilador/intérprete Una sesión tı́pica con Prolog puede ser: user$ prolog SICStus 3 #5: Lun, 09 Mar 1998 17:59:14 | ?- compile(file). {compiling file...} {file compiled, 20 msec 384 bytes} yes | ?- query. X = respuesta; X = otra respuesta INTRO | ?- [file]. {consulting file...} {file consulted, 10 msec 112 bytes} yes | ?- trace. yes | ?- otra query. | ?- ... | ?- halt. 43 Cargando programas Consulta de programas (interpretados, útil para la depuración de los programas) | ?- consult(file). | ?- [file]. Compilación de programas (mucho más rápido, menos adecuado para la depuración) | ?- compile(file). Consulta/compilación de varios programas: | ?- compile([file1,file2]). | ?- [file1,file2]. También se pueden introducir las cláusulas desde la terminal (no recomendable, excepto para pequeñas pruebas) | ?- [user]. | append([],Ys,Ys). | append([X|Xs],Ys,[X|Zs]) :- append(Xs,Ys,Zs). | ^D | {user consulted, 0 msec 480 bytes} yes | ?- 44 Interactuando a través de objetivos Fichero member.pl: member(X, [X|_]). member(X, [_|Rest]) :- member(X, Rest). | ?- [member]. {consulting /tmp/member.pl...} {/tmp/member.pl consulted, 10 msec 208 bytes} yes | ?- member(c,[a,b,c]). yes | ?- member(d,[a,b,c]). no | ?- member(X,[a,b,c]). X = a ? ; X = b ? (INTRO) yes | ?- member([X,Y],[[a,b],[c,d]]). X = a, Y = b ? ; X = c, Y = d ? ; no 45 Obteniendo la traza de la ejecución Los procedimientos (predicados) se pueden ver como cajas negras de la forma usual Sin embargo, la información llamada/resultado no es suficiente Los principales eventos en la ejecución de Prolog son: • Call: comienza la ejecución de un objetivo • Exit: ha producido una solución para el objetivo • Redo: intenta buscar soluciones alternativas • Fail: falla en encontrar ninguna solución (más) 46 Ejemplo | ?- [member]. {consulting /tmp/member.pl...} {/tmp/member.pl consulted, 10 msec 208 bytes} | ?- trace. {The debugger will first creep -- showing everything (trace)} yes {trace} | ?- member(X, [a,b]). 1 1 Call: member(_183,[a,b]) ? 1 1 Exit: member(a,[a,b]) ? X = a 1 2 2 1 ? ; 1 Redo: 2 Call: 2 Exit: 1 Exit: member(a,[a,b]) ? member(_183,[b]) ? member(b,[b]) ? member(b,[a,b]) ? X = b 1 2 3 3 2 1 ? ; 1 Redo: 2 Redo: 3 Call: 3 Fail: 2 Fail: 1 Fail: member(b,[a,b]) ? member(b,[b]) ? member(_183,[]) ? member(_183,[]) ? member(_183,[b]) ? member(_183,[a,b]) ? no {trace} | ?- 47 Opciones durante la traza h c INTRO s l f a r b “help”: ayuda “creep”: avanza hasta el siguiente call/exit/redo/fail (lo mismo que antes) “skip”: avanzar hasta la terminación del objetivo actual “leap”: avanzar hasta el siguiente “syspoint” “fail”: provocar el fallo del objetivo actual (fuerza a reconsiderar la primera alternativa pendiente) “abort”: abortar la ejecución en curso “redo”: rehacer la ejecución del objetivo actual “break”: suspende la traza e invoca al intérprete (se puede continuar con ^D) Existen muchas más opciones en los compiladores actuales Además, existe depuradores gráficos en algunos sistemas 48 Puntos espı́a (spypoints) ?- spy foo/3. Pone un spypoint en el predicado foo de aridad 3 (siempre traza los eventos de este predicado) ?- nospy foo/3. Elimina el spypoint de foo/3 ?- nospyall. Elimina todos los spypoints 49 Aritmética El tipo de las expresiones aritméticas es: • un número es una expresión aritmética • si f es un operador aritmético de aridad n y X1,...,Xn son expresiones aritméticas, entonces f(X1,...,Xn) es una expresión aritmética Operadores aritméticos: +, -, *, /, //, mod,... (// es la división entera) Ejemplos • (3*X+Y)/Z será correcta siempre que las variables X, Y, Z se evalúen a expresiones aritméticas (en otro caso, error) • a+3*X siempre será incorrecta (error) Predicados del sistema (predefinidos): • =:= (igualdad), =\= (desigualdad), <, >, =<, => • Z is X: evalúa la expresión aritmética X y el resultado se unifica con Z Sean X,Y variables enlazadas a 3 y 4, respectivamente, y Z una variable libre: • Las expresiones Y < X+1, X is Y+1, X =:= Y fallan (se producirá backtracking) • Las expresiones Y < a+1, X is Z+1 producen un error (se aborta la ejecución) 50 Programas aritméticos Definición original de suma: suma(0,X,X). suma(s(Y),X,s(Z)) :- suma(X,Y,Z). Definición alternativa: suma(X,Y,Z) :- Z is X+Y. Sólo funciona en una dirección (X e Y instanciadas a expresiones aritméticas) Aunque se pueden usar predicados meta-lógicos para que funcione en ambas direcciones Hemos perdido la estructura recursiva de los números. . . Pero hemos ganado en eficiencia! 51 Predicados de tipos Relaciones unarias referentes al tipo de los términos: • integer(X) • float(X) • number(X) • atom(X) (constante) • atomic(X) (constante o número) Versión bi-direccional de suma: suma(X,Y,Z) :- number(X), number(Y), Z is X+Y. suma(X,Y,Z) :- number(X), number(Z), Y is Z-X. suma(X,Y,Z) :- number(Y), number(Z), X is Z-Y. 52 Inspección de estructuras functor(X, F, A): • Si X es f(X1,...Xn), entonces F = f y A = n • Si F es el átomo f y A es un entero 3, entonces X = f(X1,X2,X3) • Si X, F o bien X, A están desinstanciadas, entonces se produce un error • También puede producir un fallo, e.g., functor(f(a,b,c),f,2) arg(N, X, Arg) • Si N es un entero y X es una estructura, entonces Arg se unifica con el n-ésimo argumento de X • Error si N no es un entero o bien si X es una variable desinstanciada • Falla si la unificación falla | ?- functor(Array,array,5), arg(1,Array,negro), arg(5,Array,blanco). Array = array(negro,_,_,_,blanco) Ejercicio: ¿Qué devuelve arg(2,[a,b,c],X).? 53 Inspección de estructuras (cont.) T =.. L • L es la descomposición del término T en una lista compuesta por su functor principal seguido de sus argumentos | ?- fecha(9, febrero, 1947) =.. L. L = [fecha,9,febrero,1947] ? | ?- X =.. [+,a,b]. X = a+b ? • Consejo: no lo uséis, excepto que sea estrictamente necesario (consume mucho tiempo y espacio) name(A,S) • A es el átomo cuyo nombre es la lista de los caracteres ASCII de S | ?- name(hello,S). S = [104,101,108,108,111] ? | ?- name(A, [104,101,108,108,111]). A = hello ? | ?- name(A, "hello"). A = hello ? 54 Entrada/salida Predicado write(X) nl read(X) Explicación escribe el término X en el COS nueva lı́nea en el COS lee un término (terminado en punto) del CIS y lo unifica con X escribe el carácter cuyo código ASCII es N put(X) get(X) lee el código ASCII del siguiente carácter y lo unifica con X File se convierte en el CIS see(File) seeing(File) File se unifica con el CIS Cierra el CIS seen tell(File) File se convierte en el COS telling(File) File se unifica con el COS told Cierra el COS (CIS: current input stream, COS: current output stream) Existen predicados de entrada/salida mucho más refinados (ver manuales) Todos los predicados de entrada/salida pueden tener efectos laterales 55 El operador de corte La ejecución de un corte obliga a Prolog a descartar todas las alternativas pendientes desde el comienzo de la ejecución del objetivo que invocó a la cláusula que contiene el corte. Por tanto, se descartan: • todas las cláusulas por debajo de la cláusula que contiene el corte • todas las soluciones alternativas para los objetivos que aparecen a la izquierda del corte pero no afecta al árbol de búsqueda de los átomos que aparecen a la derecha del corte s(a). s(b). r(a). r(b). p(X,Y) :- l(X). p(X,Y) :- r(X), !, ... p(X,Y) :- m(X), ... con el objetivo s(A), p(B,C) 56 Tipos de cortes Cortes blancos: no eliminan soluciones max(X,Y,X) :- X > Y, !. max(X,Y,Y) :- X =< Y. No afectan ni a la corrección ni a la completitud ⇒ usadlo con total libertad Cortes verdes: eliminan soluciones correctas que no son necesarias membercheck(X, [X|Xs]) :- !. membercheck(X, [Y|Xs]) :- membercheck(X,Xs). Afectan a la completitud, pero no a la corrección ⇒ es necesario en muchos casos, pero usadlo con precaución Cortes rojos: elimina soluciones que no son correctas de acuerdo con el significado pretendido max(X,Y,X) :- X > Y, !. max(X,Y,Y). Pueden afectar a la completitud y a la corrección (e.g., max(6,3,3)) ⇒ evitar siempre que sea posible 57 Predicados meta-lógicos var(X): éxito si X es una variable desinstanciada nonvar(X): éxito si X no es una variable libre ground(X): éxito si X está completamente instanciada No sigue las reglas de la lógica de primer orden: ?- var(X), X = 3. ?- X = 3, var(X). %% Exito %% Fallo Principal uso: aumentar la flexibilidad de ciertos programas que usan predicados predefinidos: length(Xs, N) :nonvar(Xs), length_list(Xs, N). length(Xs, N) :var(Xs), integer(N), length_num(N,Xs). length_list([], 0). length_list([X|Xs], N) :length_list(Xs,N1), N is N1 + 1. length_num(0, []). length_num(N, [X|Xs]) :N > 0, N1 is N -1, length_num(N1, Xs). 58 Comparación de términos no básicos Muchas aplicaciones requieren realizar comparaciones entre términos (posiblemente) no básicos Tests: X == Y, X @> Y, X \== Y X @>= Y, X @< Y, X @=< Y Ejemplo: insertar un elemento en una lista ordenada insert([], Item, [Item]). insert([H|T], Item, [H|T]) :- H == Item. insert([H|T], Item, [Item, H|T]) :- H @> Item. insert([H|T], Item, [H|NewT]) :H @< Item, insert(T, Item, NewT). Ejercicio: comparad el funcionamiento con el programa resultante de sustituir la segunda cláusula por: insert([H|T], Item, [Item|T]) :- H = Item. 59 Meta-llamadas (orden superior) El meta-predicado call(X) convierte el término X en un objetivo y lo lanza a ejecución. X debe estar instanciado a un término válido; en caso contrario, se produce un error Se suele emplear en la meta-programación (e.g., intérpretes, negación, etc.) Otros meta-predicados: setof/3, bagof/3, findall/3 setof(Term, Goal, List): devuelve en List una lista ordenada con todas las instancias de Term para las que Goal tiene éxito • si Goal no tiene ninguna solución, setof falla (pero no devuelve una lista vacı́a) • las variables de Term sólo pueden aparecer en Goal • el conjunto de soluciones debe ser finito (si no, bucle) • si hay variables en Goal que no aparecen en Term, setof puede generar soluciones alternativas para cada posible instanciación de dichas variables 60 Meta-llamadas: ejemplos likes(bill, wine). likes(dick, beer). likes(harry, beer). likes(jan, wine). likes(tom, beer). likes(tom, wine). | ?- setof(X, likes(X,Y), S). S = [dick,harry,tom], Y = beer ? ; S = [bill,jan,tom], Y = wine ? ; no | ?- setof((Y,S), setof(X, likes(X,Y), S), SS). SS = [(beer,[dick,harry,tom]),(wine,[bill,jan,tom])] ? ; no | ?- findall(X, likes(X,Y), S). S = [bill,dick,harry,jan,tom,tom] ? ; no 61 Negación como fallo Se implementa mediante call, el corte y un predicado predefinido fail (que siempre produce un fallo cuando se invoca) not(Goal) :- call(Goal), !, fail. not(Goal). La terminación de not(Goal) depende de la terminación de Goal. Si en la ejecución de Goal aparece al menos una solución a la izquierda de la primera rama infinita, not(Goal) terminará con éxito. not(Goal) nunca instancia variables de Goal Resulta muy útil, pero peligroso: estudiante_soltero(X) :- not(casado(X)), estudiante(X). estudiante(pedro). casado(juan). dado el objetivo estudiante_soltero(X) el sistema dice que no (cuando sı́ hay una solución) La negación como fallo funciona correctamente cuando Goal está completamente instanciado (es misión del programador asegurar que esto ocurre) 62 Corte + fallo La combinación corte + fallo fuerza al sistema a que se produzca un fallo Ejemplo: el siguiente programa comprueba si un término es básico (produciendo un fallo tan pronto como es posible determinar que no es ası́) ground(Term) :- var(Term), !, fail. ground(Term) :nonvar(Term), functor(Term,F,N), ground(N,Term). ground(0,T). ground(N,T) :n>0, arg(N,T,Arg), ground(Arg), N1 is N-1, ground(N1,T). 63 Programación dinámica Es una facilidad muy potente: permite modificar los programas en tiempo de ejecución (e.g., se puede usar para modificar una BD, para simular variables globales, etc.) En algunos casos se puede considerar un error • código difı́cil de leer, de entender, de depurar • a menudo ralentiza la ejecución assert/1 y retract/1 pueden justificarse lógicamente: • aserción de lemmas que se siguen del programa • eliminación de cláusulas lógicamente redundantes Las condiciones impuestas sobre assert/retract dependen de la implementación de Prolog 64 Programación dinámica (ejemplo) Ejemplo relate_numbers(X, Y) :- assert(related(X,Y)). unrelate_numbers(X, Y) :- retract(related(X,Y)). Ejemplos de objetivos: | ?- related(1,2). {EXISTENCE ERROR: related(1,2): procedure user:related/2 does not exist} | ?- relate_numbers(1,2). yes | ?- related(1,2). yes | ?- unrelate_numbers(1,2). yes | ?- related(1,2). no Las reglas también se pueden asertar/eliminar en tiempo de ejecución 65 Programación eficiente en Prolog Eficiencia En general, eficiencia ≡ ahorro de tiempo (número de unificaciones, pasos de reducción, etc.) y memoria Consejos generales: • usar los mejores algoritmos • usar las estructuras de datos más apropiadas Cada paradigma de programación tiene sus técnicas especı́ficas, hay que intentar no trasladar las técnicas de un paradigma a otro de manera directa 67 Estructuras de datos D.H.D Warren: “Prolog significa facilidad para los punteros” No hacer un uso excesivo de las listas: • en general, sólo cuando el número de elementos es desconocido • en ocasiones es conveniente mantenerlas ordenadas • en otro caso, es mejor utilizar estructuras: ◦ menos memoria ◦ acceso directo a cada argumento (arg/3), como los arrays Uso de estructuras de datos avanzadas: • árboles ordenados • estructuras incompletas • estructuras anidadas 68 Dejar el trabajo a la unificación La unificación es muy potente: usarla siempre que sea posible! Ejemplo: intercambiar dos elementos de una estructura f(X, Y) ----> f(Y, X) • versión larga, difı́cil de entender y lenta: swap(S1, S2) :functor(S1, f, 2), functor(S2, f, 2), arg(1, S1, X1), arg(2, S1, Y1), arg(1, S2, X2), arg(2, S2, Y2), X1 = Y2, X2 = Y1. • versión corta, intuitiva y rápida: swap(f(X, Y), f(Y, X)). Ejemplo: comprobar si una lista tiene 3 elementos • versión “mala”: three_elements(L) :length(L, N), N = 3. • versión “buena”: three_elements([_,_,_]). 69 Base de datos Evitar su uso para simular variables globales bad_count(N) :assert(counting(N)), even_worse. even_worse :- retract(counting(0)). even_worse :retract(counting(N)), N > 0, N1 is N-1, assert(counting(N1)), even_worse. | | | | | | | | | | good_count(0). good_count(N) :N > 0, N1 is N-1, good_count(N1). bad count(10000): 165000 bytes, 7.2 segs. good count(10000): 1500 bytes, 0.01 segs. Asertar resultados que se han demostrado ciertos (si van a ser reutilizados) fib(0,0). fib(1,1). fib(N,F) :N > 1, N1 is N-1, N2 is N-2, fib(N1, F1), fib(N2, F2), F is F1+F2. | | | | | | | | | | lfib(N, F) :- lemma_fib(N, F), !. lfib(N, F) :N > 1, N1 is N-1, N2 is N-2, lfib(N1, F1), lfib(N2, F2), F is F1+F2, assert(lemma_fib(N, F)). lemma_fib(0,0). lemma_fib(1,1). fib(24, F): 4800000 bytes, 0.72 segs. lfib(24, F): 3900 bytes, 0.02 segs. 70 Determinismo La mayor parte de los problemas son deterministas El indeterminismo de Prolog es • útil (búsqueda automática) • pero muy costoso Sugerencias: • No mantener las alternativas que no sean necesarias membercheck(X, [X|Xs]) :- !. membercheck(X, [Y|Xs]) :- membercheck(X,Xs). • Implementar problemas deterministas de forma determinista Ejemplo: comprobar que dos listas (ground) tienen los mismos elementos • versión “mala” same_elements(L1, L2) :not( member(X, L1), not(member(X, L2)) ), not( member(X, L2), not(member(X, L1)) ). tiempo ejecución: 7.1 segs. • versión “buena” same_elements(L1, L2) :sort(L1, Sorted), sort(L2, Sorted). (sort tiene un coste de O(N log N)) tiempo ejecución: 0.01 segs. 71 Orden de búsqueda Regla de oro: fallar tan pronto como sea posible Para ello, suele ser necesario reordenar los objetivos del cuerpo de las cláusulas Ejemplo: generación y prueba generate_z(Z) :generate_x(X), generate_y(X, Y), test_x(X), test_y(Y), combine(X, Y, Z). Mejor: realizar los tests lo antes posible: generate_z(Z) :generate_x(X), test_x(X), generate_y(X, Y), test_y(Y), combine(X, Y, Z). Mejor aún: realizar los tests tan profundamente como sea posible: generate_z(Z) :generate_x_test(X), generate_y_test(X, Y), combine(X, Y, Z). 72 Indexación La indexación se realiza sobre el primer argumento: • en tiempo de compilación se construye una tabla de indexación para cada predicado, basada en el functor principal del primer argumento de las cabezas de sus cláusulas • en tiempo de ejecución, sólo se consideran las cláusulas cuyo functor es compatible con el de la llamada Por ello, algunas cláusulas se localizan de forma más rápida y en ocasiones se eliminan puntos de elección (backtracking) En general, mejora la habilidad para detectar determinismo (importante para ahorrar espacio en memoria) Ejemplo: mayor elemento de una lista bad_greater(_,[]). bad_greater(X,[Y|Ys]) :- X > Y, bad_grater(X,Ys). tiempo ejecución: 2.3 segs. good_greater([],_). good_greater([Y|Ys],X) :- X > Y, good_grater(Ys,X). tiempo ejecución: 0.67 segs. Se puede usar con estructuras diferentes a las listas Prácticamente todos los sistemas Prolog usan este sistema de indexación 73 Iteración vs Recursión Cuando la llamada recursiva de una cláusula aparece la última del cuerpo y no hay alternativas pendientes en la ejecución del predicado, lo podemos considerar una iteración Resulta mucho más eficiente Ejemplo sum([], 0). sum([N|Ns], Sum) :sum(Ns, Inter), Sum is Inter + N. | | | | | | | sum_iter(L, Res) :sum(L, 0, Res). sum([], Res, Res). sum([N|Ns], In, Out) :Inter is In + N, sum(Ns, Inter, Out). sum/2: 0.45 segs. sum_iter/2: 0.12 segs. El esqueleto básico es: cabeza :- <computacion determinista> <llamada recursiva> Se conoce como tail recursion (recursión de cola) Caso particular de last call optimization (LCO) 74 Cortes Los cortes eliminan puntos de elección, “creando” ası́ determinismo Ejemplo a :- test1, !, ... a :- test2, !, ... ... a :- testn, !, ... Recordad: si los tests test1, . . . , testn son mutuamente exclusivos, los cortes no cambian la semántica declarativa del programa En otro caso, llevar cuidado: • declaratividad • legibilidad 75 Retrasando el trabajo En general: • no hacer algo hasta que sea necesario • meter los tests tan pronto como sea posible Ejemplo xyz([], []). xyz([X|Xs], [NX|NXs]) :NX is -X*2, X < 0, xyz(Xs, NXs). xyz([X|Xs], [NX|NXs]) :NX is X*3, X >= 0, xyz(Xs, NXs). tiempo ejecución: 1.05 segs. Retrasando las operaciones aritméticas: xyz([], []). xyz([X|Xs], [NX|NXs]) :X < 0, NX is -X*2, xyz(Xs, NXs). xyz([X|Xs], [NX|NXs]) :X >= 0, NX is X*3, xyz(Xs, NXs). tiempo ejecución: 0.9 segs. 76 Retrasando la unificación (en cabeza) + determinismo: xyz([], []). xyz([X|Xs], Out) :X < 0, !, NX is -X*2, Out = [NX|NXs], xyz(Xs, NXs). xyz([X|Xs], Out) :X >= 0, !, NX is X*3, Out = [NX|NXs], xyz(Xs, NXs). tiempo ejecución: 0.68 segs. Consejo: usar estas técnicas sólo cuando la eficiencia sea fundamental. Pueden hacer los programas: • difı́ciles de entender • difı́ciles de depurar • difı́ciles de mantener 77 Conclusiones Evitar heredar estilos de programación propios de otros lenguajes de programación Intentar programar de una forma declarativa: • mejora la legibilidad • permite aplicar optimizaciones al compilador Evitar el uso de la base de datos dinámica siempre que sea posible Programar computaciones deterministas para los problemas deterministas Poner los tests tan pronto como sea posible en el programa Retrasar las computaciones hasta que sean realmente necesarias 78