INTRODUCCIÓN A PROLOG

51
INTRODUCCIÓN La Programación Lógica es un paradigma de computación que consiste de un enfoque declarativo para escribir programas para el computador. PROLOG es un lenguaje de computación basada en ese paradigma. Los programas lógicos pueden ser entendidos y estudiados a partir de dos conceptos: verdad y deducción lógica. En programación lógica, uno puede preguntarse si un axioma de un programa es verdad bajo alguna interpretación de los símbolos del programa y si ciertas declaraciones lógicas son consecuencia del programa. Esas preguntas pueden ser respondidas independientemente de cualquier mecanismo de ejecución concreto. Por otro lado, PROLOG es un lenguaje de programación con un significado operacional preciso que toma prestado sus conceptos básicos de la programación lógica. Los programas PROLOG son instrucciones para ejecutar sobre el computador. Esas instrucciones casi siempre son leídas como instrucciones lógicas y, lo más importante, el resultado de una computación de un programa PROLOG es una consecuencia lógica de los axiomas en éste. Es ampliamente aceptado que una programación efectiva en PROLOG requiere de una comprensión de la teoría de programación lógica, por lo menos en sus aspectos fundamentales, pero también que esto redunda en un mayor dominio del procesamiento de la información usando el conmputador. UN POCO DE HISTORIA El comienzo de la programación lógica puede ser atribuido a los profesores Kowalski y Colmerauer. R. A. Kowalski. trabajando en la Universidad de Edinburgo, formuló la interpretación procedimental de la lógica de cláusulas de Horn y mostró que el axioma A si B puede ser leído como un procedimiento de un lenguaje de programación recursivo, donde A es la cabeza del procedimiento y B su cuerpo. Al mismo tiempo, a principios de 1970, Colmerauer y su grupo en la Universidad De Marseille-Aix desarrolló un probador de teorema especializado, el cual ellos usaron para implementar sistemas de procesamiento natural. El probador de teorema, lo llamaron PROLOG (for Programation et Logique or Programming in Logic), basado en la interpretación procedimental de Kowalski. El primer interpretador de PROLOG no fue tan rápido como los sistemas LISP, el lenguaje pionero propuesto por el prof. McCarthy y muy popular en los Estados Unidos. Pero esto cambió a mediados de 1970 cuando David H.D. Warren y sus colegas desarrollaron una implementación eficiente de PROLOG. El compilador, el cual fue casi completamente escrito en PROLOG, tradujo cláusulas de PROLOG hacia instrucciones de una máquina abstracta que es ahora conocida como Warren Abstract Machine (WAM). Sin embargo, la comunidad de la Inteligencia Artificial y de la computación de occidente se mantuvo aún ignorante e indiferente a la programación lógica. Esto cambió un poco con el anuncio del Proyecto de Quinta Generación Japonesa el cual claramente afirmó el rol importante de la programación lógica en la próxima generación de sistemas computarizados. No sólo los investigadores sino también el público en general comenzaron a deletrear la palabra PROLOG. Desde entonces, PROLOG está alcanzando nuevas alturas. Actualmente, una de las extensiones más prominentes es la programación lógica con restricciones (Contraint Logic Programming-CLP), con una cantidad de nuevos recursos para Ingeniería Lógica. Otras areas con desarrollos importantes son: la Programación Lógica Inductiva (ILP) y los Agentes en Programación Lógica. PRIMEROS PASOS EN PROLOG

description

Spanish of a prolog programing work at the university.Es un compilado de un escrito referente a prolog, esta intermedio pero suficiente para arrancar.

Transcript of INTRODUCCIÓN A PROLOG

Page 1: INTRODUCCIÓN  A PROLOG

INTRODUCCIÓN

La Programación Lógica es un paradigma de computación que consiste de un enfoque declarativo para escribir programas para el computador. PROLOG es un lenguaje de computación basada en ese paradigma. Los programas lógicos pueden ser entendidos y estudiados a partir de dos conceptos: verdad y deducción lógica. En programación lógica, uno puede preguntarse si un axioma de un programa es verdad bajo alguna interpretación de los símbolos del programa y si ciertas declaraciones lógicas son consecuencia del programa. Esas preguntas pueden ser respondidas independientemente de cualquier mecanismo de ejecución concreto.

Por otro lado, PROLOG es un lenguaje de programación con un significado operacional preciso que toma prestado sus conceptos básicos de la programación lógica. Los programas PROLOG son instrucciones para ejecutar sobre el computador. Esas instrucciones casi siempre son leídas como instrucciones lógicas y, lo más importante, el resultado de una computación de un programa PROLOG es una consecuencia lógica de los axiomas en éste.

Es ampliamente aceptado que una programación efectiva en PROLOG requiere de una comprensión de la teoría de programación lógica, por lo menos en sus aspectos fundamentales, pero también que esto redunda en un mayor dominio del procesamiento de la información usando el conmputador.

UN POCO DE HISTORIA

El comienzo de la programación lógica puede ser atribuido a los profesores Kowalski y Colmerauer. R. A. Kowalski. trabajando en la Universidad de Edinburgo, formuló la interpretación procedimental de la lógica de cláusulas de Horn y mostró que el axioma A si B puede ser leído como un procedimiento de un lenguaje de programación recursivo, donde A es la cabeza del procedimiento y B su cuerpo. Al mismo tiempo, a principios de 1970, Colmerauer y su grupo en la Universidad De Marseille-Aix desarrolló un probador de teorema especializado, el cual ellos usaron para implementar sistemas de procesamiento natural. El probador de teorema, lo llamaron PROLOG (for Programation et Logique or Programming in Logic), basado en la interpretación procedimental de Kowalski.

El primer interpretador de PROLOG no fue tan rápido como los sistemas LISP, el lenguaje pionero propuesto por el prof. McCarthy y muy popular en los Estados Unidos. Pero esto cambió a mediados de 1970 cuando David H.D. Warren y sus colegas desarrollaron una implementación eficiente de PROLOG. El compilador, el cual fue casi completamente escrito en PROLOG, tradujo cláusulas de PROLOG hacia instrucciones de una máquina abstracta que es ahora conocida como Warren Abstract Machine (WAM). Sin embargo, la comunidad de la Inteligencia Artificial y de la computación de occidente se mantuvo aún ignorante e indiferente a la programación lógica.

Esto cambió un poco con el anuncio del Proyecto de Quinta Generación Japonesa el cual claramente afirmó el rol importante de la programación lógica en la próxima generación de sistemas computarizados. No sólo los investigadores sino también el público en general comenzaron a deletrear la palabra PROLOG. Desde entonces, PROLOG está alcanzando nuevas alturas. Actualmente, una de las extensiones más prominentes es la programación lógica con restricciones (Contraint Logic Programming-CLP), con una cantidad de nuevos recursos para Ingeniería Lógica. Otras areas con desarrollos importantes son: la Programación Lógica Inductiva (ILP) y los Agentes en Programación Lógica.

PRIMEROS PASOS EN PROLOG

Page 2: INTRODUCCIÓN  A PROLOG

En este capítulo revisaremos unos pocos ejemplos para ilustrar las ideas básicas detrás de la programación en PROLOG. Pero primero, unas notas acerca de como usar Sistemas PROLOG.

Un programa PROLOG es un conjunto de procedimientos (el orden es indiferente). Cada procedimiento consiste de una o más cláusulas (ahora, el orden de las cláusulas si es importante). Hay dos tipos de cláusulas: hechos y reglas. El programa es almacenado en una base de conocimiento PROLOG. Usualmente cargas un programa dentro de la base de datos usando el comando consult, en la concha PROLOG, en la siguiente forma:

? consult(‘Nombre_Del_archivo_Con_El_Programa’). El comando consult agrega las cláusulas y hechos desde el archivo texto especificado a las cláusulas y hechos ya almacenados en la base de datos. Así puedes cargar más programas dentro de la base de datos a la vez, pero debes ser cuidadoso si los programas no usan los procedimientos con el mismo nombre. De otra manera, debido a la acumulación de cláusulas, esos procedimientos podrían comportarse incorrectamente. Otra forma de invocar este mismo comando es con la notacion de listas:

? [‘Nombre_Del_archivo_Con_El_Programa’].

Puedes también usar el comando reconsult para recargar un programa.

? reconsult('Nombre_Del_Archivo_Con_El_Programa').

Este comando se comporta igual al comando consult (agrega procedimientos dentro de la base de datos) pero si hay un procedimiento en la base de datos con el mismo nombre de algún procedimiento en el archivo reconsultado, entonces el primer procedimiento es reemplazado por la nueva definición. Puedes usar el comando reconsult para cambiar un programa en la base de datos durante la depuración.

El programa PROLOG es ejecutado invocando algún procedimiento del programa de la siguiente forma:

? nombre_Del_Procedimiento(parámetros).

Nota que tu ya has llamado un procedimiento cuando tu consultastes o reconsultastes el archivo. "Llamar un procedimiento" es equivalente a "preguntando una pregunta" en PROLOG.

BASE DE DATOS GENEALÓGICA

Una base de datos genealógica puede ser una buena introducción a PROLOG , pués no es exactamente un conjunto de procedimientos y tiene más elementos que una base de datos común. En ésta lección , presentamos un programa de PROLOG simple que captura relaciones de familia básicas. El programa demuestra algunas de las características de PROLOG como el usar hechos , reglas , variables y la recursión.

Primero, expresamos la propiedad de ser un hombre o mujer usando hechos de PROLOG:

hombre(adan). hombre(pedro). hombre(paul). mujer(maria).

Page 3: INTRODUCCIÓN  A PROLOG

mujer(eva).

Ahora podemos agregar la relación "pariente" el cual asocia pariente y niño:

pariente(adan,pedro). % Significa adan es pariente de pedro. pariente(eva,pedro). pariente(adan,paul). pariente(marry,paul).

Hasta ahora, hemos agregado sólo hechos a nuestro programa. Pero el poder real de PROLOG está en las reglas. Mientras que los hechos afirman la relación explícitamente, las reglas definen la relación en una forma más general. Cada regla tiene su cabeza (nombre de la relación definida), y su cuerpo (las condiciones de definición de la relación). Las siguientes reglas definen las relaciones de ser un padre y ser una madre usando las relaciones definidas previamente de ser un hombre o mujer y ser un pariente.

padre(F,C) :-hombre(F), pariente(F,C).

madre(M,C) :- mujer(M), pariente(M,C).

Notar que usamos variables (comienzan con mayúscula) para expresar la característica que cada hombre el cual es un pariente de cualquier niño es también su padre. Si algún parámetro de la relación no es importante podemos usar variables anónimas (denotadas _) como en esas definiciones:

es_padre(F) :- padre(F,_).

es_madre(M) :- madre(M,_).

Antes de avanzar más, uno debería saber como ejecutar los programas PROLOG. Tu ejecutas el programa haciendo preguntas como ésta:

?-padre(X,paul).

La cual expresa: ¿Quién es padre de Paul? (Estrictamente hablando la frase pregunta si existe "algo" que sea el padre de Paul). La respuesta es X=adan, según nuestro programa anterior.

Ahora extenderemos la base de datos de hechos y trataremos de definir otras relaciones de familia como ser un hijo, tío o abuelo. También, trataremos de preguntar al sistema PROLOG diversas preguntas para ver que sucede. Tu puedes comparar tu propio programa con las siguientes reglas:

hijo(S,P):-hombre(S),pariente(P,S).

hija(D,P):-mujer(D),pariente(P,D).

hermanos(A,B):-pariente(P,A), pariente(P,B), A\=B.

% hermanos tienen al menos un pariente común

% la prueba A\=B preserva que hermanos son personas diferentes.

hermanos_completos(A,B):-

Page 4: INTRODUCCIÓN  A PROLOG

pariente(A,F),pariente(B,F),

pariente(A,M),pariente(B,M),

A\=B, F\=M.

% hermanos completos tienen parientes comunes (ambos)

% la prueba F\=M preserva que hermanos completos tienen dos diferentes parientes(padre y madre,naturalmente)

