Javier Ibá˜nez (Despacho D-109) Germán Vidal (Despacho D

Anuncio
Ingenierı́a de Requerimientos (IDR)
(prácticas)
Facultad de Informática - 4A
El lenguaje de programación Visual Prolog
Javier Ibáñez (Despacho D-109)
Germán Vidal (Despacho D-242)
Edificio DSIC
Curso 2005/2006
Parte I
Presentación del lenguaje
1
1.- Caracterı́sticas del lenguaje
Visual Prolog es un lenguaje de programación de quinta generación especialmente
orientado al desarrollo de aplicaciones de bases de datos, interfaces basadas en lenguaje
natural, parsers y compiladores, sistemas expertos, etc.
Los tiempos de desarrollo de una aplicación en Visual Prolog pueden llegar a ser
hasta 10 veces menor que en otro lenguaje de programación como C o Pascal, debido
al alto nivel de abstracción del lenguaje. Pese a ello, dispone de un compilador capaz
de generar código máquina tan eficiente como un buen compilador de C.
Por otro lado, Visual Prolog es un lenguaje “abierto”, es decir, permite generar
rutinas que se pueden invocar desde aplicaciones escritas en C, C++ o ensamblador.
De forma análoga, una aplicación en Visual Prolog puede invocar rutinas escritas en
los lenguajes C, C++, Pascal o ensamblador.
2.- Caracterı́sticas del entorno
Visual Prolog dispone de las siguientes herramientas:
Entorno de Desarrollo Visual (VDE): es una herramienta para asistir al programador
en el desarrollo, depuración y mantenimiento de las aplicaciones, ası́ como en el
uso de los recursos gráficos. Incorpora herramientas para crear y mantener el
código asociado a las ventanas, diálogos, menús, etc.
Interfaz de Programación Visual (VPI): consiste en dominios y predicados (independientes de la plataforma hardware) para el manejo de diálogos, ventanas,
facilidades para dibujo, para el manejo de menús, ası́ como todas las facilidades
de la API (Application Programming Interface) de la plataforma subyacente.
Visual Prolog permite desarrollar aplicaciones para MS DOS, Extended DOS,
Windows 3.1, Windows 3.11, Windows 95, Windows NT y para las versiones de
16 y 32 bits de OS/2. El código generado es portable de una plataforma a otra
directamente.
Dispone de editores gráficos para la preparación de ventanas, diálogos, bitmaps,
iconos, etc.
Dispone de un editor sensible al lenguaje (Prolog y C).
El compilador incorpora distintos análisis de programas (e.g. determinismo) y
realiza varios tipos de optimizaciones de código (asignación de registros, LCO,
“peep-hole”, etc.)
Incorpora un gestor de base de datos con más de 50 predicados para desarrollar y
mantener grandes bases de datos compartidas (permitiendo el acceso multiusuario
a los ficheros).
2
Interfaz con otros lenguajes: Visual Prolog permite generar rutinas que se pueden
llamar desde otros lenguajes (C, C++, Pascal y ensamblador), ası́ como invocar
rutinas escritas en otros lenguajes. Ası́, se permite el desarrollo de aplicaciones
multilenguaje, de forma que el programador pueda aprovechar lo mejor de cada
lenguaje de programación.
3.- ¿Para qué se puede usar Visual Prolog?
Las principales aplicaciones para las que el lenguaje resulta particularmente adecuado son:
Desarrollo de prototipos
Procesadores de lenguajes: tanto lenguajes de programación (desarrollo de interprétes o compiladores) como lenguaje natural
Construcción de interfaces para el lenguaje natural, permitiendo extender ası́ las
aplicaciones existentes con una interfaz mucho más amigable
Implementación de Bases de Datos, empleando el mismo lenguaje de programación
para el desarrollo de la aplicación y para la gestión de la BD
Construcción de sistemas expertos
Generación de planes
Control y monitorización de procesos industriales
Simulación, Investigación Operativa, etc.
4.- ¿En qué se diferencia Prolog de otros lenguajes?
Prolog es declarativo: en lugar de especificar paso a paso cómo debe trabajar el
ordenador, un programa Prolog consta de una descripción del problema.
Prolog usa hechos y reglas, es decir, un programa está formado por una lista de
sentencias lógicas.
Prolog puede realizar deducciones a partir de los hechos y reglas del programa.
El control en la ejecución de un programa Prolog es automático, en oposición a la
práctica mayorı́a de lenguajes de programación.
Prolog tiene una sintaxis muy simple, lo que simplifica mucho su aprendizaje.
Prolog es un lenguaje de muy alto nivel: una aplicación Prolog contiene, en general, una décima parte de las instrucciones necesarias para desarrollar la misma
aplicación en C o Pascal.
3
Parte II
Fundamentos de Prolog
4
1.- PROgramando en LOGica
En Prolog, las soluciones se alcanzan deduciendo lógicamente una cosa de algo ya
conocido. Un programa Prolog no es una secuencia de acciones, sino una colección de
hechos, junto con una secuencia de reglas para extraer conclusiones a partir de los
hechos.
Prolog es un lenguaje declarativo, lo que significa que un programa escrito en Prolog
está mucho más cercano al lenguaje natural que los lenguajes imperativos (Basic, C,
Pascal, etc).
El lenguaje incluye una máquina de inferencia, que no es más que un procedimiento
general para razonar acerca de la información. Esta máquina de inferencia se encarga de
responder a las preguntas que se realicen al sistema, intentando deducir la información
a partir de los hechos conocidos y de las reglas del programa.
Aunque Prolog no permite escribir programas mediante sentencias del lenguaje natural (el compilador serı́a increiblemente complejo), la sintaxis está bastante cercana.
Concretamente, se usa un subconjunto de la lógica de predicados para representar la
información. Por ejemplo:
Lenguaje natural:
Un coche es divertido.
Una rosa es roja.
A Juan le gusta un coche si es divertido.
Lógica de Predicados
divertido(coche).
roja(rosa).
gusta(juan,Coche) si divertido(Coche).
1.1. Sentencias: hechos y reglas
Un programador de Prolog define objetos y relaciones. Luego, define cuando dichas
relaciones son ciertas. Por ejemplo, la sentencia:
Juan tiene un perro.
muestra una relación entre los objetos “Juan” y “perro”; la relación es “tiene”. Una
posible regla para definir cuando la sentencia anterior es cierta, es:
Juan tiene un perro si el perro es un pastor alemán.
Hechos: lo que es conocido
En Prolog, una relación entre objetos recibe el nombre de predicado. En el lenguaje
natural, una relación se expresa mediante sentencias. Por ejemplo:
Juan tiene un perro.
Elena tiene un coche.
Juan tiene un libro.
5
Por el contrario, en Prolog usamos un sı́mbolo de predicado que contiene (entre paréntesis) los distintos objetos a los que afecta dicho predicado. Por ejemplo, las tres sentencias
anteriores se expresarı́an ası́:
tiene(juan,perro).
tiene(elena,coche).
tiene(juan,libro).
Los hechos no sólo pueden expresar relaciones entre objetos, sino también propiedades
de los mismos. Por ejemplo, las sentencias del lenguaje natural “Juan es alto.” o “Elena
es rubia.” se expresarı́an en Prolog ası́:
alto(juan).
rubia(elena).
Reglas: lo que se puede inferir de los hechos
Las reglas nos permiten deducir hechos a partir de otros hechos. Por ejemplo, dadas
las reglas:
Pedro tiene todo lo que tiene Juan.
Ana es rubia si lo es Elena.
y los hechos anteriores, podemos deducir los nuevos hechos:
Pedro tiene un perro.
Pedro tiene un libro.
Ana es rubia.
Las reglas anteriores se codificarı́an en Prolog ası́:
tiene(pedro,Algo) :- tiene(juan,Algo).
rubia(ana) :- rubia(elena).
El sı́mbolo “:-” se pronuncia simplemente “si”, y sirve para separar las dos partes de
la regla: la cabeza y el cuerpo. Desde el punto de vista lógico, el sı́mbolo “:-” equivale
a una implicación de derecha a izquierda (⇐=).
1.2. Consultas
Una vez que Prolog dispone de una serie de hechos y reglas, podemos proceder a
realizar consultas (“queries”) al sistema. Por ejemplo, podemos preguntar cosas como:
6
Juan tiene un perro?
que, codificado en Prolog, es:
tiene(juan,perro).
Dada esta consulta, Prolog responde:
yes
ya que existe un hecho que dice exactamente eso. De la misma forma, podemos preguntar:
Qué tiene Juan?
que, en la sintaxis de Prolog, es:
tiene(juan,Que).
Fijaos en que juan comienza en minúscula, mientras que Que comienza en mayúscula. El
motivo es que juan es un objeto constante, mientras que Que representa una variable. En
Prolog, las variables siempre comienzan con una letra mayúscula o con un subrayado.
Prolog siempre responde a las consultas buscando hechos (o reglas) adecuados desde
el principio del programa hasta el final. Concretamente, las respuestas de Prolog a la
consulta anterior son:
Que = perro
Que = libro
2 Solutions
ya que ha encontrado los hechos:
tiene(juan,perro).
tiene(juan,libro).
Si le preguntamos a Prolog:
Qué tiene Pedro?
tiene(pedro,Que).
el sistema contesta:
Que = perro
7
Que = libro
2 Solutions
ya que Prolog sabe que Pedro tiene lo mismo que Juan, y Juan tiene un perro y un
libro.
Ejemplo
Cargad el programa ch02e01.pro1 en el sistema Prolog y ejecutarlo:
PREDICATES
nondeterm likes(symbol,symbol)
CLAUSES
likes(ellen,tennis).
likes(john,football).
likes(tom,baseball).
likes(eric,swimming).
likes(mark,tennis).
likes(bill,Activity) :- likes(tom, Activity).
GOAL
likes(bill,baseball).
El sistema contesta con yes. Lo que ha hecho es combinar la regla:
likes(bill,Activity) :- likes(tom, Activity).
con el hecho:
likes(tom,baseball).
para deducir que:
likes(bill,baseball).
Probad ahora con esta consulta:
likes(bill,tennis).
El sistema contesta no, ya que:
1.
no hay ningún hecho que afirme que a Bill le gusta el tenis, y
2.
usando la regla del programa y los hechos, no es posible deducirlo.
1
Se encuentra en el directorio Vip\Doc\Lang\Examples.
8
1.3. Variables: sentencias generales
El uso de variables en Prolog nos permite escribir sentencias de carácter general.
Por ejemplo, la sentencia “Pedro tiene todo lo que tiene Juan” se puede escribir en
Prolog ası́:
tiene(pedro,Algo) :- tiene(juan,Algo).
Recordad que las variables deben comenzar por un carácter en mayúscula o por un
subrayado (‘ ’).
2.- Del lenguaje natural a los programas Prolog
2.1. Cláusulas: hechos y reglas
En Prolog, existen únicamente dos tipos de “sentencias”: hechos y reglas. Ambos tipos de sentencias se conocen como cláusulas. El núcleo de un programa Prolog
está compuesto por una secuencia de cláusulas.
Más acerca de los hechos
Un hecho representa una instancia simple de una propiedad de un objeto o una
relación entre objetos.
Prolog no necesita mirar en ningún otro sitio para confirmar la validez de un hecho.
Además, los hechos pueden utilizarse para realizar deducciones (mediante reglas).
Más acerca de las reglas
Las reglas sirven para describir lo que se puede deducir a partir de otra información. Es decir, una regla indica que una propiedad (o relación) es cierta si ciertas
propiedades (o relaciones) son ciertas.
Veamos algunos ejemplos de reglas:
1.
Considerad la frase del lenguaje natural:
Diana es vegetariana y sólo come lo que su doctor le ha dicho que coma.
Dado un menú y la regla anterior, es posible concluir si Diana puede ordenar un
determinado item del menú o no. Esta regla se podrı́a escribir en Prolog ası́:
diana_puede_comer(Item) :- vegetal(Item), en_lista_doctor(Item).
La coma entre objetivos se lee “y”.
2.
Supongamos que disponemos de los siguientes hechos:
9
father(bill,mark).
father(tom,diane).
mother(ellen,mark).
y queremos escribir una regla que defina la relación parent(Person1,Person2)
de forma que sea cierta si Person1 es el padre de Person2, o bien Person1 es la
madre de Person2. En Prolog, la escribirı́amos ası́:
parent(Person1,Person2) :- father(Person1,Person2).
parent(Person1,Person2) :- mother(Person1,Person2).
3.
Un último ejemplo. La frase “Una persona puede comprar un coche si a la persona
le gusta el coche y el coche está en venta” se escribirı́a en Prolog ası́:
puede_comprar(Nombre,Modelo) :persona(Nombre),
coche(Modelo),
gusta(Nombre,Modelo),
en_venta(Modelo).
4.
Cargad el programa ch02e02.pro y probad con distintos objetivos. Por ejemplo,
can buy(judy,What),
can buy(kelly,What),
y
can buy(Who,What),
can buy(Who,hot rod).
2.2. Predicados (relaciones)
Al nombre de una relación lo llamamos predicado, mientras que los objetos que
relaciona se llaman argumentos.
Aquı́ hay algunos ejemplos de predicados Prolog con cero o más argumentos:
pred(integer,symbol)
persona(nombre,apellido,edad)
run
insert_mode
Un predicado persona se podrı́a usar en una consulta del tipo persona(Nombre,perez,22)
para averiguar cuál es el nombre de Pérez.
Sin embargo, ¿para qué puede servir un predicado sin argumentos? Su utilidad
puede consistir en provocar la ejecución del cuerpo de una regla (cuya cabeza sea, por
ejemplo, run), o bien para que un programa se comporte de distintas formas en función
de si un predicado se encuentra o no presente (como, por ejemplo, insert mode).
10
2.3. Variables (cláusulas generales)
En una consulta simple, las variables nos pueden servir para que que Prolog encuentre un dato. Por ejemplo, la consulta:
tiene(X,coche).
nos devuelve en la variable X el nombre de la persona que tiene un coche (o de las
personas que lo tienen, si hay más de una).
La elección de un nombre adecuado para las variables suele hacer el programa más
legible. Por ejemplo, la consulta anterior estarı́a mejor ası́:
tiene(Persona,coche).
Cómo se asignan valores a las variables
Como habréis podido observar, en Prolog no existe una instrucción de asignación.
Esto resulta una de las diferencias fundamentales entre Prolog y el resto de lenguajes
de programación.
Las variables en Prolog toman valores al ser “igualadas” a constantes en los
hechos o reglas del programa.
Hasta el momento en que una variable toma un valor, se dice que está desinstanciada;
cuando ha tomado un valor, se dice que está instanciada (a dicho valor). Sin embargo,
una variable sólo permanece instanciada hasta el momento en que obtenemos una
solución. Después, se desinstancia, y se procede a buscar nuevas soluciones.
Como consecuencia, no es posible utilizar las variables de Prolog para almacenar
información.
Cargad el ejemplo ch02e03.pro:
PREDICATES
nondeterm likes(symbol,symbol)
CLAUSES
likes(ellen,reading).
likes(john,computers).
likes(john,badminton).
likes(leonard,badminton).
likes(eric,swimming).
likes(eric,reading).
Considerad esta consulta: ¿hay alguna persona a la que le guste leer y nadar?
likes(Person, reading), likes(Person, swimming).
11
En la primera parte (objetivo) de la consulta, la variable Person está desinstanciada,
es decir, su valor es desconocido antes de que Prolog intente encontrar una solución. El
segundo argumento, reading, es conocido. De esta forma, Prolog busca algún hecho
que se pueda igualar o ajustar (haga “matching”) con la primera parte de la consulta, y
lo hace en el orden en que éstos aparecen escritos en el programa. Ası́, el primer hecho
del programa:
likes(ellen,reading).
se puede igualar a likes(Person,reading), ya que reading en la consulta es igual
a reading en el hecho, asignándole además el valor ellen a la variable Person. Al
mismo tiempo, Prolog se “anota” hasta donde ha llegado en la búsqueda de hechos que
se pudieran igualar al primer objetivo de la consulta.
Ahora hay que resolver el segundo objetivo de la consulta (recordad que la coma se
interpreta como un “y”). Ya que ahora Person se ha instanciado a ellen, Prolog debe
buscar algo que se ajuste a:
likes(ellen,swimming).
Prolog comienza la búsqueda desde el principio del programa, pero no consigue igualarlo
con nada. Por tanto, el segundo objetivo de la consulta no es cierto cuando Person toma
el valor ellen.
Entonces, Prolog desinstancia la variable Person, e intenta buscar una solución
alternativa para el primer objetivo. Para ello, comienza a buscar un hecho que se
pueda igualar de nuevo a:
likes(Person,reading).
a partir del punto que se habı́a anotado al encontrar la solución anterior. Este mecanismo de “vuelta atrás” (backtracking) lo veremos con detalle más adelante.
El primer hecho que se ajusta a la forma del primer objetivo es ahora:
likes(eric,reading).
Entonces, la variable Person se instancia a eric, y Prolog se dispone a buscar un hecho
en el programa que se ajuste con el segundo objetivo de la consulta, que ahora es:
likes(eric,swimming).
En este caso, Prolog tiene éxito (con la última claúsula del programa), y nos devuelve
la solución:
Person = eric
1 Solution
12
Variables anónimas
Cuando sólo deseamos extraer cierta información de una consulta, se usa un tipo
especial de variables denominadas variables anónimas. En Prolog, una variable anónima
se denota por un sı́mbolo de subrayado (‘ ’).
Veamos un ejemplo. Cargad el programa ch02e04.pro. En la consulta:
parent(Parent,_).
la variable anónima se interpreta como “cualquier cosa”. Es decir, estamos preguntando:
¿Quien es padre o madre de alguien? A lo que Prolog responde:
Parent = bill
Parent = sue
Parent = joe
3 Solutions
Probad con la consulta parent(Parent,X) y comprobad cuál es la diferencia.
Las variables anónimas también se pueden usar en los hechos. Por ejemplo, los
hechos:
tiene(_,zapatos).
respira(_).
estarı́an expresando las sentencias del lenguaje natural:
Todo el mundo tiene zapatos.
Todo el mundo respira.
Finalmente, tened en cuenta que una variable anónima se puede igualar a cualquier
cosa, aunque su nombre no es relevante (no se produce ninguna instanciación). Es
decir, un objetivo:
father(_,_).
se puede igualar perfectamente al hecho:
father(bill,joe).
ya que las dos apariciones de ‘ ’ en el objetivo se consideran variables distintas.
2.4. Objetivos (consultas)
Aunque ambas palabras resultan equivalentes, en lo sucesivo nos referiremos a una
consulta como un objetivo. En el caso de que un objetivo conste de varias partes, como
en:
likes(Person,reading), likes(Person,swimming).
diremos que se trata de un objetivo compuesto, y a cada parte la llamaremos subobjetivo.
Los objetivos compuestos nos permiten obtener la “intersección” de varios subobjetivos.
13
Objetivos compuestos: conjunciones y disyunciones
Como hemos visto, es posible usar un objetivo compuesto para encontrar una solución tal que el subobjetivo A y el subobjetivo B sean ciertos (una conjunción). Además,
también es posible buscar una solución tal que el subobjetivo A o el subobjetivo B
sean ciertos. Para ello, separamos los subobjetivos por un punto y coma (‘;’).
El ejemplo ch02e05.pro ilustra esta idea. Cargad el programa y ejecutar el objetivo.
La respuesta de Prolog es:
Make = ford, Odometer = 90000, Years_on_road = 4, Body = gray
1 Solution
Sin embargo, este objetivo resulta poco natural. A menudo, nos interesan más preguntas del tipo:
¿Hay algún coche que cueste menos de $25,000?
Para ello, lanzarı́amos el objetivo:
car(Make,Odometer,Years_on_road,Body,Cost),
Cost < 25000
/* subobjetivo A, y */
/* subobjetivo B */
Por el contrario, si deseamos averigurar algo como esto:
¿Hay algún coche que cueste menos de $25,000,
o algun camión que cueste menos de $20,000?
podrı́amos formular el objetivo:
car(Make,Odometer,Years_on_road,Body,Cost), Cost < 25000 ;
/* subobjetivo A, o */
truck(Make,Odometer,Years_on_road,Body,Cost), Cost < 20000.
/* subobjetivo B */
2.5. Comentarios
Los comentarios en un programa Prolog se pueden escribir de dos formas distintas:
si se trata de un comentario que ocupa varias lı́neas, debemos comenzarlo con /*
y terminarlo con */
cuando el comentario es breve, podemos usar el sı́mbolo %, el cuál provoca que
todo lo que quede a su derecha en la misma lı́nea se considere un comentario.
14
3.- ¿Cuándo dos objetos se pueden “ajustar”?
Hemos venido usando frases como “buscar un hecho que se ajuste con un objetivo”
o “la variable se puede igualar a la constante”, etc. En este punto vamos a explicar con
un poco más de detalle en qué consiste este mecanismo.
Obiviamente, dos estructuras idénticas se pueden ajustar. Por ejemplo:
parent(tom,bill) se ajusta a parent(tom,bill)
Sim embargo, usualmente nos encontramos con variables involucradas. Por ejemplo:
parent(tom,X) se ajusta a parent(tom,bill)
tomando, además, la variable X el valor bill.
Cuando una variable se encuentra instanciada, se comporta exactamente igual a
una constante. Es decir, si X se ha instanciado a bill, entonces:
parent(tom,X) se ajusta a parent(tom,bill)
pero
parent(tom,X) no se ajusta a parent(tom,joe)
Por otra parte, dos variables desinstanciadas siempre se pueden ajustar. Por ejemplo:
parent(tom,X) se ajusta a parent(tom,Y)
instanciando las variables X e Y una a otra. Mientras esta instanciación sea válida, las
variables X e Y se tratarán como si fueran una única variable (dos punteros a una misma
posición de memoria), de forma que si una toma un valor, la otra también lo tomará de
forma inmediata.
La técnica con la que igualamos (o ajustamos) un objetivo con la cabeza de una
cláusula puede considerarse como un mecanismo de paso de parámetros (bidireccional)
en Prolog. Ası́, dada una llamada:
parent(tom, Y)
y una cláusula encabezada por:
parent(X, joe)
tenemos que parent(tom, Y) se ajusta a parent(X, joe), instanciando la variable X
a tom y la variable Y a joe. Esto se corresponde con el paso del parámetro tom a la
variable X, y el paso del parámetro joe a la variable Y en el momento de la llamada.
En Prolog, los valores a los que se instancian las variables se pueden pasar en dos
modos: entrada (input) y salida (output). Si una variable en la cabeza de una cláusula
15
recibe un valor desde el objetivo, se dice que es un argumento de entrada y se denota
por (i). Si la variable sirve para devolver un valor al objetivo, se dice que es un argumento de salida, y se denota por (o). A la declaración de los distintos modos de un
predicado lo llamamos patrón de flujo de datos.
Por ejemplo, la claúsula:
doble(X,Y) :- Y = X+X.
permite calcular el doble de un número (X). Su patrón de flujo de datos es (i,o), es
decir, la X es de entrada, y la Y de salida.
16
Parte III
Programas Visual Prolog
17
A diferencia de otras versiones de Prolog, Visual Prolog es un compilador de Prolog con
tipos. Esto significa que hay que declarar los tipos de los objetos sobre los que actúa
cada predicado. De esta forma, el compilador Prolog puede generar código máquina
tan eficiente como un programa compilado en C o Pascal.
En esta parte, revisamos las principales secciones que forman parte de un programa
Visual Prolog.
1.- Secciones básicas de un programa Visual Prolog
En general, un programa Visual Prolog incluye cuatro secciones básicas: clauses,
predicates, domains, y goal.
La sección clauses forma el núcleo de un programa Visual Prolog. Aquı́ es donde
aparecen los hechos y las reglas con los que el sistema va a trabajar para ejecutar
un objetivo.
La sección predicates contiene la declaración de los predicados definidos en la
sección clauses, indicando el tipo de sus argumentos.
La sección domains se emplea para declarar dominios (tipos de datos) que no
estén predefinidos en el sistema.
La sección goal contiene el objetivo inicial que provoca el comienzo de la ejecución
del programa (algo ası́ como el cuerpo principal del programa).
1.1. La sección “clauses”
La Parte II se ha centrado en las cláusulas (hechos y reglas), cómo escribirlas,
cuál es su significado, etc. Si habéis entendido qué son los hechos y las reglas, y cómo
escribirlos en Prolog, ya conocéis que aparece en la sección clauses.
Una restricción del lenguaje es que todas las cláusulas que definan un mismo predicado deben aparecer consecutivamente en el programa. Una secuencia de cláusulas
definiendo un mismo predicado, se llama un procedimiento.
Cuando Prolog intenta resolver un objetivo, comienza a recorrer todas las cláusulas
de la sección clauses en el orden de aparición de las mismas. Cada vez que encuentra
una cláusula a la que se pueda igualar el objetivo, se anota la posición de la misma
para la búsqueda de soluciones alternativas (backtracking).
Sintaxis de las reglas
Una regla está compuesta por dos partes: la cabeza y el cuerpo, separados por el
sı́mbolo “:-” (que se interpreta como un “si”). La sintaxis genérica para una regla es:
cabeza :- subobjetivo1, subobjetivo2, ..., subobjetivoN.
18
Los subobjetivos en el cuerpo de una regla se pueden interpretar como llamadas a
procedimiento, las cuales, si tienen éxito, provocarán que ciertas variables se instancien
a ciertos valores.
Es decir, desde el punto de vista procedural, la regla anterior se interpreta ası́:
“para ejecutar el procedimiento cabeza, hay que ejecutar las llamadas a procedimiento
subobjetivo1, subobjetivo2,. . . , subobjetivoN”.
Para ejecutar una regla con éxito, todos los subobjetivos del cuerpo deben de poder
ejecutarse también con éxito. Si al menos uno de ellos falla, Prolog intenta la ejecución
de otra regla alternativa (si la hay). En esto consiste el mecanismo de backtracking, que
veremos con detalle en la Parte IV.
1.2. La sección “predicates”
Si definimos un predicado en la sección clauses, debemos declararlo en la sección
predicates, o bien Visual Prolog nos dará un error. Declarar un predicado sirve para
decirle al sistema cuál es el tipo de los argumentos de un predicado.
Visual Prolog incluye un número importante de predicados predefinidos (“built-in
predicates”), como, por ejemplo, write, read, +, sqrt, etc. Este tipo de predicados no
deben ser declarados en la sección predicates. Podéis encontrar información sobre los
predicados predefinidos en el menú de ayuda de Visual Prolog.
Cómo declarar predicados definidos por el usuario
Una declaración de predicado comienza con el nombre del predicado. A continuación,
entre paréntesis, aparecen los tipos de sus (cero o más) argumentos.
nombrePredicado(tipo_arg1,tipo_arg2,...,tipo_argN)
Fijaos en que, a diferencia de lo que ocurre en la sección clauses, aquı́ no terminamos
la declaración con un punto.
El nombre de un predicado debe comenzar con una letra (minúscula), seguida de
cualquier número de letras, dı́gitos o subrayados. Los nombres de predicado tienen una
longitud máxima de 250 caracteres.
En los nombres de los predicados no pueden aparecer espacios en blanco, signos aritméticos, asteriscos, u otros caracteres no alfanuméricos (únicamente son válidos a..z,
A..Z y 0..9). Aquı́ tenéis algunos ejemplos de nombres correctos e incorrectos:
Nombres de predicado correctos
fact
is a
has a
patternCheckList
choose Menu Item
predicateName
first in 10
Nombres incorrectos
[fact]
*is a*
has/a
pattern-Check-List
choose Menu Item
predicate<Name>
10 first in
19
Los argumentos de un predicado deben pertenecer a un dominio. El dominio puede
ser un dominio estándar, o bien un dominio definido en la sección domains. Veamos
algunos ejemplos:
1.
Si declaramos un predicado mi predicado en la sección predicates como sigue:
predicates
mi_predicado(symbol, integer)
entonces no es necesario declarar los dominios de sus argumentos en la sección
domains, ya que symbol e integer son dominios estándar. Sin embargo, si
declaramos el predicado ası́:
predicates
mi_predicado(nombre, numero)
entonces necesitaremos declarar los dominios nombre y numero en la sección
domains. Por ejemplo, si queremos definirlos simplemente del tipo symbol e
integer, respectivamente, lo harı́amos ası́:
domains
nombre = symbol
numero = integer
predicates
mi_predicado(nombre, numero)
2.
El siguiente fragmento de programa muestra una declaración de dominios y predicados algo más compleja:
domains
person, activity = symbol
car, make, color = symbol
mileage, years_on_road, cost = integer
predicates
likes(person, activity)
parent(person, person)
can_buy(person, car)
car(make, mileage, years_on_road, color, cost)
green(symbol)
ranking(symbol, integer)
Cuando declaramos un argumento del tipo symbol, estamos indicándole al compilador que sólo podra contener una constante alfanumérica, mientras que integer
hace referencia a un número entero. Más adelante veremos una lista exhaustiva
de los distintos tipos de dominios estándar.
20
1.3. La sección “domains”
La sección domains nos permite conseguir esencialmente dos cosas. En primer
lugar, podemos darle nombres particulares a ciertos dominios, pese a que éstos ya estén
predefinidos (dominios estándar). De esta forma se facilita la legibilidad del programa,
y se detecta un mayor número de errores en tiempo de compilación. En segundo lugar,
nos permite declarar estructuras de datos complejas que no están predefinidas. Veamos
algunos ejemplos.
1.
Supongamos que queremos declarar un predicado que vamos a usar para escribir
la siguiente relación:
Paco es un hombre de 45 años.
Usando los dominios estándar, podrı́amos declarar el siguiente predicado:
predicates
persona(symbol, symbol, integer)
Aunque la declaración es correcta, resultarı́a mucho más legible el código si lo
hacemos ası́:
domains
nombre, sexo = symbol
edad = integer
predicates
persona(nombre, sexo, edad)
Además, con una declaración de esta forma, el compilador detectarı́a que la siguiente cláusula es incorrecta:
mismo_sexo(X,Y) :persona(X,Sex,_),
persona(Sex,Y,_).
mientras que con la declaración original la compiları́a sin problemas (ya que los
dos primeros argumentos eran symbol).
2.
Cargad ahora el programa ch03e01.pro.
Este programa nos permite sumar y multiplicar enteros. Por ejemplo, el objetivo:
add_em_up(32,54,Sum).
tiene éxito con la solución:
21
Sum = 86
1 Solution
(es decir, la suma de 32 y 54), mientras que un objetivo:
multiply_em(31,13,Product).
nos devuelve:
Product = 403
1 Solution
Supongamos ahora que deseamos duplicar el producto de 31 y 17, por ejemplo,
con un objetivo de esta forma:
multiply_em(31,17,Sum), add_em_up(Sum,Sum,Answer).
Aunque esperarı́amos una respuesta como esta:
Sum = 527, Answer = 1054
1 Solution
lo que ocurre es que se produce un error de tipos! El problema consiste en que
el predicado add em up espera dos argumentos de tipo sum, y se le han pasado
dos argumentos de tipo product (pese a que internamente ambos son de tipo
integer).
3.
Para terminar, cargad el programa ch03e02.pro. Los dominios predefinidos byte
y ulong representan, respectivamente, un entero sin signo de 8 bits y un entero
sin signo de 32 bits (los discutiremos con detalle más adelante).
Ejecutad ahora el programa probando alternativamente los siguientes objetivos:
car(renault, 13, 40000, red, 12000).
car(ford, 90000, gray, 4, 25000).
car(1, red, 30000, 80000, datsun).
En los tres casos se produce un error de tipos. Aunque los datos pertenezcan a
un mismo dominio, gracias a la declaración de dominios es posible detectar que
se han intercambiado los argumentos.
22
1.4. La sección “goal”
Esencialmente, la sección goal contiene lo mismo que aparece en el cuerpo de
cualquier regla, es decir, no es más que una secuencia de subobjetivos.
La única diferencia es que la secuencia de subobjetivos en la sección goal se ejecuta
automáticamente al ejecutar el programa (serı́a equivalente al “cuerpo principal” del
programa).
Si alguno de los subobjetivos de goal falla, el programa completo falla (pese a que,
desde un punto de vista externo, esto no marca ninguna diferencia, simplemente se
termina la ejecución).
2.- Más sobre las declaraciones y las reglas
Visual Prolog dispone de varios dominios estándar que se pueden emplear en la
declaración de los predicados. Estos dominios no deben declararse en la sección domains.
Los dominios estándar para los números enteros son:
Dominio
short
ushort
long
ulong
integer
unsigned
byte
word
dword
Rango
-32768 .. 32767
0 .. 65535
-2147483648 .. 2147483647
0 .. 4294967295
-32768 .. 32767 (para plataformas de 16 bits)
-2147483648 .. 2147483647 (para plataformas de 32 bits)
0 .. 65535 (para plataformas de 16 bits)
0 .. 4294967295 (para plataformas de 32 bits)
0 .. 255
0 .. 65535
0 .. 4294967295
En general, los dominios integer y unsigned suelen ser suficientes y, además, se adaptan bien a la plataforma en que se esté trabajando.
Es posible declarar nuevos dominios usando las palabras reservadas signed/unsigned
y byte/word/dword. Por ejemplo,
domains
i8 = signed byte
crea un nuevo dominio i8 cuyo rango es -128 .. 127.
La siguiente tabla muestra el resto de dominios estándar de Visual Prolog.
23
Dominio
char
real
Descripción
Un carácter, implementado como un unsigned byte.
Sintácticamente, se escribe como un carácter entre comillas simples: ’a’.
Un número en coma flotante, implementado mediante 8
bytes; es equivalente al double de C. Su sintaxis es:
[+|-] DDDDD [.] DDDDDDD [e [+|-] DDD]
string
symbol
El rango permitido es de 1e-307 a 1e+308.
Una secuencia de caracteres, implementada como un puntero
a un vector de bytes, como en C. Se aceptan dos formatos: a)
una secuencia de letras, números y subrayados, siempre que
el primer carácter sea una letra minúscula, o b) cualquier
secuencia de caracteres entre comillas dobles: “The Game”.
Los strings tienen un lı́mite de 255 caracteres.
Una secuencia de caracteres, implementada como un puntero
a una entrada de una tabla de sı́mbolos. La sintaxis es la
misma que en el caso de los strings.
Los objetos de tipo symbol y string se pueden intercambiar sin problemas. Sin embargo, Visual Prolog los almacena de forma distinta: un objeto de tipo symbol se almacena
en una tabla de sı́mbolos, y se identifica únicamente por la dirección en la que está almacenado (el ajuste entre este tipo de objetos resulta muy eficiente); un objeto de
tipo string se almacena como un vector de caracteres (con lo que el ajuste entre este
tipo de objetos es algo más lento, ya que tiene que comparar los dos strings carácter a
carácter).
La elección de uno u otro dominio dependerá del uso que le vayamos a dar en el
programa. Por ejemplo, si no vamos a necesitar acceder a los caracteres del objeto, lo
más adecuado serı́a definirlo como symbol.
Observad que las secciones predicates, clauses y goal son obligatorias en todos
los programas. La sección domains, por ejemplo, es opcional. Concretamente, si todos
los dominios empleados en la sección predicates son estándar, la sección domains no
es necesaria.
Veamos algunos ejemplos.
1.
Cargad el programa ch03e04.pro. Este programa funciona como un mini-directorio
de teléfonos. La sección domains no es necesaria, ya que sólo se han empleado
dominios estándar.
Probad a ejecutar los siguientes objetivos:
phone_number("Carol", Number).
phone_number(Who, "438-8400").
phone_number("Albert", Number).
24
phone_number(Who, Number).
Supongamos ahora que Kim se va a vivir con Dorothy, y que por tanto pasan
a tener el mismo número de telefono. Actualiza el programa con este hecho y
prueba luego el objetivo:
phone_number(Who, "438-8400").
La solución deberı́a ser:
Who = Dorothy
Who = Kim
2 Solutions
2.
Cargad ahora el programa ch03e05.pro que ilustra el uso del dominio char.
Probad a ejecutar los objetivos:
isletter(’%’).
isletter(’Q’).
a los que Prolog responde No y Yes, respectivamente. Probad ahora con los siguientes objetivos:
(a)
(b)
(c)
(d)
(e)
isletter(’x’).
isletter(’2’).
isletter("hello").
isletter(a).
isletter(X).
Los objetivos (c) y (d) deben dar un error de tipos, mientras que (e) nos da un
mensaje de error del tipo Free variable, indicándonos que si le pasamos como
argumento una variable desinstanciada, no es posible comprobar si es posterior a
‘a’ o anterior a ‘z’.
Aridad múltiple
La aridad de un predicado es el número de argumentos que tiene. Podemos definir
varios predicados con el mismo nombre y distinta aridad. Visual Prolog permite la
definición de un predicado con distintas aridades, exigiendo únicamente que sus definiciones deben aparecer agrupadas tanto en la sección predicates como en la sección
clauses. Por lo demás, cada versión del predicado con una aridad distinta se considera
como un predicado completamente distinto.
En el programa ch03e06.pro podéis ver un ejemplo del uso de un mismo predicado
con distintas aridades.
25
Conversión de tipos automática
Cuando Visual Prolog iguala dos variables, éstas no tiene que pertenecer necesariamente al mismo dominio. De la misma forma, las variables se pueden instanciar a
valores de un dominio distinto. Concretamente, las siguientes conversiones de tipos son
automáticas:
Entre objetos de tipo symbol y string.
Entre cualquiera de los dominios para los enteros (incluyendo además el tipo
real). Cuando un carácter se convierte a un valor númerico, el número será su
valor ASCII.
3.- Otras secciones del programa
Vamos a introducir brevemente las secciones (opcionales) database, constants y
global (que serán vistas con detalle más adelante).
3.1. La sección “database”
Un programa Visual Prolog es un conjunto de hechos y reglas. A menudo, mientras se está ejecutando el programa resulta conveniente actualizar (añadir, borrar o
modificar) alguno de los hechos del programa. En estos casos, los hechos del programa
constituyen lo que se denomina la base de datos interna.
Visual Prolog incluye una sección database en la que se deben declarar aquellos
hechos que queremos que formen parte de la base de datos interna. Además, disponemos
de una serie de predicados predefinidos para facilitar el uso de la base de datos.
3.2. La sección “constants”
En un programa, es posible definir constantes simbólicas dentro de la sección constants. La sintaxis es la siguiente:
constants
<id> = <definicion>
donde <id> es el nombre de la constante simbólica, y <definition> es el valor que le
estamos asignando a dicha constante. Por ejemplo, podemos encontrar una declaración
de constantes como esta:
constants
cero = 0
uno = 1
dos = 2
cien = (10*(10-1)+10)
pi = 3.141592653
26
Como es habitual, antes de realizar la compilación del programa, Visual Prolog reemplaza la aparición de cada constante simbólica en el código por su valor.
Existen, sin embargo, unas pocas restricciones sobre el uso de constantes simbólicas:
La definición de una constante no puede autoreferenciarse. Por ejemplo:
error = 2*error
generarı́a un mensaje de error.
El sistema no distingue entre mayúsculas y minúsculas en los nombre de constantes. Por tanto, aunque se declare una constante:
constants
Dos = 2
en el programa habrá que emplearla en minúsculas (para evitar que se confunda
con una variable). Es decir, escribiremos:
goal
A = dos, write(A).
y no:
goal
A = Dos, write(A).
(lo que producirı́a un error).
Pueden aparecer varias secciones de declaración de constantes en un programa,
pero éstas deben declararse siempre antes de la primera vez que se usen.
La declaración de una constante es válida desde el punto en el que se define
hasta el final del fichero fuente, pero no pueden declararse varias constantes con
el mismo identificador.
3.3. Secciones globales
Visual Prolog permite la declaración de una serie de secciones globales. En principio,
las secciones son locales al fichero (módulo) en el que aparecen. Sin embargo, es posible
declarar al principio de un programa las secciones global domains, global predicates
y global database, de forma que sean visibles en todos los ficheros (módulos) de la
aplicación.
27
3.4. Directivas de compilación
Aunque éstas se pueden ver con detalle en el manual del entorno de desarrollo del
Visual Prolog (VDE), vamos a introducir aquı́ la más popular: la directiva include.
La directiva include se emplea para evitar tener que escribir repetidamente una
misma serie de procedimientos. Por ejemplo, podemos crearnos una librerı́a con los
predicados Prolog que usemos más frecuentemente en nuestras aplicaciones.
De esta forma, supongamos que hemos creado un fichero MI LIB.PRO que contiene
las secciones domains, predicates y clauses que definen los predicados que usamos
con más frecuencia.
Ahora, al escribir un nuevo programa, podemos incluir dichos predicados simplemente escribiendo:
include "mi_lib.pro"
en cualquier frontera del programa. Entendemos por “frontera” cualquier punto del
programa en el que podrı́a aparecer una sección domains, database, predicates,
clauses o goal.
28
Parte IV
Unificación y backtracking
29
En esta parte vamos a tratar cuatro temas:
En primer lugar, veremos el mecanismo de unificación. Cuando se intenta igualar
(o ajustar) un objetivo con una cláusula, hacemos uso de la unificación. Este
mecanismo incluye cosas propias de otros lenguajes tradicionales, tales como el
paso de parámetros, la selección por casos, la construcción y acceso a estructuras
de datos o la asignación.
A continuación, nos centramos en el mecanismo de backtracking, la técnica mediante la que Prolog controla la búsqueda de soluciones para los objetivos.
Después, introduciremos un predicado predefinido que se puede emplear para
modificar la estrategia de backtracking, consiguiendo ası́ programas más eficientes.
Finalmente, y con la intención de clarificar los conceptos introducidos hasta el
momento, revisaremos algunos de los principales elementos de Prolog desde un
punto de vista procedural.
1.- El mecanismo de unificación
Cargad el programa ch04e01.pro. Consideremos el objetivo:
written_by(X, Y).
Cuando se ejecuta este objetivo, Prolog intenta encontrar una cláusula en el programa
(en el orden de aparición) cuya cabeza se pueda igualar al objetivo. Entonces, le asigna
valores a las variables desinstanciadas, de forma que ambos, el objetivo y la cabeza de
la cláusula, se hagan idénticos. A este proceso le llamamos unificación.
En nuestro caso concreto, ya que el objetivo tiene como argumentos dos variables
desinstanciadas X e Y, éste unificará con cualquier cláusula cuya cabeza contenga el
predicado written by. Por ejemplo, se produce la siguiente unificación:
written_by(
X
,
Y
).
|
|
written_by(fleming, "DR NO").
instanciando la variable X al valor fleming y la variable Y al valor "DR NO". En este
instante, aparece por pantalla la solución:
X = fleming, Y = "DR NO"
y el sistema se dispone a buscar otras soluciones al objetivo (si las hay). Para ello, se
desinstancian las variables, y se intenta buscar otra cláusula con cuya cabeza unifique
el objetivo, por ejemplo:
written_by(
X
,
Y
).
|
|
written_by(melville, "MOBY DICK").
30
Ası́, Visual Prolog muestra por pantalla una nueva solución:
X = melville, Y = "MOBY DICK"
De nuevo, desinstancia las variables e intenta buscar una nueva solución. En este caso,
sin embargo, ya no hay más cláusulas que unifiquen con el objetivo, y por tanto muestra
finalmente el mensaje:
2 Solutions
y termina la ejecución del programa.
Consideremos ahora el objetivo:
written_by(X, "MOBY DICK").
Prolog comienza a buscar cláusulas con las que unificar, y prueba con la primera:
written_by(
X
, "MOBY DICK").
|
|
written_by(fleming,
"DR NO" ).
Sin embargo, puesto que "MOBY DICK" y "DR NO" son dos strings distintos, el intento
de unificación falla, y Visual Prolog prueba con otra cláusula:
written_by(
X
, "MOBY DICK").
|
|
written_by(melville, "MOBY DICK").
que sı́ tiene éxito, instanciando la variable X a melville.
Considerad ahora el objetivo:
long_novel(X).
Cuando Visual Prolog ejecuta este objetivo, comienza por buscar una cláusula cuya
cabeza unifique con él. En este caso, selecciona la única cláusula para long novel:
long_novel(Title) :written_by(_, Title),
book(Title, Length),
Length>300.
Ya que tanto la variable X como Title están desinstanciadas, entonces long novel(X)
unifica con long novel(Title) instanciándose una variable a la otra.
Ahora, Visual Prolog intenta ejecutar los subobjetivos en el cuerpo de la cláusula
anterior, comenzando por written by( , Title). Fijaos en que, puesto que el autor del
libro no es importante (sólo queremos combrobar que dicho tı́tulo existe), empleamos
una variable anónima. Este objetivo unifica con el primer hecho para written by:
31
written_by(
_
, Title ).
|
|
written_by(fleming, "DR NO").
instanciando la variable Title a "DR NO". Prolog intenta ahora la ejecución del siguiente subobjetivo book(Title, Lenght) con la instanciación realizada, es decir, intenta
la ejecución de:
book("DR NO", Length)
El intento de unificación con el hecho book("MOBY DICK", 250) falla, puesto que "DR
NO" y "MOBY DICK" no unifican. Con la segunda cláusula de book sı́ que hay éxito:
book("DR NO", Length).
|
,
|
book("DR NO",
310 ).
instanciando la variable Length a 310. Finalmente, ejecutamos Length >300 que, con
la instanciación anterior, se convierte en:
310 > 300
que tiene éxito.
En este punto, todos los subobjetivos de long novel han sido ejecutados con éxito y,
por tanto, la llamada inicial termina con éxito. ¿Cuál será el valor de la variable X
del objetivo inicial? Puesto que X se ha instanciado a Title, y Title a su vez se ha
instanciado a "DR NO", Visual Prolog devuelve:
X = "DR NO"
1 Solution
2.- Backtracking
Para ilustrar la forma en que procede el mecanismo de backtracking (“vuelta atrás”),
podemos compararlo con la búsqueda de la salida en un laberinto. Para encontrarla, un
método tradicional consiste en girar a la izquierda en cada ramificación del camino. Si
llegamos a un camino sin salida, debemos volver atrás hasta la ultima ramificación que
pasamos, y probar ahora con el camino que quede a su derecha, repitiendo de nuevo el
proceso de girar a la izquierda en cada nueva ramificación. Este método nos garantiza
que tarde o temprano habremos explorado todas las posibilidades y que, por tanto,
encontraremos la salida (si la hay).
En un programa Prolog se permite la definición de varias cláusulas para cada predicado. De esta forma, cuando se intenta ejecutar un objetivo, puede ocurrir que éste
unifique con la cabeza de varias cláusulas del programa, dando lugar a una ramificación
en el camino de ejecución. Prolog prueba las distintas cláusulas en el orden de aparición. Cuando encuentra una con la que el objetivo unifica, toma ese camino y pone una
32
“marca” en la siguiente cláusula del programa. A esta marca la llamaremos punto de
backtracking. Ası́, en caso de que llegue a un fallo (es decir, a un camino sin salida),
puede volver atrás hasta el último punto de backtracking que haya pasado, y tomar la
siguiente alternativa.
Veamos un ejemplo. Cargad el programa ch04e02.pro que define una relación
likes(bill,X) que dice que a bill le gusta X si se trata de una comida que sabe
bien, junto con un par de conjuntos de hechos para las relaciones food y tastes.
Para ver cómo funciona el mecanismo de backtracking, podéis lanzar el siguiente
objetivo al programa:
likes(bill, What).
Comprobad el resultado que os muestra Visual Prolog, e intentad realizar una traza de
la ejecución a mano, teniendo en cuenta las siguientes reglas:
1.
Cuando Prolog comienza la ejecución del objetivo, siempre empieza a recorrer las
cláusulas de programa en el orden de aparición en busca de una que unifique.
2.
Cuando se realiza una nueva llamada (subobjetivo en el cuerpo de alguna cláusula), vuelve a comenzar de nuevo la búsqueda desde el principio del programa.
3.
Cuando una llamada encuentra un hecho con el que unifica, decimos que la llamada ha tenido éxito, y se ejecuta la siguiente llamada pendiente.
4.
Una vez que una variable se ha instanciado a un valor, la única forma de desinstanciarla es mediante backtracking (más allá del punto en el que tomó dicho
valor).
2.1. Búsqueda exhaustiva de soluciones en Visual Prolog
Como ya hemos comentado, gracias al mecanismo de backtracking, Visual Prolog
no sólo computa la primera solución a un objetivo, sino todas las soluciones posibles.
Cargad ahora el programa ch04e03.pro, que contiene hechos acerca de los nombres
y edades de los jugadores de un club de tenis. Si deseamos calcular los jugadores de un
torneo para niños de 9 años, podemos ejecutar el siguiente objetivo compuesto:
player(Person1, 9),
player(Person2, 9),
Person1 <> Person2.
que se interpreta ası́: “buscar dos personas distintas de nueve años que sean miembros
del club de tenis”.
Probad primero a generar todas las respuestas sin tener en cuenta el último subobjetivo
Person1 <>Person2 (hacedlo a mano, y luego comprobad el resultado lanzando el
objetivo correspondiente). Las soluciones computadas serı́an estas:
33
Person1 = peter,
Person1 = peter,
Person1 = peter,
Person1 = chris,
Person1 = chris,
Person1 = chris,
Person1 = susan,
Person1 = susan,
Person1 = susan,
9 Solutions
Person2
Person2
Person2
Person2
Person2
Person2
Person2
Person2
Person2
=
=
=
=
=
=
=
=
=
peter
chris
susan
peter
chris
susan
peter
chris
susan
Si ahora ejecutamos el objetivo completo, las soluciones obtenidas son únicamente:
Person1 = peter,
Person1 = peter,
Person1 = chris,
Person1 = chris,
Person1 = susan,
Person1 = susan,
6 Solutions
Person2
Person2
Person2
Person2
Person2
Person2
=
=
=
=
=
=
chris
susan
peter
susan
peter
chris
ya que debe cumplirse que Person1 <>Person2. Sin embargo, aun existe el problema
de que, por ejemplo, la solución:
Person1 = peter, Person2 = chris
y la solución:
Person1 = chris, Person2 = peter
son equivalentes. Más adelante veremos como controlar el backtracking para evitar este
tipo de soluciones.
2.2. Ejecución de programas y backtracking
Los siguientes principios básicos rigen la ejecución de un programa Prolog:
1.
Los subobjetivos de un objetivo se deben de ejecutar de izquierda a derecha (en
el orden en que aparecen).
2.
La búsqueda de cláusulas del programa con las que unificar un subobjetivo se
realiza en el orden de aparición de las mismas en el programa (de arriba a abajo).
3.
Cuando un subobjetivo unifica con la cabeza de una regla, los subobjetivos en el
cuerpo de la misma son los siguientes subobjetivos a ejecutar.
4.
Cuando un subobjetivo unifica con un hecho, decimos que éste ha tenido éxito y
proseguimos la ejecución con el siguiente subobjetivo pendiente.
34
El uso de estas reglas da lugar a un esquema de ejecución como el que sigue. Teclead
el siguiente programa:
predicates
p(symbol)
q(symbol)
r(symbol)
s(symbol)
t(symbol)
clauses
s(a).
t(a).
p(X) :- q(X), r(X).
q(X) :- s(X).
r(X) :- t(X).
goal
p(Sol).
Ahora, la secuencia de objetivos que se producirı́a es la siguiente (aparece subrayado
el subobjetivo que ejecutamos):
p(X).
|
q(X),r(X).
|
s(X),r(X).
|
% X se instancia a a
r(a).
|
t(a).
|
FIN (con la solución X = a)
Fijaos en que, en cada paso, se sustituye el subobjetivo a ejecutar por la secuencia de
subobjetivos de la cláusula con la que hemos unificado dicho subobjetivo (en caso de
que sea una regla), o bien desaparece (en caso de que la cláusula sea un hecho).
Sin embargo, cuando existen distintas cláusulas definiendo cada predicado, la secuencia
de objetivos no es lineal (como en el caso anterior), sino que se produce una estructura
en forma de árbol. Cargad el programa ch04e05.pro. La ejecución del objetivo:
can_swim(What).
da lugar al siguiente árbol de objetivos:
35
can swim(What).
type(X,animal),is a(What,X), lives(What,in water).
aa
aa
aa
aa
is a(What,ungulate),
is a(What,fish),
lives(What,in water).
lives(What,in water).
!!aaa
!!
aa
!
!
aa
!
!
a
lives(zebra,in water).
lives(herring,in water).
lives(shark,in water).
FALLO!
FALLO!
EXITO!
La ejecución del programa consitirı́a en recorrer el árbol anterior en preorden (izquierdaraı́z-derecha) hasta alcanzar la hoja de EXITO, que generarı́a la solución:
What = shark
1 Solution
En la representación de la ejecución mediante un árbol, cada uno de los puntos en que
se produce una ramificación se corresponde con los llamados puntos de backtracking. Es
decir, cada vez que se produce un fallo, hay que retroceder en el árbol hasta la última
ramificación que hemos dejado atrás.
2.3. Controlando la búsqueda de soluciones
A menudo, la búsqueda automática de todas las soluciones para cada objetivo resulta innecesario. Prolog dispone de dos predicados predefinidos que permiten controlar
la búsqueda de soluciones: fail y el operador de corte (escrito como un sı́mbolo de
admiración !).
El predicado fail
Prolog realiza backtracking cada vez que una llamada falla. En ciertas situaciones,
es necesario forzar el backtracking de cara a obtener soluciones alternativas. El efecto de un subobjetivo fail es exactamente equivalente a una llamada 2 = 3, es decir,
simplemente produce un fallo.
Cargad el programa ch04e06.pro:
DOMAINS
name = symbol
36
PREDICATES
nondeterm father(name, name)
everybody
CLAUSES
father(leonard,katherine).
father(carl,jason).
father(carl,marilyn).
everybody :father(X,Y),
write(X," is ",Y,"’s father\n"),
fail.
everybody.
Comencemos por añadir la siguiente cláusula al programa:
prueba :- father(X, Y), write(X," is ",Y,"’s father\n").
Si lanzamos ahora el objetivo:
goal
prueba.
la solución de Prolog es:
leonard is katherine’s father
yes
Fijaos en que existen más soluciones, pero Visual Prolog no nos las proporciona. Cuando
el objetivo que lanzamos desde la sección goal tiene varias soluciones alternativas para
sus variables, Prolog sı́ que las proporciona todas. Por ejemplo, dado el objetivo:
goal
father(X,Y).
se obtienen las soluciones:
X
X
X
3
= leonard, Y = katherine
= carl, Y = jason
= carl, Y = marilyn
Solutions
La cuestión es, ¿cómo podemos obtener varias soluciones a un subobjetivo si no aparece
en la sección goal? La respuesta consiste en emplear el predicado predefinido fail.
Considerad esta regla del programa:
everybody :father(X,Y),
write(X," is ",Y,"’s father\n"),
fail.
everybody.
37
Dado un objetivo:
goal
everybody.
Prolog ejecuta father(X,Y), obteniendo la primera solución para X e Y, y muestra el
mensaje:
leonard is katherine’s father
Sin embargo, luego debe ejecutar el subobjetivo fail, que produce un fallo, con el consiguiente backtracking para encontrar soluciones alternativas a father(X,Y) (desinstanciando las variables). Ası́ se genera una nueva solución, y se muestra el mensaje:
carl is jason’s father
De nuevo llegamos a la ejecución de fail, y tras producirse el backtracking se muestra
el mensaje:
carl is marilyn’s father
Finalmente, ejecutamos de nuevo fail y, puesto que no existen más soluciones alternativas para la llamada a father(X,Y), se retrocede a buscar soluciones alternativas
para el objetivo inicial everybody. Este unifica con el hecho everybody, cuyo único
efecto es terminar la ejecución con éxito.
El predicado de corte
Prolog dispone de un predicado predefinido de corte, que se escribe mediante un
signo de exclamación (‘!’).
Su funcionamiente es el siguiente:
se puede usar como un subobjetivo más;
su ejecución siempre tiene éxito;
cuando se produce backtracking en una cláusula que contenı́a un corte, desaparecen las soluciones alternativas para todos los subobjetivos que aparezcan a la
izquierda del corte en el cuerpo de la cláusula, ası́ como para el predicado que se
define en la cabeza de la cláusula.
Existen dos usos principales del corte:
1.
Si en determinado punto del programa sabemos con seguridad que ya no va a
ser posible encontrar una solución, el corte puede servir para ahorrar tiempo y
espacio en la ejecución (este tipo de cortes se llaman cortes verdes).
2.
En ocasiones, aunque un predicado que hemos definido pueda tener varias soluciones alternativas, puede ocurrir que sólo nos interese una de ellas. Esto se puede
conseguir también empleando el corte (en este caso se llama un corte rojo, ya que
altera la lógica del programa).
38
Uso del corte para evitar backtracking a un subobjetivo previo
Consiste en usar una cláusula de la forma:
r1 :- a, b, !, c.
Con esta construcción, le estamos diciendo a Prolog que, si encontramos una primera
solución para a y b, ya tenemos suficiente, no es necesario buscar otras alternativas,
aunque c pueda fallar. Cargad el programa ch04e07.pro y veréis un ejemplo de este
tipo de uso del corte.
Evitar el backtracking a la siguiente cláusula
Podemos usar el corte para decirle a Prolog que ha elegido la cláusula correcta, y
que debe “olvidar” el resto. Esto se consigue con una construcción del tipo:
r(1)
r(2)
r(3)
r(_)
::::-
!, a, b, c.
!, d.
!, e.
write("este es el caso otherwise").
Ante un objetivo r(1), sólo se ejecutará la primera cláusula, ya que el corte elimina la
posibilidad de seleccionar el resto (concretamente, evitamos la ejecución de la última
cláusula). Una situación similar ocurrirı́a ante los objetivos r(2) y r(3), con los que
sólo se podrı́a ejecutar la segunda y tercera cláusula, respectivamente. Para cualquier
otro valor, sólo se ejecutará la última cláusula.
En pocas palabras, hemos conseguido un procedimiento determinista, es decir, un
procedimiento que sólo puede llevar a cabo una única acción, sin disponer de alternativas.
Además, la construcción tiene un gran parecido con la instrucción “case of” de
otros lenguajes. Concretamente, si lo escribimos ası́:
r(X)
r(X)
r(X)
r(_)
::::-
X = 1, !, a, b, c.
X = 2, !, d.
X = 3, !, e.
write("este es el caso otherwise").
El subobjetivo (o subobjetivos) que aparecen a la izquierda de los cortes harı́an el papel
de los distintos casos de una instrucción case of. Es decir, serı́a equivalente a:
r(X) =
case
1 :
2 :
3 :
otherwise :
X of
a, b, c;
d;
e;
write("este es el caso otherwise");
39
Un ejemplo similar podéis encontrarlo en el programa ch04e08.pro. Cargadlo y comprobad su ejecución con y sin los cortes.
Para finalizar, debemos tener en cuenta que el uso de predicados indeterministas
(es decir, aquellos que pueden generar varias soluciones alternativas) puede resultar
útil, pero tiene un coste muy elevado en tiempo de ejecución. Por tanto, siempre que
definamos un predicado que sólo debe generar una solución (es decir, un predicado
determinista), debemos asegurarnos de que es ası́, aunque sea empleando el predicado
de corte. Visual Prolog dispone además de una directiva de compilación check determ
que, si la activamos, se encargará de darnos un aviso cada vez que encuentre en tiempo
de compilación un predicado indeterminista.
El predicado not
Prolog dispone también de un predicado predefinido not para realizar la negación
de un subobjetivo. Su interpretación es muy intuitiva: siempre que un subobjetivo sub
tenga éxito, el subobjetivo not(sub) falla, y viceversa. Podéis encontrar un ejemplo de
su uso en el programa ch04e10.pro.
Hay algunas cosas que hay que tener en cuenta al usar el predicado predefinido not:
En el momento de ejecutarse un subobjetivo not(sub), sub no puede contener
variables desinstanciadas (o se producirá un error de ejecución).
Si queremos disponer de variables como argumento de not, deberán ser necesariamente variables anónimas.
Para finalizar, ahora que ya hemos introducido el uso del corte, cargad de nuevo
el programa ch04e03.pro y modificadlo empleando el corte para que no genere dos
soluciones del tipo:
Person1 = peter, Person2 = chris
Person1 = chris, Person2 = peter
sino sólo una de ellas (ya que son equivalentes).
4.- Prolog desde un punto de vista procedural
En primer lugar, es importante recordar que Prolog es un lenguaje declarativo y
que, por tanto, dispone de un mayor nivel de abstracción que los lenguajes imperativos
como C, Pascal, Basic, etc. Como ya hemos comentado, en un programa Prolog debe
describirse el problema, pero no es necesario introducir instrucciones de control que
digan cómo obtener la solución al mismo.
Pese a ello, es posible presentar una visión procedural de Prolog, en el estilo de los
lenguajes imperativos.
Por ejemplo, la siguiente cláusula:
40
proced(X) :write("comienzo"),
write(X),
write("fin").
se puede ver fácilmente como un procedimiento de este tipo:
proced(X);
begin
write("comienzo");
write(X);
write("fin")
end;
En el programa ch04e13.pro podéis encontrar un ejemplo más avanzado del uso de
Prolog para formar un procedimiento que se comporte como una instrucción case of.
La habitual instrucción if then else también resulta sencilla de implementar en Prolog. Por ejemplo, el procedimiento:
max(X,Y,Z);
begin
if X > Y then Z:=X
else Z:=Y
end
se podrı́a escribir en Prolog ası́:
max(X,Y,Z) :- X>Y, Z = X.
max(X,Y,Z) :- not(X>Y), Z = Y.
o, de forma más compacta (usando el corte):
max(X,Y,Z) :- X>Y, !, Z = X.
max(X,Y,Z) :- Z = Y.
En general, la visión procedural de Prolog no resulta positiva, ya que suele provocar
que se utilice Prolog como un lenguaje imperativo (para lo cuál ya disponemos de una
gran variedad de lenguajes bastante más adecuados).
La verdadera potencia de Prolog reside en su componente declarativa, la cual puede
permitir el desarrollo de cierto tipo de aplicaciones donde la heurı́stica es fundamental
(sistemas expertos, bases de datos avanzadas, redes neuronales, etc.) de forma rápida
y eficiente.
41
Descargar