Professional Documents
Culture Documents
Índice
1. Introducción 2
2. Caracterı́sticas Generales 2
2.3.2. SWI-Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.1. Sintaxis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.1.1. Términos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.1.2. Programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.1.4. Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3.2. Semántica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
4. Predicados Predefinidos 15
4.1. Aritmética . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
4.2. Entrada/Salida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
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 funciona-
miento de Prolog desde el punto de vista de la programación lógica pura, basado en la Progra-
mació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.
R. O’Keefe. The Craft of Prolog. The MIT Press, Cambridge, MA, 1990.
2. Caracterı́sticas Generales
Prolog (del francés, PROgrammation en LOGique) fue el primer lenguaje de programación ba-
sado 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. Colme-
raeur, 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 actua-
lidad 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
- 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’).
?- [’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:
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 interna-
mente 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 distin-
tas 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.
4
por lo que los programas Prolog que se generen de acuerdo con dicho estándar podrán ejecutarse
en cualquiera de estos sistemas.
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 subdirec-
torio “bin” del directorio en el que se haya instalado el editor.
5
También es posible acceder desde Emacs al sistema de ayuda de SICStus Prolog:
2.3.2. SWI-Prolog
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
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.
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.
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
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, general-
mente 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:
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 fun-
ció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 Programa-
ció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 Progra-
mació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:
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 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 des-
cribe como sort(+Lista1, ?Lista2), indicando ası́ que para utilizarlo el primer argumento de-
be estar instanciado (debe ser una lista concreta). Por lo tanto, el predicado sort/2 se podrá uti-
lizar 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”):
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) :- 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 previa-
mente, algunas consultas posibles serı́an las siguientes:
?- 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 Pro-
gramació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:
La siguiente tabla resume las distintas notaciones para cláusulas de Horn vistas hasta el mo-
mento:
3.2. Semántica
- 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:
?- [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
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.
progenitor(pepa, pepito).
progenitor(pepito, pepin).
% 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}
?- 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.
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 lo que sigue se describen sólo algunos de estos predicados predefinidos, en concreto los re-
lacionados con la realización de operaciones aritméticas, ciertos predicados para entrada/salida
y el predicado de control denominado corte.
4.1. Aritmética
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 suma de X e Y
X-Y X menos Y
X*Y producto de X por Y
X/Y cociente real de la división de X por Y
X//Y cociente entero de la división de X por Y
X mod Y resto de la división entera de X por Y
abs(X) valor absoluto de X
sqrt(X) raı́z cuadrada de X
log(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éti-
cas. El más habitual es el predicado predefinido “is”, que se usa en notación infija de la siguiente
forma:
A la hora de usar este predicado hay que tener en cuenta las siguientes consideraciones:
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:
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 (<=).
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.
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 inco-
modidad 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 reem-
plazarlos 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:
Esta versión del predicado suma es claramente mucho más cómoda (permite utilizar directa-
mente 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:
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 opera-
ciones 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:
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.
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 ope-
raciones 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).
% 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
?- 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”.
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
Los cortes permiten al programador intervenir en el control del programa, puesto que su pre-
sencia 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.
a :- b, c. b :- ....
a :- .... d.
b :- d, !, e. d :- ....
b. e :- ....
21
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.
22
Por otro lado, el corte también permite aumentar la expresividad del lenguaje: su uso, nor-
malmente en combinación con otros predicados predefinidos, aporta nuevas construcciones
de gran utilidad, como por ejemplo la negación por fallo finito.
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.
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 :- b1, !, c1.
a :- b2, !, c2.
....
a :- bn, !, cn.
a :- 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, si x ≤ 10
f un(x) = 1, si 10 < x ≤ 20
2, 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:
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:
Con esta nueva versión, la respuesta de Prolog a la consulta “?- f(0,Z), Z>1.” será tam-
bié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:
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.
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,X) :- X >= Y, !.
maximo(_X,Y,Y).
Al igual que en el ejemplo anterior, resulta que esta última versión no puede usarse de cual-
quier 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.
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