hermanos2_completos(A,B):-

padre(A,F),padre(B,F),

madre(A,M),madre(B,M), A\=B.

% otra solución para "el problema hermanos completos" que usa las relaciones padre y madre

tio(U,N):-hombre(U),hermanos(U,P),pariente(P,N).

tia(A,N):-mujer(A),hermanos(A,P),pariente(P,N).

abuelo(G,N):-pariente(G,X),pariente(X,N).

Ahora, usamos sólo una regla para expresar la relación definida nuevamente pero podemos también definir la relación usando dos o más reglas. Si queremos expresar que ser un humano significa ser un hombre o ser una mujer, podemos hacerlo por esas dos reglas:

humano(H):-hombre(H). humano(H):-mujer(H).

El cuerpo de la regla puede también usar la relación que está siendo definida. Esta característica es llamada recursión y las siguientes reglas muestran su uso típico.

descendiente(D,A):-pariente(A,D).

descendiente(D,A):-pariente(P,D),descendiente(P,A).

Uno puede usar la característica de PROLOG de variables de entrada y salida no determinadas (algunas veces llamada modo variable o multimodalidad) para definir la relación ancestro.

ancestro(A,D) :- descendiente(D,A).

PRIMERAS ESTRUCTURAS DE DATOS EN PROLOG

Esta lección cubre estructuras de datos en PROLOG. La estructura de datos básica en PROLOG es el término, el cual es expresado como: nombre(argumentos...). Si el número de argumentos es cero entonces estamos hablando de un átomo. Un tipo especial de átomo es el número.

ESTRUCTURAS BÁSICAS (en el ejemplo de las fechas)

Page 5: INTRODUCCIÓN  A PROLOG

En esta sección, introducimos una estructura de datos fecha(Dia,Mes,Año) que representa la fecha. Primero necesitamos un constructor de la estructura de datos fecha que hace la estructura de datos de día, mes y año:

hacer_fecha(D,M,A,fecha(D,M,A)).

Segundo, definimos las funciones para accesar los componentes de la estructura de datos de la siguiente forma:

obtener_año(fecha(_,_,A),A).

obtener_mes(fecha(_,M,_),M).

obtener_dia(fecha(D,_,_),D).

obtener_xxx puede ser usado para probar o generar el componente correspondiente de la estructura de datos, pero no puede ser usado para colocar el valor de la componente. Así, tenemos que definir colocar_xxx para colocar valores a los componentes de la estructura de datos fecha.

colocar_año(A,fecha(D,M,_),fecha(D,M,A)).

colocar_mes(M,fecha(D,_,A),fecha(D,M,A)).

colocar_dia(D,fecha(_,M,A),fecha(D,M,A)).

Ahora, es fácil encontrar el "mismo" día en año próximo o anterior respectivamente usando las funciones obtener y colocar.

año_proximo(Hoy,ProximoAño):- obtener_año(Hoy,A), NA is A+1, colocar_año(NA,Hoy,ProximoAño).

año_anterior(Hoy,AnteriorAño):-obtener_año(Hoy,A), PA is A-1, colocar_año(PA,Hoy,AnteriorAño).

Notar, que la definición siguiente de año_anterior usando año_proximo no es correcta. ¿Tú sabes Por Qué?

año_anterior(Hoy,AnteriorAño):- año_proximo(AnteriorAño,Hoy)). % incorrrecto

Encontrar el año próximo es relativamente fácil pero ¿Qué sucede al encontrar día próximo, es decir, mañana?. Estudiar el siguiente programa para encontrar donde los problemas posibles están ocultos. La definición de probar dia_correcto sigue en la próxima sección que cubre el trabajo con listas.

mañana(Hoy,Tomorrow) :- obtener_day(Hoy,D), ND is +1,colocar_day(Hoy,ND,Tomorrow),fecha_correcta(Mañana).

%el día dentro del mes,es decir,no el último día del mes

mañana(Hoy,Mañana):- obtener_mes(Hoy,M), NM is M+1,colocar_mes(Hoy,NM,Tmp),colocar_dia(Tmp,1,Mañana),fecha_correcta(Mañana).

% Ultimo día del mes

mañana(Hoy,Mañana):- obtener_año(Hoy,A), NA is A+1,hacer_fecha(NA,1,1,Mañana).

Page 6: INTRODUCCIÓN  A PROLOG

% Último día del año

Notar que también es posible y quizás más razonable encapsular la prueba fecha_correcta en las definiciones de hacer_fecha y colocar_xxx.

LISTAS

Una lista es estructura de datos ampliamente usada y construida en PROLOG. Es todavía un término, ejemplo, [1,2,3] es equivalente a '.'(1,'.'(2,'.'(3,nulo))). Las siguientes funciones permiten accesar los elementos de la lista.

cabeza(C,[C|_]).

cola(L,[_|L]). % L es una lista

Es fácil accesar el primer elemento de la lista, es decir, la cabeza. Sin embargo, encontrar el último elemento es un proceso que consume tiempo ya que uno tiene que recorrer la lista completa para encontrarlo. Notar que los siguientes "procedimientos" pueden ser usados para encontrar el primero/último elemento de la lista. Podría aún ser usado para generar una lista con el primero/último elemento dado.

primero(P,[P|_]). % El mismo como cabeza

ultimo(U,[U]).

ultimo(U,[C|L]):- ultimo(U,L).

La misma conclusión se mantiene para encontrar prefijo y sufijo respectivamente. De nuevo, el mismo procedimiento puede ser usado para probar o generar prefijo/sufijo respectivamente también como generar una lista con el prefijo/sufijo dado. Pruébalo.

prefijo([],_).

prefijo([C|L1],[C|L2]):-prefijo(L1,L2).

sufijo(S,S).

sufijo([C|T],L):-suffix(T,L).

Probar membresía es un método importante para trabajar con listas. La definición de PROLOG de miembro puede probar la relación de membresía también como generar miembros sucesivos de una lista. Una función similar, miembro_nth, puede también ser usado para probar o generar el miembro n-th de una lista. Sin embargo, no puede ser usado para contar un número de secuencia de un elemento dado (definir la función que cuenta un número de secuencia de un elemento dado como tarea).

miembro(X,[X|_]).

miembro(X,[_|T]):-miembro(X,T).

Miembro_nth(1,[M|_],M).

Miembro_nth(N,[_|T],M):-N>1,N1 is N-1,miembro_nth (N1,T,M).

Page 7: INTRODUCCIÓN  A PROLOG

Otra función popular sobre listas es agregar el cual agrega una lista a otra lista. Puede también ser usada para separar listas. (ver la siguiente definición de prefijo y sufijo). Este procedimiento es uno de los más representativos en la programación lógica. Sugerimos que se le compare con su equivalente en otros lenguajes (Note que no es solamente agregar una lista a otra).

agregar([],L,L).

agregar([C|T],L,[C|LT]):-agregar(T,L,LT).

Ahora, las relaciones prefijo y sufijo pueden ser fácilmente redefinidas usando agregar:

prefijo(P,L):-agregar(P,_,L).

sufijo(S,L):-agregar(_,S,L).

Agregar puede ser exitosamente usado en muchas otras operaciones con listas incluyendo probar (o generar) sublistas. La siguiente regla explota otra vez el carácter declarativo de PROLOG.

sublista(S,L):-agregar(_,S,P),agregar(P,_,L).

Hay (al menos) dos otras formas de como definir sublista, ejemplo, usando las relaciones prefijo y sufijo. Todas estas definiciones son equivalentes. Sin embargo, el procedimiento sublista3 es probablemente lo más cercano al estilo de programación tradicional (no declarativo) ya que usa la técnica conocida como "floating window".

sublista2(S,L):-prefijo(P,L), sufijo(S,P).

sublista3(S,L) :- prefijo(S,L).

sublista3(S,[_|T]) :- sublista3(S,T).

REGRESO AL EJEMPLO DE FECHA

Vamos a retornar a nuestro ejemplo de la estructura de datos fecha. Ahora, vamos a definir la prueba fecha_correcta usando listas.

Primero, agregamos dos hechos a la base de datos PROLOG con la distribución de días:

año(regular,[31,28,31,30,31,30,31,31,30,31,30,31]).

año(bisiesto,[31,29,31,30,31,30,31,31,30,31,30,31]).

Entonces, preparamos la prueba para año bisiesto (versión simplificada):

es_año_bisiesto(A):-

Z is A mod 4, Z=0. % cada cuatro años es bisiesto(simplificada)

Page 8: INTRODUCCIÓN  A PROLOG

Finalmente, es posible probar la correctitud de fecha:

fecha_correcta(fecha(D,M,A)):-

mes_correcto(M),

dia_correcto(D,M,A).

mes_correcto(M):- M>0, M<13.

dia_correcto (D,2,A):- % la única diferencia entre años bisiesto y regular es en febrero

es_año_bisiesto(A),

probar_dia_del_año(D,2,bisiesto).

dia_correcto (D,M,A):-

M\=2, probar_dia_del_año(D,2,regular).

probar_dia_del_año (Tipo,M,D):-

año(Tipo,Dias), miembro_nth(M,Dias,Max), D>0, D=<Max.

Notar, que la definición anteriorde dia_correcto asume que la única diferencia entre años regular y bisiesto es el número de días en febrero el cual es más alto en años bisiestos.

FLUJO DE DATOS Y RECURSIÓN

En ésta lección hablaremos acerca del flujo de datos especialmente durante la manipulación con estructuras de datos recursivas. Recursión es una técnica básica que es usada para manipular datos para los cuales su tamaño no es conocido al comienzo (por otro lado iteración es probablemente más conveniente). Usualmente, tales datos son representados usando definición recursiva.

REPRESENTACIÓN UNARIA DE LOS NÚMEROS

Page 9: INTRODUCCIÓN  A PROLOG

PROLOG tiene su propia representación de los números. Sin embargo, para propósito de éste tutorial, definiremos otra representación de los números naturales. Ésta simple representación unaria nos ayudará a presentar un flujo de datos durante la recursión. Notar que es un ejemplo típico de definición recursiva donde la "semilla" (cero) es definida y otros elementos (números) son sucesivamente creados de elementos definidos previamente.

0 es representado como 0

N+1 es representado como s(X),donde X es una representación de N

La recursión puede ser naturalmente expresada en PROLOG (¿recuerdas la definición de último o miembro en la lección anterior?). Así, es fácil definir una prueba si una estructura dada es un número unario. Notar otra vez que el mismo procedimiento, es decir, num_unario, puede también ser usado para sucesivamente generar "todos" los número naturales. Intentalo.

num_unario(0).

num_unario(s(X)):-num_unario(X).

Ahora, la suma de dos números usando el predicado recursivo suma(X,Y,Z) (X+Y=Z).

suma(0,X,X). % 0+X=X

suma(s(X),Y,Z) :- suma(X,s(Y),Z). % (X+1)+Y=Z <== X+(Y+1)=Z

recursión ---^ ^--- acumulador

El predicado suma usa una estructura de datos llamada acumulador para acumular el subresultado durante el calculo recursivo. Mira el siguiente rastro (trace) del calculo para comprender la idea principal de acumulador.

?-suma(s(s(s(0))), s(0) ,Sum). ^ Sum=s(s(s(s(0))))

?-suma( s(s(0)) , s(s(0)) ,Sum). | Sum=s(s(s(s(0))))

?-suma( s(0) , s(s(s(0))) ,Sum). | Sum=s(s(s(s(0))))

Page 10: INTRODUCCIÓN  A PROLOG

?-suma( 0 ,s(s(s(s(0)))),Sum). | Sum=s(s(s(s(0))))

?-suma( 0 ,s(s(s(s(0)))),s(s(s(s(0))))). %copia acumulador a resultado.

^-- el resultado es acumulado aquí

Es también posible definir la operación suma sin acumulador usando composición de sustituciones.

suma2(0,X,X). % 0+X=X

