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