Introducción al lenguaje Prolog - Universidad Autónoma de Madrid

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