INTRODUCCIÓN AL LENGUAJE PROLOG Índice 1. Introducción 2 2. Caracterı́sticas Generales 2 2.1. Evolución histórica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2.2. Esquema general de trabajo en Prolog . . . . . . . . . . . . . . . . . . . . . . . . 3 2.3. Implementaciones de Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2.3.1. SICStus Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.3.2. SWI-Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 3. Prolog y el paradigma de la Programación Lógica 6 3.1. Sintaxis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 3.1.1. Términos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 3.1.2. Programas 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.3. Consultas para la activación de programas . . . . . . . . . . . . . . . . . . 10 3.1.4. Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 3.2. Semántica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 4. Predicados Predefinidos 15 4.1. Aritmética . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 4.1.1. Operadores aritméticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 4.1.2. Predicados aritméticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 4.1.3. Programas aritméticos en Prolog . . . . . . . . . . . . . . . . . . . . . . . 17 4.2. Entrada/Salida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 4.3. Control: el corte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 4.3.1. Definición y propiedades . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 4.3.2. Usos del corte. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 1. Introducción En este documento se realiza una breve introducción al lenguaje de programación Prolog, con el objetivo fundamental de mostrar cómo se da el paso desde el concepto de programación lógica “pura” estudiado en el tema anterior a un lenguaje de programación “real”. En efecto, el lenguaje Prolog se puede ver como una extensión de la programación lógica pura, en el sentido de que, además de permitir programar de acuerdo con el paradigma de la programación lógica, incorpora una serie de elementos adicionales cuyo objetivo es ofrecer una herramienta de programación que sea útil en la práctica. Después de una descripción de las caracterı́sticas generales del lenguaje (evolución histórica, esquema general de trabajo e implementaciones existentes), el apartado 3 estudia el funcionamiento de Prolog desde el punto de vista de la programación lógica pura, basado en la Programación Lógica Definida estudiada en el tema anterior. Su extensión, que se realiza mediante la introducción de los denominados predicados predefinidos, se resume en el apartado 4. No se abordan, por lo tanto, más que algunos de los aspectos más relevantes del lenguaje. Para un estudio más en profundidad de Prolog se recomienda consultar los siguientes libros (los tres primeros son libros de carácter introductorio, mientras que el último trata aspectos más avanzados). L. Sterling and E. Shapiro. The Art of Prolog. The MIT Press, Cambridge, Mass., second edition, 1994. W.F. Clocksin and C.S. Mellish. Programming in Prolog. Springer-Verlag, Berlin, fourth edition, 1994. I. Bratko. Prolog Programming for Artificial Intelligence. Addison-Wesley, Reading, Massachusetts, third edition, 2001. R. O’Keefe. The Craft of Prolog. The MIT Press, Cambridge, MA, 1990. 2. 2.1. Caracterı́sticas Generales Evolución histórica Prolog (del francés, PROgrammation en LOGique) fue el primer lenguaje de programación basado en el paradigma de la programación lógica. Se implementó por primera vez a principios de los años setenta en la Universidad de Marsella (Francia), por un equipo dirigido por A. Colmeraeur, utilizando resultados teóricos aportados por R. Kowalski (Universidad de Edimburgo). Aunque con ciertas dificultades iniciales, debido principalmente a la novedad del paradigma y a la escasa eficiencia de las implementaciones disponibles, el lenguaje se fue expandiendo rápidamente, sobre todo en Europa y en Japón (en este último paı́s la programación lógica se incluyó como parte central del proyecto de ordenadores de quinta generación de los años ochenta). En 1995 el lenguaje se normaliza con el correspondiente estándar ISO. En la actualidad Prolog se ha convertido en una herramienta de desarrollo de software práctica y de gran aceptación para la que se dispone de múltiples compiladores, tanto comerciales como de libre distribución. 2 2.2. Esquema general de trabajo en Prolog El esquema general de trabajo en Prolog es el siguiente: 1. Escribir un programa lógico en Prolog. Como se ha visto en el tema anterior, un programa lógico es un conjunto finito de fórmulas lógicas, que reflejan el conocimiento del que se dispone acerca del problema a resolver. Por lo tanto, un programa en Prolog estará formado por una serie de fórmulas lógicas que, evidentemente, tendrán que adaptarse a la sintaxis especı́fica del lenguaje (ésta se detalla más adelante). Podrán asimismo incluirse comentarios: % El sı́mbolo % precede los comentarios en una única lı́nea /* Los comentarios en varias lı́neas empiezan con /* y terminan con */ */ Los programas se pueden escribir mediante cualquier editor de textos, aunque, como se verá después, existen editores especiales que facilitan la escritura de programas en Prolog. Una vez escrito un programa, éste se deberá guardar en un fichero (la extensión habitual para los programas en Prolog es “.pl”). 2. Iniciar el sistema Prolog y cargar un programa. Cuando se inicia un sistema Prolog, éste muestra por pantalla una lı́nea con el siguiente formato: ?que indica que el sistema está esperando la introducción por parte del usuario de alguna consulta. Para poder trabajar con un programa escrito previamente, es necesario cargar el fichero correspondiente en el sistema Prolog. Lo anterior se puede hacer de dos formas distintas: - interpretando el programa por medio del intérprete del lenguaje. Esta solución es la más habitual cuando se está probando un programa, y la acción se conoce como “consultar” un fichero. Para realizarla, basta con utilizar el predicado del sistema consult, con el nombre del fichero que se quiere cargar: ?- consult(’c:/Prolog/prueba.pl’). Lo anterior es equivalente a escribir simplemente el nombre del fichero entre corchetes: ?- [’c:/Prolog/prueba.pl’]. - compilando el programa por medio del compilador del lenguaje. El código compilado es más rápido que el código interpretado, aunque ofrece menos facilidades en lo que a depuración se refiere. La compilación de un programa se realiza por medio del predicado del sistema compile: ?- compile(’c:/Prolog/prueba.pl’). 3 En cualquiera de los dos casos, la respuesta del sistema puede ser de dos tipos: - si se ha detectado algún error sintáctico en el programa, el sistema Prolog avisa de ello mediante un mensaje. En estos casos habrá que volver a editar el programa para corregir los errores que contiene y volver a cargarlo. - en caso contrario, el sistema responde con la palabra yes, que indica que el programa se ha cargado correctamente y el sistema está listo para recibir consultas del usuario. Nota: la mayorı́a de los sistemas Prolog ofrecen un entorno de programación interactivo dotado de menús mediante los cuales se ofrecen las acciones más habituales, entre ellas la carga de ficheros tanto para ser interpretados como para ser compilados (normalmente, “File/Consult...” y “File/Compile...”). 3. Activar un programa desde el sistema Prolog. Una vez que se ha cargado correctamente un programa, el sistema está preparado para recibir las consultas del usuario relativas al problema que se pretende resolver. Para ello bastará con escribir dichas consultas, siguiendo la sintaxis especı́fica del lenguaje, en la lı́nea de consultas del sistema. El formato de estas consultas ası́ como las posibles reacciones del sistema ante ellas se detallan más adelante. Nota: Los sistemas Prolog suelen incorporar un mecanismo de depuración que permite seguir paso a paso la ejecución de las consultas, establecer puntos de corte en la ejecución, etc. Su funcionamiento básico es el siguiente: para entrar en modo de depuración debe escribirse el predicado del sistema trace en la lı́nea de consultas: ?- trace. A partir de ese momento, las consultas que se realicen se ejecutarán en modo de depuración, mostrando una después de otra todas las llamadas realizadas internamente por el sistema Prolog. Para avanzar en la depuración, basta con pulsar la tecla RETURN. También puede pulsarse la tecla h para obtener ayuda sobre las distintas opciones disponibles. En particular, la tecla n permite abandonar el proceso de depuración. para desactivar el modo de depuración se utiliza el predicado notrace: ?- notrace. Para una información más detallada sobre el mecanismo de depuración de Prolog se recomienda leer el capı́tulo 8 del libro de Clocksin y Mellish citado en la introducción o consultar el manual de referencia del sistema Prolog que se esté utilizando. 4. Salir del sistema Prolog. Para salir del sistema Prolog basta con escribir el predicado del sistema halt en la lı́nea de comandos: ?- halt. 2.3. Implementaciones de Prolog Como se ha comentado previamente, existen muchas implementaciones del lenguaje Prolog, tanto comerciales como de libre distribución. La mayorı́a de ellas se adaptan al estándar ISO, 4 por lo que los programas Prolog que se generen de acuerdo con dicho estándar podrán ejecutarse en cualquiera de estos sistemas. En esta asignatura (y en este documento) se va a utilizar la implementación comercial denominada “SICStus Prolog”, para la cual la URJC dispone de una licencia de Campus. A continuación se describe brevemente tanto este sistema como otro sistema Prolog, de dominio público, denominado SWI-Prolog. 2.3.1. SICStus Prolog SICStus Prolog es un compilador de Prolog comercial, desarrollado por el Instituto Sueco de Ciencias de la Computación (SICS), compatible con el estándar ISO-Prolog, con facilidades de depuración, interfaz con el lenguaje C, estructuración modular, bibliotecas con las estructuras de datos más habituales, etc. Aunque SICStus Prolog se puede usar directamente (activando el ejecutable correspondiente y siguiendo el esquema general de trabajo en Prolog descrito más arriba), lo más recomendable es usarlo a través del editor de textos GNU Emacs (Emacs es un potente editor de textos, programable y de libre distribución, que puede obtenerse en la URL http://www.emacs.org). Para ello, SICStus Prolog incluye en su distribución un fichero de interfaz con Emacs (.emacs), que aporta un modo de edición especial para programas en Prolog, ası́ como acceso directo desde el editor Emacs a la carga de programas, al sistema SICStus Prolog y a su manual de ayuda. Ası́, una vez instalado dicho interfaz, el modo de trabajar en SICStus Prolog a través del editor Emacs será el siguiente: 1. Iniciar Emacs. El editor Emacs se inicia mediante el ejecutable “runemacs.exe”, ubicado en el subdirectorio “bin” del directorio en el que se haya instalado el editor. 2. Crear/editar un programa Prolog. Tanto para crear un nuevo programa como para modificar un programa ya existente se usará la opción del menú de Emacs “File/Open File...”, o, alternativamente, la combinación de teclas CONTROL-X CONTROL-F. Si el nombre facilitado se corresponde con un fichero existente, el editor mostrará su contenido en pantalla; en caso contrario, mostrará una ventana vacı́a en la que se podrá escribir el nuevo programa. Las entradas de los menús “Edit” y “File” (o las combinaciones de teclas asociadas) ofrecen las opciones habituales para editar y guardar (CONTROL-X CONTROL-S) el contenido del fichero. 3. Cargar un programa e iniciar el sistema Prolog. Si el fichero abierto en el punto anterior tiene extensión “.pl”, el interfaz de SICStus Prolog con Emacs añadirá automáticamente a la barra de menús de Emacs una nueva entrada bajo el nombre Prolog que incluye, entre otras, las opciones pertinentes para consultar (CONTROL-C CONTROL-F) o compilar el programa correspondiente. Al elegir cualquiera de ellas, no sólo se cargará el programa en el sistema sino que además se abrirá una nueva ventana con el sistema SICStus Prolog, que permitirá realizar las consultas pertinentes. 5 También es posible acceder desde Emacs al sistema de ayuda de SICStus Prolog: - el menú “Help/Manuals/Browse Manuals with Info” permite acceder a los manuales de usuario de SICStus. - la opción “Help on Predicate” (CONTROL-C ?) del menú contextual “Prolog” antes citado ofrece ayuda sobre los predicados predefinidos de Prolog. 2.3.2. SWI-Prolog SWI-Prolog es un compilador de Prolog de dominio público diseñado e implementado en la Universidad de Amsterdam, compatible con el estándar ISO y disponible para distintas plataformas. Se puede obtener en la dirección http://www.swi-prolog.org. La versión para Windows consta de un único fichero ejecutable que instala automáticamente el sistema. Éste se utiliza de acuerdo con el esquema general de trabajo en Prolog descrito más arriba. 3. Prolog y el paradigma de la Programación Lógica Como se ha visto previamente, existen muchos modelos distintos de programación lógica, que se distinguen dependiendo del tipo de Lógica utilizada para representar el conocimiento y del mecanismo de demostración automática elegido. El lenguaje Prolog se basa en la Programación Lógica Definida estudiada en el tema anterior, pero con ciertas peculiaridades, tanto sintácticas como semánticas, que se discuten a continuación. 3.1. Sintaxis 3.1.1. Términos Al igual que en Lógica de Primer Orden, los términos en Prolog se clasifican en tres categorı́as: constantes, variables y términos compuestos. Constantes Prolog distingue dos tipos de constantes: Números. Este tipo de constantes se utilizan para representar tanto números enteros como números reales y poder realizar con ellos operaciones aritméticas. - La representación más corriente de los números enteros es la notación decimal habitual (por ejemplo 0, 1, -320, 539, etc) aunque también se pueden representar en otras bases no decimales. - Los números reales se pueden representar tanto en notación decimal (por ejemplo 1.0, -3.14) como en notación exponencial (por ejemplo 4.5E6, -0.12e+3, 12.0e-2). En ambos casos deberá haber siempre por lo menos un dı́gito a cada lado del punto. 6 Átomos. Los átomos (no confundir con las fórmulas atómicas de la LPO) se utilizan para dar nombre a objetos especı́ficos, es decir, representan individuos concretos. Existen tres clases principales de átomos: - cadenas formadas por letras, dı́gitos y el sı́mbolo de subrayado, que deben empezar necesariamente por una letra minúscula. Cadenas válidas: f, pepe1, libro33a, libro_blanco. Cadenas no válidas: 1libro, libro-blanco, _hola, Libro. - cualquier cadena de caracteres encerrada entre comillas simples. Ejemplos: ’SICStus Prolog’, ’Libro-blanco’, ’28003 Madrid’. Estos átomos son útiles cuando se necesita trabajar con constantes que empiecen por una letra mayúscula o por un dı́gito. - existe además otro tipo de átomos, compuestos por combinaciones especiales de signos, de uso menos común. Variables Las variables en Prolog se representan mediante cadenas formadas por letras, dı́gitos y el sı́mbolo de subrayado, pero deben necesariamente empezar por una letra mayúscula o por un sı́mbolo de subrayado. Ejemplos: X, Resultado_1, Entrada, _total3, _3bis, _ Las variables que empiezan con un sı́mbolo de subrayado, _, se denominan variables anónimas, y se usan cuando se necesita trabajar con variables cuyos posibles valores no interesan. Su utilidad se describirá más adelante al analizar la construcción de programas y consultas. Términos Compuestos Los términos compuestos, o estructuras, se construyen mediante un sı́mbolo de función, denominado functor, que se denota mediante un átomo, seguido, entre paréntesis, por una serie de términos separados por comas, denominados argumentos. Ejemplos: fecha(1,mayo,2001), punto(X,Y), recta(punto(1,2), punto(3,5)) Nota: al escribir un término compuesto, no puede haber ningún espacio entre el functor y el paréntesis abierto previo a los argumentos. Por ejemplo, “punto (X,Y)” no es un término compuesto correcto y producirá un error de compilación. Prolog también permite escribir ciertos términos compuestos en forma de operadores, generalmente en notación infija en el caso de functores de aridad 2 y en notación prefija o postfija en el caso de functores de aridad 1. Este es el caso de los operadores aritméticos predefinidos de Prolog, que se mencionarán más adelante, y que permiten escribir términos compuestos de la forma X+Y o -X en lugar de +(X,Y) o -(X). El programador también puede definir sus propios operadores. Uno de los términos compuestos más importantes y útiles que ofrece Prolog son las listas, secuencias ordenadas de cero o más elementos, donde los elementos pueden ser cualquier tipo de término. Prolog representa las listas teniendo en cuenta su estructura recursiva: - la lista vacı́a se representa mediante el átomo []. 7 - toda lista no vacı́a tiene una cabeza (que será cualquier término) y un resto (que será una lista), y se representa mediante un término compuesto de aridad 2, cuyo functor es un punto · y cuyos argumentos son, respectivamente, la cabeza y el resto de la lista. Ejemplos: la lista compuesta por un único elemento, la constante a, se representa como “·(a, [])”. La lista compuesta por los elementos a, b y c se corresponde con la estructura “·(a, ·(b, ·(c, [])))”. Dado que la notación anterior puede resultar incómoda a la hora de escribir listas complicadas, Prolog admite también una notación más sencilla que consiste en enumerar entre corchetes todos los elementos de la lista, separados por comas. Con esta notación, las dos listas del ejemplo anterior se representarı́an, respectivamente, como [a] y [a, b, c]. Prolog también dispone de otra notación para las listas, que consiste en representar la lista con cabeza X y resto Y mediante el término [X|Y ]. Esta última notación es fundamental para poder separar la cabeza del resto de una lista. Su utilidad en la práctica se verá en las clases de problemas. 3.1.2. Programas Los programas Prolog son programas lógicos definidos, y están por lo tanto compuestos por una serie de cláusulas de Horn positivas, esto es, hechos y reglas. Hay que tener en cuenta sin embargo las siguientes diferencias en cuanto a la notación empleada en la Programación Lógica Definida: Los sı́mbolos de predicado se denotan mediante átomos, por lo que no pueden empezar, como ocurre en Programación Lógica, mediante una letra mayúscula. Obsérvese por lo tanto que el lenguaje Prolog no distingue entre sı́mbolos de predicado, sı́mbolos de función y constantes, puesto que todos ellos se representan mediante átomos (el compilador distingue unos de otros dependiendo del contexto en el que aparecen). Para referirse a un predicado nombre_predicado se suele emplear la notación nombre_predicado/n, donde n indica el número de argumentos del predicado. Los hechos deben terminar con un punto y omitir el sı́mbolo “←” utilizado en Programación Lógica. Ası́, el hecho “← A” se escribe en Prolog de la forma “A.”. Las reglas deben también terminar con un punto y sustituir el sı́mbolo “←” de la Programación Lógica por el sı́mbolo “:-”. Ası́, la regla “A ← A1 , . . . , An ” se escribe en Prolog de la forma “A :- A1 , . . . , An .”. Nota: Al trabajar en Prolog se suele aplicar un convenio estándar para describir el uso de los predicados (tanto predefinidos como definidos por el programador): en los programas, las cláusulas de cada predicado deberán ir precedidas de un comentario incluyendo una lı́nea que describa su uso, ası́ como una descripción en lenguaje natural de su cometido. El convenio para describir el uso de un predicado es el siguiente: nombre_predicado(#NomVar_1, ...., #NomVar_n) donde NomVar_1, ..., NomVar_n son nombres de variables y el sı́mbolo #, que sirve para indicar cómo debe usarse el argumento correspondiente al realizarse una consulta, puede tomar uno de los tres siguientes valores: 8 + para indicar que el argumento correspondiente debe estar, en la consulta, instanciado con un término no variable (este tipo de argumentos se corresponden por lo tanto con parámetros de entrada). - para indicar que el argumento correspondiente no debe estar instanciado en la consulta, es decir, debe ser una variable (este tipo de argumentos se corresponden por lo tanto con parámetros de salida). ? para indicar que el argumento puede estar tanto instanciado como no instanciado (es decir, se trata de parámetros que se pueden usar tanto para entrada como para salida). Por ejemplo, el predicado predefinido sort/2, que sirve para ordenar listas de elementos, se describe como sort(+Lista1, ?Lista2), indicando ası́ que para utilizarlo el primer argumento debe estar instanciado (debe ser una lista concreta). Por lo tanto, el predicado sort/2 se podrá utilizar en consultas de la forma “ ?- sort([c,v,a], X).” o “ ?- sort([c,v,a], [a,c,v]).”, pero si se intenta hacer una consulta en la que el primer argumento no esté instanciado, como por ejemplo “?- sort(X, [c,v,a]).”, se producirá un error. Ejemplo: Los programas lógicos para el cálculo del factorial y la suma de números naturales estudiados en el tema anterior eran programas lógicos definidos, por lo que se pueden convertir fácilmente en programas Prolog con sólo adaptar su sintaxis de acuerdo con lo establecido más arriba (ficheros “factorial.pl” y “suma.pl”): PROGRAMA LÓGICO DEFINIDO PROGRAMA PROLOG % factorial(?X,?Y) % cierto si Y es el factorial de X factorial(0,s(0)). factorial(s(X),s(X)*Y) :- factorial(X,Y). % suma(?X,?Y,?Z) % cierto si Z es la suma de X e Y suma(X,0,X). suma(X,s(Y),s(Z)) :- suma(X,Y,Z). Factorial(0,s(0)) ← Factorial(s(x),s(x)*y) ← Factorial(x,y) Suma(x,0,x) ← Suma(x,s(y),s(z)) ← Suma(x,y,z). Obsérvese que en los ejemplos anteriores las variables X, Y y Z aparecen a ambos lados de las respectivas reglas. Existen sin embargo casos en los que una variable aparece sólo a un lado de una regla y sus posibles valores no tienen importancia. En estos casos no es necesario pensar un nombre de variable sino que basta con usar la variable anónima “_”. Ejemplo: Supóngase que, dado el predicado factorial, se necesita definir a partir de él un predicado es_factorial(X) que es cierto si X es el factorial de algún número natural. Una forma de definir este predicado serı́a estableciendo que X es un factorial siempre y cuando exista algún Y tal que X sea el factorial de ese Y , es decir: % es_factorial(?X): cierto si X es el factorial de algún número es_factorial(X) :- factorial(Y,X). Sin embargo, en este caso el posible valor de la variable Y es indiferente, puesto que lo único que se quiere saber es si X es el factorial de algún número, sin importar quien sea éste. Ası́, la definición anterior se podrı́a sustituir por: es_factorial(X) :- factorial(_,X). 9 3.1.3. Consultas para la activación de programas Al estar Prolog basado en la Programación Lógica Definida, las únicas consultas que se pueden realizar para activar un programa Prolog se corresponden con cláusulas de Horn negativas, esto es, cláusulas objetivo (cláusulas meta). Se recuerda que estas cláusulas, que son de la forma “← A1 , . . . , An ”, se corresponden con la negación de fórmulas ∃x1 . . . ∃xp (A1 ∧ . . . ∧ An ), p ≥ 0, n ≥ 1, donde A1 , . . . , An son predicados. En Prolog, las consultas deben terminar siempre con un punto, y el sı́mbolo “←” de la Programación Lógica debe sustituirse por el sı́mbolo “?-”. Ası́, la consulta “← A1 , . . . , An ” se escribe en Prolog de la forma “?- A1 , . . . , An .”. Obsérvese sin embargo que no será necesario escribir el sı́mbolo “?-”, puesto que, como se ha comentado antes, dicho sı́mbolo aparece directamente en el sistema Prolog. Ejemplo: Dados los programas para el cálculo del factorial y para la suma facilitados previamente, algunas consultas posibles serı́an las siguientes: ?- factorial(s(s(s(0))), Fact3). ¿Cuánto vale el factorial de 3? ?- factorial(s(s(0)), s(s(0))). ¿Es cierto que el factorial de 2 es 2? ?- factorial(0,Z), factorial(s(s(0)),Z). ¿Existe algún número natural que sea igual al factorial de 0 y al factorial de 2? ¿Cuál? ?- suma(s(0), s(0), X). ¿Cuánto vale la suma 1+1? ?- suma(X, Y, s(s(0))). ¿Qué números existen tales que su suma sea 2? ?- suma(s(0), Y, s(s(s(0)))). ¿Cuáles son los números tales que sumados a 1 dan 3? Las consultas anteriores incluyen variables no anónimas, puesto que el objetivo no es sólo saber si existen números que cumplan lo pedido sino que se desea además conocer su valor. Existen sin embargo ocasiones en las que las consultas pueden incluir variables anónimas. Ejemplo: En la última consulta del ejemplo anterior se pretendı́a averiguar cuál es el número natural tal que sumado a 1 da 3. Sin embargo, si lo único que se desea saber es si existe algún número natural tal que sumado a 1 dé 3, bastarı́a con utilizar una variable anónima: ?- suma(s(0), _, s(s(s(0)))). 3.1.4. Resumen Como se acaba de ver, la sintaxis del lenguaje Prolog, aunque se basa en la sintaxis de la Programación Lógica Definida, presenta ciertas diferencias respecto a esta última. A continuación se resumen las más importantes: - los sı́mbolos de variable se escriben empezando por una letra mayúscula o por un sı́mbolo de subrayado. - los sı́mbolos de predicado se escriben empezando por una letra minúscula, al igual que los sı́mbolos de función y las constantes. 10 - las cláusulas de Horn terminan siempre con un punto. Además: · en las reglas, el sı́mbolo “←” se sustituye por el sı́mbolo “:-” · en los hechos, el sı́mbolo “←” desaparece. · en las cláusulas objetivo, el sı́mbolo “←” se sustituye por el sı́mbolo “?-”. La siguiente tabla resume las distintas notaciones para cláusulas de Horn vistas hasta el momento: NOTACIÓN clausular lógica estándar prog. lógica Prolog 3.2. REGLAS {¬A1 , . . . , ¬An , A} ∀x1 . . . ∀xp [(A1 ∧ . . . ∧ An ) → A] A ← A 1 , . . . , An A :- A1 , . . . , An . HECHOS {A} ∀x1 . . . ∀xp (A) A← A. METAS {¬A1 , . . . , ¬An } ¬∃x1 . . . ∃xp (A1 ∧ . . . ∧ An ) ← A 1 , . . . , An ?- A1 , . . . , An . Semántica El mecanismo de demostración automática utilizado por Prolog es el sistema de Resolución SLD estudiado en el tema anterior, pero implementado de acuerdo con las siguientes pautas: Unificación. Prolog usa el algoritmo de unificación estándar estudiado en el tema anterior pero, por razones de eficiencia, no suele incluir el test de ocurrencia. Aunque la omisión de este test puede llevar a la obtención de resultados erróneos, esto ocurre raramente. Prolog permite al programador la utilización directa de su algoritmo de unificación mediante los dos siguientes predicados predefinidos, que se pueden usar en notación infija: - el predicado =, que devuelve cierto si las dos expresiones que se le pasan resultan ser, omitiendo el test de ocurrencia, unificables. - el predicado \=, que devuelve cierto si el predicado = falla y falso en caso contrario. Ejemplos: ?- f(X, g(b,c)) = f(Z, g(Y,c)). Y = b, Z = X ? yes ?- f(X, g(b,c)) = f(Z, g(Y,d)). no ?- [a,b,[c,d]] = [X|Y]. X = a, Y = [b,[c,d]] ? yes ?- 3+5 = 8. no % 3+5 no es más que el término compuesto +(3,5) 11 ?- X = f(X). X = f(f(f(f(f(f(f(f(f(f(...)))))))))) ? yes % no se realiza el test de ocurrencia Nota: algunas implementaciones de Prolog, como por ejemplo SICStus Prolog, incorporan un predicado predefinido especial que permite unificar con test de ocurrencia. En SICStus Prolog este predicado se denomina unify_with_occurs_check. ?- unify_with_occurs_check(X,f(X)). no % sı́ se realiza el test de ocurrencia Función de selección. Selecciona siempre el literal más a la izquierda. Regla de ordenación. Elige las cláusulas de acuerdo con el orden en el que éstas aparecen en el programa. Estrategia de búsqueda: búsqueda en profundidad (se recuerda que esta estrategia es muy eficiente pero tiene el inconveniente de que no es completa, esto es, puede conducir a una computación infinita aún en el caso de que exista una solución). Por lo tanto, cuando, una vez cargado un programa lógico, se realiza una consulta, Prolog construye el árbol de Resolución SLD correspondiente, de acuerdo con la función de selección y la regla de ordenación citadas, haciendo un recorrido en profundidad. Ası́, las posibles respuestas del sistema ante una consulta son las siguientes: Si todas las ramas del árbol SLD son ramas fallo, la respuesta del sistema será “no”. Si el árbol SLD tiene alguna rama infinita más a la izquierda que cualquier posible rama éxito, la reacción del sistema dependerá de la implementación concreta de Prolog que se esté utilizando. Algunos sistemas, como por ejemplo SWI-Prolog, presentan un mensaje de error. Otros, como es el caso de SICStus Prolog, no responden, por lo que es necesario interrumpir la ejecución de la consulta mediante CONTROL-C (si se está usando SICStus Prolog a través de Emacs, la composición de teclas CONTROL-C deberá efectuarse dos veces seguidas). Una vez interrumpida la ejecución, el sistema muestra por pantalla el mensaje: Prolog interruption (h for help)? y acepta a continuación una letra que determine la acción a seguir. Las posibles acciones se pueden consultar pulsando la tecla h, aunque lo más habitual será contestar con la letra a, cuya acción asociada es abortar la ejecución de la consulta. En otro caso (es decir, cuando el árbol de Resolución tiene por lo menos una rama éxito más a la izquierda que cualquier posible rama infinita): - Si la consulta realizada no tiene variables (o las que tiene son anónimas), la respuesta del sistema será “yes”, terminándose ası́ la ejecución de la consulta. - Si la consulta realizada tiene alguna variable no anónima, el sistema muestra por pantalla los valores de las variables que se corresponden con la primera rama éxito encontrada al buscar en profundidad en el árbol de Resolución, y queda a la espera de nuevas instrucciones por parte del usuario: 12 · si se introduce un retorno de carro, el sistema contesta “yes” y abandona la búsqueda de posibles nuevas soluciones. · si se introduce un punto y coma seguido de un retorno de carro, el sistema continua la búsqueda de nuevas soluciones en el árbol de Resolución, con lo que el proceso se vuelve a repetir, dependiendo la respuesta del resultado de la búsqueda (“no” si termina de recorrer el árbol sin encontrar ninguna nueva solución ni ninguna rama infinita, computación infinita si encuentra una rama infinita o valor de las variables correspondientes en caso de encontrarse otra solución). Como ya se comentó en el tema anterior, resulta claro que tanto el orden en el que aparecen las cláusulas en los programas como el orden de los literales dentro de las cláusulas pueden influir no sólo en el orden en el que aparecen las soluciones sino también en la terminación de las consultas que se realizan. Ejemplo: Considérese el siguiente programa (“ancestros.pl”), que incluye, además de un predicado progenitor(X,Y) que es cierto cuando X es progenitor de Y , cuatro versiones distintas del predicado ancestro(X,Y), cierto cuando X es un ancestro de Y . Las cuatro versiones varı́an dependiendo del orden de sus cláusulas y del orden en el que se colocan los literales dentro de ellas. % progenitor(?X, ?Y): cierto si X es progenitor de Y progenitor(pepa, pepito). progenitor(pepito, pepin). % ancestro(?X, ?Y): cierto si X es un ancestro de Y % VERSIÓN 1 ancestro1(X, Y) :- progenitor(X, Y). ancestro1(X, Y) :- progenitor(X, Z), ancestro1(Z, Y). % VERSIÓN 2 ancestro2(X, Y) :- progenitor(X, Z), ancestro2(Z, Y). ancestro2(X, Y) :- progenitor(X, Y). % VERSIÓN 3 ancestro3(X, Y) :- progenitor(X, Y). ancestro3(X, Y) :- ancestro3(Z, Y), progenitor(X, Z). % VERSIÓN 4 ancestro4(X, Y) :- ancestro4(Z, Y), progenitor(X, Z). ancestro4(X, Y) :- progenitor(X, Y). Aunque las cuatro definiciones anteriores del predicado ancestro son iguales desde el punto de vista lógico, su comportamiento en Prolog es distinto, ya que darán lugar a árboles de Resolución distintos. Por ejemplo, si se intenta averiguar de quién es ancestro pepa, resulta lo siguiente (compruébense estos resultados mediante la construcción de los árboles de resolución SLD correspondientes): 13 Consulta con “ancestro1”: ofrece las dos posibles soluciones y termina. ?- ancestro1(pepa, D). D = pepito ? ; D = pepin ? ; no Consulta con “ancestro2”: ofrece las dos posibles soluciones (en orden inverso al caso anterior) y termina. ?- ancestro2(pepa, D). D = pepin ? ; D = pepito ? ; no Consulta con “ancestro3”: ofrece las dos posibles soluciones, en el mismo orden que “ancestro1”, pero si se piden más soluciones la consulta no termina. ?- ancestro3(pepa, D). D = pepito ? ; D = pepin ? ; % el sistema entra en un bucle, que se interrumpe con CTRL-C CTRL-C Prolog interruption (h for help)? a {Execution aborted} Consulta con “ancestro4”: no produce ninguna solución, porque entra directamente en una rama infinita. ?- ancestro4(pepa, D). % el sistema entra en un bucle, que se interrumpe con CTRL-C CTRL-C Prolog interruption (h for help)? a {Execution aborted} Aunque no existe ninguna regla general que establezca el orden óptimo de las cláusulas ni el orden óptimo de los literales dentro de ellas, sı́ suelen ser recomendables los siguientes principios básicos, basados en la idea de “hacer antes lo más sencillo”: 1. Colocar las cláusulas que expresan las condiciones de parada de la recursividad antes que las otras (esto se cumple en las versiones 1 y 3 del ejemplo anterior). 2. Evitar las reglas con recursión a la izquierda, es decir, las reglas tales que el primer literal de su cuerpo es una llamada recursiva al mismo predicado de la cabeza de la regla (las versiones 3 y 4 del ejemplo anterior presentan recursión a la izquierda). De las cuatro versiones del ejemplo anterior, la única que cumple estas dos recomendaciones es la primera. Es por otro lado necesario evitar definiciones circulares del estilo: 14 progenitor(X,Y) :- hijo(Y,X). hijo(A,B) :- progenitor(B,A). puesto que cualquier consulta a uno de los dos predicados anteriores provocará necesariamente un bucle infinito. 4. Predicados Predefinidos En el apartado anterior se ha resumido el funcionamiento del lenguaje Prolog desde el punto de vista del paradigma de la Programación Lógica. Sin embargo, las facilidades descritas no son suficientes para obtener un lenguaje de programación útil en la práctica: por ejemplo, todo lenguaje de programación “real” necesita facilidades para leer y/o escribir o mecanismos eficientes para realizar operaciones aritméticas. Para ello, el lenguaje Prolog incorpora toda una serie de predicados del sistema o predicados predefinidos (“system or built-in predicates”) que ofrecen al usuario facilidades como las citadas ası́ como otro tipo de funcionalidades extra-lógicas o meta-lógicas. Estos predicados no pueden ser redefinidos por el programador. En lo que sigue se describen sólo algunos de estos predicados predefinidos, en concreto los relacionados con la realización de operaciones aritméticas, ciertos predicados para entrada/salida y el predicado de control denominado corte. 4.1. 4.1.1. Aritmética Operadores aritméticos Prolog tiene predefinidos los operadores aritméticos más habituales, mediante los que se pueden formar expresiones aritméticas. A continuación se enumeran algunos de los más importantes: X+Y X-Y X*Y X/Y X//Y X mod Y abs(X) sqrt(X) log(X) suma de X e Y X menos Y producto de X por Y cociente real de la división de X por Y cociente entero de la división de X por Y resto de la división entera de X por Y valor absoluto de X raı́z cuadrada de X logaritmo neperiano de X Téngase en cuenta que los operadores anteriores permiten simplemente construir expresiones aritméticas, pero éstas no son más que estructuras (términos compuestos) que no representan ningún valor. Por ejemplo, la expresión 3+5 no es más que el término compuesto +(3,5) escrito en notación infija. Ası́, no es posible hacer consultas del estilo “ ?- 3+5.”, puesto que “+” no es un predicado, y si se hiciese la consulta “ ?- 3+5 = 8.”, la respuesta de Prolog serı́a no, dado que el término compuesto +(3,5) no es unificable con el término constante 8. Para poder evaluar expresiones aritméticas en Prolog hay que utilizar los predicados aritméticos que se describen a continuación. 15 4.1.2. Predicados aritméticos Los predicados aritméticos predefinidos de Prolog se utilizan para evaluar expresiones aritméticas. El más habitual es el predicado predefinido “is”, que se usa en notación infija de la siguiente forma: X is Y Si Y es una expresión aritmética, ésta se evalúa y el resultado se intenta unificar con X. A la hora de usar este predicado hay que tener en cuenta las siguientes consideraciones: 1. Su uso puede dar lugar a un error en los dos siguientes casos: a) cuando la parte derecha no es una expresión aritmética: ?- X is a+1. {DOMAIN ERROR: _157 is a+1 - arg 2: expected expression, found a} b) cuando la parte derecha es una expresión aritmética pero no se puede evaluar: ?- X is 4*Z. {INSTANTIATION ERROR: _157 is 4*_155 - arg 2} 2. Salvo en los casos anteriores, el resultado del predicado dependerá de si la parte izquierda unifica o no con el resultado obtenido al evaluar la parte derecha: ?- X is sqrt(4). X = 2.0 ? yes ?- 5 is 2+3. yes ?- X is 5, Y is X+1. X = 5, Y = 6 ? yes ?- 3+5 is 3+5. no La respuesta negativa obtenida en el último ejemplo se debe a que la expresión “3+5” de la parte izquierda (recuérdese que se trata simplemente del término compuesto +(3,5)) no es unificable con el entero 8, resultante de evaluar la parte derecha. Además del predicado anterior, Prolog incorpora otros predicados comunes para comparaciones aritméticas, aunque en algunos casos con una notación distinta a la habitual: obsérvense en particular los sı́mbolos para la igualdad/desigualdad (que no pueden ser los habituales = y \= puesto que éstos se utilizan para la unificación) y la comparación menor o igual, que se escribe al revés de lo que suele ser normal en otros lenguajes de programación (<=). X X X X X X =:= Y =\= Y < Y =< Y > Y >= Y cierto cierto cierto cierto cierto cierto si si si si si si los valores numéricos de X e Y son iguales los valores numéricos de X e Y son distintos el valor numérico de X es menor que el de Y el valor numérico de X es menor o igual que el de Y el valor numérico de X es mayor que el de Y el valor numérico de X es mayor o igual que el de Y El funcionamiento de estos predicados es similar al del predicado “is”: producirán un error en caso de que alguno de los dos argumentos no sea una expresión aritmética o, a pesar de 16 serlo, no se pueda evaluar. En caso contrario, el sistema evalúa las dos expresiones aritméticas y devuelve el resultado de la comparación solicitada. ?- X+3 < sqrt(4). .. ERROR: .. 4.1.3. ?- 3+5 =:= 8. yes ?- 1+ 5 > abs(-8). no ?- 3 =\= 3*a. .. ERROR: .. Programas aritméticos en Prolog En el tema anterior, relativo a la Programación Lógica general, se estudiaron varios programas lógicos aritméticos, como por ejemplo los programas para el cálculo de la suma y el factorial de números naturales. Se trataba en ambos casos de programas lógicos definidos, por lo que basta con adaptar su sintaxis para obtener los correspondientes programas en Prolog (véase el apartado 3.1.2 de este documento). Los programas aritméticos del estilo de los anteriores se pueden considerar programas lógicos puros, puesto que están definidos utilizando exclusivamente propiedades lógicas, y tienen dos caracterı́sticas principales: - Son, por un lado, programas sencillos y versátiles. Recuérdese en particular cómo pueden utilizarse para varios cometidos distintos -por ejemplo el programa de la suma también sirve para restar- ya que cualquiera de sus argumentos puede usarse tanto de entrada como de salida (véanse los ejemplos de consultas dados en el apartado 3.1.3). - Son también, sin embargo, incómodos de utilizar en la práctica y poco eficientes. Su incomodidad proviene del hecho de que los números naturales se deben manipular mediante la función sucesor, y la ineficiencia se debe al cálculo recursivo utilizado para resolver operaciones aritméticas elementales. Una alternativa evidente para mejorar la comodidad y la eficiencia de estos programas es reemplazarlos por otros que hagan uso de las facilidades aritméticas ofrecidas por Prolog, tanto en lo que se refiere al uso de sus constantes numéricas como al uso de los operadores y predicados aritméticos mencionados más arriba. Ası́, si se desease disponer de un predicado para sumar números naturales, bastarı́a con definirlo como sigue, utilizando simplemente el predicado de evaluación is: % suma(+X,+Y,?Z): cierto si Z es la suma de X e Y suma(X,Y,Z) :- Z is X+Y. Esta versión del predicado suma es claramente mucho más cómoda (permite utilizar directamente números naturales en notación decimal) y mucho más eficiente (utiliza la potencia de cálculo aritmético del ordenador). Tiene sin embargo la desventaja respecto a la versión lógica pura de que pierde la versatilidad de ésta. En efecto, como se puede ver en el comentario previo a la definición del nuevo predicado, éste, a diferencia del anterior, sólo se puede usar cuando sus dos primeros argumentos están instanciados: si se intenta usar de otra forma se producirá un error. Esto es debido a la utilización en el cuerpo de la regla del predicado predefinido is, que, como se vio antes, requiere que su parte derecha esté instanciada. 17 De forma similar, una definición más eficiente -aunque menos elegante- del predicado factorial serı́a la siguiente: % factorial(+X, ?Y): cierto si Y es el factorial de X. factorial(0, 1). factorial(X, Y) :X >0, X1 is X-1, factorial(X1, FactX1), Y is X*FactX1. Por el mismo motivo que antes, el predicado anterior sólo se podrá usar cuando el primer argumento esté instanciado, es decir, el predicado es válido para calcular factoriales, pero no sirve ya para averiguar si un número es o no el factorial de algún otro número. Esta restricción no sólo afecta al uso directo del predicado, sino también a su capacidad para ser usado en la definición de otros: por ejemplo, el predicado es_factorial que se describió en el apartado 2.1.2. a partir de la versión lógica del predicado factorial ya no podrá definirse utilizando esta nueva versión. Obsérvese asimismo que en la nueva versión se ha introducido, antes de la llamada recursiva, la comprobación X>0, necesaria si se quiere evitar que se produzca una rama infinita en el árbol de Resolución SLD correspondiente (constrúyanse como ejercicio los árboles de Resolución asociados a una consulta concreta con y sin la comprobación anterior). 4.2. Entrada/Salida El lenguaje Prolog ofrece toda una serie de predicados predefinidos para la realización de operaciones de entrada/salida. Se trata de predicados que no tienen sentido desde un punto de vista puramente lógico, sino que producen un efecto colateral (escritura/lectura de algún termino, apertura/cierre de un fichero, etc). A continuación se describen algunos de los predicados de entrada/salida más básicos: open(+NombreFichero, +Modo, -Fichero) Si NombreFichero es un nombre de fichero válido, abre el fichero correspondiente de acuerdo con el modo especificado por Modo, y unifica con Fichero el identificador del fichero abierto. Los valores para el argumento Modo pueden ser: read para abrir el fichero en modo lectura. write para abrir el fichero en modo escritura (si el fichero no existe, lo crea; si ya existe, su contenido se perderá). append para abrir el fichero en modo escritura (si el fichero no existe, lo crea; si ya existe, las operaciones de escritura se realizarán al final del fichero). close(+Fichero) Cierra el fichero asociado con el identificador Fichero. set_input(+Fichero) set_output(+Fichero) Convierte al fichero con identificador Fichero en el fichero de lectura (escritura) actual. current_input(?Fichero) current_output(?Fichero) Fichero es el fichero de lectura (escritura) actual. 18 Nota: el fichero por defecto, tanto para lectura como para escritura, es la pantalla, cuyo identificador es user. read(?Termino) read(+Fichero, ?Termino) Lee el siguiente término (del fichero de lectura actual o del fichero especificado, previamente abierto en modo lectura) y unifica el resultado con Termino. El término debe acabar con un punto y un retorno de carro. write(?Termino) write(+Fichero, ?Termino) Escribe el término Termino en el fichero de escritura actual o en el fichero especificado (previamente abierto en modo escritura). nl nl(+Fichero) Escribe un retorno de carro en el fichero de escritura actual o en el fichero especificado (previamente abierto en modo escritura). Ejemplos: Se incluyen a continuación algunos ejemplos tı́picos de predicados que realizan operaciones de entrada/salida (fichero “entrada-salida.pl”): % pide_numero(-X) % X es un número leı́do del fichero de lectura actual pide_numero(X) :write(’Introduzca un número: ’), nl, read(X). % escribe_cuadrado(+X) % escribe el cuadrado de X en el fichero de escritura actual escribe_cuadrado(X) :X2 is X*X, write(’El cuadrado de ’), write(X), write(’ es ’), write(X2). % pide un número y escribe su cuadrado por pantalla cuadrado :pide_numero(X), escribe_cuadrado(X). % imprime_lista(+Fichero, +L) % Si Fichero es un identificador de fichero y L es una lista, % escribe los elementos de L en el fichero, uno por lı́nea imprime_lista(_Fichero, []). imprime_lista(Fichero, [C|R]) :write(Fichero, C), nl(Fichero), imprime_lista(Fichero, R). 19 % imprime_lista(+L) % Si L es una lista, imprime por pantalla sus elementos, uno por lı́nea imprime_lista(L) :imprime_lista(user, L). % user es el identificador de la pantalla % pide una lista y la imprime en un fichero prueba_fich :write(’Introduzca una lista: ’), nl, read(Lista), open(’prueba.txt’, write, Prueba), imprime_lista(Prueba, Lista), write(’la lista se ha escrito en el fichero prueba.txt’), close(Prueba). A continuación se reproduce la ejecución de algunos de los predicados anteriores: ?- cuadrado. Introduzca un número: |: 3. El cuadrado de 3 es 9 yes ?- imprime_lista([esto,es,una,lista]). esto es una lista yes ?- prueba_fich. Introduzca una lista: |: [h,o,l,a]. la lista se ha escrito en el fichero prueba.txt yes La ejecución de este último predicado tiene como efecto colateral la escritura de los elementos de la lista [h,o,l,a] en el fichero “prueba.txt”. 4.3. Control: el corte Los predicados de control son predicados predefinidos que permiten al programador intervenir en el mecanismo de búsqueda de soluciones de Prolog. En este apartado se va a introducir exclusivamente uno de ellos, el denominado predicado de corte. 20 4.3.1. Definición y propiedades El corte es un predicado predefinido que se denota mediante un punto de exclamación (!), no tiene argumentos, y cuya evaluación es siempre cierta. Se puede incluir, como un predicado más, en el cuerpo de las reglas o en las consultas (por ejemplo a :- b, c, !, d.). Los cortes permiten al programador intervenir en el control del programa, puesto que su presencia hace que el sistema ignore ciertas ramas del árbol SLD correspondiente. En concreto, el efecto de los cortes en la construcción y recorrido de los árboles de resolución SLD se produce cuando el sistema llega a un nodo del árbol cuyo primer predicado es un corte, es decir, un nodo de la forma “?- !,a1,..,an.”. En estos casos ocurre lo siguiente (siendo N el nodo anterior): 1. El predicado de corte siempre se evalúa como cierto, por lo que el nodo N tendrá un único hijo, que será igual a N pero sin el corte, es decir, será de la forma “?- a1,..,an.”. La expansión de este nodo se realiza igual que la expansión de cualquier otro nodo del árbol. 2. Tanto para cada uno de los nodos ascendientes de N que contengan el corte como para el primer ascendiente que no lo contiene (sea N’ dicho nodo) se ignoran todas sus posibles ramas situadas más a la derecha de la rama que lleva a N. La siguiente figura ilustra lo anterior. Las ramas tachadas con una raya son aquellas que se ignoran como consecuencia del corte. Ejemplo. Considérese el programa Prolog dado por las siguientes cláusulas: a :- b, c. a :- .... b :- d, !, e. b. 21 b :- .... d. d :- .... e :- .... La figura incluida a continuación muestra el árbol de Resolución SLD resultante al realizarse la consulta ?-a. Las ramas tachadas son las que se deben ignorar debido al corte. El efecto de la existencia de una regla de la forma “a:-a1,..,ai,!,..,an” se produce por lo tanto cuando el sistema llega a la evaluación del corte, y consiste en hacer que todas las opciones tomadas desde el momento en el que aparece la cláusula objetivo conteniendo un corte (incluida la decisión que lleva a la consideración de esta cláusula) hasta la evaluación del corte sean obligatorias, eliminándose cualquier alternativa. En concreto: En el momento de evaluar el corte, el sistema ya ha encontrado soluciones para los objetivos a1,..,ai, y no se considerarán otras alternativas para ninguno de ellos (en el ejemplo anterior, no se consideran nuevas alternativas para el objetivo d). Tampoco se considerarán otras alternativas para el predicado responsable de la introducción del corte (en el ejemplo anterior, el predicado b). El predicado de corte tiene dos ventajas fundamentales: Su utilidad básica, dado que reduce el espacio de búsqueda de soluciones, es mejorar la eficiencia de los programas, evitando la exploración de partes del árbol de resolución de las que se sabe de antemano que no conducirán a ninguna nueva solución. 22 Por otro lado, el corte también permite aumentar la expresividad del lenguaje: su uso, normalmente en combinación con otros predicados predefinidos, aporta nuevas construcciones de gran utilidad, como por ejemplo la negación por fallo finito. Sin embargo, el corte es un predicado muy controvertido. Al tratarse de una herramienta de control (interviene en cómo se debe resolver el problema), su uso entra en clara contradicción con los principios básicos de la programación lógica pura, que preconiza una separación nı́tida entre la lógica del problema (responsabilidad del programador) y la forma de resolverlo (responsabilidad del mecanismo de demostración automática). Ası́, el corte puede conducir a programas lógicos difı́ciles de leer y de validar, y puede provocar muchos errores de programación. En definitiva, el predicado de corte de Prolog constituye una herramienta de carácter extralógico, muy potente pero que debe usarse con mucho cuidado y en casos muy concretos. 4.3.2. Usos del corte. Ejemplos Aunque el predicado de corte tiene usos muy variados (en general, en combinación con otros predicados predefinidos de Prolog) uno de los más habituales es la simulación de estructuras condicionales de la forma: si b1 entonces c1 ; si no: si b2 , entonces c2 ; .... si no: si bn , entonces cn ; si no: c. En Prolog, este tipo de estructuras, con condiciones mutuamente excluyentes, se representan mediante un predicado definido por medio de n + 1 reglas, donde cada regla expresa una de las posibles formas de calcular el predicado: la regla i-ésima expresa que el predicado es cierto si no se cumple ninguna de las condiciones b1 , ..., bi−1 anteriores y, además, se cumplen las condiciones bi y ci . El corte permite simplificar la representación anterior y conseguir un uso más eficiente de este tipo de estructuras. Su representación utilizando el corte es la siguiente: a :a :.... a :a :- b1, !, c1. b2, !, c2. bn, !, cn. c. De esta forma se consigue que, en el momento en que se compruebe que se verifica una cierta condición bi, no se intente aplicar ninguna regla posterior. A continuación se describe el uso del corte mediante su aplicación a varios ejemplos (todos ellos contenidos en el fichero “corte.pl”). 23 Ejemplo 1: cálculo de una función Supóngase que se necesita definir un predicado en Prolog que permita calcular la siguiente función f un: 0, 1, f un(x) = 2, si x ≤ 10 si 10 < x ≤ 20 si x > 20 Una primera aproximación para la resolución del problema anterior es definir un predicado f(X,Y), cierto si Y es igual a f un(X), mediante las tres siguientes reglas: f(X,0) :- X =< 10. f(X,1) :- X>10, X =< 20. f(X,2) :- X > 20. La representación anterior calcula correctamente los valores de la función f un, pero tiene el siguiente inconveniente. Supóngase que se realiza la consulta “?- f(0,Z), Z>1.”. La respuesta de Prolog será “no”, pero para llegar a dicha conclusión el sistema tiene que recorrer las 3 posibles ramas del árbol de Resolución SLD correspondiente (dibújese como ejercicio dicho árbol). Lo anterior es poco eficiente, puesto que, al ser las tres reglas que describen el predicado f mutuamente excluyentes, una vez que se ha encontrado una solución con una de ellas no tiene sentido probar con el resto. En efecto, la función que se está calculando tiene la siguiente estructura condicional: si X ≤ 10 entonces Y = 0; si no: si X ≤ 20, entonces Y = 1; si no: Y = 2. Por lo tanto, una forma de remediar la ineficiencia anterior es utilizando el predicado de corte como se ha indicado al principio de este apartado: f(X,Y) :- X =< 10, !, Y=0. f(X,Y) :- X =< 20, !, Y=1. f(_X,2). Con esta nueva versión, la respuesta de Prolog a la consulta “?- f(0,Z), Z>1.” será también “no”, pero ahora, gracias a la introducción del corte en la primera regla, el sistema sólo tendrá que explorar la primera rama del árbol SLD. Obsérvese que una forma más cómoda para representar esta nueva versión consistirı́a en, al igual que en la primera versión, realizar la unificación directamente en la cabeza de las reglas: f(X,0) :- X =< 10, !. f(X,1) :- X =< 20, !. f(_X,2). 24 Sin embargo, hay que destacar que esta última versión, a pesar de ser más cómoda de escribir, tiene el inconveniente de que no siempre es correcta, porque no funciona adecuadamente para ciertos usos del predicado: resuelve correctamente consultas de la forma “?- f(5,Z).”, pero sin embargo no siempre funciona con consultas en las que ambos argumentos están instanciados: por ejemplo, la respuesta del sistema ante la consulta “?- f(0,2).” serı́a afirmativa, cuando deberı́a ser evidentemente negativa. Este problema no se da en las versiones anteriores. Ejemplo 2: cálculo del máximo de dos números El procedimiento para calcular el máximo de dos números naturales se puede implementar en Prolog mediante un predicado maximo(X,Y,Z), cierto si Z es el máximo de X e Y. Al igual que en el ejemplo anterior, una primera versión para dicho predicado podrı́a ser la siguiente: maximo(X,Y,X) :- X >= Y. maximo(X,Y,Y) :- X < Y. Dado que las dos opciones son mutuamente excluyentes, una forma más cómoda y eficiente de expresar lo anterior es utilizando el corte: maximo(X,Y,Z) :- X >= Y, !, Z=X. maximo(_X,Y,Y). Otra posibilidad, similar a lo que se comentó en el ejemplo anterior, es modificar la versión anterior de forma que la unificación se realice directamente en la cabeza de las cláusulas: maximo(X,Y,X) :- X >= Y, !. maximo(_X,Y,Y). Al igual que en el ejemplo anterior, resulta que esta última versión no puede usarse de cualquier forma, porque con ciertas consultas puede dar lugar a resultados erróneos: por ejemplo, la respuesta del sistema ante la consulta “?-maximo(3,0,0).” es afirmativa, cuando deberı́a ser evidentemente negativa. Ejemplo 3: pertenencia de un elemento a una lista La forma más inmediata para representar en Prolog la pertenencia de un elemento a una lista es la siguiente: pertenece(C, [C|_]). pertenece(C, [_|R]) :- pertenece(C,R). En la definición anterior, las dos opciones no se consideran excluyentes, por lo que si un elemento aparece varias veces en una lista, el predicado encontrará todas sus posibles ocurrencias. Ası́, 25 el predicado anterior podrá utilizarse no sólo para averiguar si un elemento pertenece a una lista determinada sino también para recorrer todos los elementos de una lista. Por ejemplo, la respuesta del sistema ante la consulta “pertenece(X,[a,b,c]).” es la siguiente (compruébese construyendo el árbol de resolución SLD correspondiente). ?- pertenece(X,[a,b,c]). X = a ? ; X = b ? ; X = c ? ; no Una versión más eficiente del predicado anterior se consigue introduciendo un corte en el cuerpo de la primera regla, de forma que el predicado termine en el momento de encontrar la primera ocurrencia de un cierto elemento: pertenece(C, [C|_]) :- !. pertenece(C, [_|R]) :- pertenece(C,R). Con esto se consigue una versión determinista y más eficiente del predicado. Sin embargo, la introducción del corte y la consiguiente poda del árbol de Resolución SLD hace que el predicado ya no se pueda usar, como con la versión anterior, para enumerar todos los elementos de una lista. En efecto, ahora se tendrá (compruébese construyendo el árbol de resolución SLD correspondiente): ?- pertenece(X,[a,b,c]). X = a ? ; no 26