suma2(s(X),Y,s(Z)): - suma(X,Y,Z). % (X+1)+Y=Z+1 <== X+Y=Z

^--- composición de sustituciones

Mira el siguiente rastro (trace) de ejecución para ver la diferencia entre acumulador y composición de sustituciones.

?-suma2(s(s(s(0))),s(0),S1). ^ S1=s(S2)=s(s(s(s(0))))

?-suma2( s(s(0)) ,s(0),S2). | S2=s(S3)=s(s(s(0)))

?-suma2( s(0) ,s(0),S3). | S3=s(S4)=s(s(0))

?-suma2( 0 ,s(0),S4). | S4=s(0)

?-suma2( 0 ,s(0),s(0)).______|

las sustituciones son compuestas aquí ----^

En este caso particular, la complejidad computacional de ambos enfoques es similar. Además, suma y suma2 son completamente declarativos y pueden ser usados para calcular suma de números también como la diferencia de números y, algunas restricciones, para generar todos los pares de números y, con algunas restricciones, generar todos los pares de números para los cuales la suma está dada (intenta ?-suma(X,Y,s(s(s(0)))).).

Siguiendo las cuatro cláusulas se muestra varias definiciones de resta (X,Y,Z) (X-Y=Z) usando suma y suma2 respectivamente.

resta1a(X,Y,Z):-suma(Y,Z,X).

Page 11: INTRODUCCIÓN  A PROLOG

resta1b(X,Y,Z):-suma(Z,Y,X).

resta2a(X,Y,Z):-suma2(Y,Z,X).

resta2b(X,Y,Z):-suma2(Z,Y,X).

Por supuesto, resta puede también ser definida desde cero usando recursión.

resta(X,0,X). % X-0=X

resta(s(X),s(Y),Z) :- resta(X,Y,Z). % (X+1)-(Y+1)=Z <== X-Y=Z

Compáralo con el próximo procedimiento resta2 el cual está basado sobre otra característica de la operación resta. Notar que resta2 requiere la existencia de solución (intenta ?-resta2(0,s(0),Z). ¿Qué sucede?)

resta2(X,X,0). % X-X=0

resta2(X,Y,s(Z)) :- resta(X,s(Y),Z). % X-Y=Z+1 <== X-(Y+1)=Z

LISTAS (continuación)

Lista es otra estructura de datos recursiva típica. Puede ser definida de la siguiente manera:

[] es una lista

[H|T] es una lista si T es una lista y H es un termino (miembro de la lista).

Algunos ejemplos de las operaciones con listas pueden ser encontrados en la lección anterior. Recuerda que la definición de agregar presentada allá usa composición de sustituciones. En el caso de agregar no es natural usar el acumulador. La definición de la operación borrar(X,L,DL) (borra un elemento X dado desde la lista L, la lista resultante es DL) es también más natural usar composición de sustituciones. Compara los siguientes dos procedimientos: borrar es definido usando composición de sustituciones mientras que borrar2 es definido usando acumulador.

borrar(X,[X|T],T).

Page 12: INTRODUCCIÓN  A PROLOG

borrar(X,[Y|T],[Y|NT]) :- borrar(X,T,NT).

borrar2(X,L,DL) :- del2(X,L,[],DL).

del2(X,[X|T],A,DL) :- agregar(A,T,DL).

del2(X,[Y|T],A,DL) :- del2(X,T,[Y|A],DL).

En este caso particular, la definición de borrar es también más efectiva que borrar2 (omitimos agregar).

Los ejemplos de agregar y borrar no implican que la técnica de acumulador no es útil. Lo opuesto es verdad y, en muchos casos, es más natural y efectivo usar acumulador. El siguiente ejemplo es el caso donde usar acumulador es más efectivo computacionalmente que la composición de sustituciones. El procedimiento revertir revierte una lista dada. La definición de revertir parece natural pero no es efectivo debido a que agrega un elemento al final de la lista usando agregar en cada paso. revertir2, el cual usa acumulador, es más eficiente.

revertir([],[]).

revertir([H|T],Rev):-revertir(T,RT),agregar(RT,[H],Rev)

revertir2(L,RL):-rev_acc(L,[],RL).

rev_acc([],Acc,Acc). % La lista revertida está en Acc

rev_acc([H|T],Acc,Rev):-rev_acc(T,[H|Acc],Rev).

% Acc contiene parte de la lista revertida hasta ahora.

Algunas veces, es posible combinar ambas técnicas acumulador y composición de sustituciones para alcanzar la solución. Mira la definición de partir la cual también explota la unificación para probar si ambas mitades tienen la misma longitud. Sin embargo, probar la longitud en cada paso, la cual es ejecutada por la primera cláusula hv, no es muy efectiva y por lo tanto partir no es realmente rápido.

partir(L,A,B) :- hv(L,[],A,B).

Page 13: INTRODUCCIÓN  A PROLOG

hv(L,L,[],L).

hv([H|T],Acc,[H|L],B) :- hv(T,[_|Acc],L,B).

La siguiente definición de partir2 es más eficiente que el partir original. partir2 también usa unificación para distribuir la lista en dos mitades pero es hecho una vez al final del calculo solamente. Compare partir y partir2 sobre ejemplos grandes (una lista con 100 000+ miembros) para ver la diferencia real.

partir2(L,A,B) :- hv2(L,L,A,B).

hv2([],R,[],R).

hv2([_,_|T],[X|L],[X|L1],R) :- hv2(T,L,L1,R).

¿ Sabes como hacer el procedimiento hv2 aún más eficiente? Hay un cambio pequeño.

Para terminar la lección de hoy, hay dos ejemplos de "vacaciones". Esperamos que estén claros cada uno de ellos. Compare subpart con la definición de sublista de la sección anterior.

subpart([],_).

subpart([H|T],[H|T2]) :- subpart(T,T2).

subpart(L,[H|T]) :- subpart(L,T).

par_impar(L,E,O) :- impar(L,E,O).

impar([],[],[]).

