Germán Vidal

Anuncio
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
Descargar