impar([H|T],E,[H|O) :- par(T,E,O).

par([],[],[]).

par([H|T],[H|E],O) :- impar(T,E,O).

Otra solución a la distribución par-impar de una lista.

Page 14: INTRODUCCIÓN  A PROLOG

par_impar2([],[],[]).

par_impar2([H|T],E,[H|O]):-par_impar2(T,O,E).

La Recursión es poder.

EJECUCIÓN DE ARRIBA HACIA ABAJO (Top Down) VS. EJECUCIÓN DE ABAJO HACIA ARRIBA (Bottom Up)

En el capítulo anterior, estudiamos la recursión, un poderoso método para resolver problemas usando la descomposición a problemas más pequeños del mismo tipo. La recursión en PROLOG usa el método de ejecución de arriba hacia abajo (top down) típicamente.

EJECUCIÓN DE ARRIBA HACIA ABAJO (Top Down)

El cálculo de arriba hacia abajo (Top Down), típicamente usado en PROLOG, comienza con el problema original y lo descompone en problemas más y más simples hasta alcanzar un problema trivial, es decir, el hecho en la base de datos PROLOG es alcanzado. Entonces, la solución de los problemas más grandes está compuesta de las soluciones de los problemas más simples, etc. Hasta que la solución del problema original es obtenida. Los siguientes dos ejemplos presentan programas que usan el cálculo de arriba hacia abajo (Top Down).

FACTORIAL

Para obtener el factorial de N primero calculamos el factorial de N-1 usando el mismo procedimiento (recursión) y entonces, usando el resultado del subproblema, calculamos el factorial de N. La recursión para cuando un problema trivial es alcanzado, es decir, cuando el factorial de 0 es alcanzado, es decir, cuando el factorial de 0 es computado.

fact_td(0,1). fact_td(N,F) :- N>0, N1 is N-1, fact_td(N1,F1), F is N*F1.

Page 15: INTRODUCCIÓN  A PROLOG

Notar, que la complejidad de procedimiento de arriba es lineal, es decir, necesitamos N+1 llamadas del procedimiento fact_td.

FIBONACCI

El siguiente programa calcula los números de Fibonacci usando el método de arriba hacia abajo (Top Down), es decir, si estamos buscando el número de Fibonacci de N>1, primero calculamos los números de Fibonacci de N-1 y N-2 (usando el mismo procedimiento) y, entonces, componemos el número de Fibonacci de N partiendo de los subresultados. En este caso, la recursión para tan pronto como el número de Fibonacci de 0 o 1 es computado.

fibo_td(0,0). fibo_td(1,1). fibo_td(N,F) :- N>1, N1 is N-1, N2 is N-2, fibo_td(N1,F1), fibo_td(N2,F2), F is F1+F2.

Notar, que el procedimiento de arriba es muy ineficiente si la regla de ejecución de PROLOG estándar es usada porque calculamos la misma cosa muchas veces. Por ejemplo, para calcular F(N-1) necesitamos calcular F(N-2) lo cual también es requerido para calcular F(N) que fue descompuesto en F(N-1) y F(N-2). De hecho, la complejidad del procedimiento de arriba es exponencial eso es "no muy eficiente".

EJECUCIÓN DE ABAJO HACIA ARRIBA (Bottom Up)

Aún si el cálculo de arriba hacia abajo es típica para PROLOG, podemos aún simular el cálculo de abajo hacia arriba (Bottom Up) también sin hacer cambios al interpretador de PROLOG. El cálculo de abajo hacia arriba (Bottom Up) comienza con conocer los hechos y extender el conjunto de verdades conocidas usando reglas, es decir, derivar nuevos hechos de reglas y hechos viejos. Esta extensión continúa hasta que el problema resuelto es presentado en el conjunto computado de hechos. En general, el método de abajo hacia arriba (Bottom Up) puro es menos eficiente que el método de arriba hacia abajo (top down)porque muchos hechos son derivados y no tienen nada en común con el problema original. Por lo tanto, PROLOG usa la evaluación de arriba hacia abajo (top down) como un método estándar de ejecución. Sin embargo, en los mismos casos, es posible guiar la evaluación de abajo hacia arriba (Bottom Up) hacia la solución del problema original. Los siguientes dos ejemplos presentan versiones de abajo hacia arriba (Bottom Up) de los dos procedimientos de arriba para calcular factorial y números de Fibonacci. La ventaja de la evaluación de abajo hacia arriba (Bottom Up) es visible principalmente en el procedimiento fibo_bu que es más rápido que fibo_td.

Page 16: INTRODUCCIÓN  A PROLOG

FACTORIAL

Ahora, calculamos el factorial usando el método de abajo hacia arriba (Bottom Up) comenzando con el problema trivial de calcular el factorial de 0 y continuar con el factorial de 1,2 y así sucesivamente hasta que el factorial de N es conocido. Ya que no queremos cambiar la conducta estándar de PROLOG, que es por naturaleza top down, necesitamos simular el cálculo de abajo hacia arriba (Bottom Up), es decir, tenemos que almacenar los hechos calculados usando parámetros adicionales. En este caso, recordamos sólo el último "hecho" calculado a saber el factorial de M en el paso M-th.

fact_bu(N,F) :- fact_bu1(0,1,N,F). fact_bu1(N,F,N,F). fact_bu1(N1,F1,N,F) :- N1<N, N2 is N1+1, F2 is N2*F1, fact_bu1(N2,F2,N,F).

Notar, que la complejidad del procedimiento de arriba es lineal otra vez, es decir, necesitamos N+1 llamadas del procedimiento fact_bu1. Sin embargo, el procedimiento fact_bu1 es más eficiente que fact_td porque puede ser traducido usando iteración, es decir, sin una pila la cual es requerida para la recursión en fact_td.

FIBONACCI

Podemos usar el mismo principio como en " factorial bottom-up" para calcular los números Fibonacci usando el método de abajo hacia arriba (Bottom Up). Ahora, necesitamos recordar los últimos dos números de Fibonacci para ser capaz de calcular el próximo número de Fibonacci.

fibo_bu(N,F) :- fibo_bu1(0,0,1,N,F). fibo_bu1(N,F,_,N,F) fibo_bu1(N1,F1,F2,N,F) :- N1<N, N2 is N1+1, F3 is F1+F2, fibo_bu1(N2,F2,F3,N,F).

Notar, que la complejidad de fibo_bu es lineal y por lo tanto el procedimiento fibo_bu es más eficiente que fibo_td el cual es exponencial.

Page 17: INTRODUCCIÓN  A PROLOG

El número de Fibonacci de 0 es F(0)=0 El número de Fibonacci de 1 es F(1)=1 El número de Fibonacci de N>1 es F(N)=F(N-1)+F(N-2)

ESTRUCTURAS DE DATOS DE PROLOG

En este punto podemos hablar más formalmente de las estructuras de datos (suponiendo que las secciones anteriores han sido cubiertas). El término (term) es la estructura de datos básica en PROLOG. Es decir, todas las cosas incluyendo un programa y los datos puede ser expresados en forma de término. Hay cuatro tipos básicos de términos en PROLOG: variables, términos compuestos, átomos y números. El siguiente diagrama muestra la correlación entre ellos también como ejemplos de los términos correspondientes:

término |-- var {X , Y} |-- nonvar {a,1,f(a),f(X)} |-- compuesto {f(a),f(X)} |-- atómico {a, 1} |-- átomo {a} |-- número {1}

Es posible usar los predicados var, nonvar, compuesto, atómico, átomo y numero para probar el tipo del término dado (ver copiar_term abajo). PROLOG también provee predicados predefinidos para accesar la estructura de términos nonvar y también para construir términos.

arg(N,Term,Arg) - obtiene el argumento N th del término (get(2,f(a,b,c),X) -> X=b) - coloca el argumento N th del término (get(2,f(X,Y,Z),b) -> f(X,b,Z)) functor(Term,Functor,NumberOfArgs) - obtiene el nombre de la función y el número de argumentos desde el término (functor(f(a,b),F,N) -> F=f,N=2) - construye el término con la función dada y el número de argumentos libres (functor(F,f,2) -> F=f(_,_)) =.. - descompone la estructura del término hacia la lista (f(a,b)=..X -> X=[f,a,b]) - construye el término desde la lista dada (T=..[f,a,X] -> T=f(a,X)) name(Text,List) - convierte el nombre hacia la lista de códigos (name(abc,Y) -> Y=[97,98,99])

Page 18: INTRODUCCIÓN  A PROLOG

- construye el nombre desde la lista de códigos (name(X,[97,98,99]) -> X=abc)

Si alguien necesita copiar un término (copiar tiene la misma estructura como el término original pero introduce nuevas variables), es posible usar el predicado copiar_term/2 el cual está incorporado en la mayoría de los sistemas PROLOG. Sin embargo, es directo escribir un código de copiar_term en PROLOG usando los predicados mencionados arriba.

copiar_term(A,B) :- cp(A,[],B,_). cp(A,Vars,A,Vars) :- atómico(A). cp(V,Vars,NV,NVars) :- var(V),registrar_var(V,Vars,NV,NVars). cp(Term,Vars,NTerm,NVars) :- compuesto(Term), Term=..[F|Args], % descomponer el término cp_args(Args,Vars,NArgs,NVars), NTerm=..[F|NArgs]. % construye copia del término cp_args([H|T],Vars,[NH|NT],NVars) :- cp(H,Vars,NH,SVars), cp_args(T,SVars,NT,NVars). cp_args([],Vars,[],Vars).

Durante el copiado alguien tiene que recordar las copias de variables las cuales pueden ser usadas adicionalmente durante el copiado. Por lo tanto, el registro de las copias de las variables es mantenido.

registrar_var(V,[X/H|T],N,[X/H|NT]) :- V\==X, % variables diferentes registrar_var(V,T,N,NT). registrar_var(V,[X/H|T],H,[X/H|T]) :- V==X. % variables iguales registrar_var(V,[],N,[V/N]).

Aquí está un ejemplo que clarifica la noción de copiar un término:

Page 19: INTRODUCCIÓN  A PROLOG

f(X,g(X)) es copia de f(Y,g(Y)) pero no de f(U,g(V)).

UNIFICACIÓN

Unificación es un mecanismo fundamental en todos los lenguajes de programación lógica y crucial en el PROLOG. Con unificación se trata de encontrar la mayoría de sustituciones generales de variables en dos términos tal que después de aplicar ésta sustitución a ambos términos, los términos lleguen a ser iguales. Para unificar los términos A y B, uno puede fácilmente invocar la unificación incorporada A=B. Tratar de unificar términos diferentes para ver lo que la noción de "unificación" realmente significa. Otra vez, es directo escribir código PROLOG de unificación (usamos '=' para probar la igualdad de dos términos atómicos o para unificar la variable con el término solamente).

unificar(A,B) :- atómico(A),atómico(B),A=B. unificar(A,B) :- var(A),A=B. % sin occurs check unificar(A,B) :- nonvar(A),var(B),A=B. % sin occurs check unificar(A,B) :- compuesto(A),compuesto(B), A=..[F|ArgsA],B=..[F|ArgsB], unificar_args(ArgsA,ArgsB). unificar_args([A|TA],[B|TB]) :- unificar(A,B), unificar_args(TA,TB). unificar_args([],[]).

¿Averiguastes lo que significa "chequeo de ocurrencia"? OK, intenta unificar los siguientes términos X y f(X). ¿Qué sucede? La mayoría de los sistemas PROLOG llenarán la memoria completa ya que tratarán de construir el término infinito f(f(f(...))) el cual debería ser el resultado de la unificación. Tales sistemas PROLOG no incorporan chequeo de ocurrencia (occurs check) debido a su consumo de tiempo. El occurs check comprueba la ocurrencia de la variable X en el término T (el cual no es una variable) durante la unificación de X y T. Pero, en general, hacer esto es difícil y consume mucho tiempo de procesamiento.

Page 20: INTRODUCCIÓN  A PROLOG

OPERADORES

Escribir términos en la forma funcion(arg1,arg2,...) no es frecuentemente apropiado desde el punto de vista humano. Sólo compara las siguientes dos transcripciones de la misma cláusula PROLOG:

p(X,Z) :- q(X,Y),r(Y,Z),s(Z). ' :- '(p(X,Z),(','(q(X,Y),','(r(Y,Z),s(Z))))).

¿Cuál prefieres?

Para simplificar la entrada de términos, PROLOG introduce los operadores para permitir el "azúcar sintáctico", es decir, una forma más natural de escribir términos. Los Operadores son usados con términos binarios y unarios solamente. Ellos permiten colocar la localización de la función (prefijo, infijo, posfijo), la característica asociativa y, finalmente, la prioridad entre operadores.

op(Prioridad, Apariencia, Nombre) | |

| -- xfy, yfx, xfx, fx, fy, xf, yf -- El número más alto tiene la prioridad más baja

En lugar de explicar el significado de la definición de arriba, mira el siguiente ejemplo.

op(400,yfx,'*'). % a*b*c significa ((a*b)*c) op(500,yfx,'+'). op(500,yfx,'-'). % cuidado a-b-c significa ((a-b)-c) op(700,xfx,'=') % no es posible escribir a=b=c op(900,fy,not). % uno puede escribir not not a % significa not(not(a)) not 1=2+3+4*5 es equivalente a: not(1=((2+3)+(4*5)))

not('='(1,'+'('+'(2,3),'*'(4,5)))).

Page 21: INTRODUCCIÓN  A PROLOG

Nota que los números indicando prioridad pueden ser diferentes en diversas implementaciones de PROLOG ( Los números en el ejemplo de arriba son tomados desde el PROLOG abierto para Macintosh).

Importante!

La definición de operador no es un nuevo programa para operador sino la "llamada" del objetivo op. Así, si tu quieres definir un operador en el programa, debes escribir :- op(400,yfx,'*').

Los Términos son la base de PROLOG.

PROCESAMIENTO DE LISTA

Lista es una estructura de datos directamente apoyada en PROLOG a través de operaciones para acceder la cabeza y la cola de la lista. Sin embargo, la lista es aún un término de PROLOG tradicional, si alguien usa la notación punto obvia. Describimos la manipulación con listas en la sección "Representando Estructuras de Datos" así miraremos sólo listas diferenciales aquí y continuaremos presentando áreas de uso de listas.

LISTAS DIFERENCIALES o en diferencias.

El problema principal de la implementación de PROLOG de listas es su naturaleza un-camino (one-way). Que significa que si uno quiere acceder al elemento n-th, el / ella tiene que acceder todos los elementos previos en la lista. En particular, si uno quiere agregar un elemento al final de la lista, es necesario ir a través de todos los elementos en la lista como el siguiente programa muestra (compárelo con la implementación de agregar):

adicionar2final(X,[H|T],[H|NewT]) :- agregar(X,T,NewT). adicionar2final(X,[],[X]).

Pero hay una técnica, llamada listas diferenciales, que permite agregar listas o agregar elementos al final de la lista en un paso. Sin embargo debería ser notado que esta técnica no remueve la desventaja de visitar todos los elementos previos en la lista si el elemento n-th es accesado.

Page 22: INTRODUCCIÓN  A PROLOG

Una lista diferencial consiste de dos partes A-B y representa la lista que es obtenida desde A removiendo la cola B, ejemplo, [1,2,3,4]-[3,4] representa la lista [1,2]. Por supuesto, si ambas listas A y B son básicas, entonces no hay ventaja de usar listas diferenciales, pero si ésta técnica es combinada con las ventajas de variables libres y unificación, podemos obtener resultados impresionantes. A saber, la lista [1,2] puede ser representada por la lista diferencial [1,2|X]-X.

Si estandarizamos las listas diferenciales en la última forma, podemos escribir procedimientos en un sólo paso para agregar listas y adicionar elementos al final de la lista:

agregar(A-B,B-D,A-D). adicionar2final(X,A-B,[A|NuevoB]-NuevoB) :- NuevoB=[X|B].

Notar, que la otra lista de operaciones, ejemplo, miembro, tienen que también ser reescritas para trabajar con listas diferenciales.

COMBINATORIAS

Esta clase cubre algoritmos combinatorios básicos los cuales generan sucesivamente todas las permutaciones, combinaciones y variaciones respectivamente. Refresca tu memoria! ¿Cuántas permutaciones, combinaciones y variaciones se pueden generar de un conjunto de N elementos? y ¿Qué pasa si los elementos repetidos son permitidos?

PERMUTACIONES

La permutación de una lista L es una lista que contiene todos los elementos de la lista L en algún orden. Adivina cual permutación es generada primero usando el siguiente procedimiento. Y ¿Qué pasa con el segundo?

perm(Lista,[H|Perm]) :- borrar(H,Lista,Rest),perm(Rest,Perm). perm([],[]). borrar(X,[X|T],T). borrar(X,[H|T],[H|NT]) :- borrar(X,T,NT).

COMBINACIONES

Page 23: INTRODUCCIÓN  A PROLOG

Una combinación es un subconjunto arbitrario del conjunto que contiene un número dado de elementos. El orden de los elementos es irrelevante.

comb(0,_,[]). comb(N,[X|T],[X|Comb]) :- N>0,N1 is N-1,comb(N1,T,Comb). comb(N,[_|T],Comb) :- N>0,comb(N,T,Comb).

Es posible programar un generador de combinaciones sin aritmética: el siguiente procedimiento comb2 asume que la lista con N variables libres como su segundo argumento y enlaza esas variables. Así, usar ?-comb2([1,2,3,4],[X,Y]) para generar combinaciones con dos elementos.

comb2(_,[]). comb2([X|T],[X|Comb]) :- comb2(T,Comb). comb2([_|T],[X|Comb]) :- comb2(T,[X|Comb]).

COMBINACIONES CON ELEMENTOS REPETIDOS

Este tipo de combinación puede contener un elemento más veces. Así, éste no es un conjunto sino un multiconjunto.

comb_rep(0,_,[]). comb_rep(N,[X|T],[X|RComb]) :- N>0,N1 is N-1,comb_rep(N1,[X|T],RComb). comb_rep(N,[_|T],RComb) :- N>0,comb_rep(N,T,RComb).

Intenta programar combinaciones con elementos repetidos también como algoritmos combinatoriales sin aritmética, es decir, sin contador numérico.

VARIACIONES

Una variación es un subconjunto con un número dado de elementos. El orden de los elementos en la variación es importante.

Page 24: INTRODUCCIÓN  A PROLOG

varia(0,_,[]). varia(N,L,[H|Varia]) :- N>0,N1 is N -1, borrar(H,L,Rest),varia(N1,Rest,Varia).

VARIACIONES CON ELEMENTOS REPETIDOS

Otra vez, este tipo de variación puede contener elementos repetidos.

varia_rep(0,_,[]). varia_rep(N,L,[H|RVaria]) :- N>0,N1 is N-1,borrar(H,L,_),varia_rep(N1,L,RVaria).

Las combinatorias, permutaciones y variaciones son poderosas para expresar la complejidad de un algoritmo pero no las incorpore en sus programas.

ORDENAMIENTOS

Esta clase cubre los algoritmos de ordenamiento. Noten que los algoritmos de ordenamiento se codifican en PROLOG en términos muy parecidos al lenguaje natural y, por demás, muy breves. Al mismo tiempo, es también posible codificar las estrategias más eficiente de ordenamiento.

ORDENAMIENTO INGENUO (naive sort)

El ordenamiento ingenuo (Naive sort) no es un algoritmo muy eficiente. Genera todas las permutaciones y luego prueba si la permutación es una lista ordenada. Sin embargo, es muy fácil entender la idea de ordenamiento en este contexto.

ordenamiento_ingenuo(Lista,Ordenado) :- perm(Lista,Ordenado),esta_ordenado(Ordenado). esta_ordenado([]). esta_ordenado)[_]).

Page 25: INTRODUCCIÓN  A PROLOG

esta_ordenado([X,Y|T]) :- X<=Y,esta_ordenado([Y|T]).

El ordenamiento ingenuo (Naive sort) usa el enfoque generar y probar para resolver problemas, el cual es usualmente utilizado en el caso que todas las demás cosas fallen. Sin embargo, en ordenamiento hay varias otras, muy eficientes, estrategias.

ORDENAMIENTO POR INSERCIÓN (insert sort)

El ordenamiento por inserción (Insert sort) es un algoritmo de ordenamiento tradicional. La implementación de PROLOG del ordenamiento por inserción (Insert sort) está basada en la idea de acumulador.

ordenamiento_insercion(Lista,Ordenado) :- ordenamiento_i(Lista,[],Ordenado). ordenamiento_i([],Acc,Acc). ordenamiento_i ([H|T],Acc,Ordenado) :- insercion(H,Acc,NAcc), ordenamiento_i (T,NAcc,Ordenado). insercion(X,[Y|T],[Y|NT]) :- X>Y,insercion(X,T,NT). insercion(X,[Y|T],[X,Y|T] :- X<=Y. insercion(X,[],[X]).

ORDENAMIENTO BURBUJA (bubble sort)

El ordenamiento burbuja (Bubble sort) es otro algoritmo de ordenamiento tradicional el cual no es muy efectivo. Otra vez, usamos acumulador para implementar el ordenamiento burbuja (Bubble sort).

ordenamiento_burbuja(Lista,Ordenado) :- ordenamiento_b(Lista,[],Ordenado). ordenamiento_b([],Acc,Acc). ordenamiento_b([H|T],Acc,Ordenado) :- burbuja(H,T,NT,Max),ordenamiento_b(NT,[Max|Acc],Ordenado). burbuja(X,[],[],X). burbuja(X,[Y|T],[Y|NT],Max) :- X>Y,burbuja(X,T,NT,Max). burbuja(X,[Y|T],[X|NT],Max) :- X<=Y,burbuja(Y,T,NT,Max).

Page 26: INTRODUCCIÓN  A PROLOG

ORDENAMIENTO DE MEZCLA (merge sort)

El ordenamiento de mezcla (Merge sort) es usualmente usado para ordenar grandes archivos pero su idea puede ser utilizada con listas. Si es apropiadamente implementado podría ser un algoritmo muy eficiente.

ordenamiento_mezcla([],[]). ordenamiento_mezcla(Lista,Ordenado) :- divide(Lista,L1,L2), ordenamiento_mezcla(L1,Ordenado1), ordenamiento_mezcla(L2,Ordenado2), mezcla(Ordenado1,Ordenado2,Ordenado). mezcla([],L,L). mezcla(L,[],L) :- L\=[]. mezcla([X|T1],[Y|T2],[X|T]) :- X<=Y,mezcla(T1,[Y|T2],T). mezcla([X|T1],[Y|T2],[Y|T]) :- X>Y,mezcla([X|T1],T2,T).

Podemos usar distribución hacia elementos pares e impares de una lista (otras distribuciones son también posible).

divide(L,L1,L2) :- par_impar(L,L1,L2).

ORDENAMIENTO RÁPIDO (quick sort)

El ordenamiento rápido (Quick sort) es uno de los algoritmos de ordenamiento más rápido. Sin embargo, su poder es frecuentemente sobrevaluado. La eficiencia del ordenamiento rápido (Quick sort) es muy sensible a la escogencia del pivote, el cual es usado para dividir la lista en dos.

quick_sort([],[]). quick_sort([H|T],Ordenado) :- pivote(H,T,L1,L2),quick_sort(L1,Ordenado1),quick_sort(L1,Ordenado2), agregar(Ordenado1,[H|Ordenado2]).

Page 27: INTRODUCCIÓN  A PROLOG

pivote(H,[],[],[]). pivote(H,[X|T],[X|L],G) :- X<=H,pivote(H,T,L,G). pivote(H,[X|T],L,[X|G]) :- X>H,pivote(H,T,L,G).

Similarmente a merge sort, quick sort explota el método divide y vencerás para resolver problemas.

La implementación de arriba de quick sort usa agregar no es muy efectiva. Podemos escribir un mejor programa usando acumulador.

quick_sort2(Lista,Ordenado) :- q_sort(Lista,[],Ordenado). q_sort([],Acc,Acc). q_sort([H|T],Acc,Ordenado) :-

pivote(H,T,L1,L2), quick_sort(L1,Acc,Ordenado1),quick_sort(L1,[H|Ordenado1],Ordenado)

Algún tipo de ordenamiento puede ser encontrado en casi todo los programas actuales.

CONJUNTOS EN PROLOG

Los Conjuntos son una poderosa estructura de datos que pueden ser naturalmente expresados usando listas en PROLOG. Para mejorar la eficiencia de la implementación usamos listas ordenadas como representación de conjuntos. Así, definimos la relación "menor que " y "está en la lista" de la siguiente manera:

lista([]). lista([_|_]). lt(X,Y) :- var(X);var(Y). lt(X,Y) :- nonvar(X),nonvar(Y),X<Y.

UNIÓN, INTERSECCIÓN, DIFERENCIA Y SELECCIÓN

Ahora, agregamos definiciones para operaciones de conjuntos obvias como unión, intersección y diferencia. Notar, como explotamos el orden de los elementos del conjunto en los procedimientos siguientes.

Page 28: INTRODUCCIÓN  A PROLOG

union([],S,S). union(S,[],S) :- S\=[]. union([X|TX],[X|TY],[X|TZ]) :- union(TX,TY,TZ). union([X|TX],[Y|TY],[X|TZ]) :- lt(X,Y), union(TX,[Y|TY],TZ). union([X|TX],[Y|TY],[Y|TZ]) :- lt(Y,X), union([X|TX],TY,TZ). interseccion([],S,[]). interseccion(S,[],[]) :- S\=[]. interseccion([X|TX],[X|TY],[X|TZ]) :- interseccion(TX,TY,TZ). interseccion([X|TX],[Y|TY],TZ) :- lt(X,Y), interseccion(TX,[Y|TY],TZ). interseccion([X|TX],[Y|TY],TZ) :- lt(Y,X), interseccion([X|TX],TY,TZ). diferencia([],S,[]). diferencia(S,[],S) :- S\=[]. diferencia([X|TX],[X|TY],TZ) :- diferencia(TX,TY,TZ). diferencia([X|TX],[Y|TY],[X|TZ]) :- lt(X,Y), diferencia(TX,[Y|TY],TZ). diferencia([X|TX],[Y|TY],TZ) :- lt(Y,X), diferencia([X|TX],TY,TZ).

Podemos también definir una operación selección la cual selecciona los elementos satisfaciendo una condición dada desde un conjunto. Noten como usamos copiar_term y llamada para probar la condición. También, usamos la operación si-entonces-sino (Cond -> entonces_rama ; sino_rama) para acortar el procedimiento.

Page 29: INTRODUCCIÓN  A PROLOG

seleccionar([],X,Cond,[]). seleccionar([H|T],X,Cond,Sel) :- copiar_term(X-Cond,XC-CondC), H=XC, (llamada(CondC) -> Sel=[H|R] ; Sel=R), seleccionar(T,X,Cond,R).

Si no comprendes completamente el significado y uso de la operación selección intenta ejemplos como el siguiente:

?-seleccionar([1,2,3,4],X, X>2,Resultado).

SUBCONJUNTO Y MEMBRESÍA

Es también posible definir relaciones naturales entre conjuntos como subconjunto y membresía. Otra vez notar el uso del orden del elemento que mejora la eficiencia de los procedimientos.

subconjunto([],V). subconjunto([H|T1],[H|T2]) :- subconjunto(T1,T2). subconjunto([H1|T1],[H2|T2]) :- lt(H2,H1),subconjunto([H1|T1],T2). in(X,[X|T]). in(X,[Y|T]) :- lt(Y,X),in(X,T).

OPERADORES PARA CONJUNTOS

Finalmente, definimos operadores los cuales nos ayudan a escribir operaciones de conjuntos y relaciones de una manera natural. Notar la prioridad diferente de los operadores.

:- op(400,yfx,/-\). % interseccion :- op(500,yfx,\-/). % union :- op(600,yfx,\). % diferencia :- op(700,xfx,es_conjunto). :- op(700,xfx,esta_in).

Page 30: INTRODUCCIÓN  A PROLOG

:- op(700,xfx,es_subconjunto).

La operación "es_conjunto" evalúa la expresión con conjuntos. Corresponde a la operación de PROLOG "is".

S es_conjunto S1 \-/ S2 :- SS1 es_conjunto S1, SS2 es_conjunto S2, union(SS1,SS2,S). S es_conjunto S1 /-\ S2 :- SS1 es_conjunto S1, SS2 es_conjunto S2, interseccion(SS1,SS2,S). S es_conjunto S1 \ S2 :- SS1 es_conjunto S1, SS2 es_conjunto S2, diferencia(SS1,SS2,S). S es_conjunto sel(X,Cond,Set) :- SSet es_conjunto Set, seleccionar(SSet,X,Cond,S). S es_conjunto S :- Lista(S).

Podemos agregar las operaciones miembro y subconjunto.

X is_in S :- SS es_conjunto S, in(X,SS). U is_subconjunto V :- US es_conjunto U, VS es_conjunto V, subconjunto(US,VS).

Ok, ahora podemos usar conjuntos en una forma obvia.

?- [1,2,3] /-\ [2,3,4] es_subconjunto [1,2,3] \-/ [2,3,4].

Page 31: INTRODUCCIÓN  A PROLOG

?- S es_conjunto ([1,2,3] \ [2,4,5] /-\ [2,6,7])\-/[2,3,6]. ?- X is_in [1,2,3] \-/ [3,4,5]. ...

REPRESENTACIÓN COMPACTA

La representación de arriba de conjuntos es satisfactoria para los conjuntos pequeños, pero no es eficiente para conjuntos grandes que contienen bloques compactos. Asi, ofrecemos la siguiente representacion compacta de conjuntos:

conjunto [1,2,3,4,5,7,8,9] es representado como [1...5,7...9]

Para usar esta representación definimos "el operador compacto ..." primero:

op(100,xfx,'...')

Por supuesto, tenemos que redefinir los procedimientos de arriba para unión, intersección, diferencia, seleccionar, subset, y in.

c_in(X,[X|T]) :- X\=A...B. c_in(X,[A...B|T]) :- in_intervalo(X,A,B). c_in(X,[Y|T]) :- Y\=A...B,lt(Y,X),c_in(X,T). c_in(X,[A...B|T]) :- lt(B,X),c_in(X,T). in_intervalo(X,X,B). in_intervalo(X,A,B) :- nonvar(X),X=B. in_intervalo(X,A,B) :- nonvar(X),lt(A,X),lt(X,B). in_intervalo(X,A,B) :- var(X),lt(A,B),A1 is A+1,in_intervalo(X,A1,B).

Reescriba los otros procedimientos como tarea.

La representación compacta en conjunción con la poderosa operación seleccionar permite compactar la descripción de diversos conjuntos.

sel(X,par(X),[1...100]) % conjunto de números pares entre 1 y 100.

Page 32: INTRODUCCIÓN  A PROLOG

Los operadores pueden mejorar legibilidad de los programas PROLOG.

PROCESADOR DE LISTA GENERALIZADO

En esta sección presentamos un procesador de lista generalizado, el cual es capaz de hacer diversas operaciones de lista dependiendo de la definición de las funciones e y f. Este programa es una versión simplificada del procesador de lista generalizado de R.A. O'Keefe's mostrado en el libro The Craft of PROLOG, MIT, 1990.

procesador_lista([],R) :- e(R). procesador_lista([H|T],R) :- procesador_lista(T,TR), f(H,TR,R).

Si definimos las funciones e y f en la siguiente forma:

e(0). f(A,B,C) :- C is A+B.

El programa resultante suma los elementos en la lista.

La siguiente definición:

e(0/0). f(X,A/B,A1/B1) :- A1 is A+X, B1 is B+1.

Puede ser usada para calcular el promedio de los elementos de la lista dada.

El mismo esquema puede ser usado para ordenar los elementos en la lista o para generar permutaciones. Primero definimos las funciones e y f.

e([]). f(X,L,R) :- insertar(X,L,R).

Page 33: INTRODUCCIÓN  A PROLOG

Si el procedimiento insertar es definido en la siguiente forma:

insertar(X,[],[X]). insertar(X,[Y|T],[X,Y|T]) :- X<=Y. insertar(X,[Y|T],[Y|NT]) :- X>Y,insertar(X,T,NT).

El programa resultante ordena la lista.

Si la siguiente definición de insertar es usada:

insertar(X,T,[X|T]). insertar(X,[Y|T],[Y|NT]) :- insertar(X,T,NT).

El programa resultante genera las permutaciones de la lista dada sucesivamente.

EXPRESIONES ARITMÉTICAS

En esta clase trabajaremos con expresiones aritméticas en forma simbólica, lo cual es natural para PROLOG. Primero escribimos un programa para evaluar expresiones aritméticas y entonces desarrollar un simple compilador el cual traduce una expresión aritmética hacia un código lineal para un mecanismo de pila.

EVALUACIÓN

Podemos fácilmente evaluar la expresión aritmética usando el evaluador incorporado de PROLOG.

eval_ingenua(Expr,Res) :- Res is Expr.

Sin embargo, para propósitos de éste tutorial preferimos el siguiente evaluador el cual atraviesa la estructura del término evaluado. Note, la descomposición natural del término a través de la unificación y las funciones incorporadas +,-,*. Para simplificar el problema, omitimos el operador división(/).

eval(A+B,CV) :- eval(A,AV),eval(B,BV),CV is AV+BV. eval(A-B,CV) :- eval(A,AV),eval(B,BV),CV is AV-BV. eval(A*B,CV) :- eval(A,AV),eval(B,BV),CV is AV*BV.

Page 34: INTRODUCCIÓN  A PROLOG

eval(Num,Num) :- numero(Num).

Ahora, podemos fácilmente extender el programa de arriba para permitir "variables" en el término evaluado. Esas variables son representadas por átomos de PROLOG como a, b o c, así ellos no corresponden a variables PROLOG. Por supuesto, tenemos que notificar los valores de las variables al programa evaluador. Así, la lista de pares variable / valor también como la expresión evaluada es pasada al evaluador. Para obtener el valor de la variable dada utilizamos la función miembro que es definido en una de las clases anteriores.

eval_v(A+B,CV,Vars) :- eval_v(A,AV,Vars),eval_v(B,BV,Vars),CV is AV+BV. eval_v(A-B,CV,Vars) :- eval_v(A,AV,Vars),eval_v(B,BV,Vars),CV is AV-BV. eval_v(A*B,CV,Vars) :- eval_v(A,AV,Vars),eval_v(B,BV,Vars),CV is AV*BV. eval_v(Num,Num,Vars) :- numero(Num). eval_v(Var,Valor,Vars) :- atomo(Var),miembro(Var/Valor,Vars).

Intenta ?-eval_v(2*a+b,Val,[a/1,b/5]) para probar el programa de arriba.

COMPILACIÓN

La evaluación de expresiones aritméticas puede ser fácilmente extendida hacia generar código lineal para algún mecanismo de pila abstracto. Usamos un mecanismo de pila con las siguientes instrucciones:

sacar(X)- coloca un elemento X en el tope de la pila meter(X)- remueve el elemento X del tope de la pila sumar,restar,(*)times(X,Y,Z)- calcula el valor correspondiente de Z a partir de los números X,Y obt_valor(X,V)- obtiene el valor de la variable X desde memoria col_valor(X,V)- coloca el valor de la variable X en memoria X,Y,Z,V son asumidos como registros permanentes del mecanismo de pila.

Notar, que usamos el acumulador para recoger el código y éste es realmente generado desde el final al comienzo. expr_gen(A+B,InCode,OutCode) :- expr_gen(B,[sacar(X),sacar(Y),sumar(X,Y,Z),meter(Z)|InCode],TempCode), expr_gen(A,TempCode,OutCode). expr_gen(A-B,InCode,OutCode) :-

Page 35: INTRODUCCIÓN  A PROLOG

expr_gen(B,[sacar(X),sacar(Y),restar(X,Y,Z),meter(Z)|InCode],TempCode), expr_gen(A,TempCode,OutCode). expr_gen(A*B,InCode,OutCode) :- expr_gen(B,[sacar(X),sacar(Y),times(X,Y,Z),meter(Z)|InCode],TempCode), expr_gen(A,TempCode,OutCode). expr_gen(Num,InCode,[meter(Num)|InCode]) :- numero(Num). expr_gen(Var,InCode,[obt_valor(Var,Valor),meter(Valor)|InCode]) :- atomo(Var).

Si podemos generar el código para evaluar expresiones es fácil adicionar un generador por asignación. El programa compilado es una lista de asignaciones entonces.

prog_gen([A=Expr|Rest],InCode,Code) :- atomo(A), prog_gen(Rest,InCode,TempCode), expr_gen(Expr,[sacar(X),col_valor(A,X)|TempCode],Code). prog_gen([],Code,Code).

Ahora, escribimos un interpretador de código máquina generado. El interpretador usa Pila para evaluar expresiones aritméticas y Memoria para recordar los valores de las variables. El código PROLOG del interpretador sigue naturalmente la semántica de las instrucciones usadas: sacar, meter, sumar, restar, times, obt_valor y col_valor.

eval_prog([meter(X)|Code],Pila,Memoria) :- eval_prog(Code,[X|Pila],Memoria). eval_prog([sacar(X)|Code],[X|Pila],Memoria) :- eval_prog(Code,Pila,Memoria). eval_prog([sumar(X,Y,Z)|Code],Pila,Memoria) :- Z is X+Y, eval_prog(Code,Pila,Memoria). eval_prog([restar(X,Y,Z)|Code],Pila,Memoria) :- Z is X-Y, eval_prog(Code,Pila,Memoria). eval_prog([times(X,Y,Z)|Code],Pila,Memoria) :- Z is X*Y,

Page 36: INTRODUCCIÓN  A PROLOG

eval_prog(Code,Pila,Memoria). eval_prog([obt_valor(X,Valor)|Code],Pila,Memoria) :- miembro(X/Valor,Memoria), eval_prog(Code,Pila,Memoria). eval_prog([col_valor(X,Valor)|Code],Pila,Memoria) :- col_valor(X,Valor,Memoria,NuevaMemoria) eval_prog(Code,Pila,NuevaMemoria). eval_prog([],Pila,Memoria) :- imprimir_Memoria(Memoria).

El colocar el valor de la variable no es tan directo como obtener el valor de la variable usando miembro. Si la variable está en la memoria, su valor tiene que ser cambiado, sino un nuevo par variable / valor es agregado a la memoria.

col_valor(X,Valor,[X/_|T],[X/Valor|T]). col_valor(X,Valor,[Y/V|T],[Y/V|NuevoT) :- X\=Y,col_valor(X,Valor,T,NuevoT). col_valor(X,Valor,[],[X/Valor]).

Finalmente, cuando el interpretador eval_prog encuentra el final del código el cual es indicado por la lista vacía, imprime los contenidos de la memoria, es decir, los valores de todas las variables las cuales son usadas en el programa.

imprimir_Memoria([X/Valor|T]) :- write(X=Valor),nl,imprimir_Memoria(T). imprimir_Memoria([]) :- nl.

Para encapsular el generador compilador /código y el evaluador interpretador/código, introducimos la siguiente cláusula.

ejecutar(Prog) :- prog_gen(Prog,[],Code), eval_prog(Code,[],[]).

Puedes intentar ?-ejecutar([a=5,b=a+2,a=3*a+b]) para probar el programa. Pero, si uno usa la variable con un valor indefinido, ejemplo, ?-ejecutar([a=b+2])? El programa falla. Mejorar el programa en una forma que

Page 37: INTRODUCCIÓN  A PROLOG

imprima un mensaje notificando las variables indefinidas durante la interpretación o mejor, detectará las variables indefinidas durante la compilación.

OPTIMIZACIÓN

Mire el código generado por prog_gen. ¿Es posible optimizar el código en alguna forma? Por supuesto, es posible. Aquí está un ejemplo de tal optimizador trivial el cual remueve todos los pares sucesivos meter-sacar y unifica sus argumentos. Está claro que si un elemento es metido a una pila y otro elemento es sacado desde la misma pila entonces inmediatamente ambos elementos son iguales (por unificación).

optimizar([meter(X),sacar(Y)|T],OptCode) :- X=Y, optimizar(T,OptCode). optimizar([H|T],[H|OptCode]) :- optimizar(T,OptCode). optimizar([],[]).

Ahora, insertamos el optimizador entre el generador y el ejecutor para obtener la ejecución del programa optimizado.

opt_ejecutar(Prog) :-

prog_gen(Prog,[],Code), optimizar(Code,OptCode), eval_prog(OptCode,[],[]).

¿Te gustó la aplicación presentada arriba? Si es así, puedes extenderlo a tu gusto desarrollando un compilador y ejecutor completo para un lenguaje de programación escogido. Por ejemplo, pensar en incorporar una construcción si-entonces-sino para el lenguaje de arriba.

PROLOG es un lenguaje de programación ideal para manejar datos simbólicos.

EXPRESIONES BOOLEANAS

Page 38: INTRODUCCIÓN  A PROLOG

Este capítulo extiende el trabajo con expresiones en una dirección diferente, pero muy interesante para la computación fundamental. Primero, escribimos un programa para evaluar una expresión booleana el cual es similar a la evaluación de expresiones aritméticas. En la segunda parte, trabajaremos con la expresión en una manera simbólica y escribiremos un programa para transformar una expresión booleana en forma normal conjuntiva.

DEFINICIÓN DE OPERADORES

Antes de comenzar a trabajar con expresiones booleanas (lógicas), definimos algunos operadores los cuales simplifican la entrada de tales expresiones.

:- op(720,fy,non). :- op(730,yfx,and). :- op(740,yfx,or).

Ahora, podemos escribir (non a and b) en lugar de and(non(a),b) que es más voluminoso.

Podemos definir meta-operaciones las cuales pueden ser completamente transformadas hacia operaciones clásicas and, or, non.

:- op(710,yfx,implica). :- op(710,yfx,equiv). :- op(740,yfx,xor).

EVALUACIÓN

Durante la evaluación de una expresión aritmética explotamos el evaluador incorporado is el cual calcula el valor de una expresión numérica. Ahora, tenemos que definir procedimientos para evaluar las operaciones and, or, not en PROLOG.

and_d(false,true,false). and_d(false,false,false). and_d(true,false,false). and_d(true,true,true).

Page 39: INTRODUCCIÓN  A PROLOG

or_d(false,true,true). or_d(false,false,false). or_d(true,false,true). or_d(true,true,true). non_d(true,false). non_d(false,true).

Deberiamos también indicar cuales valores puden ser usados en las expresiones.

logic_const(true). logic_const(false).

Ahora, es fácil escribir un evaluador para expresiones booleanas.

eval_b(X,X) :- logic_const(X). eval_b(X and Y,R) :- eval_b(X,XV),eval_b(Y,YV),and_d(XV,YV,R). eval_b(X or Y,R) :- eval_b(X,XV),eval_b(Y,YV),or_d(XV,YV,R). eval_b(non X,R) :- eval_b(X,XV),non_d(XV,R).

La evaluación de meta-operaciones es transformada hacia la evaluación de operaciones clásicas en forma obvia.

eval_b(X implica Y,R) :- eval_b(Y or non X, R). eval_b(X equiv Y,R) :- eval_b(X implica Y and Y implica X, R). eval_b(X xor Y,R) :- eval_b((X and non Y) or (Y and non X), R).

NORMALIZACIÓN

En esta sección, escribiremos un programa PROLOG para la transformación de expresiones booleanas a la forma normal conjuntiva. La forma normal conjuntiva es una expresión en la siguiente forma: (a or non b) and c and (b or d or non c).

Page 40: INTRODUCCIÓN  A PROLOG

Notar, que trabajamos con átomos arbitrarios en expresiones booleanas ahora y esos átomos no son interpretados, es decir, no conocemos su valor (true o false).

Primero, removemos las meta-expresiones, es decir, implica, equiv y xor las cuales son sustituidas por or, and y non.

ex2basic(X implica Y, R) :- ex2basic(Y or non X,R). ex2basic(X equiv Y,R) :- ex2basic(X implica Y and Y implica X, R). ex2basic(X xor Y,R) :- ex2basic((X and non Y) or (Y and non X), R). ex2basic(X or Y, XB or YB) :- ex2basic(X,XB),ex2basic(Y,YB). ex2basic(X and Y, XB and YB) :- ex2basic(X,XB),ex2basic(Y,YB). ex2basic(non X, non XB) :- ex2basic(X,XB). ex2basic(X,X) :- atomo(X).

Segundo, movemos la negación non a las fórmulas átomicas.

non2basic(non (X and Y),XN or YN) :- non2basic(non X,XN),non2basic(non Y,YN). non2basic(non (X or Y),XN and YN) :- non2basic(non X,XN),non2basic(non Y,YN). non2basic(non non X, XB) :- non2basic(X,XB) non2basic(non X,non X) :- atomo(X). non2basic(X and Y,XB and YB) :- non2basic(X,XB),non2basic(Y,YB). non2basic(X or Y,XB or YB) :- non2basic(X,XB),non2basic(Y,YB). non2basic(X,X) :- atomo(X).

Finalmente, podemos construir una forma normal conjuntiva.

ex2conj(X and Y,XC and YC) :- ex2conj(X,XC),ex2conj(Y,YC). ex2conj(X or Y, R) :- ex2conj(X,XC),ex2conj(Y,YC),join_disj(XC,YC,R). ex2conj(non X,non X). ex2conj(X,X) :- atomo(X). join_disj(X and Y,Z,XZ and YZ) :- join_disj(X,Z,XZ),join_disj(Y,Z,YZ). join_disj(X,Y and Z,XY and XZ) :- X\=(_ and _),join_disj(X,Y,XY),join_disj(X,Z,XZ). join_disj(X,Y,X or Y) :- X\=(_ and _),Y\=(_ and _).

Page 41: INTRODUCCIÓN  A PROLOG

Ahora, enlazamos los tres procedimientos de arriba hacia una forma compacta la cual transforma una expresión arbitraria a su forma normal conjuntiva. Nosotros llamamos este proceso normalización.

normalize(Ex,Norm) :- ex2basic(Ex,Bas), non2basic(Bas,NonBas), ex2conj(NonBas,Norm).

Intenta optimizar la forma normal conjuntiva resultante removiendo las disyunciones que contiene un literal y su negación (ejemplo, a or non a, lo cual se sabe es true). Escribir procedimientos similares para la transformación a forma normal disyuntiva.

PROLOG es el lenguaje ideal para la computación simbólica.

GRAFOS EN PROLOG

Un Grafo es otra estructura de datos que es ampliamente usada en los algoritmos actuales. En esta clase describiremos una representación de grafos en PROLOG y desarrollaremos algunos programas para operaciones de grafo típicas (coloreado, búsqueda).

REPRESENTACIÓN

Un grafo es usualmente definido como un par (V,E), donde V es un conjunto de vértices y E es un conjunto de arcos o aristas (edges). Hay muchas representaciones posibles de grafos en PROLOG, mostraremos dos de ellas.

Representación A mantiene los vértices y arcos en dos listas diferentes (conjuntos):

g([Vertice, ...],[e(Vertice1,Vertice2,Valor), ...])

Page 42: INTRODUCCIÓN  A PROLOG

Notar, que ésta representación es apropiada para grafos dirigidos también como para grafos no dirigidos. En caso de grafos no dirigidos, uno puede agregar cada una de los arcos no dirigidos e(V1,V2,H) como dos arcos dirigidos e(V1,V2,H), e(V2,V1,H) o, mejor, es posible ajustar el acceso al procedimiento arco (definido abajo).

Representación B está basada en la idea de vecindad (adyacencia) y el grafo es representado como una lista de vértices y sus vecinos.

[Vertice-[Vertice2-Valor, ...], ...] En este caso, la representación de grafos no dirigidos contiene cada uno de los arcos dos veces.

Aquí está el procedimiento para acceder a los arcos en la representación A.

arco(g(Es,Vs),V1,V2,Valor) :- miembro(e(V1,V2,Valor),Vs).

Si el grafo es no dirigido, el procedimiento arco puede ser ajustado de la siguiente forma:

arco(g(Es,Vs),V1,V2,Valor) :- miembro(e(V1,V2,Valor),Vs) ; miembro(e(V2,V1,Valor),Vs).

Aquí está el procedimiento arco para la representación B.

arco(Grafo,V1,V2,Valor) :- miembro(V1-NB,Grafo), miembro(V2-Valor,NB).

Ahora, es posible definir el procedimiento para encontrar la vecindad de un vértice usando el procedimiento arco.

vecindad(Grafo,V,NB) :- setof(V1-E,arco(Grafo,V,V1,E),NB).

Page 43: INTRODUCCIÓN  A PROLOG

En caso de la representación B es mejor (más eficiente) definir la vecindad directamente.

vecindad(Grafo,V,NB) :- miembro(V1-NB,Grafo).

Notar, que algunos grafos no usan valores en los arcos mientras otros asignan valores también a los vértices. En esos casos, los procedimientos de arriba tienen que ser reescritos por consiguiente.

COLOREADO

La meta del coloreado de grafo es agregar un color (de la paleta limitada de colores) a cada uno de los vértices en tal forma que los vértices adyacentes (a través de los arcos) tengan asignado diferentes colores. Aún si el coloreado de grafo parece ser un problema sólo-teórico, los algoritmos para coloreado de grafo son ampliamente usados en aplicaciones prácticas (satisfacción de restricción).

En ésta clase presentaremos tres algoritmos para coloreado de grafo. Comenzaremos con el algoritmo ingenuo (naive algoritm) que implementa un método de generar y probar en una forma basta. Entonces mejoramos el algoritmo enlazando las fases de generar y probar en un procedimiento. Finalmente, implementamos un método más sofisticado llamado chequeo hacia delante (forward checking).

El siguiente programa usa el método generar y probar para colorear los vértices de un grafo. Primero, el color es asignado a cada uno de los vértices y entonces el programa prueba la validez del coloreado.

% coloreado1(+Grafo, +Colores, -Coloreado) coloreado1(g(Vs,Es),Colores,Coloreado) :- gener(Vs,Colores,Coloreado), probar(Es,Coloreado). % gener(+Vertices,+Colores,-Coloreado) gener([],_,[]). gener([V|Vs],Colores,[V-C|T]) :- miembro(C,Colores), % generador de colores no determinista gener(Vs,Colores,T). % probar(+Arcos,+Coloreado) probar([],_). probar([e(V1,V2)|Es],Coloreado) :-

Page 44: INTRODUCCIÓN  A PROLOG

miembro(V1-C1,Coloreado), % encuentra el color del Vértice V1 miembro(V2-C2,Coloreado), % encuentra el color del Vértice V2 C1\=C2, % prueba la diferencia de colores probar(Es,Coloreado).

El programa de arriba no es muy eficiente porque genera muchos coloreados erróneos los cuales son rechazados en la fase de prueba. Además, el generador omite los vértices en conflicto y genera otros coloreados independientemente del conflicto.

Está claro que podemos probar la validez del coloreado durante la generación de colores. El siguiente programa enlaza la generación y la prueba en un procedimiento. Notar, que usamos acumulador para salvar el coloreado parcial.

% coloreado2(+Grafo,+Colores,-Coloreado) coloreado2(g(Vs,Es),Colores,Coloreado) :- gat(Vs,Es,Colores,[],Coloreado). % generar y probar % gat(Vertices,Arcos,Colores,ColoredVertices,FinalColoreado) gat([],_,_,Coloreado,Coloreado). gat([V|Vs],Es,Cs,Acc,Coloreado) :- miembro(C,Cs), % generar el color para el vértice V probar2(Es,V,C,Acc), % probar la validez del coloreado actual gat(Vs,Es,Cs,[V-C|Acc],Coloreado). % probar2(+Arcos,+Vertice,+Color,+ActColoreado) probar2([],_,_,_). probar2([e(V1,V2)|Es],V,C,CColoreado) :- (V=V1 -> (miembro(V2-C2,CColoreado) -> C\=C2 ; true) ;(V=V2 -> (miembro(V1-C1,CColoreado) -> C\=C1 ; true) ;true)), probar2(Es,V,C,CColoreado).

El programa de arriba usa backtracking para encontrar otro coloreado válido, pero no es capaz de detectar un conflicto antes de que el conflicto realmente ocurra, es decir, después de asignar el color al segundo vértice del arco en conflicto.

Page 45: INTRODUCCIÓN  A PROLOG

Es posible mejorar la conducta del algoritmo por chequeo hacia delante (forward checking) de conflictos. Primero, asignamos el conjunto de todos los colores posibles a cada uno de los vértices (prep). Entonces, escogemos un vértice y su color (del conjunto de posibles colores asignados a éste vértice) y removemos éste color de todos los vértices adyacentes (fc), es decir, removemos (alguno) de los conflictos futuros. Por lo tanto, conocemos que el color asignado no está en conflicto con los vértices ya coloreados.

Notar, que el chequeo hacia adelante agrega alguna sobrecarga adicional al algoritmo, es posible que el backtracking clásico podría ser más eficiente en algunos casos. También, la eficiencia del algoritmo con chequeo hacia adelante depende de la estrategia de escoger las variables y colores para la asignación.

% coloreado3(+Grafo,+Colores,-Coloreado) coloreado3(g(Vs,Es),Colores,Coloreado) :- prep(Vs,Colores,ColoredVs), gtb(ColoredVs,Es,[],Coloreado). % prep(+Vertices,+Colores,+SuperColoreado) prep([],_,[]). prep([V|Vs],Colores,[V-Colores|CVs]) :- prep(Vs,Colores,CVs). % gtb(+SuperColoreado,+Arcos,+PartialColoreado,-Coloreado) gtb([],_,Coloreado,Coloreado). gtb([V-Cs|Vs],Es,Acc,Coloreado) :- miembro(C,Cs), % selecciona solamente un color fc(Es,V,C,Vs,ConstrainedVs), % chequeo hacia adelante gtb(ConstrainedVs,Es,[V-C|Acc],Coloreado). % fc(+Arcos,+Vertice,+VerticeColor,+InputSuperColoreado,-OutputSuperColoreado) fc([],_,_,Vs,Vs). fc([e(V1,V2)|Es],V,C,Vs,ConstrVs) :- (V=V1 -> constr(Vs,V2,C,NuevoVs) ;(V=V2 -> constr(Vs,V1,C,NuevoVs) ;NuevoVs=Vs)), fc(Es,V,C,NuevoVs,ConstrVs). % constr(+InputSuperColoreado,+Vertice,-VerticeForbiddenColor,+OutputSuperColoreado) constr([V-Cs|Vs],V,C,[V-NuevoCs|Vs]) :- borrar(Cs,C,NuevoCs),NuevoCs\=[]. constr([V1-Cs|Vs],V,C,[V1-Cs|NuevoVs]) :- V\=V1,

Page 46: INTRODUCCIÓN  A PROLOG

constr(Vs,V,C,NuevoVs). constr([],_,_,[]). borrar([],_,[]). borrar([X|T],X,T). borrar([Y|T],X,[Y|NuevoT]) :- X\=Y, borrar(T,X,NuevoT).

Noten que borrar no falla si el elemento no está presente en la lista.

BÚSQUEDA

Otro grupo de algoritmos con relación a grafos son los de búsqueda (sobre el grafo). En ésta clase presentaremos dos algoritmos: búsqueda simple que encuentra el camino entre dos vértices y el algoritmo de Dijkstra el cual encuentra el camino de distancia mínima desde un vértice a todos los vértices.

El siguiente programa encuentra un camino desde vértice a otro vértice. El mismo programa puede ser usado para encontrar un camino en grafos dirigidos y no dirigidos dependiendo de la definición del procedimiento arco. Notar, que usamos acumulador para que contenga parte del camino y prevenir ciclos.

% camino(+Grafo,+Start,+Stop,-Camino) camino(Grafo,Start,Stop,Camino) :- camino1(Grafo,Start,Stop,[Start],Camino). camino1(Grafo,Stop,Stop,Camino,Camino). camino1(Grafo,Start,Stop,ActCamino,Camino) :- Start\=Stop, arco(Grafo,Start,Proximo), non_miembro(Proximo,ActCamino), camino1(Grafo,Proximo,Stop,[Proximo|ActCamino],Camino). non_miembro(_,[]). non_miembro(X,[Y|T]) :-

Page 47: INTRODUCCIÓN  A PROLOG

X\=Y, non_miembro(X,T).

El algoritmo de Dijkstra es bien conocido por encontrar el camino mínimo en grafos con arcos (no negativos). Aquí está su implementación en PROLOG el cual encuentra la distancia mínima a todos los vértices desde un vértice dado.

% min_dist(+Grafo,+Start,-MinDist) min_dist(Grafo,Start,MinDist) :- dijkstra(Grafo,[],[Start-0],MinDist). % dijkstra(+Grafo,+CerradoVertices,+AbiertoVertices,-Distancias) dijkstra(_,MinDist,[],MinDist). dijkstra(Grafo,Cerrado,Abierto,MinDist) :- escoger_v(Abierto,V-D,RestAbierto), vecindad(Grafo,V,NB), % NB es una lista de vértices adyacentes + distancia a V diff(NB,Cerrado,NuevoNB), merge(NuevoNB,RestAbierto,D,NuevoAbierto), dijkstra(Grafo,[V-D|Cerrado],NuevoAbierto,MinDist). % escoger_v(+AbiertoVertices,-VerticeToExpand,-RestAbiertoVertices) escoger_v([H|T],MinV,Rest) :- escoger_minv(T,H,MinV,Rest). escoger_minv([],MinV,MinV,[]). escoger_minv([H|T],M,MinV,[H2|Rest]) :- H=V1-D1, M=V-D, (D1<D -> ProximoM=H,H2=M ; ProximoM=M,H2=H), escoger_minv(T,ProximoM,MinV,Rest). % diff(+ListaOfVertices,+Cerrado,-ListaOfNonCerradoVertices) diff([],_,[]). diff([H|T],Cerrado,L) :- H=V-D, (miembro(V-_,Cerrado) -> L=NuevoT ; L=[H|NuevoT]), diff(T,Cerrado,NuevoT).

Page 48: INTRODUCCIÓN  A PROLOG

% mezclar(+ListaOfVertices,+OldAbiertoVertices,-AllAbiertoVertices) mezclar([],L,_,L). mezclar([V1-D1|T],Abierto,D,NuevoAbierto) :- (remover(Abierto,V1-D2,RestAbierto) -> VD is min(D2,D+D1) ; RestAbierto=Abierto,VD is D+D1), NuevoAbierto=[V1-VD|SubAbierto], mezclar(T,RestAbierto,D,SubAbierto). remover([H|T],H,T). remover([H|T],X,[H|NT]) :- H\=X, remover(T,X,NT).

Compara el procedimiento remover con el procedimiento borrar (parte de coloreado). ¿Ves la diferencia?

Extiende el programa de arriba en una forma que también encuentre el camino mínimo (no sólo la distancia mínima) a todos los vértices.

Los algoritmos de grafo pueden ser usados para resolver muchos tipos de problemas.

ALGORÍTMO GENERALIZADO PARA BÚSQUEDA DE GRAFOS

En esta sección presentamos un esquema general de un algoritmo para búsqueda en grafos. Este esquema está basado sobre las nociones de vértices abiertos y cerrados. Un vértice abierto fue visitado por el algoritmo pero no ha sido explorado | procesado todavía, mientras que un vértice cerrado ya ha sido visitado y explorado.

El algoritmo toma algún vértice abierto V y lo expande, es decir, el algoritmo procesa el vértice V, encuentra su vecindad, enlaza ésta vecindad con el resto de los vértices abiertos y agrega éste vértice V al conjunto de vértices cerrados. Notar, que los vértices cerrados son removidos de la vecindad antes de enlazarlos con los vértices abiertos. El algoritmo para tan pronto como el conjunto de vértices abiertos está vacío.

% busqueda_cerr_abier(+Grafo,+Abierto,+Cerrado,-Resultado)

Page 49: INTRODUCCIÓN  A PROLOG

busqueda_cerr_abier(Grafo,[],Cerrado,Resultado) :- afinar_resultado(Grafo,Cerrado,Resultado). busqueda_cerr_abier(Grafo,[Vertice|Abierto],Cerrado,Resultado) :- explorar(Vertice,Grafo,Vecindad,CerradoVertice), diff(Vecindad,Cerrado,AbiertoNB), % removedor de vértices cerrados mezclar(AbiertoNB,Abierto,NuevoAbierto), % enlaza con el resto % de vértices abiertos busqueda_cerr_abier(Grafo,NuevoAbierto,[CerradoVertice|Cerrado],Resultado).

El programa de arriba contiene "hooks", como por ejemplo, afinar_resultado o explorar, el cual tiene que ser programado para obtener un algoritmo particular. Mostramos tales extensiones ahora (los procedimientos para hooks son etiquetados con texto en bold).

COLOREADO

Primero, programamos el algoritmo de coloreado de grafos usando el esquema general presentado arriba. Este algoritmo corresponde al bien conocido algoritmo que colorea un vértice y, entonces, colorea la vecindad del vértice y así sucesivamente. Notar, que después de colorear un segmento del grafo, tenemos que recomenzar la búsqueda en el procedimiento afinar_resultado si allá permanecen otros componentes.

% abierto_close_coloreado(+Grafo,+Colores,-Coloreado) abierto_close_coloreado(Grafo,Colores,Coloreado) :-

vertices(Grafo,[V|Vertices]), busqueda_cerr_abier(Grafo-Colores,[V-Colores],[],Coloreado). explorar(V-Cs,Grafo-Colores,Vecindad,V-C) :- miembro(C,Cs), % Asignar color al vértice vecindad(Grafo,V,NB), % encontrar la vecindad borrar(C,Colores,NBColores), % preparar los posibles colores para la vecindad adicionar_Colores(NB,NBColores,Neigbourhood). % asignar colores a la vecindad adicionar_Colores([],_,[]). adicionar_Colores([V|Vs],Cs,[V-Cs|CVc]) :- adicionar_Colores(Vs,Cs,CVs).

Page 50: INTRODUCCIÓN  A PROLOG

diff([],_,[]). diff([V-Cs|CVs],Cerrado,NonCerrado) :- (miembro(V-_,Cerrado) -> NonCerrado=[V-Cs|Rest] ; NonCerrado=Rest), diff(CVs,Cerrado,Rest). mezclar([],Abierto,[]). mezclar([V-Cs|CVs],Abierto,[V-NCs|Rest]) :- (miembro(V-OCs,Abierto) -> interseccion(Cs,OCs,NCs) % interseccion de conjunto clásica ; NCs=Cs), NCs\=[], % Es posible asignar color mezclar(CVs,Abierto,Rest). afinar_resultado(Grafo-Colores,Cerrado,Resultado) :- vertices(Grafo,Vertices), adicionar_Colores(Vertices,Colores,CVertices), diff(CVertices,Cerrado,NonCerrado), (NonCerrado=[CV|_] % ¿Hay otro componente del grafo? -> busqueda_cerr_abier(Grafo-Colores,[CV],Cerrado,Resultado) ; Resultado=Cerrado).

ALGORÍTMO DE DIJKSTRA

Programaremos ahora la extensión del esquema abierto|cerrado que se comporta parecido al algoritmo de Dijkstra el cual usa los conjuntos de vértices abiertos y cerrados naturalmente. Recuerda, que el algoritmo de Dijkstra encuentra la distancia mínima a todos los vértices en el grafo desde un vértice dado.

% abierto_close_dijkstra(+Grafo,+Start,-MinDist) abierto_close_dijkstra(Grafo,Start,MinDist) :- busqueda_cerr_abier(Grafo,[Start-0],[],MinDist). explorar(V-D,Grafo,Neigbourhood,V-D) :- vecindad(Grafo,V,NB), adicionar_dist(NB,D,Vecindad). adicionar_dist([],_,[]).

Page 51: INTRODUCCIÓN  A PROLOG

adicionar_dist([V-D1|Vs],D,[V-VD|Rest]) :- VD is D+D1, adicionar_dist(Vs,D,Rest). diff([],_,[]). diff([V-D|VDs],Cerrado,NotCerrado) :- (miembro(V-_,Cerrado) -> NotCerrado=[V-D|Rest] ; NotCerrado=Rest), diff(VDs,Cerrado,Rest). mezclar([],Abierto,Abierto). mezclar([V-D1|VDs],Abierto,NuevoAbierto) :- (del(V-D2,Abierto,RestAbierto) -> min(D1,D2,D),ins(V-D,RestAbierto,SAbierto) ; ins(V-D1,Abierto,SAbierto), mezclar(VDs,SAbierto,NuevoAbierto). del(X,[X|T],T). del(X,[Y|T],Rest) :- X\=Y,del(X,T,Rest). ins(VD,[],[VD]). ins(V-D,[U-D1|T],[V-D,U-D1|T]) :- D<=D1. ins(V-D,[U-D1|T],[U-D1|Rest]) :- D>D1,ins(V-D,T,Rest). afinar_resultado(_,Cerrado,Cerrado). La generalización puede simplificar el desarrollo y comprensión del programa.