Un lenguaje lógico funcional con restricciones1 - GPD

Anuncio
T OY:
Un lenguaje lógico funcional con restricciones1
Autor:
Director:
Jaime Sánchez Hernández
Francisco J. López Fraguas
Trabajo de Tercer Ciclo
Número de créditos solicitados: 7
Departamento Sistemas Informáticos y Programación
Escuela Superior de Informática
Univ. Complutense de Madrid
Septiembre 1998
1
Este trabajo ha sido parcialmente financiado por el proyecto nacional TIC 95-0433-C03-01
”CPD” (Combinación de Paradigmas de Programación Declarativa) y el ESPRIT Working Group
22457 (CCL-II)
Agradecimientos
El sistema T OY del que trata este trabajo ha sido implementado por Francisco J.
López Fraguas, Rafael Caballero Roldán y el que escribe, y no puedo dejar pasar la ocasión
sin agradecer a ellos dos el esfuerzo que han dedicado al desarrollo del compilador. Pero
sobre todo quiero darles las gracias por la paciencia y el buen humor con el que han
afrontado los problemas que han surgido (y no han sido pocos). Creo que esta actitud
ha contribuido a mantener viva la ilusión en una empresa larga y, en algunas ocasiones,
compleja. También quiero mencionar el buen hacer y la perseverancia de Francisco ante
las dificultades con las que hemos topado en el estudio de la semántica del lenguaje. En
esta parte de nuestro trabajo se produce el “efecto mariposa”: una nimia modificación
en alguna de las reglas del cálculo tiene un efecto caótico (varias páginas después) en los
teoremas de corrección o completitud. Conseguir una formulación consistente ha sido una
tarea árdua, pero creo que ha merecido la pena.
Debo expresar también mi agradecimiento a los integrantes de nuestro grupo de Programación Declarativa, puesto que la concepción de T OY es fruto de sus investigaciones.
De hecho, en este grupo ya se habı́an implementado otros sistemas como BABLOG, que es
el antecesor de T OY. Debo recalcar que todos los miembros han manifestado un espı́ritu
de coloboración pleno en el desarrollo del nuevo sistema. Personalmente he tenido la oportunidad de mantener largas y provechosas conversaciones con muchos ellos para resolver
las dificultades que han ido apareciendo. T OY es un sistema “mimado” en el sentido de
que se ha cuidado minuciosamente cada uno de los detalles de la implementación, contrastando opiniones con otros miembros del grupo para buscar la mejor opción posible en cada
caso. Gracias también a Juan Carlos González Moreno, que se ha ocupado de facilitar el
acceso electrónico a la distribución del sistema (y ha discutido mucho conmigo).
Y por último quiero dar las gracias a mi amiga y compañera Ana, que además de
discutir mucho, es para mı́ un apoyo incondicional.
Índice general
1. Introducción
1.1. El sistema T OY . . . . . . . . . . . . . . . .
1.2. Programación lógico funcional. Generalidades.
1.3. Restricciones aritméticas . . . . . . . . . . . .
1.4. Funciones indeterministas . . . . . . . . . . .
1.5. Organización del trabajo . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2. El sistema T OY
2.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2. El entorno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1. Arrancando el sistema . . . . . . . . . . . . . . . . . . .
2.2.2. Comandos básicos . . . . . . . . . . . . . . . . . . . . .
2.2.3. Compilando y ejecutando programas T OY . . . . . . .
2.2.4. Información sobre funciones . . . . . . . . . . . . . . . .
2.2.5. Salvaguarda de sesiones . . . . . . . . . . . . . . . . . .
2.2.6. Activación de las restricciones sobre los números reales .
2.2.7. Definición de nuevos comandos para T OY . . . . . . . .
2.3. El lenguaje T OY . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.1. Definición de tipos de datos . . . . . . . . . . . . . . . .
2.3.2. Tipos predefinidos del sistema . . . . . . . . . . . . . . .
2.3.3. Alias o sinónimos de tipo . . . . . . . . . . . . . . . . .
2.3.4. Definición de funciones . . . . . . . . . . . . . . . . . . .
2.3.5. Tipos de las funciones . . . . . . . . . . . . . . . . . . .
2.3.6. Funciones de orden superior y estructuras infinitas . . .
2.3.7. Funciones indeterministas . . . . . . . . . . . . . . . . .
2.3.8. Restricciones de igualdad y desigualdad . . . . . . . . .
2.3.9. Restricciones sobre los números reales . . . . . . . . . .
2.3.10. Definición de predicados . . . . . . . . . . . . . . . . . .
2.3.11. Operadores infijos y secciones . . . . . . . . . . . . . . .
2.3.12. Inclusión de archivos . . . . . . . . . . . . . . . . . . . .
2.3.13. Regla de indentación . . . . . . . . . . . . . . . . . . . .
2.3.14. Objetivos . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.15. Funciones primitivas . . . . . . . . . . . . . . . . . . . .
2.4. Ejemplo 1. Regiones en el plano . . . . . . . . . . . . . . . . . .
2.5. Ejemplo 2. Puzle aritmético . . . . . . . . . . . . . . . . . . . .
2.6. Comparación con otros estilos de programación . . . . . . . . .
2.6.1. Ordenación de listas . . . . . . . . . . . . . . . . . . . .
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
. 8
. 9
. 11
. 12
. 12
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
13
13
13
13
14
15
16
17
17
18
19
19
21
22
23
25
27
28
29
31
32
33
34
34
35
39
42
53
56
57
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
ÍNDICE GENERAL
4
2.6.2. El problema del laberinto revisitado . . . . . . . . . . . . . . . . . . 58
3. Mecanismo de cómputo
3.1. Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2. Visión general del proceso de compilación y ejecución de objetivos . .
3.3. Preliminares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4. Los objetos sintácticos de T OY y su representación Prolog . . . . . .
3.5. Orden superior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.1. Reglas de apply . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.2. Información sobre constructoras y funciones . . . . . . . . . . .
3.6. El sharing o compartición de variables . . . . . . . . . . . . . . . . . .
3.7. El código intermedio . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.8. Las restricciones de desigualdad estricta . . . . . . . . . . . . . . . . .
3.8.1. Gestión de las desigualdades . . . . . . . . . . . . . . . . . . .
3.9. Cómputo de formas normales de cabeza (hnf) . . . . . . . . . . . . . .
3.10. Generación de código para las funciones . . . . . . . . . . . . . . . . .
3.10.1. Preliminares . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10.2. Construcción del árbol definicional . . . . . . . . . . . . . . . .
3.10.3. Generación de código para las funciones. Primera aproximación
3.10.4. Incorporación de desigualdades en la traducción de funciones .
3.10.5. Optimizaciones de código . . . . . . . . . . . . . . . . . . . . .
3.10.6. Código para las funciones primitivas y apply . . . . . . . . . .
3.11. Igualdad estricta (==) . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.11.1. El occurs-check . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.11.2. El estudio de la frontera . . . . . . . . . . . . . . . . . . . . . .
3.11.3. Ligadura de variables a f.n.c.’s (binding) . . . . . . . . . . . . .
3.11.4. Igualdad estricta entre formas normales de cabeza (equalHnf )
3.11.5. El predicado equal . . . . . . . . . . . . . . . . . . . . . . . . .
3.12. Restricciones de desigualdad (notEqual) . . . . . . . . . . . . . . . . .
3.12.1. Restricciones de desigualdad entre formas normales . . . . . . .
3.13. La función igualdad (eqF un) . . . . . . . . . . . . . . . . . . . . . . .
3.14. La función desigualdad (notEqF un) . . . . . . . . . . . . . . . . . . .
3.15. Las restricciones sobre los números reales . . . . . . . . . . . . . . . .
4. De la semántica
4.1. Introducción . . . . . . . . . . . . . . . . . .
4.2. Preliminares . . . . . . . . . . . . . . . . . .
4.2.1. Signaturas, términos y c-términos . .
4.2.2. Sustituciones . . . . . . . . . . . . .
4.3. El cálculo de pruebas GORC6= . . . . . . .
4.3.1. Caracterización de == en función de
4.3.2. Sustituciones . . . . . . . . . . . . .
4.4. Cálculo de resolución de objetivos . . . . .
4.4.1. El cálculo LN C6= . . . . . . . . . . .
4.4.2. Corrección . . . . . . . . . . . . . .
4.4.3. Completitud . . . . . . . . . . . . .
4.5. Estrategia DDS . . . . . . . . . . . . . . . .
4.5.1. Transformación de programas . . . .
. .
. .
. .
. .
. .
→
. .
. .
. .
. .
. .
. .
. .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
61
61
62
64
64
66
68
70
72
73
74
76
79
82
84
85
90
93
97
102
104
105
106
109
110
111
113
118
119
122
123
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
129
. 129
. 130
. 130
. 131
. 131
. 137
. 139
. 144
. 145
. 161
. 163
. 169
. 171
ÍNDICE GENERAL
5
4.5.2. Algoritmo de transformación . . . . . . . . . . . . . . . . . . . . . . 175
5. Conclusiones y trabajo futuro
179
A. Gramática de T OY .
181
B. Declaración de primitivas (archivo basic.toy)
187
C. Funciones de uso común (archivo misc.toy)
189
D. Archivo toycomm.pl
197
E. Primitivas sin restricciones aritméticas (archivo primitives.pl)
211
F. Primitivas con restricciones aritméticas (archivo primitivesClpr.pl)
219
G. Construcción del arbol definicional
229
H. Generación de código
241
I. Salida de respuestas
255
6
ÍNDICE GENERAL
Capı́tulo 1
Introducción
El objetivo fundamental de los lenguajes de programación declarativa en sentido amplio, es proporcionar un alto nivel de abstracción, de forma que la especificación de un
problema sea un programa capaz de resolver el problema. En definitiva se intenta liberar
al programador de describir detalladamente la secuencia de acciones que debe realizar la
máquina para obtener el resultado buscado, como es habitual en programación imperativa. Los lenguajes declarativos están basados en formalismos matemáticos que permiten
hacer un estudio riguroso y preciso de los aspectos semánticos que subyacen. Se abstraen
los detalles concretos del hardware y, en general, los programas son más breves y más
sencillos de mantener.
Sin embargo, la programación declarativa no tiene un paradigma representativo único.
Los dos más importantes, el lógico y el funcional han evolucionado de forma independiente,
pero manteniendo como prioridad común la expresividad. Como resultado se han forjado
dos estilos de programación distintos que intentan aprovechar las ventajas que ofrece
uno u otro enfoque. El potencial, en el caso de la programación funcional, viene dado
fundamentalmente por la evaluación perezosa, el orden superior y los tipos, mientras que
en programación lógica las variables lógicas, los modos múltiples de uso de los predicados
y el indeterminismo suponen la principal aportación.
Como amalgama de estas dos vertientes surgen los lenguajes lógico funcionales ([Han94]),
en los que se pretende reunir las principales ventajas de ambos paradigmas en uno nuevo. Esta fue la motivación de lenguajes como K-LEAF ([BCM89]) y BABEL ([MR89]).
El mecanismo operacional de estas propuestas es el resultado de combinar los que utilizan ambos tipos de lenguajes. En general, el mecanismo operacional de los lenguajes
lógicos se apoya en la unificación y la resolución, mientras que en los lenguajes funcionales es la reescritura la que juega este papel. Para los lenguajes lógico-funcionales el
mecanismo más estudiado y extendido es la “reescritura con unificación” o, más comunmente, narrowing (estrechamiento). Este método tiene sus orı́genes en Reddy ([Red85]) y
en [GM86] se prueba que es completo para la resolución de sistemas de ecuaciones confluentes y terminantes. El narrowing es también utilizado por lenguajes como BABLOG
([AG94]) o CURRY([He97, HKM95]). Existen, no obstante otros como ESCHER ([Llo95])
que se basan en reescritura.
El narrowing presenta un alto grado de indeterminismo debido a la elección del redex
(exprersión a reducir en un paso de cómputo) y de la regla de reescritura a utilizar, lo
que supone que el espacio de búsqueda generado puede ser muy grande. Para reducir
este espacio se utilizan estrategias de estrechamiento con el fin de conseguir cómputos
“más” deterministas, y en consecuencia, más eficientes. En particular, es posible guiar
7
CAPÍTULO 1. INTRODUCCIÓN
8
la evaluación de una función estudiando la demanda de patrones de las reglas que la
definen, y en este sentido surge la Estrategia Guiada por la Demanda ([LLR93]). Esta
estrategia responde a la idea del estrechamiento perezoso ([Red85]) y su funcionamiento
consiste en retrasar la evaluación de los argumentos de la función de llamada, mientras no
sean necesarios para continuar el cómputo. El lenguaje BABLOG ya implementaba dicha
estrategia.
Otro de los aspectos que ha evolucionado notablemente en programación lógica es la
introducción de restricciones ([JM94, Coh90]), que enriquecen el poder expresivo de estos
lenguajes. En programación lógico funcional también es posible incorporar restricciones
([L94, LR91]). En particular, las restricciones de desigualdad ([AGL94, L94]) suponen un
recurso expresivo importante que ya incluı́a el lenguaje BABLOG.
Otro tipo de restricciones ampliamente estudiadas en programación lógica son las
ariméticas, que tienen extenso rango de aplicación y se han incluido en lenguajes como
CLP (R) ([JMS+ 92a, JMS+ 92b, HJM+ 91]). También este tipo de restricciones puede incorporarse a los lenguajes lógico funcionales ([AHL+ 96, HLS+ 97]).
1.1.
El sistema T OY
En este trabajo se presenta el sistema T OY ([CLS97]), una implementación de un
lenguaje lógico-funcional que incorpora restricciones sobre los reales. Esta implementación
ha sido desarrollada por Rafael Caballero Roldán, Francisco J. López Fraguas y el autor
de este documento.
Toda la información contenida en este trabajo se refiere a la versión 2.0 del sistema,
que es la única disponible al público en la actualidad y que está diseñada para plataformas
UNIX1 . Esta versión está disponible en la dirección:
http://mozart.sip.ucm.es/incoming/comprimidos/toy.tar.gz
En lo sucesivo utilizaremos la palabra ‘T OY’ para referirnos tanto al sistema como al
lenguaje que implementa.
En T OY se combinan los dos estilos de programación lógica y funcional tomando
Prolog ([SS86, O’K90]) como representante lógico y Haskell ([HFP97, PH97, JJ97]) como
representante funcional. Las caracterı́sticas más destacables del lenguaje son:
sintaxis funcional (inspirada en Haskell),
programación lógica pura mediante definición de predicados al estilo Prolog,
programación funcional con:
• tipos polimórficos,
• funciones de orden superior y cómputos lógicos de orden superior (bajo determinadas condiciones)
• evaluación perezosa y estructuras de datos infinitas,
restricciones de igualdad estricta,
1
Hay versiones experimentales no distribuidas que corren sobre MS-DOS y Windows95. También hay
una versión que incorpora corte dinámico ([LW91, LW95]) y algunas pruebas de entrada/salida e introducción de restricciones de dominio finito.
1.2. PROGRAMACIÓN LÓGICO FUNCIONAL. GENERALIDADES.
9
restricciones de desigualdad estricta,
funciones indeterministas,
restricciones sobre los números reales.
T OY, al igual que su predecesor BABLOG, lleva a cabo una traducción a Prolog de los
programas fuente ([Che90, CF93]) de acuerdo con la Estrategia Guiada por la Demanda
que se propone en [LLR93]. Esta estrategia está inspirada en la construcción de árboles
definicionales ([Ant92]). Las restricciones sobre reales también se transforman de acuerdo
con la estrategia en otras más simples que son procesadas por un resolutor de restricciones
([Hol95]).
Las principales aportaciones de T OY a la programación lógico funcional son la introducción de restricciones sobre los números reales y las funciones indeterministas. También
admite variables lógicas de orden superior e incorpora muchas optimizacciones en la traducción de las funciones, ası́ como en la resolucion de igualdades y desigualdades. El interés
de nuestro lenguaje en el ámbito de la programación declarativa viene avalado por el hecho
de está presente en la página web más importante sobre programación lógico funcional:
http : //www − i2.inf ormatik.rwth − aachen.de/˜hanus/F LP
También ha sido mencionado y descrito en el boletı́n de noticias (Febrero de 1998) de la
Association for Logic Programming, que es la asociación sobre Programación Lógica más
relevante a nivel internacional. La referencia es accesible electrónicamente en:
http : //www − lp.doc.ic.ac.uk : 80/alp/news/f ree − langs/f lps/toy.html
T OY soporta el desarrollo de programas no triviales, ası́ como metodologı́as de programación interesantes ([CR98]). Ha contribuido notablemente al desarrollo de otras investigaciones en el grupo de Programación Declarativa del Depto. de Sistemas Informáticos
y Programación de la UCM y ha sido utilizado por otros investigadores como sistema para realizar pruebas sobre evaluación parcial ([AAF+ 98]). Por otro lado, T OY constituye
el sistema de partida para el proyecto TREND (Técnicas Avanzadas de Desarrollo de
Programas en Entornos Declarativos), recientemente concedido por la CICYT.
1.2.
Programación lógico funcional. Generalidades.
Uno de los aspectos más destacados de la programación lógico funcional es la posibilidad de utilizar las funciones de forma reversible, de modo análogo al uso de predicados
en Prolog. Por ejemplo, la concatenación de listas, en Prolog puede definirse como:
append([],Ys,Ys).
append([X|Xs],Ys,[X|Zs]) :- append(Xs,Ys,Zs).
El predicado append está pensado, en principio, para recibir dos listas y devolver su concatenación en el último argumento. Por ejemplo, el objetivo append([1, 2], [3, 4], L) producirı́a
la respuesta L = [1, 2, 3, 4]. Pero también, con esta misma definición, se puede resolver el
objetivo append(Xs, [3, 4], [1, 2, 3, 4]), que producirá la respuesta Xs = [1, 2, 3, 4].
En Haskell, se puede definir una función similar:
append []
ys = ys
append (x:xs) ys = (x:append xs ys)
CAPÍTULO 1. INTRODUCCIÓN
10
Con esta definición se puede reducir la expresión append [1, 2] [3, 4] a la lista [1, 2, 3, 4],
pero si se intenta reducir append xs [3, 4] el sistema producirá un error, porque no puede
reducir expresiones que contengan variables.
En programación lógico funcional y en particular, en T OY, se admite la definición
anterior de la función append, pero además es posible resolver una restricción como
append Xs [3, 4] == Zs, que producirá el resultado Xs == [1, 2], Zs == [1, 2, 3, 4].
Aquı́ el sı́mbolo ‘==’ representa restricciones de igualdad y la respuesta obtenida quiere
decir que la restricción planteada append Xs [3, 4] == Zs es cierta si son ciertas las restricciones Xs == [1, 2] y Zs == [1, 2, 3, 4]. Las restricciones de la respuesta representan
los valores que deben tomar las variables de la restricción para satisfacerla. Informalmente
podemos decir que se han “despejado” las variables Xs y Zs (formalmente se dice que las
restricciones de la respuesta están en forma resuelta).
Es posible ademas que una restricción tenga más de una solución, como ocurre en
el caso de append Xs Y s == [1, 2]. En este caso T OY devolverá una primera solución
Xs == [ ], Y s == [1, 2] y, si el usuario lo solicita, calculará a continuación las siguientes
Xs == [1], Y s == [2] y también Xs = [1, 2], Y s = [ ]. Si se solicitan más respuestas, el
sistema informará de que no existen más. Este proceso se realiza por backtracking (vuelta
atrás) de modo similar a como operarı́a Prolog con el objetivo correspondiente.
En T OY se admiten además restricciones de desigualdad como append [1] Xs /=
[1, 2]. En este caso el sistema devuelve como única respuesta Xs /= [2]. El interés de las
desigualdades reside en que con ellas se pueden ofrecer respuestas tan concisas como la del
ejemplo anterior. Las desigualdades aparecen en situaciones comunes en el contexto lógico
funcional y en muchos casos, como el del ejemplo anterior, una respuesta que contenga una
desigualdad no puede reemplazarse por un número finito de respuestas que sólo contengan
igualdades.
En Haskell, sin embargo, la evaluación perezosa de las funciones mejora considerablemente el rendimiento del sistema y permite manejar estructuras infinitas que Prolog no
puede tratar. Por ejemplo, consideremos en Haskell las dos funciones siguientes2 :
from n = [n | from (n+1)]
take 0 xs
take n []
take n (x:xs) | n>0
= []
= []
= x : take (n-1) xs
Una llamada como f rom 3 produce una lista que comienza con 3, seguida de el resultado de
evaluar f rom 4, que a su vez produce una lista que empieza por 4 seguida de el resultado
de evaluar f rom 5... En definitiva, el resultado de la llamada f rom 3 se reducirı́a a la
lista infinita [3, 4, 5, ...], que dejarı́a al sistema sumido en un cómputo infinito (en realidad,
hasta agotar los recursos de memoria).
La función f rom no tiene interés aisladamente, pero sı́ lo tiene en combinación con
otras como take. La función take devuelve los n primeros elementos de una lista dada
(o la lista completa si su longitud es menor o igual que n). Utilizando esta función se
puede reducir la expresión take 5 (f rom 3) que devolverá los 5 primeros elementos de la
lista infinita [3, 4, 5, ...], es decir, la lista [3, 4, 5, 6, 7]. Esto es posible debido a la evaluación
perezosa (2.3.6).
2
En la última regla de take la expresión | n > 0 es una guarda que puede leerse como “si n es mayor
que 0”.
1.3. RESTRICCIONES ARITMÉTICAS
11
En Prolog no es posible hacer un cómputo como el que acabamos de describir ya que
cualquier predicado que genere una estructura infinita, una vez invocado, intentará generar
la estructura completa y producucirá un cómputo no terminante (la pereza se puede
simular, pero Prolog no es perezoso). T OY implementa la evaluación perezosa y permite
hacer el cómputo de Haskell que acabamos de describir. De hecho, aunque T OY hace una
traducción a Prolog, debido a la pereza es más eficiente que Prolog en algunos casos (con
programas similares), como veremos en 2.6.
Otra de las virtudes de la programación funcional que también incopora Haskell es el
orden superior, es decir, la posibilidad de utilizar funciones como argumentos de funciones,
y también funciones que devuelven funciones. T OY no sólo admite orden superior en este
sentido, sino que admite también variables lógicas de orden superior como veremos en
(2.3.6).
Por último T OY también incorpora un inferidor de tipos similar al de Haskell3 y
permite declarar tipos para las funciones y predicados. Por ejemplo, de la definición de la
función append anterior se deduce que debe tomar dos listas como argumento y devolver
otra lista; pero además las tres listas deben ser de elementos del mismo tipo (este tipo se
representa como [A] → [A] → [A]). Por ejemplo, una expresión como append [1, 2] [0 a0 ]
está mal tipada porque la primera es una lista de números y la segunda es de caracteres.
Los tipos suponen una valiosa ayuda para detectar errores en los programas y, en general,
aportan claridad a los programas.
1.3.
Restricciones aritméticas
En la sección anterior hemos comenzado viendo el significado de la reversibilidad de los
predicados en Prolog. Sin embargo, esta reversibilidad se pierde en el momento en el que
se implican operaciones aritméticas. Por ejemplo, un predicado add para sumar números
en Prolog se podrı́a definir como4 :
add(X,Y,Z) :- Z is X+Y.
El objetivo add(3, 4, Z) producirı́a la respuesta Z = 7, que es el resultado esperado. Pero
el objetivo add(X, 4, 7) produce un error en ejecución (no es capaz de encontrar la respuesta X = 3). Aquı́ se ha perdido la reversibilidad. El objetivo anterior, en realidad,
está planteando al sistema la restricción lineal X + 4 = 7 en la forma 7 is X + 4, pero
el predicado is tiene condiciones en cuanto al modo de uso: en una llamada de la forma
X is E, E debe ser una expresión aritmética sin variables.
En programación lógica, y en Prolog en particular, este problema se ha abordado
mediante la incorporación de restricciones aritméticas sobre reales. Algunas implementaciones de Prolog incluyen un resolutor de restricciones aritméticas (en particular el sistema
Siscstus Prolog,[Gro96]) y son capaces de solucionar restricciones lineales5 . Con estos resolutores, un sistema de ecuaciones lineales como X + Y = 2, X − Y = Z, puede resolverse
obteniendo la respuesta Y = 2 − X, Z = −2 + 2 ∗ X. T OY también admite este tipo de
restricciones que se estudiarán en 2.3.9.
3
Aunque sin clases de tipos, una potente extensión ([Jon94, PJ93]) al sistema de Hindley-Milner
([DM82]), incorporada a Haskell.
4
En Prolog la llamada de la forma X is E, produce la unificación de la variable X con el resultado de
evaluar la expresión aritmética E. Por ejemplo, X is (3 + 4) ∗ 2 unifica X con 14.
5
Sobre restricciones no lineales también se han hecho estudios, como [Hon92, Han93, Han95a]
CAPÍTULO 1. INTRODUCCIÓN
12
1.4.
Funciones indeterministas
Otro de los aspectos destacados de T OY son las funciones indeterministas. En programación funcional las funciones son entendidas en el sentido matématico usual. Una
función, tomando los mismos argumentos, sólo puede producir un resultado (que está determinado por la función y los argumentos en cuestión). En T OY esto no es ası́ y una
función puede devolver distintos valores al evaluarse sobre los mismos argumentos (pueden
entenderse como funciones multivaluadas).
Supongamos la siguiente definición:
coin = 0
coin = 1
Según esta definición coin (resultado de lanzar una moneda al aire) es una función que no
toma ningún argumento y puede producir tanto el resultado 0, como 1. Esta definición en
Haskell no es correcta, aunque el compilador no produce error6 . En T OY esta definición
es correcta y para una restricción como coin == X, el sistema encuentra las respuestas
X == 0 y X == 1 (coin es una función indeterminista).
Este tipo de funciones permite expresar de forma concisa y simple algunas operaciones de naturaleza indeterminista. También pueden utilizarse en vez de los predicados en
algunas situaciones, con ciertas ventajas. Las secciones 2.4 y 2.5 contienen algunos ejemplos ilustrativos del interés de las funciones indeterministas desde el punto de vista de la
programación.
1.5.
Organización del trabajo
El contenido del trabajo esta organizado en cinco capı́tulos, de los que éste es el primero.
En el segundo se hace una descripción del sistema que incluye el manejo del entorno. A
continuación se hará un recorrido por las distintas construcciones sintácticas que admite el
lenguaje, mostrando ejemplos en los que se aprecia la utilidad de cada una de ellas. Después
veremos dos ejemplos de programación en los que se resuelven problemas concretos. El
capı́tulo termina con una breve compación con otros estilos de programación declarativos.
El segundo capı́tulo trata exhaustivamente la traducción de programas T OY a código
Prolog. Aquı́ se estudiará el manejo y resolución de las restricciones de igualdad y desigualdad, el tratamiento del orden superior, la Estrategia Guiada por la Demanda que se
utiliza para la evaluación de funciones y, por último, las restricciones sobre reales.
En el tercero se abordan algunos aspectos de la semántica del lenguaje. Presentaremos
un cálulo operacional que refleja la evaluación perezosa, las funciones indeterministas y las
restricciones de igualdad y desigualdad. También justificaremos formalmente la corrección
de la Estrategia Guiada por la Demanda explicada en el capı́tulo anterior. No obstante,
el cálculo que presentamos no incluye el orden superior, los tipos y las restricciones sobre
reales.
El último contiene algunas conclusiones.
Por último se incluyen algunos apéndices con la gramática del lenguaje, los programas
que implementan la construcción de árboles definicionales y la generación de código, el
programa de procesado y salida de respuestas, y los archivos de primitivas del lenguaje.
6
La reducción de la expresión coin produce (sólo) el resultado 0.
Capı́tulo 2
El sistema T OY
2.1.
Introducción
En este capı́tulo se presenta una introducción al sistema T OY en la que se explica el
manejo del entorno y la sintaxis del lenguaje, a modo de guı́a de usuario.
Con respecto al entorno, T OY ofrece un sencillo intérprete de comandos integrado en
el sistema, que facilita las tareas de escritura, compilación y ejecución de un programa.
Todos los comandos, y en general, todo el proceso de comunicación con el sistema se realiza
desde la lı́nea de comandos del mismo.
La sintaxis está inspirada en el lenguaje funcional Haskell [HFP97, PH97], y su aspecto es, por tanto, claramente funcional (el apéndice A contiene la gramática completa
del lenguaje). A lo largo de la exposición se ha procurado incluir ejemplos ilustrativos
que muestren la utilidad y el potencial expresivo de las distintas construcciones sintácticas de T OY. El sistema cuenta con un repertorio de funciones predefinidas que también
estudiaremos.
El capı́tulo contiene además, algunos ejemplos de programación que integran las distintas posibilidades del lenguaje, en especial las restricciones sobre reales y las funciones
indeterministas, que son las dos cualidades fundamentales que aporta T OY a la combinación de los paradigmas lógico y funcional. Por último se hace una breve comparación
con la programación lógica y con la funcional, en especial con Prolog.
2.2.
El entorno
2.2.1.
Arrancando el sistema
T OY (versión 2.0) está disponible en
http://mozart.sip.ucm.es/incoming/comprimidos/toy.tar.gz
Este archivo se puede desempaquetar y descomprimir con el comando UNIX:
gunzip -c toy.tar.gz | tar -xvf que genera tres directorios:
./toySystem: contiene los archivos del compilador propiamente dicho (toy.ql, toy.ini,
basic.toy, primitives.ql, primitivesClpr.ql y misc.toy),
13
CAPÍTULO 2. EL SISTEMA T OY
14
./examples: algunos ejemplos de programación en T OY,
./manual: manual de usuario.
El archivo toy.ini es un archivo de configuración que se tratará en 2.2.7, en basic.toy se
encuentran los tipos de las funciones predefinidas de T OY que se abordarán en 2.3.15 y
misc.toy es un archivo de utilidades (funciones y predicados comunes) que se le proporcionan al usuario (véase el apéndice C). El resto de archivos pertenecen al núcleo del sistema
y no son editables (salvo que se disponga de una distribución con el código fuente).
Debe tenerse presente que T OY está completamente implementado en Prolog y es
imprescindible disponer del sistema Sicstus Prolog (versión 3#3 o superior) para ejecutarlo.
Para arrancar T OY basta con seguir estos pasos:
desde el directorio toySystem iniciar una sesión de Sicstus,
cargar T OY desde el prompt de Sicstus:
| ?- load(toy).
Sicstus cargará todas las librerı́as necesarias y los archivos de T OY.
Cuando el paso previo ha concluido T OY está cargado en memoria y mostrará el
siguiente mensaje:
TOY 2.0
30th September, 1997
TYPE "/h." FOR HELP.
y el prompt del sistema:
TOY>
El sistema está preparado para funcionar.
En sistemas Unix, para automatizar el proceso, es útil incluir un alias de la forma
toy=sicstus -l ~/toySystem/toy, reemplazando el path y la invocación a Sicstus de
forma apropiada.
Es posible suspender la ejecución en cualquier momento invocando al mecanismo de
interrupción de Sicstus mediante la combinación de teclas < Ctrl > +c1 . Entonces Sicstus
mostrará el mensaje:
Prolog interruption (h for help)?
En este punto se puede continuar la ejecución con ‘c’ o bien abortarla con ‘a’.
2.2.2.
Comandos básicos
T OY proporciona un sencillo interface de comunicación con el usuario mediante un
pequeño repertorio de comandos. Con el comando /h. o /help. se obtiene un breve
menú de ayuda. Todos estos comandos deben ir precedidos del sı́mbolo especial ‘/’ y
terminar con ‘.’, y se interpretarán como un término Prolog, es decir: los argumentos
deben ir entre ‘(’ y ‘)’, No se admiten espacios en blanco entre el comando y ‘(’. Si los
1
En una sesión T OY siempre está ejecutándose Sicstus en segundo plano, sin embargo, el usuario no
puede interactuar directamente con Sicstus más que para suspender la ejecución con esta combinación de
teclas.
2.2. EL ENTORNO
15
argumentos contienen espacios en blanco o sı́mbolos como ‘/’ (para especificar un path,
por ejemplo), entonces deben ir entre comillas simples (’). Por ejemplo, los siguientes
comandos son reconocidos por T OY:
/compile(my_program).
/
compile( my_program
/load(one_of_my_programs).
/system(’cd ..’).
/cd(..).
/cd(’/home/local/bin’).
/q.
/help.
).
Y los siguientes no son reconocidos:
/compile (my_program).
/cd(/home/local/bin).
/cd(../..).
Algunos comandos hacen llamadas directas al sistema operativo:
/cd(< dir >) cambia el directorio de trabajo de T OY al directorio < dir >,
/system(< comm >) lanza el comando < comm > al sistema operativo (a la shell
correspondiente). Es el modo más directo que ofrece T OY para comunicarse con
el sistema operativo, de hecho, el comando /cd(< dir >) puede obtenerse como
/system(’cd < dir >’) y se incluye expresamente por comodidad para el usuario.
Otro ejemplo válido es:
/system(’cp my_program.toy /home/users/axterix/examples/.’).
/q, /quit, /e o /exit terminan la sesión T OY.
En los siguientes apartados se explica el funcionamiento del resto de comandos.
2.2.3.
Compilando y ejecutando programas T OY
La extensión de los archivos que contienen programas T OY es por defecto ‘.toy’,
aunque se admite cualquier otra. Cualquier referencia a un archivo fuente sin extensión
explı́cita será completada con la extensión ‘.toy’. T OY traduce los programas de usuario
a código Prolog, es decir, el código objeto generado por el sistema es Prolog. Ası́, la compilación del archivo < f ile >.toy produce como resultado el archivo Prolog < f ile >.pl 2
en el que el sistema escribe el código Prolog correspondiente a traducción de las funciones
y predicados que contiene el archivo de entrada, junto con otra información. Para que
T OY pueda ejecutar este programa (código Prolog) debe ser compilado previamente por
Sicstus. Este proceso se simplifica y se hace relativamente transparente al usuario mediante
tres comandos que proporciona T OY:
/compile(< f ile >) compila el archivo < f ile > (o < f ile >.toy si no tiene
extensión) y genera el archivo < f ile >.pl. Este archivo no es compilado por Sicstus,
de modo que no se pueden resolver objetivos para este programa al final del proceso.
2
La extensión .pl es la que utiliza por defecto Sicstus Prolog.
CAPÍTULO 2. EL SISTEMA T OY
16
/load(< f ile >) carga el programa < f ile >.pl, previamente compilado por T OY.
Realmente se invoca a Sicstus para que compile el programa Prolog generado por
T OY tras ejecutar el comando compile.
Cuando (mediante programa) se incorpora una definición para una función previamente utilizada en la sesión actual de T OY, Sicstus producirá un mensaje de
advertencia:
The procedure
Old file:
New file:
Do you really
Name/Arity is being redefined.
...
...
want to redefine it? (y, n, p, or ?)
Para cargar la nueva definición debe escogerse ‘y’, sin embargo, es frecuente redefinir
simultáneamente varias funciones y es útil elegir la opción ‘p’ para que el sistema no
haga una consulta de este tipo por cada una de ellas y tome directamente todas las
definiciones nuevas.
/run(< f ile >) hace en secuencia las dos operaciones anteriores /compile(< f ile >)
+ /load(< f ile >). Este será el comando que se utilice con más frecuencia.
No es posible compilar simultáneamente varios programas (compile, load y run admiten sólo un argumento), pero puede conseguirse el mismo efecto con la directiva include
que se explicará en 2.3.12.
En la compilación de un programa, T OY procesa la entrada en varias fases. En primer
lugar chequea la sintaxis, las dependencias funcionales y los tipos. Si no se produce ningún
error se procede a la generación de código objeto (código Prolog). Durante la compilación
proporciona al usuario información sobre el estado en que se encuentra y sobre los errores
que pueda contener la entrada.
Una vez compilado el programa el usuario puede plantear objetivos para dicho programa y lanzalos al sistema desde la lı́nea de comandos como veremos en 2.3.14. Ejecutar un
programa en T OY es resolver objetivos (restricciones) para dicho programa, como ocurre
en otros sistemas como Prolog.
2.2.4.
Información sobre funciones
Hay dos comandos que permiten obtener información sobre las funciones definidas en
un programa. El primero de ellos es /type(< f un >) que muestra el tipo de la función
< f un > en el caso de que dicha función esté definida en el programa cargado en memoria.
Por el momento, no es posible obtener el tipo de una expresión general con este comando,
como hacen otros lenguajes funcionales.
El otro comando es /tree(< f ile >,< f un >) que ofrece información sobre la traducción que ha producido T OY para la función < f un > definida en el archivo < f ile >. En
concreto, muestra el árbol definicional (véase 3.10) asociado a dicha función. Esta información es sólo útil para aquellos usuarios que posean algunas nociones sobre la estrategia
que utiliza el sistema para la evaluación de las funciones, o bien para depuración.
2.2. EL ENTORNO
2.2.5.
17
Salvaguarda de sesiones
Otro de los comandos del sistema es /save(< f ile >), que sirve para volcar al archivo
< f ile > el estado actual de una sesión con todas las definiciones de funciones, predicados,
tipos, etc, que existen en ese momento en memoria. Además, se hace una copia de todos
los archivos que necesita el sistema para arrancar. Al ejecutar el archivo < f ile > desde
un intérprete de comandos de UNIX (shell) se restaura automáticamente el estado en que
se encontraba el sistema cuando se salvó el contexto con el comando save.
Este comando es una forma de simular un auténtico programa ejecutable y es útil
cuando se utilice con frecuencia un programa T OY. Sin embargo, este programa no es
realmente un ejecutable porque necesita el sistema Sicstus. Puede utilizarse también para
conseguir una copia compilada de T OY que reduce notablemente el tiempo de carga del
sistema, siguiendo los siguientes pasos:
iniciar una sesión T OY del modo habitual,
cambiar el directorio actual a aquel en el que se desea obtener la copia compilada
mediante el comando cd,
ejecutar el comando /save(< f ile >) siendo < f ile > el nombre de la copia que se
desea obtener (por ejemplo /save(toy)),
abandonar la sesión actual con el comando /q
Tras este proceso, en el directorio elegido se ha creado el ejecutable < f ile > y algunos
archivos más. La ejecución de < f ile > arrancará inmediatamente el sistema.
2.2.6.
Activación de las restricciones sobre los números reales
El sistema tiene básicamente dos modos de uso: con restricciones sobre los reales y
sin ellas. Como es natural, el primer modo de uso incrementa la potencia del sistema
haciéndolo más expresivo. De hecho, cualquier objetivo que se pueda resolver sin hacer uso
de las restricciones puede resolverse exactamente igual en el modo de uso que las incluye
(no hará uso de ellas), excepto en eficiencia. La resolución de restricciones sobre reales
se apoya directamente en el resolutor que ofrece Sicstus en la librerı́a clpr (véase [Hol95]
o el propio manual de Sicstus [Gro96, Gro97] para una descripción detallada de dicha
librerı́a). Una vez cargado en memoria dicho resolutor, Sicstus no es capaz de distinguir
a priori si un determinado objetivo hará o no uso de las restricciones y mantiene una
actitud conservadora suponiendo que sı́ que utilizará tales restricciones. De este modo
debe mantener información adicional3 para las variables que intervienen en el cómputo,
incrementando, en consecuencia, el coste de la resolución de objetivos independientemente
de que hagan uso o no de las restricciones4 .
Por lo tanto, la razón de los dos modos de uso se encuentra en la eficiencia del sistema.
Hay dos comandos para intercambiar dichos modos:
3
La información sobre las restricciones se almacena en forma de atributos asociados a las variables,
mediante la librerı́a de atributos con la que cuenta Sicstus ([Gro97]).
4
Algunos predicados deben redefinirse para tener en cuenta los atributos de las variables para mantener
la consistencia. El tratamiento de los atributos supone un incremento del coste computacional. En particular, una operación tan frecuente como la unificación de dos términos necesita efectuar otras operaciones
sobre los atributos de las variables de ambos términos.
CAPÍTULO 2. EL SISTEMA T OY
18
/cflpr (constraint functional-logic programming over reals) prepara el sistema para
trabajar con restricciones sobre los reales. Las funciones primitivas aritméticas necesitan nuevas definiciones que se cargarán automáticamente y producirán el mensaje:
The procedure primInfix/3 is being redefined.
Old file: /home/jaime/toy/toySystem/toy.pl
New file: /home/jaime/toy/toySystem/primitivesClpr.pl
Do you really want to redefine it? (y, n, p, or ?)
El usuario debe responder p para permitir la carga de las nuevas definiciones. A partir
de este momento todos los cómputos numéricos, una vez procesados por T OY y
reducidos a restricciones inteligibles para el resolutor, serán enviados a éste.
/nocflpr descarga el resolutor y restaura las funciones primitivas originales.
Una vez que se han activado las restricciones sobre reales con el comando ‘/cflpr’, el
resolutor permanece en memoria durante toda la sesión aunque se desactiven las restricciones. El comando ‘/nocflpr’ le indica al sistema que no debe hacer uso de las restricciones,
pero no puede restaurar totalmente el estado anterior del sistema, no puede descargar el resolutor de memoria5 . En consecuencia, los programas que no hagan uso de las restricciones
sobre reales ofrecerán un mejor rendimiento si éstas no se activan durante la sesión, ya que
no se forzarán determinadas operaciones (relativamente costosas) que no son necesarias.
Por todo lo expuesto, el procedimiento de carga descrito en 2.2.1, por defecto carga el
sistema sin activar las restricciones sobre los reales y es el usuario quien debe activarlas
expresamente. Sobre la forma de uso de las restricciones trataremos en 2.3.9.
2.2.7.
Definición de nuevos comandos para T OY
En los apartados anteriores hemos hecho un recorrido por todos los comandos que
proporciona el sistema. El repertorio es reducido, sencillo y suficiente para acceder a todas
las prestaciones de T OY.
Sin embargo, hay otros comandos que, sin ser imprescindibles, son de uso frecuente
y permiten una utilización más amigable del sistema. En T OY, una vez fijado un repertorio mı́nimo hemos optado por facilitar al usuario un mecanismo sencillo y versátil
para definir nuevos comandos personalizados de comunicación con el sistema operativo. El archivo ‘toy.ini’ que se incluye en la distribución admite hechos Prolog que
T OY interpretará como comandos. Por ejemplo, para definir un nuevo comando /dir
que muestre todos los archivos del directorio actual (en sistemas Unix), podemos introducir la siguiente lı́nea en el archivo toy.ini:
command(dir,0,[],["ls -l"]).
El primer argumento es el nombre del comando (dir), el segundo el número de argumentos (0, en este caso), el tercero es la lista de argumentos que se representarán como
variables Prolog (deben comenzar por mayúscula) que van usarse en la construcción del
comando; y el último es la lista de cadenas que han de concatenarse para construir el
comando (una sola cadena en este caso). Con esta definición el comando /dir ya forma
parte del repertorio del sistema.
5
verb+/nocflpr+ no es capaz de descargar el resolutor de memoria porque Sicstus no proporciona un
predicado para descargar librerı́as.
2.3. EL LENGUAJE T OY
19
Otra utilidad común es el acceso a un editor de texto desde la lı́nea de comandos. Si
queremos utilizar el editor emacs podemos incluir la lı́nea:
command(edit,1,[File],["emacs ",File,".toy &"]).
De esta forma, el comando /edit(< f ile >) arrancará el editor emacs con el archivo
< f ile > .toy y devolverá el control a T OY. Nótese el uso de la variable File en la
construcción del comando en el cuarto argumento. El sistema interpretará el contenido de
las variables lógicas como cadenas.
2.3.
El lenguaje T OY
La sintaxis de T OY es muy similar a la de algunos lenguajes funcionales, como Haskell
([PH97]) o Gofer ([Jon]), aunque utiliza algunas construcciones de Prolog y otras propias.
La diferencia más notable de la sintaxis de T OY con respecto a la de Haskell es
que las variables comienzan con una letra mayúscula, excepto las variables anónimas que
siempre comienzan con ‘ ’. Los identificadores para tipos de datos, constructoras, funciones
y predicados comienzan con una letra minúscula.
Los comentarios se escriben como en Sicstus:
comenzando con ‘ % ’ y terminando con la lı́nea,
encerrados entre ‘/*’ y ‘*/’ (se admiten comentarios anidados)
En general un programa en T OY está formado por definiciones de tipos de datos, alias
de tipo, funciones, predicados, operadores infijos y sentencias de inclusión de arhivos. A
continuación hacemos una descripción de cada una de estas construcciones. El apéndice A
contiene la gramática completa del lenguaje.
2.3.1.
Definición de tipos de datos
Las definiciones de tipos de datos pueden aparecer en cualquier parte del programa
y tienen la misma forma que en Haskell; de hecho, T OY utiliza el sistema de tipos de
Hindley-Milner. Por ejemplo, el tipo de los pares puede definirse ası́:
data pair A = p A A
Esta declaración introduce el tipo de las parejas de cualquier tipo (pero el mismo para
las dos componentes). El parámetro o variable de tipo A que aparece en la definición hace
que sea un tipo un tipo polimórfico: representa tanto las parejas de enteros, como las
de caracteres o las de listas. Además, introduce el nuevo sı́mbolo de constructora p, con
el que puede representarse, por ejemplo, el par de enteros p 4 5, o el par de caracteres
p ’a’ ’b’, con tipos pair int y pair char respectivamente (T OY tiene predefinido el tipo
de las tuplas con el que pueden representarse los pares de una forma más sencilla, como
se verá más adelante).
La sintaxis general de una declaración de tipo es:
data T X1 ...Xn = c1 T S11 . . . T S1n1
| ...
| ck T Sk1 . . . T Sknk
(i)
CAPÍTULO 2. EL SISTEMA T OY
20
donde T es el identificador de tipo, las Xi son variables de tipo que parametrizan el tipo.
El sı́mbolo ‘|’ se utiliza para separar las distintas construcciones de las que se compone el
tipo. Cada ci es una constructora de datos de aridad ni y tipo:
T Si1 → . . . → T Sini → T X1 . . . Xn
siendo cada T Sij un tipo construido con la siguiente sintaxis:
TS = X
| tc
| (tc T S1 . . . T Sm )
% variable de tipo
% tipo constante
% tipo construido
Las declaraciones de tipos de datos en un programa T OY deben satisfacer además las
siguientes restricciones:
no puede haber dos declaraciones de tipo con el mismo nombre,
el lado izquierdo de la declaración debe ser lineal, es decir, las variables X1 , ..., Xn
que aparecen en lado izquierdo de la declaración deben ser distintas,
las variables del lado derecho de la declaración deben ser del conjunto {X1 , ..., Xn },
El caso n = 0 en la expresión i corresponde a la definición de tipos constantes o no
polimórficos como, por ejemplo, el tipo de los pares de enteros que puede declararse ası́:
data pairInt = p int int
y el caso ni = 0 para todo i = 1..k corresponde al caso de constructoras constantes, como
el tipo enumerado de los colores:
data colour = red | green | blue
que introduce el nombre colour como un nuevo tipo de datos y los nombres red, green
y blue como (únicas) constructoras o valores del tipo.
T OY admite tipos recursivos como la siguiente definición para los números naturales:
data nat = zero | suc nat
ası́ como la construcción de nuevos tipos a partir de tipos ya definidos (o predefinidos en
el sistema) como los árboles binarios con números naturales en las hojas:
data treenat = leaf nat | branch treenat treenat
y tipos mutuamente recursivos como (pares e impares):
data even = evenzero | evensuc odd
data odd = oddsuc even
Otro ejemplo de tipo polimórfico y recursivo es el de los árboles binarios con hojas de
tipo polimórfico, que puede definirse como:
data tree A = leaf A | branch (tree A) (tree A)
2.3. EL LENGUAJE T OY
2.3.2.
21
Tipos predefinidos del sistema
Los siguientes tipos están predefinidos en T OY:
bool, definido como data bool = true | false (esta declaración se encuentra en
basic.toy),
int, real, que representan los tipos especiales de los números enteros y reales respectivamente. Cada número (entero o real) es una constructora de aridad 0 y hay por
tanto un número infinito de ellas. La definición de los enteros serı́a de la forma:
data int = ... -2 | -1 | 0 | 1 | 2 ...
que obviamente no es una definición válida para el sistema. Tanto los enteros como
los reales están definidos a bajo nivel, es decir, su declaración no es visible al usuario
(no tienen declaración en basic.toy).
char es el tipo predefinido para caracteres individuales. Los caracteres deben ir entre
comillas simples como ’a’, ’@’, ’9’ o ’-’. Puesto que hay un número finito de ellos
(códigos ascii) son definibles en el sistema, sin embargo, también se implementan a
bajo nivel como los números. La siguiente tabla muestra el conjunto de caracteres
especiales (no-imprimibles) que necesitan secuencias de escape (barra invertida ‘\’):
Caracter T OY
’\\’
’\’ ’
’\”’
’\n’
’\r’
’\t’
’\v’
’\f’
’\a’
’\b’
’\d’
’\e’
significado
barra invertida
comilla
comilla doble
salto de lı́nea
retorno de carro
tabulador horizontal
tabulador vertical
salto de pagina
señal acústica
espacio
borrado
escape
El tipo de las cadenas no es predefinido, pero puede definirse fácilmente como lista
de caracteres con la declaración (alias de tipo, tratados en el siguiente apartado):
type string = [char]
como puede verse en el archivo de utilidades misc.toy que acompaña a la distribución.
Para facilitar el uso de cadenas T OY admite la representación de constantes de tipo
cadena entre comillas dobles. Por ejemplo, la cadena
[’t’,’h’,’i’,’s’,’ ’,’i’,’s’,’ ’,’a’,’ ’,’s’,’t’,’r’,’i’,’n’,’g’]
puede representarse como
CAPÍTULO 2. EL SISTEMA T OY
22
"this is a string"
El tipo de las listas polimórficas tiene una sintaxis especial, similar a la de Haskell.
Por ejemplo, [int] representa el tipo de las listas de números enteros. Igual que
en Haskell la constructora ‘[]’ denota la lista vacı́a y ‘:’ es el operador infijo de
construcción de listas. T OY no contiene una definición explı́cita de las listas, pero
informalmente puede asumirse la siguiente declaración:
data [A] = [] | A:[A]
(Una lista de tipo A es, o bien la lista vacı́a, o bien una lista formada por un elemento
de tipo A seguida de una lista de tipo A).
El sistema admite además la representación de listas en notación Prolog, es decir, la
expresión [X | Xs] representa la lista (X:Xs).
Las tuplas son también un tipo predefinido especial, además de por la notación
que utilizan, porque representan un tipo de datos con infinitas constructoras. Por
ejemplo, (int,int) representa el tipo de los pares de enteros y (int,int,int)
representa el tipo de las ternas de enteros. Es decir, se utiliza una construcción
similar para representar tuplas de cualquier aridad: (T1 , ..., Tn ) representa el tipo de
las n-tuplas con componentes de tipos T1 , ..., Tn . En el lenguaje serı́a definible el tipo
de las tuplas de aridad determinada; por ejemplo para las tuplas de aridad 3 (ternas)
se podrı́a hacer la declaración:
data tup3 A B C= t A B C
que introduce la constructora t (las ternas de enteros se pueden definir instanciando
este tipo: tup3 int int int). Pero no se puede definir el tipo de las tuplas de cualquier
aridad y por eso está predefinido a bajo nivel (no visible al usuario).
2.3.3.
Alias o sinónimos de tipo
Los alias de tipo son construcciones muy sencillas que pueden aparecer en cualquier
parte del programa. Son innecesarios en realidad, pero permiten escribir programas más
breves y legibles. Pueden entenderse como macros paramétricas definidas por el usuario
que sirven para dar nombre a tipos ya existentes. El tipo de las cadenas o tipo string ya
se definió en el apartado anterior como lista de caracteres mediante el alias:
type string = [char]
La forma general de un alias de tipo es:
type AliasName X1 ...Xn = typeExpression
donde AliasName es el nuevo nombre de alias de tipo, X1 , ..., Xn son n variables y typeExpression es un tipo válido que puede hacer uso de las variables X1 , ..., Xn . Deben satisfacer
además las siguientes restricciones:
el lado izquierdo de la declaración debe ser lineal, es decir, las variables X1 , ..., Xn
deben ser distintas,
2.3. EL LENGUAJE T OY
23
las variables del lado derecho typeExpression deben ser del conjunto {X1 , ..., Xn },
la definición de un alias puede depender de otros alias o tipo de datos, siempre que
estos hayan sido previamente definidos (o estén predefinidos en el sistema),
no se admite recursión, ni recursión mutua en las definiciones de alias de tipo.
Supóngase, por ejemplo, un programa que utiliza números complejos representados
como pares de reales. Estos pares no son una construcción nueva y, en consecuencia, el
tipo de los complejos no es un tipo nuevo en realidad, ni introduce sı́mbolos de constructora nuevos. Es decir, no tiene sentido hacer una declaración data complex = .... Sin
embargo, será de utilidad poder hacer referencia al tipo de los complejos en las declaraciones de funciones que operan sobre ellos. Esto puede conseguirse con la declaración del
alias:
type complex = (real,real)
También podrı́a definirse el tipo (más general) de los pares y declarar los complejos
como una instancia:
type pair A B = (A,B)
type complex = pair real real
2.3.4.
Definición de funciones
Las funciones en T OY se definen mediante reglas de reescritura condicionales representadas como ecuaciones condicionales. Por ejemplo, la función append que toma dos
listas y devuelve la lista resultante de concatenarlas se puede definir como:
append []
Ys = Ys
append [X|Xs] Ys = [X|Zs] <== append Xs Ys == Zs
Esta es la notación currificada tı́pica (salvo por la restricción <== append Xs Ys == Zs)
de algunos lenguajes funcionales como Haskell, y como es habitual en estos lenguajes, la
aplicación funcional asocia a la izquierda. Una aplicación de append es, por ejemplo,
append [1,2] [3,4], que es equivalente a (append [1,2]) [3,4]. En otras palabras,
la aplicación de append al argumento [1,2] devuelve como resultado la nueva función
(append [1,2]) que se aplica a su vez a [3,4].
La sintaxis general de una regla es:
e <== C1 , ..., Cm
f t1 ...tn = |{z}
| {z }
| {z }
cabeza
cuerpo
(ii)
restricciones
donde f es el nombre de la función, los ti son patrones (ver abajo), e es una expresión (ver
abajo) y los Ci son restricciones de la forma e1 3e2 separadas por ‘,’ y donde 3 ∈ {==, /=}.
Una regla de esta forma tiene una lectura condicional bastante intuitiva: la expresión
f t1 ...tn se puede reducir a la expresión e si se satisfacen las restricciones C1 , ..., Cm . En
las restricciones el sı́mbolo == se utiliza para la igualdad estricta y /= para desigualdades.
En 2.3.8 estudiaremos con mayor detalle las restricciones de igualdad y desigualdad (sobre
las aritméticas trataremos en 2.3.9).
En el caso de que la regla no tenga restricciones (m = 0) se omitirá el sı́mbolo <==
y una restricción de la forma e == true puede abreviarse a e (o dicho de otro modo, una
CAPÍTULO 2. EL SISTEMA T OY
24
expresión sin uno de los sı́mbolos {==, /=} en la raı́z es interpretada automáticamente
por el sistema como la restricción e == true).
La sintaxis general para las expresiones es:
E =X
| num
| (E1 , ..., En )
|c
|f
| (E1 E2 )
%
%
%
%
%
%
variable
número (entero o real)
tupla
constructora
función
aplicación
(iii)
siendo E1 , E2 expresiones. Dado que la aplicación funcional asocia por la izquierda, pueden
omitirse los paréntesis de acuerdo con ello. Ası́, por ejemplo, ((c X) Y ) es lo mismo que
c X Y . En general, E1 E2 E3 ...En es lo mismo que (...((E1 E2 )E3 )...En ). Nótese que tanto
los números como las tuplas aparecen explı́citamente en la regla de formación a pesar
de ser constructoras. El motivo es que representan infinitas constructoras (véase 2.3.2) y
necesitan un tratamiento especial (no quedan capturadas por las otras alternativas de la
regla).
Otro hecho importante es que la regla de formación (iii) admite aplicaciones parciales
tanto de funciones como de constructoras; por ejemplo, append [1] es una aplicación
parcial de append; (1:) es una aplicación parcial de la constructora de listas ‘:’. Ambas
son expresiones funcionales y tienen el mismo efecto al aplicarlas sobre una lista: colocan
1 como cabeza y la lista argumento como cola.
Los patrones son un caso particular de expresiones que no contienen llamadas a función
(aplicaciones totales de funciones). Esto quiere decir que son expresiones irreducibles o
formas normales. La sintaxis general de los patrones es:
T
=X
| num
| (T1 , ..., Tn )
| (c T1 ...Tm )
| (f T1 ...Tm )
%
%
%
%
%
variable
numeros
tuplas
c constructora de aridad n, 0 ≤ m < n
f función de aridad n, 0 ≤ m < n
En particular, T OY admite patrones de primer orden que corresponden a los patrones
de Haskell y son de la forma:
T
=X
| num
| (T1 , ..., Tn )
| (c T1 ...Tn )
%
%
%
%
variable
número (entero o real)
tuplas
c constructora de aridad n
siendo T1 , ..., Tn patrones de primer orden. Pero además, T OY también admite patrones
de orden superior, es decir, admite como patrones expresiones en las que pueden aparecer
constructoras y funciones aplicadas parcialmente. La sintaxis de estos patrones es:
T
=X
| (c T1 ...Tm )
| (f T1 ...Tm )
% variable
% c constructora de aridad n, 0 ≤ m < n
% f función de aridad n, 0 ≤ m < n
siendo T1 , ..., Tn patrones cualesquiera (de orden superior en el caso de las tuplas). Por
ejemplo, el siguiente programa es correcto en T OY:
2.3. EL LENGUAJE T OY
25
% tipo de los naturales
data nat = zero | suc nat
% suma de naturales
plus zero
Y = Y
plus (suc X) Y = suc (plus X Y)
f suc
= true
f (plus X) = true
% suc es un patrón de orden superior
% (plus X) es un patrón de orden superior
Por lo que acabamos de ver, en T OY los conceptos de forma normal y patrón son
sinónimos y pueden definirse como expresiones irreducibles. En programación funcional
(en concreto en Haskell) un patrón no es cualquier expresión irreducible: no se admiten
aplicaciones parciales. Por lo tanto, esto una caracterı́stica destacada de T OY que ofrece
posibilidades muy interesantes ([CR98, GHR97]).
Desde el punto de vista sintáctico, T OY es muy poco restrictivo en cuanto a la forma
de las reglas que definen una función f : basta con que dichas reglas sean de la forma
(ii) y que todas tengan el mismo número n de argumentos, que es la aridad de programa
(sobre los tipos hay algunas restricciones más que veremos después). Hay una condición
más que no se refleja en la sintaxis y que el usuario debe conocer: por razones semánticas,
la cabeza f t1 ...tn debe ser lineal, lo que significa que las variables no pueden tener más
de una aparición. T OY admite repeticiones de variables en las cabezas, pero en este
caso hace automáticamente una transformación sintáctica que elimina dichas repeticiones
introduciendo nuevas variables y restricciones de igualdad estricta en la regla. Por ejemplo,
una regla como:
f X X = 0
será traducida a:
f X Y = 0 <== X==Y
El hecho de que T OY admita cabezas no lineales debe entenderse como un azúcar sintáctico que permite abreviar la escritura de las reglas. En realidad, en ejecución el sistema
siempre utiliza reglas con cabezas lineales6 .
2.3.5.
Tipos de las funciones
Toda función f definida en un programa T OY (mediante reglas de la forma f t1 ...tn =
e <== e1 3e01 , ..., em 3e0m , debe tener asociado un tipo principal τ1 → ... → τk → τ , donde
τ no es de la forma ‘ → ’ (no es un tipo funcional). Diremos que k es la aridad del tipo
de f (en contraste con la aridad de programa que es el número de argumentos que tienen
las reglas de f ) y deben cumplirse las condiciones:
n ≤ k (la aridad de programa es menor o igual que la aridad del tipo de la función),
para toda regla de f :
6
En distribuciones anteriores, el sistema producı́a un WARNING cuando precisaba hacer esta transformación. Sin embargo, en la práctica esta información no era especialmente relevante para el usuario y en
la distribución actual el mensaje está deshabilitado.
CAPÍTULO 2. EL SISTEMA T OY
26
• el tipo de cada patrón ti es τi ,
• el tipo de la expresión e debe ser τn+1 → ... → τk → τ ,
• para toda restricción ei 3e0i , las expresiones ei y e0i deben tener el mismo tipo.
Los tipos de las funciones son inferidos por T OY y, opcionalmente, pueden ser declarados en el programa de la misma forma que en Haskell:
f :: T1 → ... → Tk → T
Para la función append definida anteriormente se podrı́a declarar el tipo:
append :: [int] -> [int] -> [int]
El operador -> asocia por la derecha (al revés que la aplicación de funciones), con lo que
este tipo es equivalente a [int]->([int]->[int]) que es consistente con la forma de
aplicación de funciones: al aplicar append sobre un argumento de tipo [int] se obtiene
una nueva función con tipo [int]->[int].
Con las dos reglas anteriores para append el inferidor de tipos de T OY determina
el tipo más general de la función. De estas reglas se deduce que append toma dos listas
y devuelve otra. Las tres listas deben ser del mismo tipo, pero éste no está concretado,
puede ser cualquiera. Es decir, se infiere el tipo polimórfico [A]->[A]->[A], donde A es
una variable de tipo. El sistema entonces hace un contraste o comprobación de tipos entre
el tipo inferido y el tipo declarado y detecta que el tipo declarado es un caso particular
del inferido. Esto está permitido y puede entenderse como una restricción impuesta por el
usuario sobre el modo de uso de la función: el usuario proporciona una definición general
para una función pero restringe el modo de uso a un caso más particular (en este caso
la definición sirve para listas cualesquiera y la declaración de tipo restringe el modo de
uso a listas de enteros). Cuando sucede esto, T OY produce un mensaje de advertencia o
warning en tiempo de compilación, pero respeta la declaración del usuario:
TYPE WARNING: Inferred type is more general than declared one in
function append
Inferred type: [ _A ] -> [ _A ] -> [ _A ]
Declared type: [ int ] -> [ int ] -> [ int ]
Declared one remains.
También puede declararse append como una función polimórfica que podrá utilizarse
para concatenar dos listas del cualquier tipo y producir otra lista, siendo las tres listas del
mismo tipo:
append :: [A] -> [A] -> [A]
Naturalmente, con esta definición queda capturada la anterior cuyo uso era más restringido. Ahora el tipo declarado y el inferido son el mismo y el sistema no produce ningún
mensaje.
La situación que falta por explorar es cuando el tipo declarado es más general que
el inferido, pero tal situación no es admisible. Intuitivamente, el usuario pretende definir
una función más general que la que realmente está definiendo. Por ejemplo, si declara el
tipo A->B->C para append definida por las dos reglas anteriores, el usuario “define una
función que opera sobre listas, pero pretende que opere sobre cualquier tipo de datos
2.3. EL LENGUAJE T OY
27
y no necesariamente del mismo”. Sin embargo, no se pueden evaluar expresiones como
append 1 2 o append [1,2] [’a’]7 . En este caso se produce un mensaje de error:
TYPE ERROR: Contradictories types for function append
Declared type: _A -> _B -> _C
Inferred type: [ _A ] -> [ _A ] -> [ _A ]
with no possible conversion.
Variable type is assumed to go on with the inference.
Cuando el sistema detecta un error de tipos, asocia un tipo variable a la expresión que lo
provoca y continúa la inferencia. Esto puede entenderse como la polı́tica de recuperación
de errores del inferidor, con la que se pretende evitar la propagación del error a otras
expresiones que también producirı́an error de tipo. Si se propagase el error, el resultado
podrı́a ser una larga secuencia de errores de tipo provocados por una sola expresión, que en
vez de ayudar al usuario a localizar su error servirı́a para lo contrario. Al asociar un tipo
variable a la expresión problemática (más interna) queda garantizado que esa expresión
no provocará más errores de tipo.
2.3.6.
Funciones de orden superior y estructuras infinitas
Al igual que en Haskell, T OY admite funciones de orden superior, es decir, funciones
que toman funciones como argumentos, o funciones cuyo resultado es de tipo funcional.
Un ejemplo de orden superior es la función map, que puede definirse como:
map F []
= []
map F [X|Xs] = [F X|map F Xs]
Esta función recibe un parámetro F de tipo funcional como primer argumento y una
lista como segundo, y produce la lista resultante de aplicar F a cada elemento de la lista
argumento. La expresión F X en el cuerpo de la segunda regla es la aplicación de la expresión funcional F al elemento X, donde lo llamativo es que F viene dada como argumento, es
decir, la función F concreta es desconocida en la definición de map. Por ejemplo, si tenemos
el tipo de los naturales:
data nat = zero | suc nat
se puede evaluar la expresión map suc [zero, suc zero, suc (suc zero)]. El resultado se consigue aplicando suc a cada uno de los elementos de la lista argumento y
es [(suc zero), (suc (suc zero)), (suc (suc (suc zero)))]. La potencia expresiva de map reside en el hecho de que permite aplicar cualquier función a cualquier lista de
argumentos, siempre que los tipos sean consistentes. En general, las funciones de orden
superior son útiles para definir nuevas funciones de una forma sencilla y elegante. En el
archivo de utilidades misc.toy pueden encontrarse diversos ejemplos que utilizan map y
otras funciones de orden superior.
El orden superior en T OY no termina aquı́. A diferencia de los lenguajes funcionales
puros, en T OY es posible hacer cómputos que impliquen variables lógicas de orden superior. Por ejemplo, dado un programa que contenga una definición para append como la
que vimos en 2.3.4, es posible resolver el objetivo F [1,2] [3] == [1,2,3]. Obsérvese
7
En Prolog, que no posee sistema de tipos, sı́ es posible concatenar listas de distintos tipos. Sin embargo,
T OY es un lenguaje fuertemente tipado como Haskell, en el que esto no es posible.
CAPÍTULO 2. EL SISTEMA T OY
28
que aquı́ la varible lógica F representa un valor funcional, es decir, es una variable de
orden superior. El sistema es capaz de resolver esta restricción, encontrando para F el
valor append. Este tipo de variables suponen un recurso expresivo importante sobre el que
volveremos en 2.4.
Las posibilidad de manejar estructuras inifinitas es también habitual en los lenguajes
funcionales actuales. Un ejemplo clásico es la función from que ya se comentó en 1.2, y
que definı́amos como:
from N = [N|from (N+1)]
Esta función genera (potencialmente) una secuencia de enteros infinita. Por ejemplo, la
expresión from 1 se evaluarı́a a la lista [1,2,3,4,5,6,...]. En realidad este cómputo no
terminarı́a y este tipo de funciones no tienen mucha utilidad por sı́ mismas. Sin embargo,
combinadas con otras funciones ofrecen posibilidades interesantes. Por ejemplo, la función
nth definida como
nth N [X|R] = if (N==1) then X else nth (N-1) R
calcula el n-ésismo elemento de una lista dada. Ahora es posible reducir la expresión
nth 5 (from 100) que devolverá el valor 105.
Esta última reducción es posible debido a la pereza del lenguaje: para calcular el
quinto elemento de la lista generada por from 100, no es necesario evaluar completamente
esta lista (el cómputo no termina). En realidad sólo es necesario calcular los primeros 5
elementos, con los que la función nth ya puede calcular el quinto, que es 105.
2.3.7.
Funciones indeterministas
La libertad sintáctica que ofrece T OY en la definición de funciones tiene otras consecuencias especialmente interesantes desde el punto de vista semántico, que requieren
mención especial. No se han impuesto condiciones que impidan que el cuerpo de las reglas contenga variables que no aparecen en la cabeza, ni tampoco se exigen condiciones
de confluencia8 . La ausencia de estas condiciones esta relacionada con el hecho de que
T OY admita funciones indeterministas. Por ejemplo
% elección indeterminista entre dos valores
choice X Y = X
choice X Y = Y
es una función que toma dos valores y devuelve uno de ellos de forma indeterminista. Una
función indeterminista puede entenderse como una función multivaluada, es decir, una
función que, para unos mismos argumentos produce varios valores distintos.
La evaluación de choice 0 1 producirı́a 0 como primer valor, pero por backtracking se
obtendrı́a 1 como segunda posibilidad. En general, las funciones indeterministas pueden
reemplazar a predicados en muchas ocasiones con algunas ventajas, como veremos en
algunos ejemplos en al final del capı́tulo.
La introducción de este tipo de funciones plantea algunas cuestiones en cuanto a la
forma de evaluación que debe implementar el sistema. Para ilustrarlo, consideremos el
siguiente ejemplo inspirado en [Hus92]:
8
En otros sistemas como BABLOG estas condiciones se conocı́an como inexistencia de variables extra
(o determinismo local) y no ambigüedad respectivamente (en [AG94] y [LLR93] pueden encontrarse las
definiciones formales)
2.3. EL LENGUAJE T OY
29
coin = 0
coin = 1
double X = X+X
Aquı́, coin es una función indeterminista que puede reducirse tanto a 0 como a 1 (por
backtraking el sistema llevará a cabo ambas reducciones). La función double duplica el
valor del argumento (numérico) que recibe y no plantea problemas por sı́ misma. Pero,
¿que sucede al reducir la expresión double coin?. Si se reduce (utilizando la regla de double)
a la expresión coin + coin, es posible que más tarde la primera expresión coin se reduzca
a 0 y la segunda a 1 (o viceversa), con lo que la expresión double coin se habrı́a reducido
a 1. Este no es un resultado deseado de acuerdo con la semántica por call-time choice que
adoptamos para nuestro lenguaje, siguiendo el enfoque de [Hus93].
Intuitivamente, call-time choice significa lo siguiente: dada una llamada f e1 ..., en , se
elige un valor fijo para cada uno de los argumentos e1 , ..., en antes de aplicar las reglas
para f . En nuestro ejemplo double coin, esto se puede conseguir evaluando coin antes de
aplicar la regla de double, pero entonces la evaluación no serı́a perezosa. Para preservar
la pereza del lenguaje y capturar la semántica por call-time choice se utiliza compartición
o sharing: double recibe el argumento coin sin evaluar, pero de modo que cuando en la
expresión coin + coin una de las expresiones coin se reduzca, la otra tome el mismo valor
automáticamente. De este modo, las soluciones obtenidas para la expresión inicial son 0 y
2 como cabrı́a esperar.
En (3.6) volveremos sobre el sharing y explicaremos en detalle cómo se implementa en
T OY.
2.3.8.
Restricciones de igualdad y desigualdad
En la sección 2.3.4 veı́amos la forma sintáctica de las restricciones de igualdad y desigualdad pero no abordamos en profundidad el significado que tienen. En programación
funcional existen igualdades (==) y desigualdades ( /=). Sin embargo, el significado es
notablemente diferente al que tienen en T OY. Por ejemplo, en Haskell una igualdad entre
dos expresiones es cierta si ambas se reducen a formas normales iguales. Ası́, 3 + 4 == 7 se
reduce a true mientras que 3 + 4 == 5 produce f alse. Pero una restricción que contenga
variables como x == 4, no puede evaluarse.
En Prolog tanto la igualdad (==) como la desigualdad (\ ==) entre términos se tratan
desde el punto de vista sintáctico. Por ejemplo, 3 + 4 == 7 produce un fallo y 3 + 4\ == 7
tiene éxito (3 + 4 y 7 no son el mismo término); X == X también produce éxito, pero
X == Y falla. No obstante, en Prolog también existe la unificación: X = Y tiene éxito,
ligando X con Y . Pero no existen funciones, es decir, no hay reducción o evaluación como
en funcional. Por ejemplo, X = 3 + 7 tiene éxito unificando X con el término 3 + 7 (no se
evalúa la suma).
En T OY se habla de restricciones de igualdad y restricciones desigualdad (estrictas9 )
porque efectivamente ambas se comportan como restricciones. Una igualdad entre dos
expresiones es cierta si ambas expresiones pueden reducirse a una forma normal común.
Pero el concepto de reducción en programación lógico funcional es más amplio que en
funcional puro, ya que, por un lado son reducibles expresiones con variables, y por otro, la
9
El calificativo estricta hace referencia a una cuestión puramente semántica y quiere decir que si alguno
de sus argumentos es indefinido (⊥) el resultado es infefinido.
30
CAPÍTULO 2. EL SISTEMA T OY
reducción es, en general, un cómputo indeterminista (una misma expresión puede reducirse
a más de una forma normal). De hecho, la reducción en el contexto lógico funcional se
suele llamar narrowing (estrechamiento), porque no es simplemente reescritura (también
hay unificación).
Por ejemplo, en T OY una igualdad como 3 + 4 == 7 tiene éxito (se evalúa la suma
como en funcional). También 3 + 4 == X tiene éxito y además unifica la variable X
con el valor 7. Es decir, la igualdad es cierta siempre que X sea 7, por lo que el sistema
producirá un éxito y en la respuesta incluirá la restricción X == 7. Las restricciones
de igualdad en una respuesta pueden entenderse como restricciones o como sustituciones
(ligaduras). En 2.3.14 veremos la forma y la interpretación que tienen las respuestas en el
sistema.
Una desigualdad estricta entre dos expresiones es cierta en T OY si ambas expresiones
pueden reducirse a expresiones que continen una constructora distinta en la misma posición
(conflicto de constructoras). Por ejemplo, [2, 3 + 4] /= [2, 5] tiene éxito porque el primer
argumento (el lado izquierdo de la desigualdad) puede reducirse a la expresión [2, 7] y el
segundo elemento de las listas (que ocupa la misma posición en ambas expresiones) [2, 7]
y [2, 5] es diferente (7 en un caso y 5 en el otro). Esta desigualdad en funcional también
se reducirı́a a true; pero en T OY, como restricciones que son, las desigualdades también
pueden contener variables. Por ejemplo, [X, 3 + 4] /= [2, 7] produce un éxito restringido a
que X sea distinto de 2 (la restricción X /= 2 forma parte de la respuesta).
Las desigualdades resultarán especialmente útiles no sólo en los programas, sino también en las respuestas que calcula el sistema. Consideremos las funciones member, que
comprueba si un elemento pertenece a una lista dada, y size que calcula la longitud de
una lista:
member X [] = false
member X [Y|Ys] = if X==Y then true else member X Ys
size [] = 0
size [X|Ys] = if member X Ys then size Ys else (size Ys)+1
(La función if then else es una primitiva del sistema, 2.3.15).
La restricción size [X,Y] == N es cierta bajo las condiciones X == Y, N == 1,
pero también es cierta siendo N == 2 y X e Y distintos entre sı́. La condición de que
X e Y sean distintos es fácilmente expresable con una desigualdad: X /= Y . Pero sin
desigualdades darı́a lugar a una familia infinita de soluciones (todos los posibles pares de
enteros distintos).
Otro ejemplo, puede ser la restricción size [X] /= X. En este caso la desigualdad
X /= 1 es una respuesta elegante que captura todas las posibles soluciones a la restricción
(de otro modo habrı́a también infinitas soluciones).
Ya hemos comentado que una misma expresión puede tener más de una reducción
posible. Este indeterminismo permite obtener distintas respuestas a una misma restricción.
Consideremos la función append definida en 2.3.4 y la restricción append X [2] == Y.
Haciendo reducción (estrechamiento) con la primera regla de append, la igualdad se hace
cierta con las restricciones X == [ ], Y == [2]. Aplicando la segunda regla (y después
la primera), también se satisface la igualdad con las restricciones X == [A], Y == [A, 2]
(independientemente del valor que pueda tomar la variable A). También es cierta con las
restricciones X == [A, B], Y == [A, B, 2]. Esta restricción tiene infinitas respuestas que
el sistema irá calculando por backtraking.
2.3. EL LENGUAJE T OY
31
Pero el indeterminismo del sistema no acaba aquı́. El hecho de admitir funciones indeterministas en nuestro lenguaje tiene algunas consecuencias sobre las igualdades y desigualdades que merecen algún comentario. Una misma igualdad puede producir un éxito
y un fallo. Por ejemplo, consideremos la función coin definida en el apartado 2.3.7: la
igualdad coin==0 tiene éxito reduciendo coin a 0, pero produce fallo si coin se reduce a
1.
Llevando lo anterior al extremo, en T OY una igualdad y una desigualdad entre dos
mismas expresiones pueden ser satisfactibles simultáneamente. Las restricciones coin == 0,
coin /= 0 tienen éxito y esto es coherente con nuestra semántica de la igualdad y la desigualdad: puesto que coin es una función indeterminista, la primera expresión coin puede
reducirse a 0 y la segunda a 1, con lo que se satisfacen ambas restricciones. En las restricciones X==coin, X ==0, X /= 0 es distinto, puesto que ahora coin se reduce una sola
vez y el resultado se asocia a la variable X. Por lo tanto, X puede tomar los valores 0
ó 1, pero en ambos casos no se pueden hacer ciertas simultáneamente las restricciones
X==0, X /= 0.
2.3.9.
Restricciones sobre los números reales
En 2.3.4 vimos que el sistema admite restricciones de igualdad y desigualdad, dejando excluidas las ariméticas deliberadamente. Realmente T OY no maneja restricciones de
reales directamente, sino funciones aritméticas. Decı́amos que una expresión e en las restricciones sin uno de los sı́mbolos {==, /=} en la raı́z es interpretada automáticamente
como la restricción e == true. Ası́ pues, una expresión como 3 + X <= 5 se interpreta
como (3 + X <= 5) == true, que es una igualdad que involucra a la función ’<=’ (menor
o igual).
No obstante, el usuario dispone de toda la potencia de este tipo de restricciones
([JM94, FHK+ 93, Coh90, Gro97]) y, en general, la conversión anterior pasará inadvertida.
El sistema arranca inicialmente sin activar las restricciones (sin el resolutor en memoria)
por motivos de eficiciencia según se explicó en 2.2.6. Puede activarlas con el comando
/clpr.
Para reales se tienen las restricciones correspondientes a los operadores aritméticos
habituales, {<, >, <=, >=}. Pero además se tienen restricciones de igualdad y desigualdad,
que utilizan los mismos sı́mbolos de la igualdad y desigualdad estrictas, == y /=. Sin
embargo, no tienen tienen el mismo comportamiento (las de reales deben ser tratadas por
el resolutor). El sistema se encargará de distinguir en cada caso la clase de restricción que
se le presenta y la forma de procesarla, liberando al usuario de la incomodidad de utilizar
distintos sı́mbolos. Todas las funciones aritméticas son primitivas del sistema (2.3.15) cuya
declaración se encuentra en el archivo de la distribución basic.toy (apéndice B) y son ellas
las que se encargan de producir las verdaderas restricciones y enviarlas al resolutor de
Sicstus de manera apropiada.
No todas las restricciones numéricas necesitan el resolutor. Por ejemplo, una restricción
como 4 + 5 < 10 puede resolverse sin él. Sin embargo, 4 + X < 10 debe utilizar el resolutor
para encontrar la respuesta X < 6. En general, aquellas en las que no aparecen variables
no necesitan utilizar el resolutor.
El usuario es responsable de determinar si su programa utiliza o no restricciones sobre
los reales. Sin embargo, si se intenta resolver un objetivo que utiliza restricciones sin activarlarlas previamente, T OY detectará la anomalı́a en tiempo de ejecución y producirá un
mensaje de error sugiriendo la activación:
CAPÍTULO 2. EL SISTEMA T OY
32
RUNTIME ERROR: Variables are not allowed in arithmetical operations.
(/cflpr. should be active to do this)
El resolutor de Sicstus es un resolutor de restricciones lineales, lo que significa
que admite todo tipo de restricciones, pero sólo resuelve las lineales. Las no lineales se
suspenden (previa normalización) a la espera de que se conviertan en lineales. Por ejemplo,
un sistema de ecuaciones lineales como X + 2 ∗ Y == 8, 3 ∗ X − Y == 3 puede resolverse
obteniendo las sustituciones (en forma de restricciones) X == 2, Y == 3. Las no lineales
como X 2 == 4, X > 0 se suspenden, y si no llegan a convertirse en lineales como en
este caso, T OY las presentará en forma normalizada como parte de la respuesta. En este
ejemplo presentará 4 − X 2 == 0, X > 0 (a pesar de que X == 2 es solución). En [Hol95]
o en [Gro96] se trata este asunto con mayor profundidad.
2.3.10.
Definición de predicados
T OY admite definición de predicados al estilo Prolog. Por ejemplo, el predicado append
puede definirse como:
p_append []
Ys Ys
:- true
p_append [X|Xs] Ys [X|Zs] :- p_append Xs Ys Zs
Obsérvese que, a diferencia de Prolog, se utiliza notación currificada, las cláusulas no
acaban en “.” y no se admiten cuerpos vacı́os, por lo que los hechos (como la primera
cláusula de p_append) tienen como cuerpo true.
La sintaxis general de los predicados es:
p t1 ...tn : − C1 , ..., Cn
(iv)
siendo t1 , ..., tn patrones y C1 , ..., Cn restricciones (de la misma forma que en las funciones).
En realidad, un predicado en T OY no es más que una función booleana; de hecho
es una función que devuelve el valor true. La expresión general anterior (iv) se traduce
automáticamente a la regla de función:
p t1 ...tn = true <== C1 , ..., Cn
Por ejemplo, el predicado p_append anterior no es más que un “azúcar sintáctico” de la
función:
p_append []
Ys Zs
= true <== Ys==Zs
p_append [X|Xs] Ys [Z|Zs] = true <== X==Z, p_append Xs Ys Zs
Obsérvese que en la primera regla se introduce una nueva variable Zs y la restricción
Ys==Zs para conseguir la linealidad. Y en la segunda se hace lo propio con la variable Z y
la restricción X==Z.
Además de las diferencias de notación con respecto a Prolog mencionadas antes, T OY
hace inferencia de tipos para los predicados y opcionalmente se puede declarar tipo para
ellos, que será de la forma:
p :: T1 → ... → Tn → bool
es decir, es el tipo de la función asociada (debe cumplir todas las condiciones que se
imponı́an a los tipos de las funciones).
Para el predicado p_append el tipo inferido es:
[A] -> [A] -> [A] -> bool
2.3. EL LENGUAJE T OY
2.3.11.
33
Operadores infijos y secciones
T OY admite notación infija para constructoras y funciones binarias (de aridad 2). Hay
dos formas de uso de esta notación:
mediante una declaración expresa de operador (constructora o función) infijo. Estas
declaraciones son de la forma:
infix[l,r] priority operatorN ame1 , ..., operatorN amen
donde priority es un entero positivo que representa la precedencia (cuanto mayor
es este entero mayor es la precedencia) y operatorN ame1 , ..., operatorN amen son
los nombres de los operadores (separados por comas) que se están declarando. La
palabra reservada infix se utiliza para operadores no asociativos. Los operadores
asociativos por la izquierda (derecha resp.) se declaran con infixl (infixr resp.).
Los nombres de constructora deben ir siempre precedidos del sı́mbolo ‘:’.
Para la elección de los nombres hay dos alternativas:
• poner el nombre del operador entre apóstrofes (‘). Por ejemplo:
infixr 40 ‘and‘
% and paralelo
% (asociativo por la derecha)
• si no se utilizan los apóstrofes, puede utilizarse un repertorio limitado de sı́mbolos, teniendo en cuenta además, que el sistema ya posee algunos operadores
reservados. Por ejemplo:
infix
infix
50 +++
60 :/
% suma de complejos
% constructora del tipo de
% los racionales
En el apéndice A hay una descripción detallada de los sı́mbolos que pueden
utilizarse, ası́ como de los operadores reservados del sistema.
la otra manera de utilizar constructoras o funciones binarias como operadores infijos
consiste en escribir dicho operador entre apóstrofes ‘. Por ejemplo, la división entera
div (primitiva del sistema) está utilizada de forma infija en la expresión 5 ‘div‘ 2.
Por otro lado, todo operador infijo puede utilizarse de forma prefija escribiéndolo entre
paréntesis, como en la expresión (+) 3 5 (equivalente a 3 + 5).
En el archivo basic.toy que se distribuye con el sistema se encuentra la declaración de
los operadores infijos predefinidos de T OY, que se tratarán en el apartado de funciones
primitivas (2.3.15).
Otro asunto relacionado con los operadores infijos son las secciones. Una sección es una
notación especial que puede utilizarse para aplicaciones parciales de dichos operadores. Por
ejemplo, la función “suma 3” puede escribirse como aplicación parcial en forma de sección
como (3+). Esta función puede aplicarse a otro argumento como en la expresión (3+) 5
(que se evaluará a 8). La anterior es una sección izquierda, pero también pueden definirse
secciones derechas. La correspondiente en este caso serı́a (+3) que, al ser ’+’ una operación
conmutativa es equivalente a la primera. Un caso más interesante puede ser el de la función
CAPÍTULO 2. EL SISTEMA T OY
34
’-’. Ahora la sección (-3) corresponde a la función “resta 3”, mientras que la sección (3-)
es la función “resta a 3”.
Las secciones resultan útiles combinadas con otras funciones. Por ejemplo, sea la función map definida como en 2.3.6. Utilizando la sección (3-) la expresión
map (3-) [1,2,3,4,5,6] se reducirı́a a [-2,-1,0,1,2,3].
En 2.3.15 veremos cómo las secciones pueden traducirse a sintaxis T OY, utilizando la
primitiva flip.
2.3.12.
Inclusión de archivos
En muchos casos puede resultar útil dividir los programas grandes en piezas más pequeñas de código, o simplemente, agrupar funciones de uso frecuente en un archivo (este
es el caso del archivo misc.toy) para reutilizarlo en otros programas. Para este propósito el
sistema cuenta con la directiva include. Por ejemplo, si se está escribiendo un programa en
un archivo < F ile1 >, se puede usar en cualquier punto del programa la directiva include
“< F ile2 >” para poder hacer uso de las definiciones que contiene < F ile2 >. Nótese
que el argumento de include es una cadena que debe ir entre comillas dobles. El efecto de
esta directiva es el mismo que tendrı́a incluir literalmente el archivo < F ile2 > en el lugar
donde aparecı́a el include.
El archivo misc.toy que se distribuye con el sistema contiene muchas funciones de uso
frecuente que pueden utilizarse mediante la directiva:
include ‘‘misc.toy’’
Este archivo es similar al preludio de Gofer o Haskell, pero contiene además otras definiciones propias del paradigma lógico funcional (véase el apéndice C).
2.3.13.
Regla de indentación
En los ejemplos vistos hasta ahora se han utilizado algunas reglas implı́citas de indentación. En este apartado se precisan estas reglas que están tomadas parcialmente de Haskell.
Su conocimiento, en Haskell es especialmente interesante para comprender las construcciones reservadas where y let que son bastante habituales. Por el momento T OY no incorpora
tales construcciones, sin embargo estas reglas pueden ayudar a comprender algunos mensajes de error sintáctico.
La unidad básica de código en un programa T OY es la sentencia. Una sentencia
comienza con una llave abierta ({) seguida de una o varias secciones y termina con una
llave cerrada (}). Una sección puede ser una inclusión de archivo (include), una definición
de un tipo de datos (data), declaración de un alias de tipo (type), una declaración de tipo
de una función o predicado, una declaración de un operador infijo (infix) o un conjunto de
reglas de función o predicado. T OY delimita automáticamente las sentencias, pero admite
también la anotación explı́cita, como en el siguiente programa:
{
include ‘‘misc.toy’’
}
{
append :: [A] -> [A] -> [A]
}
{
2.3. EL LENGUAJE T OY
35
append [] Ys = Ys;
append [X|Xs] Ys = [X|append Xs Ys]
}
Las declaraciones que aparecen al mismo nivel (en la misma sentencia) deben ir separadas por punto y coma, como ocurre con las dos reglas de append. Las reglas de indentación
pueden resumirse en:
antes de leer el primer carácter de cada sección se inserta automáticamente una llave
abierta,
el final de una sección se alcanza cuando la única llave abierta que se tiene es la
descrita en el apartado anterior y el primer carácter de la lı́nea actual se encuentra
en una columna igual o menor que la columna que ocupa el primer carácter de la
sección,
si el primer carácter de la lı́nea actual está en la misma posición que el primer
carácter del nivel más interno de llaves abiertas y este nivel es estrictamente mayor
que 1, entonces se inserta un punto y coma,
se inserta una llave cerrada siempre que se encuentra un token desconocido,
las lı́neas vacı́as, las que contienen sólo espacios en blanco o tabuladores y los comentarios, no forman parte de las reglas de indentación. Todas ellas se tratan como
espacios en blanco.
2.3.14.
Objetivos
Computar en T OY es resolver objetivos, es decir, una vez que el usuario ha escrito y
compilado su programa, el sistema está preparado para buscar respuestas a restricciones
planteadas para ese programa. Por lo tanto, los objetivos siguen la misma sintaxis que las
restricciones en la definición de funciones:
C1 , ..., Cn
donde cada Ci es de la forma e1 3e2 y 3 ∈ {==, /=}. Recordemos que el sistema admite
como restricción una expresión booleana e que se interpreta como la restricción de igualdad
estricta e == true (las restricciones aritméticas se interpretan de este modo como vimos
en 2.3.9).
T OY interpreta como objetivo todo lo que encuentra en el prompt que no sea un
comando, es decir, cualquier cadena de texto que no comienza con “/”. Nótese los objetivos,
a diferencia de los comandos, no terminan con “.”.
Si las restricciones del objetivo son insatisfactibles el sistema responde no y finaliza
el cómputo (segundo cómputo del ejemplo anterior). En el caso de que sean satisfactibles responde yes, muestra en forma resuelta las restricciones producidas por el cómputo
y pregunta al usuario si desea obtener más repuestas: more solutions [y]?. En caso
afirmativo, si el usuario responde y (o simplemente pulsa Intro), T OY buscará más respuestas haciendo backtracking. En todos los casos, al final de la respuesta, muestra el
tiempo invertido en el cómputo10 .
10
En este tiempo no se incluyen los tiempos de traducción del objetivo y tipado del objetivo, ası́ como
tampoco el tiempo de salida de la respuesta. Esta medida es más apropiada para hacer comparaciones
entre distintos programas T OY ya que recoge el tiempo real de resolución de objetivos.
CAPÍTULO 2. EL SISTEMA T OY
36
T OY incorpora un sofisticado mecanismo para minimizar la cantidad de información
que se presenta en las respuestas, ası́ como para conseguir una lectura sencilla de las
mismas. El código que lleva a cabo este proceso se muestra en el apéndice I. A continuación
mostramos algunos ejemplos reales de cómputo.
Una respuesta afirmativa puede no tener restricciones asociadas, como en el objetivo:
TOY> append [1] [2] == [1,2]
yes
Elapsed time: 0 ms.
more solutions [y]?
no.
Elapsed time: 0 ms.
TOY>
Si hay restricciones asociadas, T OY las presenta en forma resuelta en este orden:
1. Las primeras son las de igualdad que representan ligaduras de variables o sustituciones. En forma resuelta significa que son de la forma X == t, es decir, el lado
izquierdo es una variable y el lado derecho es una forma normal. Por ejemplo:
TOY> append [1,2] [3,4] == L
yes
L ==
[ 1, 2, 3, 4 ]
Elapsed time: 0 ms.
more solutions [y]?
no.
Elapsed time: 0 ms.
TOY> append [1,2] [3] == L, append L L == K
yes
L ==
K ==
[ 1, 2, 3 ]
[ 1, 2, 3, 1, 2, 3 ]
Elapsed time: 0 ms.
2.3. EL LENGUAJE T OY
37
more solutions [y]?
no.
Elapsed time: 0 ms.
TOY>
Las formas normales también pueden contener aplicaciones parciales puesto que son
expresiones irreducibles, como en el siguiente cómputo:
TOY> append [1] == F
yes
F == (append
[ 1 ])
Elapsed time: 0 ms.
more solutions [y]?
no.
Elapsed time: 0 ms.
TOY>
2. A continuación, en caso de existir, se muestran entre llaves las restricciones de desigualdad. Las desigualdades resueltas tienen la forma X /= t, donde el lado izquierdo
es una variable y el lado derecho es una forma normal. Un cómputo con desigualdades
puede ser:
TOY> append [1] X /= [1,2]
yes
{ X /=
[ 2 ] }
Elapsed time: 0 ms.
more solutions [y]?
no.
Elapsed time: 0 ms.
TOY>
CAPÍTULO 2. EL SISTEMA T OY
38
3. Por último, y sólo en caso de que el sistema esté utilizando restricciones sobre reales
(modo de uso /cflpr.) también mostrará (entre llaves) las restricciones aritméticas
resultantes del cómputo, en la forma resuelta que proporciona el resolutor ([Gro97]).
Por ejemplo:
TOY> X + Y + Z == 3, X - Y + Z == 1
yes
Y == 1
{ X==2.0-Z }
Elapsed time: 10 ms.
more solutions [y]?
no.
Elapsed time: 0 ms.
TOY> X^2 == 4, X>0
yes
{ X>0.0 }
{ 4.0-X^2.0==0.0 }
Elapsed time: 0 ms.
more solutions [y]?
no.
Elapsed time: 0 ms.
TOY>
En el último objetivo se introduce la restricción no lineal X^2 == 4. En este caso
el resolutor simplemente la suspende. Si al final del cómputo hay restricciones no
lineales (suspendidas), éstas se presentarán en la forma resuelta que proporciona el
resolutor como en este caso (no se presenta la respuesta X==2).
Otro ejemplo, puede ser el siguiente:
TOY> X^2 + sin Y == Z
yes
{ Z==_D+_E }
{ _E-sin(Y)==0.0 }
2.3. EL LENGUAJE T OY
39
{ -(X^2.0)+_D==0.0 }
Elapsed time: 10 ms.
more solutions [y]?
no.
Elapsed time: 0 ms.
TOY>
2.3.15.
Funciones primitivas
T OY cuenta con algunas funciones predefinidas o primitivas que pueden utilizarse en
cualquier programa sin necesidad de definirlas de nuevo. Dichas funciones están programadas a “bajo nivel” y su código no es accesible al usuario, pero las declaraciones de tipo
de todas ellas pueden verse en el archivo basic.toy que se incluye en la distribución (véase
el apéndice B).
Entre dichas funciones están las operaciones aritméticas habituales:
(+),(-),(*),(/) :: real -> real -> real
cuyas precedencias y asociatividades vienen dadas por las declaraciones de operadores
infijos (a mayor prioridad mayor precedencia):
infix 80 *,/
infixl 70 +,Aunque estos operadores están declarados para números reales pueden funcionar también
con enteros. En cierto sentido estos operadores pueden considerarse funciones sobrecargadas: tienen también de modo implı́cito la declaración int → int → int y si para una
ocurrencia de uno de estos operadores, el inferidor tiene suficiente información para determinar que el tipo de alguno de los argumentos o el tipo del resultado es entero, entonces
el tipo de dicha ocurrencia será int → int → int. En caso de que el inferidor no tenga
información suficiente para determinar si se trata de la suma de enteros o de reales, interpretará que se trata de la suma de reales. Informalmente, puede decirse que la suma
de enteros está “incluida” en la suma de reales y si T OY no puede determinar de cual se
trata interpreta la más general, es decir, la de reales. Esto no puede considerarse sobrecarga
en sentido general, ya que el código de uno u otro modo de uso es el mismo; sin embargo,
con esta distinción en el tipo de las operaciones aritméticas el sistema nunca intentará,
por ejemplo, hacer la división entera con números reales, ya que el inferidor detectará la
anomalı́a y producirá un mensaje de error en tiempo de compilación.
A continuación se presentan el resto de primitivas del sistema, algunas de las cuales también están sobrecargadas en el sentido que se acaba de exponer. Por ejemplo, las
funciones max/2 y min/2, que calculan el máximo y el mı́nimo de dos números respectivamente, están sobrecargadas en el mismo sentido. Para la exponenciación T OY incorpora
tres funciones:
CAPÍTULO 2. EL SISTEMA T OY
40
infix
90 ^,**
(^) :: real -> int -> real
(**) :: real -> real -> real
exp :: real -> real
La primera de ellas toma un exponente entero (segundo argumento) y como base puede
tomar tanto un entero, como un real (devuelve un real). La segunda puede tomar un entero
o un real como base, pero el exponente y el resultado serán de tipo real. Y la última, es
la exponencial natural (toma como base e) y devuelve un real.
Otras funciones: ln/1 es logaritmo natural y log/2 opera con cualquier base. La función
uminus/1 es la de cambio de signo y abs/1 es el valor absoluto (ambas para enteros y
reales); sqrt/1 es la raı́z cuadrada (para enteros y reales) que siempre devuelve un real.
T OY también tiene predefinidas las funciones trigonométricas usuales (véase el apéndice
B) que operan siempre sobre reales. Para enteros el sistema cuenta con las operaciones de
división entera div/2 y resto de la división entera mod/2 y también con el máximo común
divisor gcd/2.
Para la conversión de enteros a reales existe la función toReal/1 y los reales se pueden
redondear o truncar para convertirlos a enteros con las funciones round/1 y trunc/1.
También existen las funciones floor/1, que calcula el mayor entero menor o igual que el
argumento, y ceiling/1 que calcula el menor entero mayor o igual que el argumento.
Los operadores relacionales tienen la siguiente declaración:
infix
50 < ,<=,>,>=
(<),(<=),(>),(>=) :: real -> real -> bool
y están también sobrecargados en el sentido anterior, es decir, tienen implı́cita la declaración int → int → bool; si alguno de los argumentos es de tipo entero este es el tipo que el
inferidor asocia al operador. Estos operadores sirven también para expresar restricciones
aritméticas tal y como se explicó en 2.3.9.
Otras primitivas que introduce T OY son las funciones de igualdad y desigualdad cuya
declaración es:
infix 20 ==, /=
(==),(/=) :: A -> A -> bool
La sobrecarga aquı́ se hace más patente. Una restricción de igualdad no es lo mismo
que una llamada a la función igualdad. Por ejemplo, en (1 + 2 == 4) == B, el primer
sı́mbolo == es una llamada a la función de igualdad, mientras el segundo juega el papel
de restricción de igualdad. La función igualdad, al evaluarse con los argumentos 1 + 2 y 4
devuelve f alse, y este valor es el que toma B, por lo que el sistema devolverá la respuesta
B == f alse. La distinción entre la función y la restricción puede entenderse del siguiente
modo: una llamada a la función igualdad es algo que debe evaluarse, mientras que una
restricción es algo que debe satisfacerse. Lo mismo ocurre con la función desigualdad y las
restricciones de desigualdad. En este sentido, los sı́mbolos == y /= están sobrecargados,
ya que se utilizan tanto para funciones como para restricciones con significado distinto.
Otro ejemplo del uso de la función igualdad puede ser la función member que, dado un
elemento y una lista (de elementos del mismo tipo), devuelve true si el elemento está en
la lista y f alse en caso contrario. Repasemos la definición que vimos en 2.3.8:
2.3. EL LENGUAJE T OY
41
member X [] = false
member X [Y|Ys] = if X==Y then true else member X Ys
En la segunda regla, cuando la lista no es vacı́a, si el elemento es igual a la cabeza de
la lista, entonces la función se evalúa a true; si no es igual a la cabeza, se estudia la
pertenencia de dicho elemento al resto de la lista. La comprobación de si el elemento es
igual a la cabeza de la lista se realiza mediante la función igualdad. Si esta función se
evalúa a true, entonces se toma la primera opción del if then else que es true y si se evalúa
a f alse se selecciona la segunda opción.
Las funciones de igualdad y desigualdad podrı́an definirse en sintaxis T OY utilizando
las restricciones del siguiente modo:
X == Y = true <== X == Y
X == Y = false <== X /= Y
X /= Y = false <== X == Y
X /= Y = true <== X /=Y
Sin embargo, T OY no utiliza estas definiciones sino que incorpora código especı́fico para
ellas a bajo nivel (3.13,3.14). La razón es que estas funciones son de uso relativamente
frecuente y admiten algunas optimizaciones que no podrı́an llevarse a cabo sobre las dos
definiciones anteriores.
T OY también incorpora como primitivas las funciones:
if_then :: bool -> A -> A
if_then_else :: bool -> A -> A -> A
que son las funciones correspondientes a las construcciones if <Condición> then y if
<Condición> then <Expresión1 > else <Expresión2 >, donde <Condición> es una expresión booleana, y <Expresión1 > y <Expresión2 > deben ser del mismo tipo. En realidad estas construcciones son un “azúcar sintáctico” que el analizador sintáctico traduce
automáticamente a las funciones correspondientes. Estas funciones, como todas las primitivas, están definidas a bajo nivel pero podrı́an definirse en la sintaxis de T OY del
siguiente modo:
if_then true X = X
if_then_else true X Y = X
if_then_else false X Y = Y
Por último, T OY incorpora como primitiva la función de orden superior flip que sirve
para intercambiar el orden de los argumentos en la aplicación de una función. El tipo es:
flip :: (A -> B -> C) -> B -> A -> C
y la definición en sintaxis T OY podrı́a ser (aunque se codifica a bajo nivel):
flip F X Y = F Y X
Por ejemplo, la expresión f lip (−) 4 2 se evaluarı́a a (−) 2 4, o lo que es lo mismo 2−4.
Esta función se incorpora como primitiva para la traducción de secciones (2.3.11) como
(−3) 5. En esta expresión (−3) es una función que le resta 3 al argumento que se le pasa;
si se le pasa 5 debe evaluar 5 − 3. El problema es que el parámetro 5 aparece como primer
argumento y para aplicar (−3) hay que invertir el orden de los argumentos, y esto es lo
que hace flip. La expresión (−3) 5 se traduce a f lip (−) 3 5 que tiene el comportamiento
deseado.
CAPÍTULO 2. EL SISTEMA T OY
42
2.4.
Ejemplo 1. Regiones en el plano
En esta sección desarrollamos paso a paso un programa que trata sobre regiones en el
plano y que muestra algunas de las posibilidades que ofrece el uso conjunto de las funciones
y las restricciones. En [HLS+ 97] se presenta una versión de este mismo ejemplo, ası́ como
una discusión que compara la programación lógica pura con restricciones (CLP (R)) frente
a la programación lógico funcional con restricciones (CF LP (R)). Este ejemplo también
se incluye en la distribución en el archivo region.toy. Para ejecutarlo T OY debe tener
activadas las restricciones sobre reales (comando /cflpr).
Las regiones serán conjuntos de puntos en el plano que vienen representados por su
función caracterı́stica. Esta aproximación es tı́pica en programación funcional. La novedad
aquı́ es que las restricciones proporcionan un modo mucho más flexible para utilizar las
funciones, e incluso para definir otras nuevas. Por otro lado, las funciones no deterministas
aportan una gran riqueza expresiva que permite definiciones muy directas para algunas
operaciones de naturaleza indeterminista.
Comenzamos definiendo dos alias de tipo: los puntos son parejas de reales y las regiones
están definidas por su función caracterı́stica (tipo funcional):
type point = (real,real)
type region = point -> bool
Una primera operación habitual en este contexto es la de pertenencia que notaremos
con el operador infijo <<-, asociativo por la derecha:
infixr 50 <<(<<-):: point -> region -> bool
P <<- R = R P
La regla de esta función puede leerse como: el valor de la afirmación “el punto P pertenece
a la región R” es el resultado de aplicar R (función caracterı́stica) a P .
Utilizaremos los operadores lógicos habituales de conjunción (/\), disyunción (\/) y
la negación lógica (not):
infixr 40 /\
infixr 30 \/
false /\ X = false
true /\ X = X
true \/ X = true
false \/ X = X
not true = false
not false = true
En este caso utilizamos la definición secuencial para la conjunción y la disyunción. La
versión paralela de la conjunción podrı́a definirse como: f alse ∧ X = f alse, X ∧ f alse =
f alse y true∧true = true. Aunque ambas versiones de la conjunción son lógicamente equivalentes, operacionalmente tienen un comportamiento distinto (esto se comprenderá mejor
cuando analicemos la estrategia de evaluación de T OY en 3.10).
Ya estamos en disposición de definir las primeras regiones. La más sencilla es el punto
(un punto es una región):
2.4. EJEMPLO 1. REGIONES EN EL PLANO
43
point :: point -> region
point P Q = P==Q
La regla puede leerse como: el punto P pertenece a la región que sólo contiene el
punto Q si P y Q son el mismo. Nótese que estamos empleando deliberadamente el mismo
nombre point tanto para un alias de tipo, como para una función: nos interesa pensar
en los puntos como pares de reales y también como regiones en el plano. Esto no supone
ningún problema para el compilador.
Otras dos regiones triviales son la región vacı́a y el plano (ningún punto pertenece a
la región vacı́a y todos los puntos pertenecen al plano):
emptyReg, thePlane :: region
emptyReg P = false
thePlane P = true
Un rectángulo queda definido (por ejemplo) por la esquina inferior izquierda y la
superior derecha, y un cı́rculo por su centro y su radio (ecuación habitual del cı́rculo):
rectangle :: point -> point -> region
rectangle (A,B) (C,D) (X,Y) =
(X >= A) /\ (X <= C) /\ (Y >= B ) /\ (Y <= D)
circle :: point -> real -> region
circle (A,B) R (X,Y) = (X-A)*(X-A)+(Y-B)*(Y-B) <= R*R
Sobre las regiones definimos algunas operaciones tı́picas como la intersección y la unión,
que serán funciones de orden superior:
intersct, union :: region -> region -> region
intersct R R’ P = P <<- R /\ P <<- R’
union
R R’ P = P <<- R \/ P <<- R’
La lectura de las reglas es simple. Por ejemplo, la de la intersección afirma que un punto
P pertenece a la intersección de las regiones R y R0 si P pertenece a R y también a R0 .
T OY ofrece posibilidades interesantes en cuanto a los patrones en la definición de
funciones: admite como patrón cualquier forma normal y en particular admitirá patrones de
orden superior. En la práctica, esto significa que podemos distinguir casos en la definición
de una función de orden superior, de acuerdo con las distintas “formas intensionales” que
los argumentos pueden adoptar. Por ejemplo, la función intersect puede definirse de
modo que contemple el hecho de que la intersección de cualquier región con la región vacı́a
es la región vacı́a o que la intersección de dos rectángulos es otro rectángulo:
intersect’
intersect’
intersect’
intersect’
:: region -> region -> region
emptyReg R = emptyReg
R emptyReg = emptyReg
(rectangle (A,B) (C,D)) (rectangle (A’,B’) (C’,D’)) =
if (A’’ <= C’’) /\ (B’’ <= D’’)
then rectangle (A’’,B’’) (C’’,D’’)
else emptyReg
<== A’’== max A A’ , B’’== max B B’,
C’’== min C C’ , D’’== min D D’
CAPÍTULO 2. EL SISTEMA T OY
44
intersect’ R R’ =
intersect R R’
<== R /= emptyReg, R’ /= emptyReg,
(R,R’) /= (anyRectangle,anyRectangle)
anyRectangle = rectangle undefined undefined
undefined :: A
undefined = if false then undefined
Las dos primeras reglas cubren el caso de que una de las regiones sea vacı́a y la tercera
el caso en el que ambas regiones son rectángulos (las funciones max y min calculan el máximo y el mı́nimo entre dos números respectivamente). La última regla se aplica si no es
aplicable ninguna de las anteriores y utiliza la anterior definición de intersect. Para que
esta regla no sea aplicable en los casos que tratan las anteriores, tiene que comprobar que
ninguno de los argumentos es la región vacı́a y que no son dos rectángulos. Esta última
restricción (R,R’) /= (anyRectangle,anyRectangle) requiere algunos comentarios. Se trata
de una desigualdad entre dos parejas de elementos y para resolverla habrá que resolver
alguna de las (dos) desigualdades entre las componentes. En este caso ambas desigualdades tienen la misma forma R /= anyRectangle, cuya lectura es “R no es un rectángulo”.
Tal y como hemos definido los rectángulos, todos son de la forma rectangle . Entonces,
para comprobar que R no es un rectángulo, debemos comprobar que no es de esta forma
(que no se ajusta a este patrón). En la definición de anyRectangle se utiliza la función
(constante) undefined, que es una función que siempre falla. Con esta definición, la condición R /= anyRectangle es equivalente a R /= rectangle\ undefined\ undefined,
(el valor de los argumentos ’ ’ no nos importa),
es decir, R no toma la forma rectangle
11
que es precisamente lo que se busca .
La definición de undefined utiliza la función igualdad y puede leerse como: si es cierto
que f alse es igual a true entonces devolver el resultado de undefined (realmente lo que
devuelva en este caso importa poco). Obviamente, f alse no es igual a true y la función
igualdad devuelve f alse; como el if no tiene alternativa se produce un fallo automático,
que es justo lo que se pretende.
El exterior o complementario de una región es otra región que puede definirse utilizando
la negación:
outside :: region -> region
outside R
P = not (P <<- R)
Las función interserc’ utiliza patrones de orden superior, que no se permiten en
lenguajes funcionales puros como Haskell y, por lo tanto, no admisible en este lenguaje. El
resto de definiciones vistas hasta el momento sı́ son admisibles en este tipo de lenguajes y,
en particular, en Haskell. Sin embargo, debido a la reversibilidad de las funciones y a las
restricciones T OY es capaz de hacer cómputos que carecen de sentido en programación
funcional pura utilizando las funciones expuestas. Por ejemplo, se le puede pedir al sistema
que calcule los puntos P que pertenecen a la intersección de dos rectángulos:
TOY> P <<- intersect (rectangle (0,0) (3,2)) (rectangle (1,1) (4,3))
11
En general, una condición de la forma X /= c undef ined (c constructora de aridad 1) expresa de
forma implı́cita una cuantificación universal. Esta condición es equivalente a decir ∀Y (X /= c Y ), que a
su vez, equivale a decir “X no toma la forma c ”.
2.4. EJEMPLO 1. REGIONES EN EL PLANO
yes
P == (_A, _B)
{ _B>=1.0
{ _B=<2.0
{ _A>=1.0
{ _A=<3.0
45
}
}
}
}
La respuesta es que P debe tener la forma (A, B), donde A y B cumplen las restricciones
que cabe esperar.
Las operaciones de intersección y unión pueden generalizarse para que operen sobre
conjuntos de regiones representados como listas. Para ello es útil el combinador foldr
clásico en programación funcional. Este operador toma una función, una lista y un elemento
“neutro” y utiliza el tercer argumento como parámetro acumulador. En T OY se puede
definir como:
foldr :: (A -> B -> B) -> B -> [A] -> B
foldr F Z []
= Z
foldr F Z [X|Xs] = F X (foldr F Z Xs)
Por ejemplo la llamada foldr (*) 1 [2,3,4] hace la multiplicación (2 ∗ (3 ∗ (4 ∗ 1)) y
devuelve 24 (1 es el elemento neutro para ∗).
Con esta función, la generalización de la intersección y unión de regiones se puede
definir como:
intersectAll, unionAll :: [region] -> region
intersectAll = foldr intersect thePlane
unionAll = foldr union emptyReg
Obsérvese que en la intersección se toma el plano como elemento neutro, mientras que en
la unión se toma la región vacı́a.
Para definir nuevas operaciones sobre regiones introducimos el nuevo alias de tipo de
los vectores, representados por un par de reales que se interpreta como un número complejo
(coordenadas cartesianas). Para los vectores definimos también las operaciones habituales
de suma, resta, multiplicación, división y producto por un escalar, que notaremos con los
sı́mbolos aritméticos seguidos de ‘.’ (#. para el producto por un escalar):
type vector = (real,real)
(+.),(-.),(*.),(/.) :: vector -> vector -> vector
(#.) :: real -> vector -> vector
(X,Y) +. (U,V) = (X+U,Y+V)
(X,Y) -. (U,V) = (X-U,Y-V)
(X,Y) *. (U,V) = (X*U-Y*V,X*V+Y*U)
(X,Y) /. (U,V) = ((X*U+Y*V)/A,(Y*U-X*V)/A) <== A == U*U+V*V, A > 0
K #. (X,Y) = (K*X,K*Y)
Con este tipo y sus operaciones podemos definir, por ejemplo la envoltura convexa
(conv hull) de un conjunto de puntos. Identificamos los puntos con sus vectores de posición
y utilizamos la equivalencia:
CAPÍTULO 2. EL SISTEMA T OY
46
P ∈ convh ullP1 , ..., Pn ⇔ P =
P
i=1..n λi Pi ,
con λi ≥ 0, ∀i = 1..n y
P
i=1..n λi
=1
De acuerdo con esta definición el predicado conv_hull puede programarse como:
conv_hull :: [point] -> region
conv_hull Ls P :- P == lin_comb Ls 1
lin_comb [] 0 = (0,0)
lin_comb [X|Xs] Sum = (K #. X) +. lin_comb Xs Rest
<== Sum == Rest + K, K >=0, Rest >=0
La lectura de la regla de conv_hull es la siguiente: un punto P está en la envoltura convexa
de una lista de puntos Ls si dicho punto puede obtenerse como combinación lineal de los
vectores de posición de los puntos de Ls; la función lin_comb se encargará de que los
coeficientes de dicha combinación lineal sean todos positivos o nulos y que sumen 1. La
función lin_comb devuelve una combinación lineal de los puntos que se le pasan como
primer argumento, cuyos coeficientes suman la cantidad que se le pasa como segundo
argumento (por eso en conv_hull el segundo argumento es 1). Este segundo argumento es
una especie de parámetro acumulador que se va decrementando hasta llegar a 0. Ası́ una
combinación lineal de una lista de puntos [X|Xs] (segunda regla) será el primero de ellos X
multiplicado por una constante K más una combinación lineal del resto; en las restricciones
nos ocupamos de que K ≥ 0 y de que todos los coeficientes al final sumen 1.
Una observación importante acerca de la segunda regla de lin_comb es que K y Rest
(que aparecen en el cuerpo y las restricciones) no están determinadas por los argumentos de
la función [X|Xs] y Sum, lo que implica que lin_comb es una función indeterminista.
Ahora podemos hacer el siguiente cómputo:
TOY> P <<- conv_hull [(1,2), (2,3), (4,3), (4,1), (3,2)]
yes
P == (_A, _B)
{ _A-_B>= -0.9999999999999996 }
{ _A+3.0*_B>=6.999999999999999 }
{ _A=<4.0 }
{ _B=<3.0 }
El resultado puede comprobarse gráficamente en la figura 2.1.
(2,3)
(4,3)
(3,2)
x
(1,2)
(4,1)
Figura 2.1: Envoltura convexa de un conjunto de puntos
2.4. EJEMPLO 1. REGIONES EN EL PLANO
47
Otras operaciones sobre regiones son las traslaciones, que desplazan una región (segundo argumento) de acuerdo con un vector de traslación (primer argumento) y producen
otra región, y las homotecias, que toman un punto como el centro de homotecia (primer
argumento), un real como razón de homotecia (segundo argumento) y una región (tercer
argumento) y devuelven otra región (véase la figura 2.2):
translate:: vector -> region -> region
homothety:: point -> real -> region -> region
translate V R P = (P -. V) <<- R
homothety C F R P = (C +. ((1/F) #. (P -. C))) <<- R
R
C
P
F=2
Figura 2.2: Homotecia
Como T OY es un lenguaje perezoso, es posible definir estructuras infinitas, como la lista infinita de regiones obtenidas por sucesivas traslaciones de una región inicial (rayItems),
e incluso es planteable la posibilidad de hacer la unión de todas esas regiones, como hace
ray:
ray :: vector -> region -> region
ray V R = unionAll (rayItems V R)
rayItems :: vector -> region -> [region]
rayItems V R = [R|rayItems V (translate V R)]
Se puede lanzar el objetivo (5,1) <<- ray (1,0) (circle (0,0) 2), que produce
una respuesta afirmativa. En el cómputo se va desplazando el cı́rculo una y otra vez, hasta
que alguno de los cı́rculos resultantes captura al punto (5, 1); en ese momento se obtiene
un éxito y para el cómputo. La estructura infinita se ha evaluado sólo parcialmente. No
ocurre lo mismo si lanzamos el objetivo (0,3) <<- ray (1,0) (circle (0,0) 2). En
este caso el cómputo nunca termina ya que, este punto no pertenece a esa región, pero para
averiguarlo T OY “debe calcular infinitas regiones”. De hecho la función ray sólo termina
cuando el punto sı́ pertenece a la región, es decir, computa una función semicaracterı́stica.
Sin embargo, esto no es realmente un problema del sistema ya que las uniones infinitas
de conjuntos recursivos es, en general, no recursiva, sino recursivamente enumerable. O lo
que es lo mismo, determinar si un punto pertenece a una unión infinita de regiones no es
decidible en general (es parcialmente decidible).
La traslación rı́gida o sólida es una operación que toma un vector de traslación y una
región y devuelve la región formada por: la región original, la región trasladada y todos
los puntos que se encuentran en el movimiento. Esto es lo que hace la función move:
move :: vector -> region -> region
move (U,V) R P :- 0 <= K, K <= 1, translate (K*U,K*V) R P
CAPÍTULO 2. EL SISTEMA T OY
48
Nótese que la variable K de la regla anterior es local a las restricciones: no aparece en
la cabeza del predicado, es decir, es una variable cuantificada existencialmente de forma
implı́cita.
El siguiente es un ejemplo de objetivo que combina la potencia de las restricciones con
la reversibilidad de las funciones:
TOY> P <<- move (3,1) (rectangle (0,0) (2,3))
yes
P == (_A, _B)
{ _A-3.0*_B=<2.0 }
{ _A-3.0*_B>= -9.0 }
{ _A>=0.0 }
{ _A=<5.0 }
{ _B>=0.0 }
{ _B=<4.0 }
En el resultado aparecen las ecuaciones de seis semiplanos que determinan la región
resultante de hacer el movimiento (gráficamente en la figura 2.3). Además obsérvese que
la variable existencial K de la definición de move no aparece en la respuesta.
(2,3)
xP
(3,1)
(0,0)
Figura 2.3: Movimiento sólido
Planteemos ahora un problema que utiliza algunas de las funciones anteriores:
PROBLEMA DEL LABERINTO: Dada una región R, una secuencia de movimientos M vs y un conjunto de puntos P ts, reordenar M vs en una nueva secuencia M vs0
de modo que la aplicación secuencial de los movimientos de M vs0 a la región R no encuentre ningún punto de P ts.
Para ilustrar el objetivo que pretendemos véase la figura 2.4. Dada la región R, una
homotecia (1) y una traslación (2), y los puntos P 1 a P 5, podemos aplicar a R primero
la traslación (2) y luego a la región resultante la homotecia (1), sin tocar ninguno de los
puntos P 1 a P 5.
La solución se consigue por generate & test12 :
12
Este es un método clásico de búsqueda de soluciones que consiste en generar posibles candidatos a
solución (generalmente por backtraking) y comprobar después si, efectivamente, son soluciones al problema.
2.4. EJEMPLO 1. REGIONES EN EL PLANO
49
(1)
P1
(2)
P2
P3
R
P5
P4
Mvs = { (1), (2) }
Figura 2.4: Laberinto
solution::region -> [point] -> [region->region] -> [region->region]
solution R Pts Mvs = Mvs’ <== check R Pts Mvs’
% test
where Mvs’ == permut Mvs % generate
Donde permut genera una permutación de la lista dada y check comprueba que, efectivamente, la secuencia de movimientos Mvs’ aplicada a R no toca los puntos de Pts.
Después veremos la definición de estas funciones.
Por el momento, T OY no admite construcciones where, por lo que lo reemplazaremos
por una construcción equivalente, pero admisible para T OY (esta transformación siempre
puede hacerse, aunque en la versión actual T OY no la hace de modo automático):
solution R Pts Mvs = solAux R Pts (permut Mvs)
solAux R Pts Mvs = Mvs <== check R Pts Mvs
La función permut produce, de forma indeterminista, las distintas posibles permutaciones de una lista dada. Para ello inserta (insert) el primer elemento de dicha lista en
una posición cualquiera de una permutación del resto de la lista:
permut [] = []
permut [X|Xs] = insert X (permut Xs)
insert X [] = [X]
insert X [Y|Ys] = [X,Y|Ys] // [Y|insert X Ys]
X // Y = X
X // Y = Y
El indeterminismo de permut, en última instancia, se concentra en la función // que es
la elección indeterminista entre dos valores (es otra notación para la función choice que
vimos en 2.3.7).
Para la función check utilizaremos algunas funciones auxiliares:
CAPÍTULO 2. EL SISTEMA T OY
50
(.) :: (B -> C) -> (A -> B) -> (A -> C)
(F . G) X = F (G X)
all:: (A -> bool) -> [A] -> bool
all P = andL . (map P)
andL:: [bool] -> bool
andL = foldr (/\) true
map:: (A -> B) -> [A] -> [B]
map F [] = []
map F [X|Xs] = [F X | map F Xs]
La primera es la composición habitual de funciones y all comprueba si todos los
elementos de una lista verifican una condición determinada. andL comprueba que todos
los elementos de una lista de booleanos son true y map es la habitual. Con estas funciones
tenemos:
check R Pts Mvs :- avoids Pts (doMoves R Mvs)
avoids Pts R = all (not.(<<- R)) Pts
doMoves R []
= R
doMoves R [Mv|Mvs] = union R (doMoves (Mv R) Mvs)
La función doMoves toma una región y una secuencia de movimientos, y devuelve la
unión de las regiones que resultan de ir aplicando los movimientos a la región dada, de
forma secuencial. Y avoids toma una lista de puntos y una región, y comprueba que la
región no contiene a ninguno de tales puntos. Combinando estas dos funciones, check
comprueba que las regiones producidas por una secuencia de movimientos dada no tocan
a un conjunto de puntos, también dados.
Un objetivo concreto para este problema:
TOY> X == solution (rectangle (0,0) (2,2)) [(1,3),(3,3),(2,5)]
[(homothety (0,0) 2), (homothety C 0.5),
(translate (2,2)), (translate (1,1))],
C <<- (rectangle (-1,-1) (0,0))
Nótese que la segunda homotecia de la lista de posibles movimientos tiene como centro
una variable C, y que la última restricción impone que dicho centro esté en un cuadrado
concreto del plano. Una de las tres soluciones que encuentra el sistema es la siguiente:
yes
X ==
[ (homothety (0, _A) 0.5), (translate (2, 2)),
(homothety (0, 0) 2), (translate (1, 1)) ]
C == (0, _A)
{ _A>= -1.0 }
{ _A< -0.0 }
La secuencia que propone el sistema es una solución para el objetivo, supuesto que el
centro de la homotecia está en el segmento (0, −1], (0, 0). En la figura 2.5 se ha representado gráficamente esta solución: sobre el cuadrado inicial se aplica la primera homotecia,
2.4. EJEMPLO 1. REGIONES EN EL PLANO
51
obteniendo el cuadrado (a); por traslación de este se obtiene (b); por la segunda homotecia
(c) y por la última traslación (d). Ninguno de los cuadrados intermedios contiene ninguno
de los puntos que se querı́an evitar.
(2,5)
(d)
(3,3)
(1,3)
inicial
(c)
(b)
(a)
(0,A)
Figura 2.5: Solución al laberinto
Para concluir este ejemplo, veamos un objetivo con variables lógicas de orden superior,
es decir, variables de tipo funcional, como (0,0) <<- R. Aquı́ se le está pidiendo al sistema
que calcule las regiones que contienen al punto (0, 0). Algunas de las respuestas obtenidas
son:
R == (’(==)’ (0, 0))
R == (’(/=)’ _A)
{ _A /= (0, 0) }
R == thePlane
R == (point (0, 0))
R == (rectangle (_A, _B) (_C, _D))
{ _C>=0.0 }
{ _A=<0.0 }
{ _B=<0.0 }
{ _D>=0.0 }
R == (circle (_A, _B) _C)
{ _B== -(_H) }
{ _G==_H }
{ _D==_E }
CAPÍTULO 2. EL SISTEMA T OY
52
{
{
{
{
_A== -(_E) }
-(_C^2.0)+_J==0.0 }
_F-_H*_G==0.0 }
_I-_D*_E==0.0 }
R == (outside (’(==)’ _A))
{ _A /= (0, 0) }
R == (outside emptyReg)
R == (outside (outside (’(==)’ (0, 0))))
En este caso la función outside proporciona un amplio espectro de posibilidades (haciendo dobles negaciones como en la última respuesta) y T OY continuarı́a produciendo
muchas más soluciones.
Sin embargo, las variables de orden superior pueden provocar fenómenos inesperados
en el sistema en determinadas circunstancias. Por ejemplo, si lanzamos el objetivo:
TOY> map F [true] == [false]
el sistema produce entre otras, las siguientes respuestas:
F == (’(==)’ false)
F == (’(/=)’ true)
F == emptyReg
F == (point false)
Las dos primeras son correctas, pero la tercera liga la variable F con emptyReg, que
tiene tipo point → bool. Por la forma del objetivo es fácil apreciar que F debe ser de
tipo bool → bool. En la última respuesta el problema es más acusado, ya que la expresión
point false carece de sentido (la función point tiene tipo point → region). Estas dos
últimas respuestas están mal tipadas.
El problema podrı́a solucionarse haciendo que T OY manejase tipos en tiempo de ejecución y comprobando que cuando una variable se liga, lo hace con una expresión del
mismo tipo. Sin embargo, para hacer esto es necesario arrastrar información de tipos
en los cómputos, lo que (previsiblemente) afectarı́a seriamente al sistema en cuanto a la
eficiencia.
Otra posible solución es dejar que el sistema opere como hasta ahora, pero comprobando tipos a la hora de presentar las respuestas. En el ejemplo anterior, se dejarı́a anotado el
tipo de F; una vez calculada la respuesta F == emptyReg, se utilizarı́a el inferidor de tipos
pasándole además el tipo anotado para F. El inferidor producirı́a un fallo, la respuesta no
se mostrarı́a y se solicitarı́a una nueva respuesta. Este es un mecanismo que simula un
fallo en ejecución (es simulado porque no se produce en ejecución realmente).
Esta última solución no es completamente satisfactoria porque básicamente se trata de
desestimar determinadas respuestas que han sido calculadas por el sistema. En determinados pasos de cómputo se pueden producir inconsistencias de tipo que no serı́an detectadas
2.5. EJEMPLO 2. PUZLE ARITMÉTICO
53
hasta el final. Por este motivo hemos optado por dejar que se presenten respuestas mal tipadas que, esporádicamente, nos servirán para recordar que aquı́ hay un problema abierto.
Remarquemos que esta anomalı́a sólo se produce en determinados cómputos que
utilizan variables lógicas de orden superior.
2.5.
Ejemplo 2. Puzle aritmético
En el archivo martians.toy que se incluye entre los ejemplos del sistema, se encuentra
el programa correspondiente a un problema clásico en programación lógica. El enunciado
de este problema es el siguiente:
Hay dos números, M y N , tales que 1 < M < 100, 1 < N < 100 y dos hombres muy
listos, Mr. S y Mr. P. A Mr. S se le dice la suma de los dos números y a Mr. P se le
dice el producto. Además Mr. S sabe que Mr. P conoce el producto y Mr. P sabe que Mr.
S conoce la suma. Ambos hombres mantienen el siguiente diálogo:
Mr. P: No sé cuáles son los números.
Mr. S: Ya sabı́a que tú no lo sabı́as; yo tampoco los sé.
Mr. P: Ahora sé cuáles son los números!.
Mr. S: Ahora yo también lo sé!.
¿Cuáles son los números?
A primera vista, el diálogo anterior resulta sorprendente, sin embargo, la solución al
dilema es sólo cuestión de lógica y de hacer algunas comprobaciones.
Estudiemos detenidamente la información del enunciado: buscamos dos números M y
N , siendo 1 < M ≤ N < 100 (con M ≤ N descartamos posibles soluciones simétricas como
(23, 56) y (56, 23)); ası́ pues, los candidatos serán parejas de números entre 2 y 99, cuyo
primer elemento es menor o igual que el segundo. Sabiendo que Mr. P conoce el producto
P = M ∗ N y Mr. S conoce la suma S = M + N , de cada una de las intervenciones de la
conversación pueden hacerse las siguientes deducciones:
si conociendo el producto P , Mr. P desconoce los números es porque “debe existir
más de una pareja de candidatos cuyo producto sea P ”;
Mr. S sabe que Mr. P conoce el producto P y además sabı́a (antes de que Mr. P
lo dijese) que Mr. P no conocı́a los números. Esto debe ser porque para todas las
parejas de candidatos cuya suma es S, el producto de tales parejas puede obtenerse
como producto de los elementos de más de una pareja de candidatos. Además Mr.
S también desconoce los números, lo que indica que hay más de una posible descomposición en sumandos. Esta última información en realidad es redundante, ya
que para que sólo hubiese una posible descomposición en sumandos, nuestras parejas
candidatas tendrı́an que ser (2, 2) ó (99, 99) cuyos productos son 4 y 9801, que, en
ambos casos, sólo admiten una descomposición para Mr. P. Es decir, Mr. P conocerı́a
los números M y N , pero anteriormente dijo que no los conocı́a;
ahora Mr. P conoce los números. Esto es porque entre las posibles parejas candidatas
sólo hay una cuya suma harı́a que Mr. S dijese lo anterior;
Mr. S también tiene los números. Del mismo modo que antes, sólo hay una pareja
candidata que le permite Mr. P conocer los números con la información que tenı́a
hace un momento.
CAPÍTULO 2. EL SISTEMA T OY
54
Desde el punto de vista lógico el problema está casi resuelto (no es difı́cil formalizar
lógicamente el problema), pero seguimos sin conocer efectivamente los números (en el caso
de que existan). Traslademos el razonamiento anterior a un programa T OY que haga la
búsqueda de la solución (o soluciones) por generate & test: se generan candidatos y se
comprueba si el diálogo del enunciado tiene sentido:
solution X :candidates X,
doesntKnowP X, knowsSthatPdoesntKnow X, nowPknows X, nowSknows X
Para la generación de candidatos utilizaremos una función indeterminista:
candidates :: (int,int) -> bool
candidates (M,N) :- between 2 99 M, between M 99 N
between :: int -> int -> int -> bool
between X Y X :- X <= Y
between X Y Z :- X < Y, between (X+1) Y Z
Primero se genera un candidato M entre 2 y 99, y luego otro N entre M y 99. Ası́ tenemos 1 < M ≤ N < 100 y se evitan las simetrı́as. La función between devuelve indeterministamente un número perteneciente a un intervalo dado.
Para los predicados correspondientes al test necesitaremos dos funciones auxiliares.
Una de ellas, summands, debe generar todas las posibles descomposiciones de un número
como suma de otros dos:
summands :: int -> [(int,int)]
summands N = summands2 (firstSummands N)
firstSummands N = if N <= 101 then (2,(N-2)) else ((N-99),99)
summands2 :: (int,int) -> [(int,int)]
summands2 (N,M) = if N>M then []
else [(N,M)|summands2 ((N+1),(M-1))]
La idea es generar un par inicial mediante firstSummands, cuyo segundo elemento
sea máximo (dos casos). Por ejemplo, si el número a descomponer es 145 el primer par
será (46, 99) y si es 56 el par será (2, 54). Los siguientes pares se forman “quitando uno al
segundo elemento y sumándolo al primero” (la suma del par permanece invariable) y esto
es lo que hace summands2.
La segunda función auxiliar factors sirve para generar todas las posibles descomposiciones de un número N como producto de otros dos. Para ello se utiliza un contador que
inicialmente vale 2, y se comprueba si existe otro número tal que al multiplicarlo por el
contador sea N . En factors se llama a factors2 (con el contador inicializado a 2), que
hará las comprobaciones e incrementará el contador:
factors :: int -> [(int,int)]
factors N = factors2 N 2
factors2 :: int -> int -> [(int,int)]
factors2 N M = if M > (trunc (sqrt N))
then []
2.5. EJEMPLO 2. PUZLE ARITMÉTICO
55
else if (((N ‘mod‘ M) /= 0 ) \/ ((N ‘div‘ M) > fin))
then factors2 N (M+1)
else [(M,(N ‘div‘ M)) | (factors2 N (M+1))]
Los cuatro predicados del test correspondientes a cada una de las sentencias del diálogo
ahora se pueden programar con bastante naturalidad como funciones booleanas, que operarán una pareja (A, B) candidata a solución. La primera de ellas afirma que deben existir
al menos dos descomposiciones como producto de dos números para el número A ∗ B:
doesntKnowP (A,B) = atLeastTwo (factors (A*B))
atLeastTwo [] = false
atLeastTwo [X] = false
atLeastTwo [X,Y|Z] = true
La segunda función debe comprobar que todas las posibles descomposiciones de A + B
en sumandos verifican el test anterior. Utilizamos la función all que vimos en el ejemplo
anterior (también está en el archivo de utilidades misc.toy). Esta función es:
knowsSthatPdoesntKnow (A,B) = all doesntKnowP (summands (A+B))
El tercer test comprueba que sólo existe un posible candidato que verifique el segundo
test. Y el último test comprueba que sólo hay un candidato que cumpla el tercer test. De
este modo estamos propagando la información de un test al siguiente, que restringirá aún
más el conjunto de posibles soluciones. El tercer y el cuarto test quedan de este modo:
nowPknows (A,B) = existsOne (factors (A*B)) knowsSthatPdoesntKnow
nowSknows (A,B) = existsOne (summands (A + B)) nowPknows
existsOne :: [A] -> (A -> bool) -> bool
existsOne [] F = false
existsOne [P|R] F = if (F P == false) then existsOne R F
else noOne R F
noOne :: [A] -> (A -> bool) -> bool
noOne [] F = true
noOne [P|R] F = if (F P == false) then noOne R F
else false
Con esto concluye el programa. Nótese que se han utilizado varias funciones booleanas
en vez de predicados. La diferencia entre unas y otros es que las funciones pueden devolver
el valor false, mientras que los predicados sólo pueden tener éxito o fallar. En nuestro
programa nos interesan las funciones porque el hecho de que un test no se verifique sobre
una pareja determinada es una información útil. Por ejemplo, existOne comprueba que
de una lista de candidatos uno y sólo uno satisface un determinado test, es decir, el test
“toma el valor false sobre todos los elementos de la lista excepto uno”. El hecho de “no
satisfacer un test”, para un predicado supone un fallo que finaliza el cómputo.
El resultado de la ejecución produce una única solución:
CAPÍTULO 2. EL SISTEMA T OY
56
TOY> solution X
yes
X == (4, 13)
Reconstruyamos algunos pasos del razonamiento que hacen los dos hombres: Mr. S
conoce el dato S = 17 y Mr. P sabe P = 52. Mr. P razona sobre los posibles candidatos
M y N entre 2 y 99 tal que M ∗ N = 52. Hay dos posibles pares:
(2, 26), (4, 13)
Pero no sabe cuál de las parejas es la solución. Mr. S hace un razonamiento similar y
obtiene los pares:
(2, 15), (3, 14), (4, 13), (5, 12), (6, 11), (7, 10), (8, 9)
Mr. S sabı́a que Mr. P tampoco tenı́a el resultado porque los productos que obtiene
con estos candidatos tienen todos más de una factorización posible (la única posibilidad
para que esto no ocurriese serı́a que hubiese un par de números primos).
Mr. P ahora conoce el resultado, porque en el supuesto de fuese el par (2, 26), Mr.
S habrı́a operado con la suma, 28, obteniendo entre otras, las parejas (5, 23) y (11, 17)
(pares de primos). Pero Mr. S estaba seguro de que Mr. P no conocı́a el resultado, y esto
es porque el producto no podı́a descomponerse en producto de dos primos. Luego, la pareja
buscada debe ser (4, 13).
Mr. S intenta reconstruir todo el razonamiento con cada uno de sus candidatos (no lo
detallamos) y concluye que para que Mr. P conozca el resultado los números deben ser 4
y 13.
2.6.
Comparación con otros estilos de programación
En el código que produce T OY hemos procurado obtener el mayor rendimiento como
veremos en el próximo capı́tulo. Sin embargo, no debemos olvidar que T OY hace una
traducción a Prolog. Para tratar seriamente el tema de la eficiencia y poder hacer comparaciones equitativas con otros sistemas (en especial lógicos y funcionales) serı́a deseable
contar con una implementación de T OY que generase un código objeto especializado para
este tipo de lenguajes (máquina abstracta). De hecho, la comparación con Prolog puede
parecer paradójica (realmente se comparan dos programas Prolog), pero es posible como
veremos en un ejemplo.
Este apartado no pretende, por tanto, hacer una comparativa exhaustiva con otros
lenguajes en cuanto al rendimiento. Con respecto a los lenguajes funcionales simplemente
diremos que se ha podido apreciar que en determinados ejemplos T OY hace un mejor
aprovechamiento de la memoria debido al indeterminismo y, en consecuencia, es capaz
de completar cómputos en los que otros sistemas como Hugs [JJ97] agotan los recursos.
En cuanto al estilo de programación o expresividad, las principales diferencias vienen
dadas por las restricciones de igualdad, desigualdad y aritméticas, el indeterminismo y los
patrones de orden superior. En [CR98] puede encontrarse una comparativa más extensa
entre los estilos lógico funcional y funcional puro, que se centra en el diseño de parsers
de reconocimiento/generación de lenguaje y que saca especial provecho de los patrones de
orden superior.
2.6. COMPARACIÓN CON OTROS ESTILOS DE PROGRAMACIÓN
57
Con respecto a Prolog, en la práctica, T OY saca especial partido de la pereza en cuanto
a la eficiencia. En cuanto a la expresividad, el potencial de las funciones en general y de las
funciones de orden superior en particular es enorme. Por otro lado, el hecho de disponer de
un sólido sistema de tipos confiere al lenguaje un estilo de programación preciso y limpio.
A continuación vamos a explorar dos ejemplos en los que se compara T OY con Prolog,
en cuanto a la eficiencia en el primero, y en cuanto al estilo en el segundo.
2.6.1.
Ordenación de listas
En este apartado compararemos dos programas para ordenación de listas por generate
& test: se generan permutaciones de la lista dada y se comprueba si están ordenadas. En
la tabla 2.1 se muestra la versión T OY del programa y la versión análoga en Prolog.
VERSIÓN T OY
VERSIÓN PROLOG
sort lst Xs = sort aux (permut Xs)
insert X Ys = [X| Ys]
insert X [Y| Ys] = [Y| insert X Ys]
sort lst(Xs,Ys):-permut(Xs,Ys),
sorted(Ys).
permut([],[]).
permut([X|Xs],Ys):-permut(Xs,Zs),
insert(X,Zs,Ys).
insert(X,Ys,[X|Ys]).
insert(X,[Y|Ys],[Y|Zs]):-insert(X,Ys,Zs).
sorted [] = true
sorted [X] = true
sorted [X,Y|Ys] = sorted [Y|Ys]
<== X<=Y
sorted([]).
sorted([ ]).
sorted([X,Y|Ys]):-X=<Y,
sorted([Y|Ys]).
sort aux Xs = Xs <== sorted Xs
permut [] = []
permut [X| Xs] = insert X (permut Xs)
Cuadro 2.1: Ordenación de una lista por generate & test
En la tabla 2.2 se encuentran los tiempos de ejecución13 de dos objetivos medidos en
milisegundos. En el primero de ellos han de calcularse todas las permutaciones de una
lista con 8 elementos (40320 en total). En este caso Sicstus es unas 22 veces más rápido
que T OY. En el segundo ejemplo se trata de ordenar una lista con 10 elementos, y en este
caso, T OY es unas 77 veces más rápido que Sicstus.
Objetivo
Todas las permutaciones de [0,1,2,3,4,5,6,7]
Ordenación de [4,6,2,8,1,3,9,0,5,7]
Sicstus
990
5400
T OY
21750
70
Cuadro 2.2: Tiempos de ejecución
La explicación del fenómeno, aunque sencilla, no deja de sorprender. En el primer caso
hay que calcular todas las permutaciones sin posibilidad de poda, y Sicstus saca partido de
su máquina abstracta. En el segundo ejemplo, sin embargo, T OY aprovecha la pereza para
hacer un cómputo más inteligente. Genera poco a poco las permutaciones, comprobando
en cada paso si se mantiene el orden. En caso afirmativo sigue generando y si no, deja de
construir la permutación actual y vuelve atrás. De este modo la única permutación que
13
Para las medidas de tiempo se ha utilizado una máquina con procesador Pentium 100, bajo el sistema
operativo Linux.
CAPÍTULO 2. EL SISTEMA T OY
58
genera completamente es precisamente la que está ordenada, mientras que Sicstus genera
realmente todas las permutaciones y posteriormente hace la comprobación.
Por supuesto, en Prolog se podrı́a simular la pereza (de hecho es lo que hace T OY),
pero el programa resultante no serı́a tan sencillo como el que presentamos. En T OY el
programador no se preocupa de programar explı́citamente este comportamiento. Es el
sistema el que se encarga de propiciarlo y ahı́ está su elegancia.
Resaltemos que en un programa funcional no se podrı́a hacer una generación indeterminista de las permutaciones, precisamente porque no se admiten funciones indeterministas. Es decir, el programa T OY de la tabla 2.1, aunque tiene aspecto funcional, no
es ejecutable por un lenguaje como Hugs o Haskell. Es cierto que es posible simular este
comportamiento utilizando la técnica de la “lista de éxitos” propuesta en [Wad85].
Aunque este ejemplo es un ejemplo “escogido” para mostrar las bondades de la pereza,
debemos recordar que la cantidad de problemas interesantes que pueden resolverse por
generate & test no es en absoluto despreciable. Este es sólo un ejemplo ilustrativo escogido
por su sencillez.
2.6.2.
El problema del laberinto revisitado
Nuestro objetivo ahora es mostrar la riqueza expresiva que ofrece la programación
lógico funcional frente a la programación lógica, cuando se utilizan restricciones sobre los
reales. Para ello nos apoyaremos en el Problema del Laberinto que se trató en 2.4. Para este
problema se propuso una solución por generate & test, pero ahora, más que la eficiencia,
nos interesa comparar los estilos de programación. Construiremos una versión Prolog (con
restricciones) para resolver la misma cuestión.
Antes de abordar el problema en sı́ debemos abordar otro aspecto: ¿cómo representar
las regiones en Prolog? (en T OY eran funciones caracterı́sticas, pero ahora no hay funciones). La primera idea que puede surgir es: una región es un par de variables restringidas
(X, Y ). Por ejemplo, el rectángulo definido por las esquinas (0, 0), (2, 2) podrı́a representarse mediante el par de variables (A, B), con las restricciones 0 ≤ A ≤ 2, 0 ≤ B ≤ 2.
Pero esto plantea problemas, ya que si ahora deseamos saber si los puntos (0, 0), (1, 1)
pertenecen a dicho rectángulo (cuya respuesta debe ser afirmativa), debemos verificar que
ambos satisfacen las restricciones pertinentes. Para verificar la primera, el par (A, B) debe
unificarse con (0, 0) y para la segunda con (1, 1), lo cual no es posible y harı́a fallar el
objetivo. Esta representación no funciona de forma adecuada.
Vamos a representar las regiones mediante términos Prolog. Por ejemplo, el rectángulo
anterior se representarı́a con el término rectangle((0, 0), (2, 2)). Ahora necesitaremos un
predicado explı́cito de pertenencia a una región. Definimos el predicado belongs, común
para todas las regiones y otros especı́ficos para las regiones particulares (para nuestro
ejemplo sólo utilizaremos rectángulos):
belongs(Bool,(X,Y),R):R =..[Name | Args],
R1=..[Name,Bool,(X,Y) | Args],
call(R1).
rectangle(true,(X,Y),(A,B),(C,D)):{ A=<X, X=<C, B=<Y, Y=<D }.
rectangle(false,(X,Y),(A,B),(C,D)):{ X<A;
2.6. COMPARACIÓN CON OTROS ESTILOS DE PROGRAMACIÓN
59
A=<X, C<X;
A=<X, X=<C, Y<B;
A=<X, X=<C, B=<Y, D<Y }.
El primer argumento de estos predicados indicará la pertenencia o no de un punto
a una región (true o f alse). Es importante que ocupe el primer lugar para evitar la
concatenación de listas en la construcción de las llamadas en el predicado belongs.
Las definiciones de traslaciones y homotecias no necesitan muchas explicaciones:
translate(Bool,(X,Y),R,(V1,V2)):{ X=X1+V1, Y=Y1+V2 },
belongs(Bool,(X1,Y1),R).
homothety(Bool,(X,Y),R,(C1,C2),K):{ X=C1+K*(X1-C1), Y=C2+K*(Y1-C2) \},
belongs(Bool,(X1,Y1),R).
En este contexto la solución por generate & test al Problema del Laberinto puede ser
la siguiente:
solution(R,Pts,Mvs,Mvs1):- permut(Mvs,Mvs1), check(R,Pts,Mvs1).
permut([],[]).
permut([X | Xs],Ys):- permut(Xs,Xs1), insert(X,Xs1,Ys).
insert(X,[],[X]).
insert(X,[ Y | Ys],[X,Y | Ys]).
insert(X,[Y | Ys],[Y | Zs]):- insert(X,Ys,Zs).
check(R,Pts,Mvs):- doMoves(R,Mvs,R1), avoids(Pts,R1).
doMoves(R,[],R).
doMoves(R,[M | Mvs], union(R,R2)):M=..[N | Args],
R1=..[N,R | Args], doMoves(R1,Mvs,R2).
avoids([],_).
avoids([P | Pts],R):- belongs(false,P,R), avoids(Pts,R).
union(Bool,(X,Y),R1,R2):belongs(B1,(X,Y),R1),
belongs(B2,(X,Y),R2),
or(B1,B2,Bool).
or(true,_,true).
or(_,true,true).
or(false,false,false).
Las versiones T OY y Prolog son bastante similares, pero debemos hacer algunos
comentarios. En Prolog hemos tenido que utilizar caracterı́sticas impuras del lenguaje,
CAPÍTULO 2. EL SISTEMA T OY
60
como son el predicado de descomposición = .. y el de llamada call. Dejando de lado los
inconvenientes teóricos derivados del uso de tales predicados, el hecho es que en la práctica afectan negativamente a la legibilidad del programa. En T OY estos artificios no son
necesarios; las funciones de orden superior son una forma elegante de prescindir de ellos.
Objetivo
Todas las soluciones al laberinto
Sicstus
2320
TOY
1770
Cuadro 2.3: Tiempos de ejecución para el laberinto
En el predicado rectangle ha sido necesario incluir una cláusula explı́cita (la segunda)
para detectar la no pertenencia de un punto al rectángulo en cuestión. Y además en dicha
clausula se ha tenido que tomar la precaución de que las restricciones sean mutuamente
excluyentes para evitar respuestas redundantes. Este problema ni siquiera se plantea en
la versión T OY.
Por último, y aunque no era la meta de este apartado, T OY nuevamente es más
rápido debido a la pereza. En la figura 2.3 se muestran los tiempos de búsqueda de todas
las soluciones al objetivo concreto que se planteó para este problema en 2.4.
Capı́tulo 3
Mecanismo de cómputo
3.1.
Introducción
El mecanismo clásico de cómputo de los lenguajes funcionales está basado en reescritura, mientras que en los lenguajes lógicos, la unificación es la operación fundamental.
Una de las capacidades más notables de los lenguajes lógico-funcionales, quizá la más
importante, es la posibilidad de utilizar las definiciones de funciones de forma reversible
de modo análogo al uso de predicados en Prolog. Esta habilidad exige un mecanismo de
cómputo que, de algún modo, sea capaz de integrar la reescritura y la unificación. En este
sentido se han presentado distintas propuestas, algunas de ellas basadas en reescritura,
con la que se simula la unificación como ESCHER ([Llo94, Llo95]). Pero la más extendida y estudiada es el narrowing o estrechamiento que surge como extensión natural de la
reescritura y que utilizan lenguajes como BABEL ([MR89, MR92]) , BABLOG, ([AG94])
Curry ([HKM95, He97]), y el propio T OY. Informalmente podemos definir el narrowing
como reescritura con unificación. El trabajo [Han94] es una referencia bastante completa
y actual del narrowing como mecanismo operacional de los lenguajes lógico funcionales.
El narrowing tiene, en general, un alto grado de indeterminismo (don’t know) debido
a la elección del redex y de la regla de reescritura a utilizar. En consecuencia, los espacios
de búsqueda generados pueden ser enormes, lo que sugiere la necesidad de encontrar una
estrategia de narrowing que reduzca este espacio, para trasladar la técnica a un sistema
real. En [LLR93] se estudia una estrategia de narrowing perezoso guiada por la demanda,
cuya especificación se presenta como una traducción de las reglas de las funciones a código
Prolog. El mecanismo operacional de T OY está basado inicialmente en el que se propone
en este trabajo. Las desigualdades de T OY se apoyan en [L92, L94] y [AGL94], si bien
la implementación final difiere bastante de estas propuestas. El tratamiento del orden superior está fundamentado en [G94] y [AGL94]. Acerca de la incorporación de restricciones
sobre los números reales en programación lógico funcional puede consultarse [AHL+ 96] y
sobre este tipo de restricciones en el caso concreto de T OY, véase [HLS+ 97].
En este capı́tulo nos ocuparemos del mecanismo operacional de T OY. Estudiaremos
la traducción de funciones y predicados T OY a predicados Prolog, ası́ como el código
(también Prolog) para resolver restricciones (igualdad, desigualdad y aritméticas). Como
vimos en 2.3.10, en realidad un predicado en T OY no es más que una función que devuelve
el valor true y la notación clausal es sencillamente un azúcar sintáctico. Por lo tanto, en
adelante hablaremos únicamente de traducción de funciones.
Los predicados correspondientes a la traducción de un programa T OY y los de resolución de restricciones tienen un alto grado de interdependencia, que en el código se
61
CAPÍTULO 3. MECANISMO DE CÓMPUTO
62
manifiesta en forma de recursión mutua. Por lo tanto, las referencias a apartados tanto
anteriores como posteriores son inevitables en la exposición y será necesario un cierto nivel
de abstracción en algunos puntos de la misma. El orden en la presentación de los contenidos
pretende, en la medida de lo posible, facilitar una comprensión incremental. Quizá resulte
llamativo que el orden superior sea uno de los primeros puntos que se abordan, pero con
ello quedarán explicados algunos conceptos que aparecerán (a veces de forma implı́cita)
en diversos apartados del resto del capı́tulo.
Por otro lado, en esta parte suponemos que el lector está familiarizado con el lenguaje
Prolog, aunque trataremos de explicar brevemente los detalles más crı́ticos o de carácter
técnico que puedan aparecer, ası́ como las caracterı́sticas propias del sistema Sicstus Prolog
que utilicemos.
3.2.
Visión general del proceso de compilación y ejecución
de objetivos
El proceso de compilación de un programa en T OY sigue el esquema de la figura 3.1.
Sobre el programa de usuario se lleva a cabo, en primer lugar, un análisis léxico (autómatas
finitos) y sintáctico. El analizador sintáctico es básicamente una DCG ([SS86, O’K90]),
aunque no se utiliza el traductor de DCG’s incorporado en Sicstus Prolog, sino que se
ha desarrollado un traductor propio basado en [CM87], con bastantes modificaciones;
la mayorı́a de ellas para incluir nuevos parámetros (tablas de compilación y gestión de
errores). En esta fase se hace además un análisis de dependencia funcional en el que se
establece el conjunto de funciones que utiliza cada función en su definición. El inferidor
utiliza estas dependencias para construir el grafo de dependencias, sobre el que se hace
la inferencia de tipos (basada en [AJ93]). Si en todo este proceso no se detectan errores
sintácticos ni de tipos se genera el código intermdedio (3.7).
analisis lexico
analisis sintactico
PROGRAMA
analisis de dependencias
FUENTE
CODIGO
INTERMEDIO
generacion de codigo
CODIGO
OBJETO
inferencia de tipos
Figura 3.1: Proceso de compilación
Sobre el código intermedio se lleva a cabo la generación de código. Para cada una de
las funciones, T OY hace un sofisticado análisis de demanda sobre las reglas que la definen
y genera, como paso intermedio, un árbol definicional ([Ant92]). En este árbol se recoge
toda la información necesaria para la evaluación de la función estructurada de acuerdo con
la demanda de patrones de la misma. De hecho, describe la secuencia de cómputos que
se han de realizar para evaluar una llamada a dicha función. El orden implı́cito en esta
secuencia trata de minimizar el número de pasos de cómputo e intenta no reevaluar las
expresiones. La idea responde a la noción de pereza tı́pica en programación funcional, que
en nuestro caso puede recogerse en el siguiente principio: “evaluar en cada momento sólo
aquello que es estrictamente necesario para continuar el cómputo”. El árbol es construido
explı́citamente (puede mostrarse con el comando /tree como se explicó en 2.2.4) y sobre
él se apoyará la generación del código Prolog correspondiente. En la sección 3.10 veremos
los algoritmo de construcción de árboles y de generación de código.
3.2. VISIÓN GENERAL DEL PROCESO DE COMPILACIÓN Y EJECUCIÓN DE OBJETIVOS63
Una vez compilado el programa fuente, la resolución de objetivos utiliza tres bloques
de código separados en tres archivos distintos, como muestra la figura 3.2.
CODIGO OBJETO
<file.pl>
OBJETIVO
a. lexico, a. sintactico, tipos
+
toycomm.pl
RESPUESTA
+
primitives.pl
Figura 3.2: Resolución de objetivos
El contenido de estos archivos es el siguiente:
<file>.pl contiene propiamente el código producido por la traducción de un programa, es decir, los predicados especı́ficos para la evaluación de las funciones del
programa de usuario y otra información sobre las mismas. El nombre de este archivo
será el mismo que utilizó el usuario para el archivo fuente, pero con extensión .pl
(véase 2.2.3). En la sección 3.10 trataremos en profundidad los predicados correspondientes a la traducción de funciones y otros que contiene este archivo.
toycomm.pl contiene, entre otros, los predicados de resolución de restricciones y
cómputo de formas normales de cabeza. Todos los predicados que se estudiarán en
este capı́tulo, a excepción de los de la sección 3.10, están incluidos en este archivo.
De hecho, trataremos exhaustivamente su contenido. En el apéndice D se muestra el
archivo toycomm.pl tal y como está en el sistema.
primitives.pl almacena el código de las funciones primitivas del sistema. Este archivo será reemplazado por primitivesClpr.pl (nunca están los dos en memoria
simultáneamente), cuando se activan las restricciones sobre reales. Ambos contienen
exactamente las mismas funciones, pero con definiciones diferentes. El apéndice E es
el código del archivo primitives.pl y el F corresponde a primitivesClpr.pl.
El archivo toycomm.pl y los de primitivas son comunes en la ejecución de objetivos.
Una observación de carácter global es que en los predicados del archivo toycomm.pl se usa
frecuentemente el predicado Prolog de corte ‘!’, en algunos casos por razones de eficiencia
para evitar hacer tentativas que fallarán con certeza y provocar el fallo cuanto antes. Estos
tipos de corte son en principio prescindibles. Sin embargo, en otras ocasiones, se utilizan
para establecer alternativas mutuamente excluyentes y son fundamentales, ası́ como el orden de las cláusulas. Un caso particular de este último uso es el de aquellos predicados cuya
última cláusula representa el caso “si no es ninguno de los anteriores” (el predicado hnf
de la sección 3.9 es un ejemplo), y es fundamental que las cláusulas anteriores contengan
el corte.
Nota: Algunos de los sı́mbolos que maneja internamente el sistema como los de función,
susp, apply y otros, en el código real del sistema aparecen precedidos de uno o varios ‘$’.
Este es un detalle técnico para evitar colisiones entre los nombres internos que utiliza el
compilador y los definidos por el usuario. A lo largo de la exposición se han omitido todos
los sı́mbolos ‘$’ para facilitar la legibilidad de la traducción (esta observación debe tenerse
presente al editar archivos generados por T OY).
CAPÍTULO 3. MECANISMO DE CÓMPUTO
64
3.3.
Preliminares
En lo sucesivo suponemos que un programa T OY tiene asociada de forma implı́cita
una signatura Σ = DC ∪ F S, siendo DC el conjunto de sı́mbolos de constructora y F S
el conjunto de sı́mbolos de función. Suponemos además que cada uno de estos sı́mbolos
lleva asociada una aridad; en el caso de las funciones esta aridad se refiere a la aridad de
programa (número de argumentos que tienen las reglas que la definen). Con DC n y F S n
denotaremos a los conjuntos de constructoras y funciones de aridad n respectivamente. En
algunas ocasiones utilizaremos la notación l/n que representa el sı́mbolo l (constructora o
función) de aridad n. Por ejemplo, : /2 es la constructora de listas de aridad 2.
Asumimos también un conjunto infinito de variables V. Las expresiones se construyen
sobre los conjuntos V, DC y F S de acuerdo con la regla de formación que vimos en 2.3.4:
E = X|num|(E1 , ..., En )|c|f |(E1 E2 )
(i)
siendo X ∈ V, num un número entero o real, c ∈ DC, f ∈ F S y E1 , E2 expresiones.
Como en la sintaxis de T OY, utilizaremos cadenas que comienzan por mayúscula para
las variables, y cadenas que comienzan por minúscula para constructoras y funciones.
Para las sustituciones utilizaremos en algunas ocasiones la notación t[X/s] que representa
el término resultante de reemplazar todas las ocurrencias de la variable X por s en t,
donde s y t son términos.
3.4.
Los objetos sintácticos de T OY y su representación Prolog
En la traducción a Prolog que hace el sistema cada expresión T OY tiene una representación concreta como término Prolog (tanto en el código intermedio como en el final).
Por ejemplo, T OY utiliza notación currificada (sin paréntesis en las aplicación de sı́mbolos de función y constructora), mientras que en la representación Prolog no es posible
tal representación (la expresión suc X que tiene perfecto sentido en T OY, en Prolog
deberá representarse como suc(X)).
En gran medida esta representación viene dada por la transformación a primer orden
que realiza el sistema y que veremos en 3.5. Pero esta transformación es relativamente
abstracta y algunos aspectos no quedarán reflejados. En particular, conviene aclarar cómo
se tratan los casos especiales de los números, los caracteres y las tuplas, ası́ como sus tipos
(véase 2.3.2).
Los números son constructoras de aridad 0 cuya representación depende del estado del
proceso de compilación. Antes de que el inferidor haya verificado la corrección de los tipos
en el programa, los enteros se presentan en el formato int(3) y los reales como f loat(4,3).
De este modo el inferidor puede distinguir enteros y reales. En el código intermedio y
en la traducción final ambos tipos se presentan en coma flotante (3 se representa como
3.0), ya que una vez que se sabe que los tipos son correctos, no importa la distinción. La
representación del tipo de un número depende de la información que se tenga del mismo:
Si es un entero el tipo se anotará como num(int).
Si es un real, num(f loat)
Y si puede ser tanto un entero, como un real, tendremos num(A), siendo A una
variable. Esta notación está ı́ntimamente relacionada con la sobrecarga de algunas
3.4. LOS OBJETOS SINTÁCTICOS DE T OY Y SU REPRESENTACIÓN PROLOG65
primitivas que se trató en 2.3.15. Por ejemplo, la función ‘+’ está declarada con
tipo real → real → real, pero como se explicó, puede funcionar con enteros, en
cuyo caso tomará el tipo int → int → int. Esta sobrecarga, se consigue a bajo
nivel con la declaración num(A) → num(A) → num(A), de modo que si el inferidor
tiene suficiente información para decidir que en una llamada a la función el tipo de
alguno de los argumentos o el resultado es de tipo entero, anotará el tipo num(int)
para ese tipo. Como la variable A es compartida por las tres expresiones num(A),
automáticamente el tipo de la llamada se instanciará a num(int) → num(int) →
num(int), que representa el tipo int → int → int como se pretendı́a.
Los caracteres también son constructoras de aridad 0. Tanto en las fases intermedias
como en la traducción final se representan en el formato char(97), donde el número es el
código ASCII correspondiente. Y el tipo será sencillamente char.
Para las tuplas se utiliza internamente la constructora de aridad 2 ‘,’, pero aquı́ surge
un problema técnico ya que Sicstus tiene definido este operador con asociatividad por la
derecha lo cual tiene un efecto indeseado. Por ejemplo, en Sicstus la expresión (1, 2, 3) es
equivalente a (1, (2, 3)), mientras que en T OY estas expresiones son distintas: la primera
es una terna y la segunda un par, cuyo segundo elemento es a su vez un par. T OY resuelve
este problema obviando la asociatividad del operador ’,’ mediante la introducción de la
nueva constructora tup de aridad 1 por encima de ‘,’. Por ejemplo, la tupla (1, 2, 3) se
representa como tup(1, 2, 3), mientras que (1, (2, 3)) se traduce a tup((1, tup((2, 3)))). De
este modo, ambas expresiones son claramente diferenciables por el sistema. El tipo asociado
a una tupla de la forma (e1 , ..., en ) se escribirá como tuple(T1 , ..., Tn ) donde Ti es el tipo
de ei .
Una peculiaridad de las tuplas es que no admiten aplicación parcial porque esto plantearı́a un problema de ambigüedad. Como la misma constructora representa un número
infinito de constructoras (una para cada aridad, según se vio en 2.3.2), no serı́a posible
distinguir entre aplicaciones totales y parciales. Por ejemplo, si utilizásemos ‘,’ como constructora de tuplas, la expresión (, ) 1 2 se puede interpretar como una aplicación total de la
constructora ‘,’ entendida como constructora de pares, pero también como una aplicación
parcial de la misma constructora entendida como la constructora de ternas (en general de
n-tuplas). Este problema en realidad no se plantea porque el usuario puede utilizar tuplas
en sus programas de acuerdo con la sintaxis de la regla de formación, pero no tiene accesible directamente ningún sı́mbolo de constructora de tuplas. En contraste, por
ejemplo para las listas tiene accesible el sı́mbolo ‘:’ que admite aplicación parcial como en
el siguiente cómputo:
TOY> (:) 1 == F, F [2] == L
yes
F == (1:)
L == [ 1, 2 ]
Obsérvese que F (pone 1 como cabeza de la lista que se le pasa como argumento) es una
función que se define utilizando la constructora de listas ‘:’. No es posible hacer algo similar
con las tuplas. Lo análogo serı́a (, ) 1 == F , que no está permitido porque ’,’ (ni ningún
otro sı́mbolo) se reconoce como constructora de tuplas.
La representación del resto de constructoras quedará establecida en la siguiente sección.
Únicamente destacaremos que todas las expresiones que hacen uso de operadores infijos
CAPÍTULO 3. MECANISMO DE CÓMPUTO
66
(tanto constructoras como funciones) en la traducción final aparecen en forma prefija. Por
ejemplo, la lista [1, 2, 3] (equivalente a 1 : (2 : (3 : [ ]))) en la traducción aparecerá como
0 :0 (1,0 :0 (2,0 :0 (3, [ ]))). Los tipos de estas constructoras se representan de manera natural;
el tipo de la lista anterior es sencillamente [int].
En cuanto a las llamadas a función (aplicación total) la representación también quedará establecida en el siguiente apartado, salvo en el caso de las llamadas que aparecen
como argumento de otra función. En este caso, se representará en forma suspendida debido
al sharing que trataremos en 3.6.
3.5.
Orden superior
En el capı́tulo anterior (2.3.6) vimos que T OY es un lenguaje que admite funciones
de orden superior. En esta sección explicaremos cómo se trata el orden superior. La idea
es hacer una transformación a primer orden siguiendo las ideas de [G94] (con ligeras
modificaciones). En el código producido por el sistema el orden superior se simula con
funciones de primer orden. Esta es una transformación sintáctica que produce un programa
T OY a partir de un programa T OY, y no debe confundirse con la traducción que hace el
sistema y que produce código Prolog a partir de un programa T OY. La transformación
puede entenderse como un paso intermedio previo a la traducción.
Antes de formalizar el mecanismo de transformación vamos a explorar la intuición que
encierra. Para ello recordemos la función map que se definió en 2.3.6:
map :: (A -> B) -> [A] -> [B]
map F []
= []
map F [X|Xs] = [F X|map F Xs]
Supongamos que contamos con una función apply que toma dos argumentos (el primero
de tipo funcional) y produce como resultado la aplicación del primero a segundo, sin
preocuparnos por el momento de cómo podrı́a definirse esta función apply. Por ejemplo,
apply add1 zero se evaluarı́a a add1 zero (que a su vez se evaluarı́a a suc zero). Utilizando
apply, la función map podrı́a definirse como:
map F []
= []
map F [X|Xs] = [apply F X|map F Xs]
Aparentemente no se ha hecho un gran avance con la introducción de esta función,
puesto que ella misma es de orden superior. Sin embargo, no es difı́cil intuir que todas
las aplicaciones de orden superior pueden representarse usando apply en esta forma. En
consecuencia hemos acotado o reducido el problema original, ya que el orden superior se ha
“concentrado” en apply y nuestro problema ahora es definir apply. En 3.5.1, veremos esta
definición, pero antes vamos a definir la transformación a primer orden de modo preciso.
Un primer efecto, a la vista de lo anterior, es que el conjunto de sı́mbolos de función F S
de la signatura original del programa se incrementa con el nuevo sı́mbolo apply de aridad
2.
Vamos a desglosar la transformación en dos pasos: en el primero se introducirán los
apply’s necesarios (como en el caso de map) y en el segundo haremos propiamente la
traducción a primer orden. Para explicar cómo se introducen los apply’s en un programa
analizaremos las distintas expresiones del lenguaje, de acuerdo con la regla de formación
3.5. ORDEN SUPERIOR
67
(i) que vimos en 3.3. Las variables, los números y las tuplas (primera, segunda y tercera
alternativas) no son afectados por la introducción de apply’s y quedan igual.
Los sı́mbolos de constructora y función (cuarta y quinta alternativas) tampoco sufren
modificaciones; sin embargo, cada sı́mbolo de constructora de aridad n y cada sı́mbolo
de función de aridad m, en este primer paso van a generar n − 1 y m − 1 nuevas constructoras respectivamente, correspondientes a todas las aplicaciones parciales de dichos
sı́mbolos. Esto quiere decir que el conjunto de sı́mbolos de la signatura original se incrementa con nuevas constructoras. De esta forma, con el nuevo sı́mbolo de función apply y
las nuevas constructoras la signatura original del programa, Σ = DC ∪ F S, se amplı́a en
la transformación a Σ0 = DC 0 ∪ F S 0 , donde:
DC 0 = DC ∪ {φi | φ ∈ DC ∪ F S, 0 ≤ i < aridad(φ)}
F S 0 = F S ∪ {apply/2}
En DC 0 , cada φi es una constructora de aridad i, que se utilizará para representar la aplicación parcial de la constructora o función φ con aridad i. Por ejemplo, para una función
f de aridad 3, además del sı́mbolo de función f de aridad 3, la nueva signatura inclurá las
constructoras f0 , f1 y f2 (para la función map, se incluirán las nuevas constructoras map/0
y map/1).
Las tuplas son una excepción como se vio en 3.4 y no producen ninguna constructora
adicional.
Una primera consecuencia que debe tenerse presente es que una aplicación parcial
de una función es en la traducción, a todos los efectos, una constructora. Eventualmente, si esta aplicación parcial recibe más argumentos hasta convertirse en aplicación
total, las reglas de apply se encargarán de hacer la llamada a la función correspondiente.
Informalmente podemos decir que esta constructora “se convierte” en función al recibir
todos sus argumentos.
La última alternativa de la regla de formación de expresiones es (E1 , E2 ), que permite la
construcción de aplicaciones. A continuación se presentan las distintas formas que pueden
tener las aplicaciones y la introducción de apply’s correspondiente:
c e1 ...em , con c ∈ DC n y m ≤ n, se reemplaza por c re1 ...rem siendo rei el resultado
de introducir apply’s en ei .
f e1 ...em , con f ∈ F S n y m ≤ n, se reemplaza por f re1 ...rem siendo rei el resultado
de introducir apply’s en ei .
f e1 ...en en+1 , con f ∈ F S n , se reemplaza por apply (f re1 ...ren ) ren+1 , siendo rei
el resultado de introducir apply’s en ei . En este caso, la función f está totalmente
aplicada a los n primeros argumentos y el resultado se aplica a su vez al n + 1-ésimo
argumento. Este mecanismo es fácilmente generalizable al caso f e1 ...en en+1 ...en+k
(f ∈ F S n ) que se reemplaza por apply (...(apply(f re1 ...ren ) ren+1 )...) ren+k .
X e, con X ∈ V, se reemplaza por apply X re, siendo re el resultado de introducir
apply’s en e. La generalización de este caso corresponde a la expresión X e1 e2 ...en ,
traducida a apply (...(apply (apply X re1 ) re2 )...) ren .
Los efectos de la introducción de apply’s son, por tanto: se amplı́a la signatura del
programa con nuevos sı́mbolos de constructora y el sı́mbolo de función apply, se introducen llamadas a la función apply en determinados puntos del programa según hemos
CAPÍTULO 3. MECANISMO DE CÓMPUTO
68
@ : ExpΣ −→ ExpΣ0
@(X)
@(num)
@((e1 , ..., em ))
@(c e1 ...em )
@(f e1 ...em )
@(f e1 ...en en+1 )
@(X e)
=X
= num
= (@(e1 ), ..., @(en ))
= cm @(e1 )...@(em )
= fm @(e1 )...@(em )
= apply (f @(e1 )...@(en )) @(en+1 )
= apply X @(e)
X∈V
(números)
(tuplas)
c ∈ DC
f ∈ F Sn, m < n
f ∈ F Sn
X∈V
Cuadro 3.1: Introducción de apply’s
visto y se generan las reglas para apply como veremos en el siguiente apartado. Formalmente podemos definir una función de transformación para la introducción de apply’s
@ : ExpΣ → ExpΣ0 tal y como aparece en la tabla 3.1.
El resultado de esta primera transformación ha generado expresiones en la signatura
extendida Σ0 . Podemos considerar el rango de esta transformación como un subconjunto
de @ ExpΣ0 cuya regla de formación es:
e = X|num|(e1 , ..., en )|c e1 ...en |f e1 ...en
donde ei ∈ @ ExpΣ0 , i = 1..n; c ∈ DC 0n y f ∈ F S 0n . Nótese que en estas expresiones
todas las aplicaciones son totales y lo único que falta para convertirlas en expresiones
de primer orden es eliminar la sintaxis currificada, esto es, introducir los paréntesis y las
comas apropiadas. Este es el cometido de la función f o (first order) de la tabla 3.2.
f o : @ ExpΣ0 −→ T ermΣ0
f o(X)
f o(num)
f o((e1 , ..., em ))
f o(c e1 ...em )
f o(f e1 ...em )
=X
= num
= (f o(e1 ), ..., f o(en ))
= c(f o(e1 ), ..., f o(em )
= f (f o(e1 ), ..., f o(em )
X∈V
(números)
(tuplas)
c ∈ DC 0
f ∈ F S0
Cuadro 3.2: Transformación de la notación currificada
Componiendo @ con f o tenemos que f o · @ : ExpΣ −→ T ermΣ0 es una traducción a
primer orden de ExpΣ . El conjunto T ermΣ0 es el conjunto términos de primer orden sobre
Σ0 .
La función map con la que iniciamos la sección traducida a primer orden queda de este
modo:
map(F, [])
= []
map(F, [X|Xs]) = [apply(F, X)|map(F, Xs)]
Estudiemos ahora la función apply.
3.5.1.
Reglas de apply
La función función apply existe en el sistema, pero ni su definición ni su tipo, son
accesibles para el usuario (no está declarada como primitiva). De hecho el usuario no
3.5. ORDEN SUPERIOR
69
podrá hacer uso de ella directamente; no se puede por ejemplo, intentar evaluar una
expresión como apply add1 zero. No obstante, esto no limita el poder expresivo en modo
alguno; la expresión anterior es equivalente a add1 zero. El hecho de que apply sea una
función tan especial se debe a que no admite tipo en nuestro sistema de tipos1 . Esto quiere
decir que no es una función T OY en sentido estricto, pero no que su incorporación pueda
abrir agujeros en el sistema de tipos, sino que debe ser tratada de forma especial. Por eso
se trata a bajo nivel y no es visible al usuario. Por lo demás, apply puede traducirse como
una función más como se verá en 3.10.6.
Como curiosidad, aunque el usuario no tiene acceso directo a la función interna apply
nada impide que la defina él, incluso con el mismo nombre:
apply :: (A -> B) -> A -> B
apply F X = F X
El sistema se encargará de evitar las colisiones de nombres y ahora el usuario dispone de
su propio apply con el tipo correspondiente. No debe sorprender que en la traducción de
esta función T OY utilice su apply interno. En lo que sigue nos referimos al apply de bajo
nivel de T OY y no al que se acaba de definir.
Las reglas de apply no son fijas en el sistema, sino que dependen de las funciones y
constructoras que contenga el programa fuente de usuario. Por lo tanto estas reglas se
generan en tiempo de compilación del modo siguiente:
Para cada constructora c ∈ DC n se generan n reglas de apply correspondientes a
todas las posibles aplicaciones parciales y totales de la misma.
Para cada función f ∈ F S n se generan n reglas de apply, donde n − 1 corresponden
a las aplicaciones parciales y una a la aplicación total.
apply(ck (X1 , ..., Xk ), Xk+1 )
= ck+1 (X1 , ..., Xk , Xk+1 ) c ∈ DC n , k + 1 < n
apply(cn−1 (X1 , ..., Xn−1 ), Xn ) = c(X1 , ..., Xn )
c ∈ DC n
apply(fk (X1 , ..., Xk ), Xk+1 )
= fk+1 (X1 , ..., Xk , Xk+1 ) f ∈ F S n , k + 1 < n
apply(fn−1 (X1 , ..., Xn−1 ), Xn ) = f (X1 , ..., Xn−1 , Xn )
f ∈ F Sn
Cuadro 3.3: Reglas para apply
En la tabla 3.3 se encuentran, en notación de primer orden, las reglas de apply producidas para un programa sobre una signatura determinada Σ = DC ∪ F S. Obsérvese
que estas reglas utilizan sı́mbolos de constructora de la signatura extendida Σ0 . La última
regla corresponde a la aplicación de la constructora fn−1 a un argumento, siendo f una
función de aridad n. En este caso el resultado es una aplicación (total) de la función f .
Obsérvese que en las reglas de apply no hay ninguna aplicación parcial, todas son totales.
Este es un conjunto de reglas de primer orden que, junto con la transformación a primer
orden del apartado anterior, nos permiten expresar cualquier programa T OY en notación
de primer orden.
Si tenemos un programa con la declaración de naturales:
data nat = zero | suc nat
1
No admite tipo en el sistema de tipos de Hindley-Milner.
CAPÍTULO 3. MECANISMO DE CÓMPUTO
70
y la (ya conocida) función map, las reglas para apply producidas son las siguientes:
apply(suc0 , X)
= suc(X)
apply(map0 , F )
= map1 (F )
apply(map1 (F ), X) = map(F, X)
Nótese que la primera regla produce como resultado la constructora map1 (F ) para
la que se utiliza el sı́mbolo de constructora map1 , mientras que la segunda devuelve una
llamada a la función map (de aridad 2). Esta distinción será fundamental en la traducción
que se hará de la función apply en 3.10.6.
Con las reglas primera y tercera del ejemplo anterior es sencillo ver que apply no
admite tipo. La primera de ellas devuelve suc(X) que es de tipo nat, mientras que la
última devuelve map(F, X) cuyo tipo será de la forma [ ]. Ambos tipos son incompatibles.
Una observación importante es que se generan las reglas para apply correspondientes a
todas las constructoras, funciones primitivas y funciones definidas por el usuario, pero no
las del propio apply. Estas reglas no serán necesarias puesto que, como se vio, el usuario
no puede hacer uso directamente de la función apply y en la transformación nunca se
producirá una llamada a apply que utilice apply en alguno de sus argumentos.
3.5.2.
Información sobre constructoras y funciones
En tiempo de ejecución será necesario conocer la aridad y los tipos de los distintos
sı́mbolos de constructora y función. Por ejemplo, cuando se lanza un objetivo, para procesarlo es necesario conocer el significado de cada sı́mbolo ası́ como los tipos asociados,
para verificar la consistencia (sintáctica y de tipos) del mismo. Por este motivo durante el
proceso de traducción, T OY genera toda esta información que formará parte del código
objeto producido.
Para cada sı́mbolo de constructora del programa de usuario (no de la signatura extendida) se genera una cláusula de la forma:
const(< N ombre >, < Aridad >, < T ipo >, < T ipo destino >).
donde < N ombre > es el nombre de la constructora, < Aridad > representa la aridad de
la aplicación total de la misma, < T ipo > es el tipo que tiene asociado y el argumento
< T ipo destino > es el tipo de la constructora aplicada totalmente. Este último tipo es
deducible de < T ipo > y se incluye expresamente por motivos de eficiencia. Por ejemplo,
para la constructora de naturales suc se genera la cláusula:
const(suc, 1, ->(nat, nat), nat)
Obsérvese que para escribir el tipo se utiliza el mismo operador -> que en T OY y siempre
en forma prefija. Esta cláusula representa realmente dos constructoras (de la signatura
extendida): suc0 y suc. En general, si la aridad es n, la cláusula representará n + 1 constructoras como veremos en breve, al estudiar el predicado constructor.
De forma análoga, para cada sı́mbolo de función del programa de usuario se genera
una cláusula (que representará todas las constructoras correspondientes a las aplicaciones
parciales de la misma, ası́ como la función correspondiente a la aplicación total). Esta
cláusula es de la forma:
f unct(< N ombre >, < Aridad de programa >, < Aridad del tipo >, < T ipo >,
< T ipo destino >).
3.5. ORDEN SUPERIOR
71
cuyos argumentos tienen un significado similar a los de las cláusulas const. Por ejemplo,
para la función map se genera la cláusula:
funct(map, 2, 2, ->(->(A, B), ->(:(A, []), :(B, []))), :(B, [])).
Para los tipos predefinidos del sistema se generan también las cláusulas correspondientes a sus constructoras. Las siguientes cláusulas aparecen siempre en la traducción:
const(’,’, 2, ->(A, ->(B, ’,’(A, B))), ’,’(A, B)).
const(tup, 1, ->(’,’(A, B),
tuple(’,’(A, B))), tuple(’,’(A, B))).
const([], 0, :(A, []), :(A, [])).
const(:, 2, ->(A, ->(:(A, []), :(A, []))), :(A, [])).
const(char, 1, ->(A, char), char).
const(true, 0, bool, bool).
const(false, 0, bool, bool).
De manera análoga, para las funciones primitivas se generan las cláusulas apropiadas.
Las siguientes, son un ejemplo (hay muchas más):
funct(+, 2, 2, ->(num(A), ->(num(A), num(A))), num(A)).
funct(<, 2, 2, ->(num(A), ->(num(A), bool)), bool).
funct(==, 2, 2, ->(A, ->(A, bool)), bool).
funct(flip, 3, 3, ->(->(A, ->(B, C)), ->(B, ->(A, C))), C).
La propia función apply produce la siguiente cláusula:
funct(apply, 2, 2, ->(->(->(A, B), A), B), B).
Para el procesamiento sintáctico de los objetivos y la salida de respuestas, el sistema
también necesita conocer el nombre de los operadores infijos, ası́ como su asociatividad
y su precedencia. Esta información se genera en la forma de cláusulas inf ix. Para los
operadores infijos predefinidos se generan las siguientes cláusulas (que se completarán con
los definidos por el usuario en caso de existir):
infix(^, noasoc, 90).
infix(**, noasoc, 90).
infix(/, noasoc, 80).
infix(*, left, 80).
infix(+, left, 70).
infix(-, left, 70).
infix(<, noasoc, 50).
infix(<=, noasoc, 50).
infix(>, noasoc, 50).
infix(>=, noasoc, 50).
infix(==, noasoc, 20).
infix(/=, noasoc, 20).
Con frecuencia será necesario hacer un test en tiempo de ejecución sobre la categorı́a
sintáctica a la que pertenece una determinada expresión del lenguaje: si es variable, término
que comienza por constructora o llamada a función. En el primer caso el predicado var de
aridad 1, standard en Prolog, es suficiente y el último caso no plantea problemas porque
72
CAPÍTULO 3. MECANISMO DE CÓMPUTO
todas las llamadas a función sobre las que se hará este test aparecerán en forma suspendida,
que es fácilmente reconocible como veremos en 3.6. El segundo caso podrı́a resolverse por
exclusión, sin embargo, esto no serı́a lo más eficiente. Además, por regla general, en el caso
de un término construido nos interesará conocer el sı́mbolo de constructora de la raı́z (el
functor principal), su aridad y sus argumentos. Por este motivo en T OY se incluyen dos
predicados especı́ficos para este test, definidos en la tabla 3.4.
constructor(C,C/0):-number(C),!.
constructor(T,C/N):functor(T,C,N),!,
(
const(C, , , ),!
;
funct(C,Ar, , , ),!,N<Ar
).
constructor(C,C/0,[]):-number(C),!.
constructor(T,C/N,Args):functor(T,C,N),!,
(
const(C, , , ),!
;
funct(C,Ar, , , ),!,N<Ar),
T=..[ | Args].
Cuadro 3.4: Chequeo de términos construidos
Ambos predicados se llaman constructor, pero uno es de aridad 2 y el otro de aridad
3. El primero comprueba que el functor principal es un sı́mbolo de constructora, o bien,
un sı́mbolo de función aplicado parcialmente (que corresponde a una de las constructoras
nuevas introducidas en la signatura extendida, 3.5). En ambos casos devuelve el nombre
del functor y aridad con la que aparece en la forma N/A. El segundo realiza la misma tarea,
pero además, en el tercer argumento devuelve la lista de argumentos correspondiente. Es
claro que el primer predicado “está contenido” en el segundo, ya que éste devuelve más
información. Sin embargo, la lista de argumentos que devuelve el segundo está construida
por descomposición (= ..), que es una operación costosa en Prolog y esta lista no es siempre
necesaria. Por razones de eficiencia se mantienen ambas versiones.
3.6.
El sharing o compartición de variables
En general, el sharing es un mecanismo que pretende evitar la reevaluación de las
expresiones que se pasan como argumentos a las funciones. No obstante, en T OY el
sharing es imprescindible si se adopta una semántica por call-time choice (como es
nuestro caso) debido al uso de funciones indeterministas. En 2.3.7 se justificó esta necesidad
con un ejemplo (funciones coin y double).
En cuanto al uso del sharing para evitar reevaluaciones, consideremos la función double
definida en 2.3.7 por la regla double X = X+X, y supongamos que queremos evaluar double
3*4. Si simplemente hacemos la sustitución de X por 3 ∗ 4 al aplicar la regla de la función,
3.7. EL CÓDIGO INTERMEDIO
73
la evaluación se reducirı́a a la expresión (3∗4)+(3∗4). Y ahora, para evaluar esta expresión
es necesario evaluar la expresión 3 ∗ 4 dos veces.
Esta reevaluación podrı́a evitarse evaluando primero los argumentos de la llamada y
luego el cuerpo de la función. En el ejemplo anterior efectivamente el problema queda
resuelto: si en la llamada double 3*4 primero reducimos 3 ∗ 4 a 12 y después evaluamos
el cuerpo de la función con el resultado 12, la expresión que resulta es 12 + 12, en la que
no hay nada que se reevalue. Sin embargo, esta solución destruye la pereza del lenguaje:
supongamos la función tres definida por la regla tres X = 3 y la llamada tres 5*4. Claramente en este ejemplo no es necesario evaluar el argumento 5 ∗ 4 para evaluar la llamada.
En un lenguaje perezoso la llamada tres 5*4 deberı́a reducirse directamente a 3 sin evaluar
el argumento 5 ∗ 4.
La solución a este dilema consiste en pasar las expresiones sin evaluar a las funciones
como en el primer ejemplo de double, pero de modo que cuando se reduzca una expresión
el resultado pase a todas las apariciones de es expresión. En los lenguajes funcionales
esto se consigue mediante grafos de evaluación. En T OY la implementación del sharing
está basada en la técnica de las suspensiones ([Che90]) para representar llamadas a función,
y que se apoya a su vez, en el uso de variables lógicas. Una suspensión es un término Prolog
que representa una llamada a función. Tiene la forma
susp(Fun,Args,R,S)
donde F un es el nombre de la función, Args es la lista de argumentos de la misma, R es
el resultado de la evaluación (si ha sido evaluada, variable en otro caso) y S es un flag que
indica si la llamada ha sido evaluada (S = hnf ) o no (S variable). La evaluación de una
función puede dar como resultado una variable, por lo que el hecho de que R sea variable
no implica que la evaluación no se haya realizado (ni lo contrario); de ahı́ la necesidad
del flag S. Todas las llamadas a funciones que aparecen en argumentos de alguna función
aparecen en forma suspendida, puesto que son susceptibles de tener múltiples ocurrencias
en el cuerpo o las restricciones de esta segunda función.
En el ejemplo anterior, el argumento que le pasamos a double es susp(∗, [3, 4], R, S)
con R y S variables nuevas. La expresión a evaluar queda entonces susp(∗, [3, 4], R, S) +
susp(∗, [3, 4], R, S). Para evaluar una suma es necesario reducir sus argumentos. Al evaluar
el primero de ellos, que es la primera suspensión, resulta susp(∗, [3, 4], 12, hnf ), es decir,
las variables R y S se instancian y la expresión original queda susp(∗, [3, 4], 12, hnf ) +
susp(∗, [3, 4], 12, hnf ). El segundo argumento ha quedado reducido automáticamente sin
evaluarlo expresamente y ahora la suma tiene sus dos argumentos evaluados y puede
reducirse a 24.
3.7.
El código intermedio
En 3.5 detallamos el mecanismo para transformar un programa T OY a sintaxis de
primer orden. El código intermedio es básicamente el resultado de introducir formas suspendidas sobre esta transformación. En T OY todas las suspensiones se generan en tiempo
de compilación y aparecen de forma explı́cita en la traducción final.
Para introducir suspensiones en las funciones partimos de sintaxis general de una regla
de función traducida a primer orden :
f (t1 , ..., tn ) = e <== e1 3e01 , ..., em 3e0m
CAPÍTULO 3. MECANISMO DE CÓMPUTO
74
donde f ∈ F S 0 y t1 , ..., tn , e, e1 , e01 , ..., em , e0m ∈ T erm0Σ .
Sobre estas reglas transformadas T OY incorpora las suspensiones en todas las llamadas a función que aparecen en el cuerpo o las restricciones ([Che90, LLR93]), ya que
potencialmente pueden aparecer a su vez en otra llamada a función. En la tabla 3.5 se
define la función sf , que introduce las suspensiones sobre los términos de primer orden.
Utilizando esta función definimos sf r que introduce suspensiones en los lados derechos y
restricciones de las reglas de programa (también en primer orden). En tiempo de ejecución
el sistema incorpora mecanismos de evaluación o activación de estas suspensiones.
Forma suspendida de
sf (X)
=
sf (num)
=
sf ((e1 , ..., en ))
=
sf (c(e1 , ..., en )) =
sf (f (e1 , ..., en )) =
Forma suspendida de
los términos de primer orden:
X
num
(sf (e1 ), ..., sf (en ))
c(sf (e1 ), ..., sf (en ))
susp(f, [sf (e1 ), ..., sf (en ), R, S])
las reglas de programa:
X∈V
(números)
(tuplas)
c ∈ DC 0
f ∈ F S0
sf r(f (t1 , ..., tn ) = e <== e1 3e01 , ..., em 3e0m ) ≡
f (t1 , ..., tn ) = sf (e) <== sf (e1 )3sf (e01 ), ..., sf (em )3sf (e0m )
Cuadro 3.5: Introducción de suspensiones
Por ejemplo, el código intermedio producido para las reglas de la función map (3.5),
es el siguiente:
map(F, [ ])
= []
map(F, [X|Xs]) = [susp(apply, [F, X], R, S) | susp(map, [F, Xs], R0 , S 0 ]
El proceso de traducción (3.10) trabajará sobre este código intermedio para producir
el código Prolog correspondiente a la traducción.
3.8.
Las restricciones de desigualdad estricta
Las variables T OY son variables lógicas pero no son exactamente iguales que las variables Prolog, fundamentalmente porque sobre una variable Prolog no se pueden imponer
restricciones de desigualdad estricta, mientras que sobre las de T OY sı́. En Prolog existe
el predicado \== de aridad 2 que examina la desigualdad de dos términos, pero no impone
la restricción de que sean distintos. Por ejemplo, el siguiente objetivo tiene éxito en Prolog
X \== Y, X = Y . La primera parte del objetivo chequea que la variables X e Y no son el
mismo objeto sintáctico y tiene éxito; la segunda parte simplemente unifica las variables.
En T OY el objetivo análogo X /= Y, X == Y falla porque la desigualdad /= impone la
restricción de que X e Y sean distintas y la segunda parte intenta resolver una restricción
de igualdad sobre ellas2 . Por otro lado, nuestro lenguaje admite funciones, cuya evaluación
puede ser necesaria para resolver una desigualdad. Por ejemplo, la desigualdad 3 /= 1 + 2
2
En el sistema Sicstus Prolog existe el predicado dif de aridad 2 que aproxima nuestra semántica de
la desigualdad. Existe una versión experimental de T OY que lo utiliza, pero no forma parte de la versión
final porque nuestra noción de respuesta para un objetivo se ve afectada; en concreto altera nuestra noción
de desigualdad en forma resuelta (en breve veremos lo que son las formas resueltas). Por otra parte, el
predicado dif no es un predicado standard en los sistemas Prolog.
3.8. LAS RESTRICCIONES DE DESIGUALDAD ESTRICTA
75
falla porque la llamada a la función (primitiva) ‘+’ se evalúa a 3, mientras que en Prolog
la desigualdad 3 \ == 1 + 2 tiene éxito porque los miembros son sintácticamente distintos.
Ası́ pues, aunque las variables T OY se representen como variables Prolog, la existencia
de desigualdades enriquece el significado de variable lógica en T OY. Esta “información
extra” exige unas labores de mantenimiento. En particular, el sistema tiene que ser capaz
de almacenar restricciones. Realmente, con nuestra semántica para las desigualdades, sólo
será necesario almacenar las formas resueltas que tienen el aspecto X /= t, donde X es una
variable y t es una forma normal (variable o término construido sin llamadas a función).
El resto de desigualdades se procesan hasta obtener formas resueltas.
Sobre el modo de almacenamiento hay varias alternativas, como la que implementa
BABLOG ([AG94, AGL94]). La idea es que una variable X sin restricciones se representa
como la variable Prolog X; cuando se añade la restricción X /= t1 , X se unifica con un
término de la forma neq(RX, [t1 |L]), donde la variable nueva RX representa la variable X
y el segundo argumento es una lista abierta que almacena las desigualdades asociadas a
X. Esta lista abierta permite añadir nuevas restricciones. Por ejemplo, con la restricción
X /= t2 tendrı́amos neq(RX, [t1 , t2 |L0 ]) (se unifica L con [t2 |L0 ]). Este método tiene la
ventaja de que la información sobre las restricciones de una variable se localiza junto con
la propia variable. Pero si tenemos una restricción como X /= Y el término al que se liga
X serı́a de la forma neq(RX, [Y |L]) y el de Y serı́a neq(RY, [X|L0 ]). Pero entonces, en
neq(RX, [Y |L]) en lugar de Y tendriamos su representación neq(RY, [X|L0 ]) que incluye
a la propia variable X. El resultado es una ligadura cı́clica (en Prolog se admiten por la
ausencia del occurs-check) que complica notáblemente el manejo de las desigualdades.
Las versiones distribuidas de T OY utilizan un almacén explı́cito para mantener las
desigualdades. Este almacén consiste en una lista de la forma
[X : [t1 , ..., tn ], Y : [s1 , ..., sm ], ...]
que representa las desigualdades X /= t1 , ..., X /= tn , Y /= s1 , ..., Y /= sm , siendo t1 , ..., tn ,
s1 , ..., sn formas normales. Utilizando esta representación la unificación de dos variables
T OY X, Y con conjuntos de restricciones CX , CY sólo es posible si sus restricciones de
desigualdad lo permiten, en cuyo caso se hará la unificación Prolog X = Y que las convierte
en una misma variable, a la que asociaremos el conjunto de restricciones CX ∪ CY . Si en el
almacén existiese la restricción X /= Y , por ejemplo, si tuviese la forma [X : [Y ], Y : [X]],
la unificación no serı́a posible. Obsérvese que, como variables Prolog, esta unificación sı́ que
es posible y es T OY el que debe examinar las restricciones para impedir que se lleve a
cabo. En el siguiente apartado se detallará la operación de unificación. En lo sucesivo
hablaremos de unificación T OY para la referirnos a la unificación de variables T OY (que tiene en cuenta las restricciones de desigualdad). Para la unificación de
variables Prolog diremos simplemente unificación o ligadura.
Veamos mediante un ejemplo el funcionamiento de los almacenes:
Ejemplo:
Supongamos que queremos resolver el objetivo:
suc zero}, X
X /= Y, Y /= Z , |Y == {z
| ==
{z Z}
{z
}
|
(1)
(2)
(3)
Tras evaluar (1), en el almacén de desigualdades tendremos:
[X : [Y ], Y : [X, Z], Z : [Y ]]
CAPÍTULO 3. MECANISMO DE CÓMPUTO
76
La información del almacén no provoca fallo en la resolución de (2) que produce:
[X : [suc zero], Z : [suc zero]]
Y por último al evaluar (3) obtenemos en el almacén:
[X : [suc zero, suc zero]]
(se unen los conjuntos (listas) de desigualdades asociadas a X e Y ).
La respuesta que produce el sistema es:
X == Z, Y == suc zero, {X /= suc zero}
que es la que cabrı́a esperar.
¥
Como se puede apreciar esta representación almacena para cada variable todas las
restricciones que le afectan, lo que implica cierta redundancia en desigualdades entre variables (X /= Y se almacena como [X : [Y ], Y : [X]]). Con esta redundancia, sin embargo,
se mejora notablemente la eficiencia en el acceso a las desigualdades asociadas a una
variable. Y por otro lado, el usuario nunca advertirá estas repeticiones, puesto que el
proceso de salida de respuestas se encarga de minimizar la información en las respuestas,
y en particular elimina la redundancia (véase el apéndice I).
Para mantener el almacén de desigualdades, los predicados de la traducción contienen
dos argumentos extra que notaremos por: Cin (constraints in) y Cout (constraints out),
que representan el almacén de restricciones entrada y el de salida respectivamente. Con
esta representación las restricciones asociadas a una variable no se encuentran junto con
la misma variable, sino en la estructura almacén. Sin embargo, la ventaja de este método
es que no se necesita desreferenciaciación porque las variables T OY se representan directamente mediante variables Prolog y esto supone, en general, una ganancia en la eficiencia
del sistema, como se ha probado experimentalmente.
3.8.1.
Gestión de las desigualdades
El mantenimiento de las desigualdades requiere algunas operaciones, que han sido
pensadas como interface de comunicación con el almacén para conseguir un cierto nivel
de ocultamiento y de independencia de la estructura concreta del mismo. Este interface
consta de tres operaciones:
añadir una restricción,
extraer las restricciones asociadas a una variable y
unificar dos variables.
Con estas operaciones la estructura del almacén es transparente al resto del código,
ya que sólo ellas acceden directamente a la representación concreta del mismo. Por este
motivo son ellas las encargadas de realizar de forma implı́cita otra labor: mantener la
consistencia de las desigualdades. Por ejemplo, si X es una variable que tiene asociada la
restricción X /= [ ], la operación de unificación debe fallar al intentar unificar X con [ ]
y entre las desigualdades asociadas a una variable X no debe encontrarse la desigualdad
X /= X.
La especificación de las operaciones de inserción y extracción es la siguiente:
3.8. LAS RESTRICCIONES DE DESIGUALDAD ESTRICTA
77
addCtr(X, T erm, Cin, Cout) ⇔ Cout es el almacén resultante de añadir la desigualdad
X /= T erm al almacén de entrada Cin, donde X es una variable y T erm una forma normal.
extractCtr(X, Cin, Cout, CX) ⇔ CX es la lista de restricciones asociadas a la variable
X en el almacén Cin, y Cout es el almacén resultante de eliminar X : CX de Cin.
El código se muestra en la tabla 3.6.
addCtr(X,Term,[ ],[X:[Term]]):- !.
addCtr(X,Term,[Y:CY|RCin],[Y:[Term|CY]|RCin]):- X==Y,!.
addCtr(X,Term,[C|RCin],[C|RCout]):- addCtr(X,Term,RCin,RCout).
extractCtr( ,[ ],[ ],[ ]):- !.
extractCtr(X,[Y:CY|R],R,CY):- X==Y,!.
extractCtr(X,[Y:CY|RCin],[Y:CY|RCout],CX):- extractCtr(X,RCin,RCout,CX).
Cuadro 3.6: Inserción y extracción de desigualdades
La unificación de variables es algo más compleja. La especificación es:
unif yV ar(X, Y, Cin, Cout) ⇔ la restricción X /= Y no está presente en Cin, liga X
con Y convirtiéndolas en la misma variable a la que asociamos en Cout la unión de las
desigualdades asociadas a las antiguas X e Y .
unifyVar(X,Y,Cin,Cout):-X==Y,!,Cout=Cin.
unifyVar(X,Y,Cin,Cout):extractTwoCtr(X,Y,Cin,Cout1,CX,CY),
X=Y,
!,
update(X,CX,CY,Cout1,Cout).
Cuadro 3.7: Unificación de variables
En la tabla 3.7 se muestra el código del predicado unif yV ar. En la primera cláusula,
si las variables a unificar son la misma no hay nada que hacer excepto devolver intactas
las desigualdades de entrada. El caso interesante es el que se trata en la segunda cláusula.
Aquı́ se utilizan los predicados auxiliares extractT wo y update cuyo código aparece en la
tabla 3.8. El primero hace la extracción simultánea (en un solo recorrido de la lista) de las
restricciones asociadas a dos variables X e Y . Serı́a igualmente correcto utilizar dos veces el
predicado extractCtr, pero menos eficiente. update genera la nueva lista de desigualdades
asociada a una variable X que se ha ligado con otra Y . Esta lista es el resultado de unir
las desigualdades de X con las de Y (concatenación de listas), pero además en el recorrido
se comprueba que no existı́a la restricción X /= Y , que tras la ligadura X = Y aparecerı́a
como X /= X. El orden de los predicados en el cuerpo de la segunda cláusula de unif yV ar
es especialmente crı́tico para que todo funcione correctamente.
Las operaciones de manejo de los almacenes de desigualdades que acabamos de exponer
mantienen la consistencia en el sentido que se indicaba al principio de este apartado. Son
planteables algunas ideas más en cuanto a consistencia de restricciones. Supongamos, por
ejemplo, el siguiente tipo de datos:
CAPÍTULO 3. MECANISMO DE CÓMPUTO
78
extractTwoCtr( , ,[ ],[ ],[ ],[ ]).
extractTwoCtr(X,Y,[Z:CZ|R],Cout,CX,CY):(
X==Z,!,CX=CZ,extractCtr(Y,R,Cout,CY)
;
Y==Z,!,CY=CZ,extractCtr(X,R,Cout,CX)
).
extractTwoCtr(X,Y,[Ctr|R],[Ctr|R1],CX,CY):extractTwoCtr(X,Y,R,R1,CX,CY).
update(Y,[ ],CY,Cin,Cout):- insertCtrs(Y,CY,Cin,Cout).
update(Y,[T|Ts],CY,Cin,Cout):Y \ ==T,!,update(Y,Ts,[T|CY],Cin,Cout).
insertCtrs( ,[ ],Cin,Cin):-!.
insertCtrs(Y,CY,Cin,[Y:CY|Cin]).
Cuadro 3.8: Extracción simultánea de restricciones y unión de restricciones
data colour = red | green | blue
El objetivo X /= red, X /= green tiene éxito y en la respuesta muestra como restricciones X /= red y X /= green. Esta respuesta es correcta, pero intuitivamente “el sistema
podrı́a llegar más lejos” y devolver como respuesta X == blue. El razonamiento informalmente es sencillo: X es una variable del tipo colour (esto lo deduce el inferidor), por
lo que puede tomar los valores red, green o blue (y sólo estos); como además no es red ni
green, tiene que ser blue.
Otro ejemplo más llamativo puede ser el objetivo X /= red, X /= green, X /= blue para
el que T OY obtiene un éxito devolviendo como como restricciones las mismas tres que se le
plantean. Por un razonamiento similar al anterior, este objetivo deberı́a provocar un fallo.
Lo que tiene de particular este tipo de datos es que es finito, es decir, hay un número finito
de valores de este tipo (tres en este caso): representa un dominio finito. Las restricciones
de dominio finito están ampliamente estudiadas y son las que ofrecen una justificación
formal a los argumentos intuitivos de consistencia que veı́amos ([V89, FHK+ 93, Car95]).
Sin embargo, T OY no incorpora restricciones de dominio finito3 , por lo que en
general el tratamiento de la desigualdad en T OY sólo es adecuado para dominios infinitos.
Para dominios finitos puede ser incluso incorrecto4 .
La excepción a este tratamiento de la desigualdad en el caso de dominios finitos es el
tipo de los booleanos para el que T OY, debido a lo frecuente de su uso, da un tratamiento
especial. Éste es también un tipo finito con dos valores: true y f alse. Las restricciones en
este caso son parcialmente tratadas como restricciones de dominio finito. Ası́ por ejemplo,
el objetivo X /= true produce la respuesta X == f alse, y el objetivo X /= true, X /=
f alse produce un fallo5 . El sistema incorpora código especı́fico para el tratamiento de este
3
Existen versiones no distribuidas del sistema, en las que se han incluido restricciones de dominio finito
de forma experimental, pero por el momento no forman parte de la versión definitiva.
4
Por ejemplo, dado el predicado p : − X /= red, X /= blue, X /= green, el objetivo p devuelve éxito
sin más (sin restricciones), lo cual es incorrecto.
5
Desde el punto de vista lógico este modo de operar corresponde al principio del tercio excluido.
3.9. CÓMPUTO DE FORMAS NORMALES DE CABEZA (HNF)
79
tipo de restricciones que veremos en 3.12.
3.9.
Cómputo de formas normales de cabeza (hnf )
Una forma normal de cabeza (f.n.c., en lo sucesivo) es cualquier expresión del lenguaje
que no sea una llamada a función, es decir, una variable o una expresión que comienza por
constructora (aunque sus argumentos contengan llamadas a función). En las respuestas
a un objetivo, T OY presenta formas normales (nunca aparece una llamada a función
sin evaluar). Sin embargo T OY no incorpora ninguna operación explı́cita de reducción a
forma normal, sino que dichas formas se consiguen mediante sucesivas reducciones a f.n.c.
de las expresiones, los argumentos de las expresiones, los argumentos de los argumentos,
etc. El mecanismo operacional del sistema hace que estas reducciones se realizan a medida
que avanza el cómputo y sólo cuando son necesarias (en general es posible evaluar una
llamada a función sin evaluar completamente sus argumentos). Y ésta es la idea de la
pereza, que está estrechamente relacionada con el cómputo de formas normales de cabeza.
La reducción a f.n.c. es una de las operaciones fundamentales y más frecuentes que
realiza el sistema. Es similar a la operación de reducción de los lenguajes funcionales
puros. De hecho, las diferencias vienen dadas por la reversibilidad de las funciones en
programación lógico-funcional, el indeterminismo y las desigualdades de nuestro sistema.
En un programa que no haga uso de las restricciones y en el que las funciones sean
deterministas la evaluación a f.n.c. tiene un comportamiento funcional.
La especificación del predicado hnf (head normal form) es la siguiente:
hnf (E, H, Cin, Cout) ⇔ H es el resultado de evaluar una forma normal de cabeza para
E tomando como restricciones de entrada Cin. Cout son las restricciones resultantes de este
cómputo. Hay una condición adicional en el modo de uso: H es una variable nueva o un
término de la forma c(X1 , ..., Xn ) con X1 , ..., Xn variables nuevas (lo importante es que las
variables de H no tengan asociadas desigualdades y que el propio H sea una forma normal de
cabeza).
A diferencia de los lenguajes funcionales en los que la reducción de una expresión a
f.n.c. produce un resultado, en nuestro lenguaje esta reducción es indeterminista y puede,
en general, producir más de un resultado. En concreto, al admitir definiciones de funciones
indeterministas, una llamada a una de estas funciones puede tener más de una reducción
posible a f.n.c.. Los distintos resultados se irán calculando por backtracking.
hnf está definido para los tres tipos de objetos sintácticos que manejamos: variables,
términos construidos (con un sı́mbolo de constructora en la raı́z) y funciones (que serán
siempre suspensiones). Las variables y los términos construidos son f.n.c.’s (no hay que
hacer nada con ellos); la reducción a f.n.c. de una función corresponde a la evaluación de
la misma (debido a la pereza la evaluación de una función no devuelve una forma normal,
sino una f.n.c.).
En la tabla 3.9 se muestra el código para la evaluación de f.n.c.’s, que realmente es más
complicado de lo que cabrı́a esperar a la vista de la especificación. En algunas ocasiones,
como al resolver determinadas igualdades, es útil hacer una reducción a f.n.c. “predictiva”
o “acotada”: solicitamos la evaluación a f.n.c. de una expresión sabiendo que el resultado
debe tener una forma determinada c(...) (c sı́mbolo de constructora), por lo que se orienta
(instancia) el resultado a dicha forma. En el apartado de igualdad se abordará este punto
en detalle. Por el momento basta con tener presente que el resultado H de la reducción
a f.n.c. puede venir instanciado con una expresión de la forma c(X1 , ..., Xn ), siendo c
80
CAPÍTULO 3. MECANISMO DE CÓMPUTO
hnf(E,H,Cin,Cout):var(E),!,
(
var(H),!,H=E,Cin=Cout
;
extractCtr(E,Cin,Cout1,CE),H=E,
propagate(H,CE,Cout1,Cout)
).
hnf(susp(Fun,Args,R,S),H,Cin,Cout):!,
(
S==hnf,!,hnf(R,H,Cin,Cout)
;
H=R,S=hnf,hnf susp(Fun,Args,H,Cin,Cout)
).
hnf(T,H,Cin,Cin):- H=T.
Cuadro 3.9: Código del predicado hnf
constructora de aridad n y X1 , ..., Xn variables nuevas. Conviene tener presente que en
una llamada a hnf de la forma hnf (e, H), H puede no ser variable, pero en cualquier caso
los argumentos e y H no tienen ninguna variable en común (no será necesario hacer
occurs-check, 3.11.1).
Las tres cláusulas del predicado corresponden a los casos de variable, función y términos
que comienzan por sı́mbolo de constructora respectivamente. En el caso de una variable
(primera cláusula), su f.n.c. es ella misma (H = E). Si el resultado no viene orientado
(primera parte de la disyunción), se hace la unificación H = E que convierte H y E en
la misma variable y las restricciones de desigualdad de H serán las mismas que tuviese
E; en otro caso, si el resultado está orientado (segunda parte de la disyunción), hay que
resolver las nuevas desigualdades que se generan. Supongamos, por ejemplo, que X es una
variable sobre la que se que se tienen las restricciones X /= [1], X /= Y y que se tiene que
reducir a una f.n.c. de la forma [A]. Evidentemente debemos hacer la ligadura X = [A],
pero también hay que “propagar” o resolver las desigualdades [A] /= [1], [A] /= Y (que
en este caso se transformarán en las formas resueltas A /= 1, Y /= [A]). El predicado
propagate es el encargado de transformar estas desigualdades a forma resuelta. Como se
aprecia en el código de hnf , lo primero que se hace es extraer del almacén las desigualdades
de E, porque si se hiciese la ligadura H = E antes de la extracción (H tiene la forma
c(X1 , ..., Xn )) en el almacén quedarı́an desigualdades en forma no resuelta y por tanto,
habrı́a una inconsistencia que no se detectarı́a. Después propagate se encarga de introducir
las formas resueltas que produce la resolución de las desigualdades generadas.
La especificación de propagate es la siguiente:
propagate(H, Ls, Cin, Cout) ⇔ Cout es el almacén de desigualdades resultante de resolver todas las desigualdades entre H y los elementos de Ls, tomando como almacén de entrada
Cin.
Veamos cómo se utiliza la información de la especificación volviendo al ejemplo anterior.
Tenı́amos la variable X con las restricciones X /= [1], X /= Y , lo que en el almacén de
restricciones tendrá la forma [X : [[1], Y ], Y : [X]]. Una vez realizada la unificación Prolog
3.9. CÓMPUTO DE FORMAS NORMALES DE CABEZA (HNF)
81
X = [A]6 , para completar la unificación T OY habrá que resolver las desigualdades [A] /=
[1], [A] /= Y , mediante la llamada propagate([A], [[1], Y ]). La primera de ellas produce la
forma resuelta A /= 1 y la segunda Y : [[A]].
propagate( ,[ ],Cin,Cin):-!.
propagate(Y,[C|R],Cin,Cout):notEqual(Y,C,Cin,Cout1)),
propagate(Y,R,Cout1,Cout).
Cuadro 3.10: Código del predicado propagate
El código para propagate se muestra en la tabla 3.10. La primera cláusula recoge el
caso en el que no hay desigualdades que resolver. La segunda resuelve las desigualdades
entre el primer argumento y todos los de la lista mediante el predicado notEqual. Este
predicado resuelve una desigualdad entre dos expresiones cualesquiera como estudiaremos
en 3.12.
Sobre este código es posible hacer algunas optimizaciones sutiles. Volvamos una vez
más a nuestro ejemplo X /= [1], X /= Y . Veı́amos que habı́a que resolver las desigualdades
[A] /= [1], [A] /= Y . La primera se resuelve y produce A /= 1, pero no es necesario resolver
la segunda. En realidad, ya está resuelta porque cuando se hizo la unificación X = [A], en
el almacén de restricciones para Y quedó Y : [[A]], que ya está en forma resuelta. En este
punto es fundamental el hecho de que “Ls sean las desigualdades asociadas a la variable
con la que se acaba de unificar H”.
Podemos reemplazar la segunda cláusula de propagate para hacer esta optimización,
tal como aparece en la tabla 3.11.7
propagate( ,[ ],Cin,Cin):-!.
propagate(Y,[C|R],Cin,Cout):(
var(C),!,Cout1=Cin
;
notEqualTerm(Y,C,Cin,Cout1)),
propagate(Y,R,Cout1,Cout).
Cuadro 3.11: Optimización de propagate
En la primera parte de la disyunción comprueba si la cabeza de la lista es una variable, en cuyo caso la desigualdad correspondiente ya está en forma resuelta, como
veı́amos en el ejemplo. En otro caso, para resolver dicha desigualdad utilizamos el predicado notEqualT erm. Las desigualdades a resolver aquı́ están en forma “casi resuelta”,
esto es, son desigualdades entre formas normales y sin variables en común (no es necesario
el occurs-check, 3.11.1). Naturalmente, puede utilizarse el predicado genérico notEqual en
este caso, pero la resolución de dichas desigualdades no necesita ninguna reducción, ya
6
El test de ocurrencia (véase 3.11.1) no es necesario por la propia especificación de hnf
La especificación se complica bastante: propagate(H, Ls, Cin, Cout) ⇔ H es una forma normal no
variable, Ls la lista de desigualdades asociadas a una variable que acaba de ligarse a H y tal que H y Ls
no tienen ninguna variable en común; Cout es el almacén de desigualdades resultante de resolver todas las
desigualdades entre H y los elementos de Ls, tomando como almacén de entrada Cin.
7
CAPÍTULO 3. MECANISMO DE CÓMPUTO
82
que no hay ninguna llamada a función, y esta información puede aprovecharse para hacer
un cómputo más eficiente. Esto es lo que hace notEqualT erm (en 3.12.1 se estudiará este
predicado en detalle).
La segunda cláusula para hnf se encarga de calcular una f.n.c. para una llamada a
función. Según vimos en 3.7 las llamadas a función pueden aparecer en la forma suspendida
susp(F un, Args, R, S), siendo F un el nombre de la función, Args los argumentos, S un
flag que toma el valor hnf si la llamada ya ha sido evaluada previamente y variable en
caso contrario, y R es el resultado de la evaluación en caso de que se haya producido. Lo
primero que tiene que hacer hnf es comprobar si la llamada ha sido evaluada, en cuyo
caso devolverá el resultado de tal evaluación. Este resultado está en R pero no podemos
devolverlo directamente porque H puede no ser variable (ver la especificación), en cuyo
caso habrá que propagar desigualdades. Invocando de nuevo a hnf , pero esta vez con el
argumento R, la primera cláusula se encargará de hacer esta propagación si es necesaria.
En otro caso (segunda parte de la disyunción), hay que evaluar la llamada y para
ello utilizamos el predicado hnf susp. Este predicado se genera en la traducción (una
cláusula por cada función definida) y se encarga de “construir” la llamada a la función
y de invocarla. Las llamadas se construyen con el nombre de la función como functor
principal, los argumentos de la misma, un argumento extra que recoge el resultado de la
evaluación (al que normalmente llamaremos H) y los almacenes de restricciones Cin y
Cout (el código para las funciones se estudiará en 3.10). De acuerdo con lo anterior, por
ejemplo, para la primitiva ‘+’ se genera la cláusula:
hnf_susp(+, [A,B], H, Cin, Cout):+(A, B, H, Cin, Cout).
que puede leerse como: el resultado de evaluar ‘+’ sobre los argumentos A y B es H si
la llamada a la función ‘+’ con esos mismos argumentos produce H, siempre teniendo en
cuenta los almacenes de restricciones.
El predicado hnf susp podrı́a suprimirse construyendo la llamada a la función correspondiente en la segunda cláusula de hnf y haciendo dicha llamada con el predicado call8 .
Sin embargo, se comprobó experimentalmente que la versión que hemos presentado tiene
una eficiencia notablemente superior.
En la tercera cláusula se trata el caso que falta, la reducción a f.n.c. de una expresión
de la forma c(e1 , ..., en ) que ya está en f.n.c. y se reduce a sı́ misma. Las restricciones no
sufren modificaciones, por lo que el almacén de salida se unifica con el de entrada en la
cabeza de la cláusula. La corrección de esta cláusula está garantizada por la restricción
adicional en el modo de uso que se indicaba en la especificación de hnf , por la que H es
una variable o un término plano de la forma c(X1 , ..., Xn ) siendo X1 , ..., Xn variables sin
desigualdades asociadas.
3.10.
Generación de código para las funciones
El mecanismo operacional de T OY está basado en narrowing, pero como ya apuntabamos al principio del capı́tulo, los espacios de búsqueda generados por este mecanismo
8
La llamada a hnf susp se reemplaza por el siguiente código: Call = ..[F un, H, Cin, Cout |
Args], call(Call). Nótese que se han reordenado los argumentos en la llamada a la función (el resultado H
y los almacenes Cin, Cout aparecen al principio) para evitar hacer concatenaciones de listas. Lógicamente
en el código de las funciones también habrı́a que reflejar este cambio.
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
83
pueden ser muy grandes. Al explorar estos espacios es frecuente además que algunas expresiones se reevalúen varias veces, lo que empeora aún más el problema. No es necesario
plantear situaciones muy complejas para que se produzcan varias reevaluaciones de una
misma expresión. Consideremos el siguiente programa (leq es la función “menor o igual”
y “add” es la suma, ambas para naturales):
data nat = zero | suc nat
leq zero
Y
= true
leq (suc X) zero
= false
leq (suc X) (suc Y) = leq X Y
add zero
Y = Y
add (suc X) Y = suc (add X Y)
El cómputo que describimos a continuación corresponde a una estrategia ingenua.
Supongamos que lanzamos el objetivo
leq (add zero (suc zero)) (add (suc zero) (suc zero)) == B+
Probando con las reglas para leq en el orden textual, por la primera se intenta reducir la
expresión add zero (suc zero) a zero (paso de parámetros para el primer argumento).
Para ello se evalúa la expresión add zero (suc zero) y se obtiene el valor suc zero.
Como este valor no encaja con el de la primera regla de leq se produce un fallo. Entonces
se prueba con la segunda regla de leq y add zero (suc zero) vuelve a evaluarse desde
el principio, porque el resultado de la evaluación anterior se ha perdido. El resultado
suc zero ahora sı́ encaja con el primer argumento de la segunda regla de leq y se procede
a operar con los segundos argumentos.
A continuación es necesario evaluar la expresión add (suc zero) (suc zero) que
produce suc (suc zero), que no encaja con el segundo argumento de la segunda regla
de leq y se produce otro fallo. Los resultados de los cómputos anteriores se han perdido
y al probar con la tercera regla de leq hay que hacer las evaluaciones desde el principio.
Con esta última regla se obtiene la respuesta B == true.
En el cómputo que acabamos de describir la expresión add zero (suc zero) se ha
evaluado 3 veces y add (suc zero) (suc zero) 2 veces. Pero, ¿son estas reevaluaciones
necesarias realmente?. La respuesta es negativa: obsérvese que todas las reglas de leq
tienen una constructora como primer argumento, lo que significa que el primer parámetro
de cualquier llamada a leq debe reducirse a una de esas constructoras para que el cómputo
no produzca fallo. Entonces no estaremos perdiendo nada si hacemos algunos pasos de
reducción sobre el primer argumento, add zero (suc zero), para obtener una f.n.c. antes
de aplicar ninguna regla de leq. Esta f.n.c. tendrá la forma suc Z. Ahora, la primera regla
de leq fallará, pero la f.n.c. que hemos calculado se puede reutilizar para probar con el
resto de reglas.
De hecho no es necesario probar la primera regla porque con seguridad producirá un
fallo (incompatibilidad en el primer argumento). En las dos que quedan, cuyos primeros
argumentos encajan con la f.n.c.calculada, vuelve a suceder algo similar: ambas tienen
una constructora como segundo argumento. Procedemos como antes, calculando una f.n.c.
para el segundo argumento que tendrá la forma suc U, que sólo encaja en la tercera regla
(la primera ya quedó excluida). De este modo, no se ha reevaluado ninguna expresión y
se aplica la tercera regla, que es la única que tiene éxito.
CAPÍTULO 3. MECANISMO DE CÓMPUTO
84
El último cómputo que hemos descrito es el correspondiente a la Estrategia Guiada
por la Demanda que implementa T OY. Esta estrategia queda reflejada en la traducción (a
código Prolog) de las funciones. Para generar el código, previamente T OY construye un
árbol de decisión para cada cada función al que llamaremos árbol definicional ([Ant92]).
Tanto el algoritmo de construcción de árboles definicionales como el de generación de
código están basados en los que se presentan en [LLR93]. En la generación se han incorporado los cambios pertinentes para el tratamiento de desigualdades, ası́ como algunas
optimizaciones. En la presentación que se hace aquı́, primero se introduce el algoritmo sin
considerar las desigualdades y las optimizaciones para facilitar la comprensión; a continuación, sobre esta versión se realizan los cambios oportunos para llegar a la versión final
que utiliza T OY.
Los algoritmos que exponemos a continuación operan sobre el código intermedio que
se precisó en 3.7 (transformación a primer orden + suspensiones).
3.10.1.
Preliminares
En este apartado introducimos las nociones fundamentales sobre demanda que guiarán
la construcción del árbol definicional.
Sea f una función definida en un programa R:
• al conjunto de reglas que definen f o reglas de f lo denotamos con Rf .
• si f tiene aridad de programa n, un patrón de llamada para f es cualquier
expresión lineal (sin variables repetidas) de la forma f (t1 , ..., tn ), siendo t1 , ..., tn
patrones (expresiones irreducibles). Un patrón genérico de llamada para la
función f es f (X1 , ..., Xn ), siendo X1 , ..., Xn variables distintas.
Una posición es una secuencia de enteros positivos de la forma p1 · ... · pm . Una
posición u en un término e identifica tanto el subtérmino que contiene e en la posición
u, como el sı́mbolo (de variable, constructora o función) que aparece en e en la
posición u. Por ejemplo, si e ≡ f (g(X), c(Y, d)), la posición 2 en e identifica el
subtérmino c(Y, d), ası́ como el sı́mbolo c.
Denotamos con V P (e) al conjunto de posiciones de variable de la expresión e. En el
ejemplo anterior V P (e) = {1 · 1, 2 · 1} (son las posiciones que ocupan X e Y ). Este
conjunto puede construirse inductivamente como:
V P (X) = {ε}
S
V P (l(e1 , ..., en )) = i=1..n {i · u | u ∈ V P (ei )}, si l ∈ DC n ∪ F S n
donde ε representa la secuencia vacı́a y u · ² = u.
Sea f una función definida en el programa R y u una posición:
• diremos que u es una posición demandada por una regla f (t1 , ..., tn ) = t <==
C si el lado izquierdo f (t1 , ..., tn ) contiene una constructora c en la posición u.
• Diremos que u es una posición uniformemente demandada por un conjunto de reglas S ⊆ Rf si todas las reglas de S demandan una constructora
(posiblemente distinta para cada regla) en la posición u.
Ejemplo:
Supongamos un programa R que contiene la función append (2.3.4), cuyas reglas son
(en representación intermedia):
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
85
R1 ≡ append([ ], Y s)
=Ys
R2 ≡ append([X|Xs], Y s) = [X|susp(append, [Xs, Y s], R, S)]
De acuerdo con las definiciones anteriores tenemos:
Rappend = {R1 , R2 }
el patrón genérico de llamada es append(X, Y )
V P (append(A, B)) = {1, 2}
la posición 1 es demandada por ambas reglas, ya que ambas tienen una constructora
como primer argumento, luego esta posición es uniformemente demandada.
¥
3.10.2.
Construcción del árbol definicional
Para una función f definida en un programa R el árbol definicional se construye con
el el algoritmo que se presenta a continuación.
La llamada inicial al algoritmo se hace con un patrón de llamada genérico y con
el conjunto de reglas que definen la función f y tendrá la forma: dt(f (X1 , ..., Xn ), Rf ),
siendo n la aridad de programa de f .
El algoritmo es recursivo y una llamada genérica tendrá la forma: dt(pat, S), siendo
pat un patrón de llamada y S un subconjunto de reglas de f . Además, por la construcción
del algoritmo, cada lado izquierdo de las reglas de S es una instancia de pat.
Algoritmo:
Si S = ∅ devolver ∅
En otro caso, aplicar una de las siguientes alternativas (sólo una es aplicable)
Alguna posición en V P (pat) es uniformemente demandada por S.
Sea u la menor de dichas posiciones en el orden lexicográfico usual (esta elección es
arbitraria) y X la variable que aparece en pat en la posición u.
Sean c1 , ..., cm las constructoras demandadas por las reglas de S en la posición u
tomadas en orden textual. Hacemos una partición de S de la forma:
Sea Su1 el subconjunto de reglas de S que demandan c1 en la posición u.
...
Sea Sum el subconjunto de reglas de S que demandan cm en la posición u.
Para cada ci construimos el patrón:
pati = pat[X/ci (X1 , ..., Xki )]
donde ki es la aridad de ci y X1 , ..., Xki son variables frescas.
devolver:
pat − case X of
c1 : dt(pat1 , Su1 );
c2 : dt(pat2 , Su2 );
...
cm : dt(patm , Sum )
CAPÍTULO 3. MECANISMO DE CÓMPUTO
86
Alguna posición en V P (pat) es demandada, pero ninguna es uniformemente demandada.
Sean u1 , ..., uk el conjunto de posiciones demandadas tomadas según el orden textual
de las reglas y en orden lexicográfico dentro de cada regla9 .
Hacer la siguiente partición de S:
Sea Su1 el conjunto de reglas de S que demandan la posición u1 , Q1 = S − Su1 .
Sea Su2 el conjunto de reglas de Q1 que demandan la posición u2 , Q2 = Q1 − Su2 .
...
Sea Suk el conjunto de reglas de Qk−1 que demandan la posición uk .
Y sea S0 el conjunto de reglas de S que no demandan ninguna posición (este
conjunto puede ser vacı́o).
devolver:
pat − or
dt(pat, S0 );
dt(pat, Su1 );
...
dt(pat, Suk )
Ninguna posición en V P (pat) es demandada.
Todos los lados izquierdos de las reglas de S deben ser variantes de pat. Entonces
podemos hacer un renombramiento de estas m reglas de foma que los lados izquierdos
sean idénticos a pat. Las reglas (tomadas en orden textual) serán ahora:
pat = ei <== Ci ,
i = 1..m
devolver:
pat − try
he1 <== C1
|e2 <== C2
...
|em <== Cm i
¥
En 4.5.2 veremos un algoritmo similar a este para el que demostraremos terminación.
La idea es que en cada llamada recursiva disminuye el número de reglas que se pasan, o
bien, el número de posiciones que contienen un sı́mbolo de constructora en el patrón de
llamada se incrementa. Este número está acotado por el número máximo de posiciones de
constructora en las reglas de S.
El código Prolog que implementa este algoritmo puede verse en el apéndice G.
Ejemplo:
Sea R el siguiente programa:
9
Esta elección es arbitraria, el algoritmo funcionarı́a igual con cualquier otro orden
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
87
data nat = zero | suc nat
leq
leq
leq
leq
:: nat -> nat -> bool
zero
Y
= true
(suc X) zero
= false
(suc X) (suc Y)
= leq X Y
below
below
below
below
below
below
:: nat -> [nat] -> bool
X
[]
=
zero
Y
=
(suc X) [zero | _ ]
=
(suc X) [suc Y | _ ] =
(suc X) [suc Y | Ys] =
true
true
false
false
below (suc X) Ys
<== leq X Y == false
<== leq X Y
En este programa se definen el tipo nat de los números naturales del modo habitual
y dos funciones que utilizan este tipo. La primera, leq (less or equal), según vimos al
principio de la sección, define la operación “menor o igual”. Y below toma un natural y
una lista de naturales, y devuelve true si el primer número es menor o igual que todos los
de la lista, f alse en otro caso. Denotaremos con LEQ1 , LEQ2 y LEQ3 a las reglas de leq
tomadas en orden textual y con BELOW1 , ..., BELOW5 a las de below.
La representación intermedia de ambas funciones es:
leq(zero, Y )
= true
leq(suc(X), zero)
= f alse
leq(suc(X), suc(Y )) = susp(leq, [X, Y ], R, S)
below(X, [ ])
below(zero, Y )
below(suc(X), [zero| ])
below(suc(X), [suc(Y )| ])
below(suc(X), [suc(Y )|Y s])
=
=
=
=
=
f alse
true
f alse
f alse <== susp(leq, [X, Y ], R, S) == f alse
susp(below, [suc(X), Y s], R, S)
<== susp(leq, [X, Y ], R0 , S 0 ) == true
(Nótese que la restricción de la última regla de below se ha interpretado como una
igualdad). La construcción del árbol definicional para leq de acuerdo con el algoritmo
serı́a:
Llamada inicial:
dt(leq(A, B), {LEQ1 , LEQ2 , LEQ3 })
El patrón de llamada inicial es pat ≡ leq(A, B) y V P (pat) = {1, 2}. La posición 1 es
uniformemente demandada por lo que el algoritmo seleccionará la primera alternativa y
tenemos:
dt(leq(A, B), Rleq ) = leq(A, B) − case A of
zero : dt(leq(zero, B), {LEQ1 })
suc(X) : dt(leq(suc(X), B), {LEQ2 , LEQ3 })
Para la primera llamada recursiva, tenemos el patrón pat1 = leq(zero, B) y V P (pat1 ) =
{2}. Como 2 es la única posición variable de pat y no es uniformemente demandada se
aplica la tercera alternativa del algoritmo y se obtiene:
CAPÍTULO 3. MECANISMO DE CÓMPUTO
88
dt(leq(zero, B), {LEQ1 }) = leq(zero, B) − tryhtruei
Para la segunda llamada tenemos pat2 = dt(leq(suc(X), B) y V P (pat2 ) = {1 · 1, 2}. La
posición 2 es uniformemente demandada por las reglas LEQ2 y LEQ3 , luego el algoritmo
aplicará de nuevo la primera alternativa:
dt(leq(suc(X), B), {LEQ2 , LEQ3 }) = leq(suc(X), B) − case B of
zero : dt(leq(suc(X), zero), {LEQ2 })
suc(Y ) : dt(leq(suc(X), suc(Y )), {LEQ3 })
Ahora la primera rama, por la tercera alternativa produce:
dt(leq(suc(X), zero), {LEQ2 }) = leq(suc(X), zero) − try hf alsei
y la segunda:
dt(leq(suc(X), suc(Y )), {LEQ3 }) = leq(suc(X), suc(Y )) − try hsusp(leq, [X, Y ], R, S)i
El algoritmo termina produciendo el árbol (uniendo los resultados anteriores):
dt(leq(A, B), {LEQ1 , LEQ2 , LEQ3 }) = leq(A, B) − case A of
zero : leq(zero, B) − try htruei
suc(X) : leq(suc(X), B) − case B of
zero : leq(suc(X), zero) − try hf alsei
suc(Y ) : leq(suc(X), suc(Y )) − try hsusp(leq, [X, Y ], R, S)i
leq(A,B)
A/zero
A/suc(X)
case
leq(zero,B)
leq(suc(X),B)
B/zero
try
leq(suc(X),zero)
try
true
false
case
B/suc(Y)
leq(suc(X),suc(Y))
try
susp(leq,[X,Y],R,S)
Figura 3.3: Árbol definicional de leq
La figura 3.3 representa gráficamente este árbol. Las ramas case recogen las alternativas
o formas que pueden tener los argumentos y las ramas try corresponden a la aplicación
de reglas de función. En las ramas try, cuando sólo hay una alternativa omitimos los
sı́mbolos ‘<’ y ‘>’. La lectura de este árbol podrı́a ser: “para evaluar una llamada a leq
estudiar la forma del primer argumento; si este argumento es (reducible a) zero entonces
devolver true; si es (reducible a) una expresión de la forma suc(X), estudiar la forma del
segundo argumento; si este segundo argumento es (reducible a) zero devolver f alse y si es
(reducible a) suc(Y ) devolver el resultado de evaluar leq(X, Y )”. Cuando se dice que un
argumento es de una forma determinada, esta forma es siempre una expresión que tiene
un sı́mbolo de constructora en la raı́z y la reducción a f.n.c. de dicho argumento debe tener
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
89
esa misma constructora en su raı́z. Esto quiere decir que las ramas case, en la traducción
a Prolog producirán llamadas a hnf como se verá en 3.10.3.
Las ramas try corresponden a la aplicación de una regla de la función, una vez que se
tiene suficiente información sobre los parámetros de llamada y estos encajan con los de la
regla.
below(A,B)
{BELOW1 }
or
below(A,B)
B/[ ]
below(A,B)
A/zero
case
below(A,[ ])
{BELOW2 , BELOW3 , BELOW4 , BELOW5 }
case
below(zero,B)
A/suc(X)
below(suc(X),B)
B/[Y | Ys]
below(suc(X),[Y | Ys])
try
try
Y/zero
below(suc(X),[zero | Ys])
case
Y/suc(Z)
below(suc(X),[suc(Z) | Ys])
try
true
true
false
try
< true <== susp(leq,[X,Z],R,S)==false |
susp(below,[suc(X),Ys],R,S) <== susp(leq,[X,Z],R’,S’)==true >
Figura 3.4: Árbol definicional de below
La construcción detallada del árbol definicional para la función below es algo más
extensa y se ha omitido. Presenta en forma gráfica dicho árbol en la figura 3.4. La función
below no demanda uniformemente ninguna posición, por lo que la primera ramificación
del árbol es un or que hace una partición del conjunto de reglas. La rama izquierda de
este or opera únicamente sobre la primera regla de la función; a partir de ahı́ el algoritmo
opera como si la función estuviese definida sólo por esa regla, lo que significa que la
primera posición es uniformemente demandada y produce una rama case (con una sola
alternativa). Después ya se puede aplicar la regla (rama try).
Para la segunda rama del or el algoritmo opera como si la función estuviese definida
únicamente por las reglas R2 a R5 . Una rama or en la traducción producirá un predicado
con varias cláusulas (tantas como ramas), como se veremos en 3.10.3.
Obsérvese que las hojas de los árboles definicionales siempre corresponden a ramas
try y que después de una rama try no pueden aparecer más ramificaciones. Y por otro
lado, una rama try puede contener varias alternativas o reglas de función, como la última
de below; esto ocurre cuando hay varias reglas de función con la misma cabeza (salvo
renombramientos de variables), como ocurre con las reglas BELOW4 y BELOW5 . En
este caso, mediante análisis de demanda, no se puede determinar cual de estas reglas
debe aplicarse; en realidad, es posible que ambas se puedan aplicar. Por ejemplo, el árbol
definicional de la función choice definida por las reglas (esta función se trató en 2.3.7):
choice X Y = X
choice X Y = Y
tendrá un try en la raı́z con dos alternativas correspondientes a las dos reglas. Es decir, la
CAPÍTULO 3. MECANISMO DE CÓMPUTO
90
demanda de patrones no determina la regla a aplicar, que es lo que se pretende para esta
función indeterminista: aplicar ambas reglas (por backtracking) para hacer una elección
indeterminista de uno de los argumentos.
En [AGL94] la construcción de los árboles definicionales también contempla el caso de
que ramas try con varias alternativas. Sin embargo, a las reglas que definen una función
se les impone una condición de no ambigüedad, que básicamente, excluye las funciones
indeterministas: dos reglas de una misma función con cabezas compatibles deben contener
restricciones proposicionalmente insatisfactibles, o bien, devolver el mismo resultado (tener
la misma expresión en el cuerpo). En T OY tal condición no se exige, porque de hecho, se
admiten funciones indeterministas como choice y, en consecuencia, las ramas try pueden
contener alternativas que produzcan distintos valores con los mismos argumentos. En otras
palabras, no se exigen condiciones de confluencia para las reglas de función.
3.10.3.
Generación de código para las funciones. Primera aproximación
A partir de los árboles definicionales del apartado anterior T OY produce el código Prolog correspondiente a cada una de las funciones del programa. En esta sección presentamos
una primera versión de la generación de código, que no tiene en cuenta las restricciones
de desigualdad. En consecuencia, tampoco se toman en cuenta los almacenes de las desigualdades, por lo que en las llamadas al predicado hnf hemos omitido dichos almacenes
(puede asumirse que ambos son vacı́os). En las siguientes secciones, sobre esta versión se
harán los cambios necesarios para el tratamiento de las desigualdades y se detallarán las
optimizaciones de código que lleva a cabo el sistema.
La traducción de una función produce, en general, varios predicados. Uno de “entrada”
(al que se invocará para evaluar una llamada a la función) que tiene el mismo nombre que
la función y otros, cuyos nombres se construyen de acuerdo con las posiciones para las
que se ha obtenido una f.n.c. anteriormente. Las posiciones se notarán como secuencias de
enteros separados por ‘.’ y formamos secuencias de posiciones separándolas mediante ‘,’.
Para formar un nuevo nombre de predicado a partir de uno dado se concatenarán por la
derecha a dicho nombre. Por ejemplo, el nombre f 1 2 2,1 hace referencia a un predicado
correspondiente a la función f que ya cuenta con las f.n.c.’s para las posiciones 1, 2 y 2 · 1
(en realidad, si la posición 2 · 1 está en f.n.c., la posición 2 también debe estarlo). Además,
todos estos predicados llevan como último argumento un parámetro extra que representa
el resultado de la evaluación de la función (que normalmente se notará por H).
El algoritmo de generación de código para una función f trabaja por análisis de
casos sobre la forma que tiene el árbol definicional de la misma. En la llamada inicial, se le pasará el árbol completo dt(f (X1 , ..., Xn ), Rf ), siendo n la aridad de programa de f y la secuencia de posiciones vacı́a ε, de modo que dicha llamada tiene la forma gen code(dt(f (X1 , ..., Xn ), Rf ), ε). El algoritmo opera recursivamente y una llamada
genérica tendrá la forma gen code(tree, positions), siendo tree un árbol definicional y
positions una secuencia de posiciones.
Algoritmo:
Supongamos
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
91
tree ≡ pat − case X of
c1 : tree1 ;
c2 : tree2 ;
...
cm : treem
donde pat = f (t1 , ..., tn ) y u es la posición de X en f (t1 , ..., tn ). Sea HX una variable
nueva y sea (t01 , ..., t0n ) construido como (t1 , ..., tn )[X/HX]. Entonces se genera la
cláusula:
f positions(t1 , ..., tn , H) : −hnf (X, HX), f positions u(t01 , ..., t0n , H).
seguido del código producido por:
gen code(tree1 , positions u)
gen code(tree2 , positions u)
...
gen code(treen , positions u)
Supongamos
tree ≡ pat − or
tree0 ;
tree1 ;
...
treek
Entonces el código generado es el generado por las llamadas:
gen code(tree0 , positions)
gen code(tree1 , positions)
...
gen code(treek , positions)
Supongamos
tree ≡ pat − try
he1 <== C1
|e2 <== C2
...
|em <== Cm
donde pat = f (t1 , ..., tn ). Entonces, para cada una de las alternativas se genera una
cláusula de la forma:
f positions(t1 , ..., tn , H) : −solve(Ci ), hnf (ei , H).
Estas cláusulas se generan en el mismo orden en el que aparecen las alternativas, que
corresponde al orden textual de las reglas de f . El predicado solve se utiliza para la
resolución de restricciones y está definido por las cláusulas:
92
CAPÍTULO 3. MECANISMO DE CÓMPUTO
solve([e == e0 |R]) : −equal(e, e0 ), solve(R).
solve([ ]).
Cuando la regla no tiene restricciones se omitirá la llamada a solve (si no se omitiese
serı́a solve([ ]), que tiene éxito automáticamente). equal es el predicado de resolución
de igualdades que se estudiarán en detalle en 3.11.5.
Si tree = ∅, entonces no se genera ningún código.
¥
Ejemplo:
Para la función leq definida anteriormente, apoyándose en el árbol definicional de la
figura 3.3, el algoritmo de generación de código opera del siguiente modo:
Inicialmente la secuencia de posiciones es vacı́a (positions = ε). El primer case produce
la cláusula:
leq(A,B,H) :- hnf(A,HA), leq_1(HA,B,H).
Para las dos ramas del case, el algoritmo opera recursivamente sobre ambas con
positions = 1. La primera rama try produce la cláusula:
leq_1(zero,B,H) :- hnf(true,H).
Para la segunda rama, que vuelve a ser un case se genera:
leq_1(suc(X),B,H) :- hnf(B,HB), leq_1_2(suc(X),HB,H).
y se invoca de nuevo al algoritmo con las dos ramas try y position = 1 2. Estas ramas
producen las cláusulas:
leq_1_2(suc(X),zero,H)
:- hnf(false,H).
leq_1_2(suc(X),suc(Y),H) :- hnf(susp(leq,[X,Y],R,S),H).
¥
Ejemplo:
Para la función below, el código producido serı́a:
below(A,B,H) :- hnf(B,HB), below_2(A,HB,H).
below(A,B,H) :- hnf(A,HA), below_1(HA,B,H).
below_2(A,[],H) :- hnf(true,H).
below_1(zero,B,H)
:- hnf(true,H).
below_1(suc(X),B,H) :- hnf(B,HB), below_1_2(suc(X),HB,H).
below_1_2(suc(X),[Y|Ys],H) :- hnf(Y,HY),
below_1_2_2.1(suc(X),[HY|Ys],H).
below_1_2_2.1(suc(X),[zero|Ys],H)
:- hnf(false,H).
below_1_2_2.1(suc(X),[suc(Z)|Ys],H) :- solve([susp(leq,[X,Z],R,S)==false]),
hnf(true,H).
below_1_2_2.1(suc(X),[suc(Z)|Ys],H) :- solve([susp(leq,[X,Z],R,S)==true]),
hnf(susp(below,[suc(X),Ys],R’,S’),H).
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
93
En este ejemplo, la primera rama es or, lo que provoca que el predicado de entrada
below tenga dos cláusulas. La primera de ellas es un case que opera sobre la segunda
posición. La segunda es otro case sobre la primera posición y que produce las dos cláusulas
de below 1. Obsérvese que las dos últimas cláusulas de below 1 2 2,1 corresponden a las
dos alternativas del último try del árbol. Estas dos cláusulas tienen la misma cabeza,
pero en este caso no se trata de una función indeterminista, ya que las restricciones (los
argumentos de solve) de ambas cláusulas son lógicamente incompatibles.
¥
3.10.4.
Incorporación de desigualdades en la traducción de funciones
En este apartado vamos a modificar la especificación Prolog de la sección anterior
para tratar las desigualdades. Ahora todos los predicados de la traducción deben llevar
los dos argumentos extra Cin y Cout correspondientes al almacén de restricciones de
entrada y salida respectivamente. Los predicados hnf , solve, equal también llevar estos
dos argumentos. Por otro lado, debemos ampliar el predicado solve con una nueva cláusula
(la segunda) para que pueda tratar también desigualdades, que ahora quedará:
solve([e == e0 |R], Cin, Cout) : −equal(e, e0 , Cin, Cout1), solve(R, Cout1, Cout).
solve([e /= e0 |R], Cin, Cout)
: −notEqual(e, e0 , Cin, Cout1), solve(R, Cout, Cout).
solve([ ], Cin, Cin).
El predicado notEqual de resolución de desigualdades se estudiará en 3.12.
El último cambio es más sutil. En la traducción propuesta en el apartado anterior, hay
unificaciones Prolog que se hacen (de forma implı́cita) al unificar el predicado de llamada
con la cabeza de las cláusulas. Por ejemplo, en el código generado para la función leq del
apartado anterior las dos cláusulas para leq 1 discriminan en el primer argumento los casos
zero y suc(X) respectivamente, una vez que se ha evaluado una f.n.c. para esta posición
en el predicado leq. Sin embargo, en este argumento pueden aparecer variables T OY con
restricciones de desigualdad asociadas, es decir, no basta la unificación Prolog, sino que
debe hacerse unificación T OY. Por ejemplo, es posible hacer una llamada de la forma
leq(X, suc(zero), H), siendo X una variable que tiene asociada la desigualdad X /= zero;
si se hiciese simplemente unificación lógica, serı́a aplicable la primera cláusula de leq 1
que instanciarı́a X a zero. Esto generarı́a una inconsistencia en los almacenes que no serı́a
detectada.
Debemos hacer una unificación T OY que tenga en cuenta las desigualdades asociadas
a las variables que aparecen en ambas expresiones. Pero obsérvese que de las expresiones
que debemos unificar sabemos con certeza que están en f.n.c.: la primera acaba de calcularse (en el ejemplo se calcula en el predicado leq) y la segunda es una expresión que
comienza por constructora (por construcción de la rama case del árbol definicional, es de
hecho una forma normal). Además se sabe que ambas expresiones no comparten ninguna
variable (no es necesario hacer el occurs-check) porque todas las variables de la expresión
que introduce el case son nuevas. Para aprovechar esta información, T OY incorpora el
predicado especializado unif yHnf s, encargado de unificar formas normales de cabeza sin
occurs-check. El código se muestra en la tabla 3.12.
La primera cláusula de unif yHnf s es la que trata el caso que podrı́a causar problemas:
cuando la f.n.c. que se acaba de evaluar es una variable que puede tener restricciones de
desigualdad. En esta situación se hace exactamente lo mismo que en la segunda cláusula
de hnf (3.9). Se extraen las restricciones asociadas a dicha variable en CH antes de hacer
la unificación y después se resuelven todas las restricciones de desigualdad provocadas por
la unificación.
CAPÍTULO 3. MECANISMO DE CÓMPUTO
94
unifyHnfs(H,L,Cin,Cout) :var(H),
!,
extractCtr(H,Cin,Cout1,CH),
H=L,
propagate(H,CH,Cout1,Cout).
unifyHnfs(H,H,Cin,Cin).
Cuadro 3.12: Unificación de formas normales de cabeza (unificación T OY)
En la segunda cláusula, cuando la f.n.c. que se ha calculado no es una variable la
unificación es sencillamente la unificación Prolog. No hay que resolver ninguna restricción
de desigualdad porque el segundo argumento de unif yHnf s es siempre una expresión de
la forma c(X1 , ..., Xn ), con c ∈ DC n y X1 , ..., Xn , que proviene de una rama case; por
tanto los argumentos de la constructora c serán variables nuevas, sobre las que no puede
haber ninguna restricción (los case solo estudian la constructora más externa).
En el algoritmo de generación de código del apartado anterior, al incluir las desigualdades, debe tenerse en cuenta en las ramas case, que tras evaluar una f.n.c., las cláusulas que
se producen a continuación deben utilizar este predicado para hacer la unificación. Este
algoritmo funciona esencialmente como el anterior, pero ahora lleva el parámetro adicional
oldpat en el que se pasa el patrón que se ha utilizado en la última rama case. Este patrón
será el que se utilice para construir la cabeza del predicado Prolog correspondiente y en la
llamada inicial será idéntico a pat (no hay case anterior).
El algoritmo utiliza el hecho de que pat y oldpat son idénticos, o bien, difieren en una
sola posición en la que oldpat contiene una variable Y y pat una expresión que comienza
por constructora de la forma d(Z). Esto es ası́ por la construcción del árbol definicional.
La llamada inicial tiene la forma gen code(dt(f (X1 , ..., Xn ), Rf ), ε, f (X1 , ..., Xn )),
siendo n la aridad de programa de f y ε la secuencia de posiciones vacı́a. Una llamada
genérica tendrá la forma gen code(tree, positions, oldpat).
Algoritmo:
Supongamos
tree ≡ pat − case X of
c1 : tree1 ;
c2 : tree2 ;
...
cm : treem
Sea u la posición de X en pat y HX una variable nueva.
Si oldpat = pat ≡ f (t1 , ..., tn ) entonces sea (t01 , ..., t0n ) ≡ (t1 , ..., tn )[X/HX]. La
cláusula generada es:
f positions(t1 , ..., tn , H, Cin, Cout) : −
hnf (X, HX, Cin, Cout1),
f positions u(t01 , ..., t0n , H, Cout1, Cout).
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
95
y si no son idénticos, tendremos oldpat = f (t1 , ..., tn ) y pat = oldpat[Y /d(Z)]
(además X 6≡ Y ). Sea (t01 , ..., t0n ) ≡ (t1 , ..., tn )[Y /d(Z)][X/HX]. Entonces se genera la cláusula:
f positions(t1 , ..., tn , H, Cin, Cout) : −
unif yHnf s(Y, d(Z), Cin, Cout1),
hnf (X, HX, Cout1, Cout2),
f positions u(t01 , ..., t0n , H, Cout2, Cout).
En ambos casos se generan las cláusulas correspondientes a las siguientes llamadas:
gen code(tree1 , positions u, pat)
gen code(tree2 , positions u, pat)
...
gen code(treen , positions u, pat)
Supongamos
tree ≡ pat − or
tree0 ;
tree1 ;
...
treek
Entonces el código generado es el generado por las llamadas:
gen code(tree0 , positions, oldpat)
gen code(tree1 , positions, oldpat)
...
gen code(treek , positions, oldpat)
Supongamos
tree ≡ pat − try
he1 <== C1
|e2 <== C2
...
|em <== Cm
Si oldpat = pat ≡ f (t1 , ..., tn ) entonces para cada una de las alternativas se genera
una cláusula de la forma:
f positions(t1 , ..., tn , H, Cin, Cout) : −
solve(Ci , Cin, Cout1),
hnf (ei , H, Cout1, Cout).
y si no son idénticos, será oldpat = f (t1 , ..., tn ) y pat = oldpat[Y /d(Z)]. Sea e0i =
ei [Y /d(Z)]. Para cada una de las alternativas se genera una cláusula de la forma:
96
CAPÍTULO 3. MECANISMO DE CÓMPUTO
f positions(t1 , ..., tn , H, Cin, Cout) : −
unif yHnf s(Y, d(Z), Cin, Cout1),
solve(Ci , Cout1, Cout2),
hnf (e0i , H, Cout2, Cout).
Si tree = ∅, entonces no se genera ningún código.
¥
Por ejemplo, para la función leq el código producido es:
leq(A,B,H,Cin,Cout) :- hnf(A,HA,Cin,Cout1), leq_1(HA,B,H,Cout1,Cout).
leq_1(A,B,H,Cin,Cout) :- unifyHnfs(A,zero,Cin,Cout1),
hnf(true,H,Cout2,Cout).
leq_1(A,B,H,Cin,Cout) :- unifyHnfs(A,suc(X),Cin,Cout1),
hnf(B,HB,Cout1,Cout2),
leq_1_2(suc(X),HB,H,Cout2,Cout).
leq_1_2(suc(X),B,H,Cin,Cout) :- unifyHnfs(B,zero,Cin,Cout1),
hnf(false,H,Cout1,Cout).
leq_1_2(suc(X),B,H,Cin,Cout) :- unifyHnfs(B,suc(Y),Cin,Cout1),
hnf(susp(leq,[X,Y],R,S),H,Cout1,Cout).
Ahora, tras evaluar una f.n.c. para el primer argumento en la primera cláusula, las dos
cláusulas de leq 1 hacen la distinción de casos mediante el predicado unif yHnf s, y no
en la cabeza como se hacı́a anteriormente (sin desigualdades). Se puede observar también
cómo se pasan los almacenes de entrada y salida de unos predicados a otros con los dos
argumentos extra. Puede entenderse el almacén como un acumulador de restricciones. El
primer predicado al que se llama en el cuerpo de una cláusula toma como almacén de
entrada Cin y genera otro de salida Cout1, que se le pasará al siguiente como almacén
de entrada. Utilizaremos tantas variables auxiliares Cout1, Cout2... como sea necesario,
teniendo en cuenta que la última llamada debe producir como almacén de salida Cout (el
mismo que en la cabeza).
Para below tenemos:
below(A,B,H,Cin,Cout) :- hnf(B,HB,Cin,Cout1),
below_2(A,HB,H,Cout1,Cout).
below(A,B,H,Cin,Cout) :- hnf(A,HA,Cin,Cout1),
below_1(HA,B,H,Cout1,Cout).
below_2(A,B,H,Cin,Cout) :- unifyHnfs(B,[],Cin,Cout1),
hnf(true,H,Cout1,Cout).
below_1(A,B,H,Cin,Cout) :- unifyHnfs(A,zero,Cin,Cout1),
hnf(true,H,Cout1,Cout).
below_1(A,B,H,Cin,Cout) :- unifyHnfs(A,suc(X),Cin,Cout1),
hnf(B,HB,Cout1,Cout2),
below_1_2(suc(X),HB,H,Cout2,Cout).
below_1_2(suc(X),B,H,Cin,Cout) :-
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
97
unifyHnfs(B,[Y|Ys],Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
below_1_2_2.1(suc(X),[HY|Ys],H,Cout2,Cout).
below_1_2_2.1(suc(X),[Y|Ys],H,Cin,Cout) :unifyHnfs(Y,zero,Cin,Cout1),
hnf(false,H,Cout1,Cout).
below_1_2_2.1(suc(X),[Y|Ys],H,Cin,Cout) :unifyHnfs(Y,suc(Z),Cin,Cout1),
solve([susp(leq,[X,Z],R,S)==false],Cout1,Cout2),
hnf(true,H,Cout2,Cout).
below_1_2_2.1(suc(X),[Y|Ys],H,Cin,Cout) :unifyHnfs(Y,suc(Z),Cin,Cout1),
solve([susp(leq,[X,Z],R,S)==true],Cout1,Cout2),
hnf(susp(below,[suc(X),Ys],R’,S’),H,Cout2,Cout).
Con esta traducción queda completamente resuelto el asunto de las desigualdades. Sin
embargo, también se ha perdido la posibilidad de hacer una optimización de código basada
en la indexación de argumentos ([Gro96]). Por ejemplo, en las dos cláusulas de below 1 de
la traducción anterior, los primeros argumentos del predicado eran zero y suc(X) respectivamente. Sicstus Prolog es capaz de aprovechar esta información para utilizar la cláusula
apropiada sin intentar otra alternativa, cuando el primer argumento de la llamada está suficientemente instanciado, es decir, cuando es de la forma zero o suc(Y ). De esta forma
evita intentar hacer unificaciones que con seguridad provocarán fallo. En la traducción
que se acaba de presentar, tal optimización no será posible porque las unificaciones sobre las que Sicstus podrı́a optimizar se hacen explı́citas en el cuerpo de las cláusulas.
Obsérvese que ahora las cabezas de todas las cláusulas para un mismo predicado tienen
cabezas idénticas. No obstante, el rendimiento del sistema no se verá seriamente afectado
por la ausencia de dicha optimización. Son de mayor relevancia las que se proponen en la
siguiente sección.
3.10.5.
Optimizaciones de código
Sobre la traducción anterior, T OY realiza, de forma automática, algunas optimizaciones que incrementan notablemente el rendimiento del sistema. Todas estas mejoras de
código se realizan en el proceso de traducción, no en una etapa posterior, por lo que
T OY utilizará únicamente la información del árbol definicional para llevarlas a cabo (no
hay etapa de postproceso). Las optimizaciones son:
En muchos casos, en el código generado aparecen llamadas a hnf que tienen como
primer argumento una suspensión (últimas cláusulas de la traducción leq y below).
La forma general de estas llamadas es hnf (susp(F, Ls, R, S), H, Cin, Cout) y en este
caso se sabe con seguridad que la suspensión no ha sido evaluada aún porque las
variables R y S son locales a la cláusula (nuevas). En esta situación la segunda
cláusula de hnf siempre evaluarı́a la llamada invocando a hnf susp, por lo que el
sistema puede hacer un unfolding reemplazando la llamada a hnf por la llamada
concreta a la función. Por ejemplo, la última cláusula de leq quedará de esta forma:
leq_1_2(suc(X),B,H,Cin,Cout) :- unifyHnfs(B,suc(Y),Cin,Cout1),
98
CAPÍTULO 3. MECANISMO DE CÓMPUTO
leq(X,Y,H,Cout1,Cout).
Sobre el predicado solve de la traducción se hace un unfolding, es decir, cada restricción de la forma e == e0 se traduce por equal(e, e0 , Cin, Cout) y cada desigualdad
e /= e0 a notEqual(e, e0 , Cin, Cout). De hecho, el predicado solve no existe en el
sistema y en su lugar aparecerá un secuencia de igualdades y desigualdades.
Hay otro posible unfolding cuando se tiene una llamada a un predicado definido por
una sola cláusula. En este caso, T OY reemplaza dicha llamada por el cuerpo del
predicado al que se llama, y esto lo hace para todas las llamadas a dicho predicado.
De este modo, la cláusula que definı́a dicho predicado queda obsoleta y no se genera.
Por ejemplo, en la traducción de la función below, el predicado below 2 está definido
por una sola cláusula; entonces T OY reemplaza la llamada que tiene en la primera
cláusula de below por el cuerpo de below 2, con lo que la primera cláusula queda:
below(A,B,H,Cin,Cout) :hnf(B,HB,Cin,Cout1),
unifyHnfs(HB,[],Cout1,Cout2),
hnf(true,H,Cout2,Cout).
y desaparece el predicado below 2. En el árbol definicional T OY detecta esta situación cuando se encuentra con una rama case seguida de otro case que, a su vez, o
sólo tiene una rama, o bien, va seguida de un try con una sola alternativa. En el
ejemplo, se trata de un case seguido de un try con una sola alternativa.
Cuando en una rama try el valor que devuelve la función es una f.n.c., no es necesario recalcular dicha forma llamando al predicado hnf . Por ejemplo, en la primera
cláusula de below sobre la que se ha hecho un unfolding en el punto anterior, el
valor que devolverá la función es f alse, que es una f.n.c. (de hecho, una forma normal). La última llamada a hnf es redundante y lo único que hará será ligar f alse
al valor de retorno H. Esta última llamada a hnf puede reemplazarse por una sencilla unificación H = f alse, que puede hacerse incluso en la cabeza del predicado
obteniendo:
below(A,B,true,Cin,Cout) :hnf(B,HB,Cin,Cout1),
unifyHnfs(HB,[],Cout1,Cout).
Como resultado de esta optimización tendremos en muchos casos el cálculo de una
f.n.c. seguido de una unificación de f.n.c.’s, como en el ejemplo que acabamos de ver.
Puesto que hnf hace una unificación teniendo en cuenta las desigualdades asociadas
a las variables, ambos predicados se pueden fusionar en una llamada a hnf donde se
pasa como segundo argumento el segundo argumento de unif yHnf s. De este modo
se aprovecha la unificación que hace hnf . La cláusula anterior para below quedarı́a
reducida a:
below(A,B,true,Cin,Cout) :- hnf(B,[],Cin,Cout).
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
99
La optimización del punto anterior puede generalizarse aún más mediante subida de
constructoras, cuando en el árbol definicional se tiene un nodo tal que todas las hojas
que dependen de él tienen un esqueleto o cáscara común: el valor que devuelve la
función es una expresión cuya parte construida es siempre la misma para todas las
hojas que dependen del mencionado nodo. Esto ocurre, por ejemplo para la siguiente
función, que comprueba si una lista de naturales es no vacı́a:
not_empty [zero | R] = true
not_empty [suc X | R] = true
not_empty(A)
case A/[X|Xs]
not_empty([X|Xs])
X/zero
not_empty([zero|Xs])
try
case
X/suc(Y)
not_empty([suc(Y)|Xs])
try
true
true
Figura 3.5: Árbol definicional de not empty
El árbol definicional correspondiente se muestra en la figura 3.5. La traducción correspondiente serı́a:
not_empty(A,H,Cin,Cout) :hnf(A,HA,Cin,Cout1),
not_empty_1(HA,H,Cout1,Cout).
not_empty_1(A,H,Cin,Cout) :unifyHnfs(A,[X|Xs],Cin,Cout1),
hnf(X,HX,Cout1,Cout2),
not_empty_1_1.1([HX|Xs],H,Cout2,Cout).
not_empty_1_1.1([X|Xs],H,Cin,Cout) :unifyHnfs(X,zero,Cin,Cout1),
hnf(true,H,Cout1,Cout).
not_empty_1_1.1([X|Xs],H,Cin,Cout) :unifyHnfs(X,suc(Y),Cin,Cout1),
hnf(true,H,Cout1,Cout).
Aplicando unfolding sobre not empty 1 y la optimización del punto anterior tendrı́amos:
not_empty(A,H,Cin,Cout) :hnf(A,HA,Cin,Cout1),
unifyHnfs(A,[X|Xs],Cout1,Cout2),
hnf(X,HX,Cout2,Cout3),
CAPÍTULO 3. MECANISMO DE CÓMPUTO
100
not_empty_1_1.1([HX|Xs],H,Cout3,Cout).
not_empty_1_1.1([X|Xs],true,Cin,Cout) :unifyHnfs(X,zero,Cin,Cout).
not_empty_1_1.1([X|Xs],true,Cin,Cout) :unifyHnfs(X,suc(Y),Cin,Cout).
Observemos que esta función sólo puede devolver el valor true. Si hiciésemos una
llamada de la forma not empty(A, f alse, Cin, Cout) el cómputo falları́a, pero antes
evaluarı́a una f.n.c. para A que serı́a el propio A, la unificarı́a con [X|Xs], evaluarı́a
una f.n.c. para X que serı́a el propio X y luego producirı́a el fallo en la llamada
not empty 1 1,1([X|Xs], f alse, Cout3, Cout), al no poder unificarlo con ninguna cabeza. No obstante este fallo podrı́a haberse anticipado sin evaluar las dos f.n.c.’s y
la unificación: puesto que la función, en caso de tener éxito devolverá el valor true
se puede colocar este valor en la cláusula para not empty en lugar de la variable H.
De este modo, la cláusula para not empty serı́a:
not_empty(A,true,Cin,Cout) :hnf(A,HA,Cin,Cout1),
unifyHnfs(A,[X|Xs],Cout1,Cout2),
hnf(X,HX,Cout2,Cout3),
not_empty_1_1.1([HX|Xs],H,Cout3,Cout).
De este modo, la llamada que proponı́amos, not empty(A, f alse, Cin, Cout), fallará automáticamente sin evaluar ninguna f.n.c. ni hacer ninguna unificación. Esta
situación es fácilmente detectable en el árbol definicional, ya que todas las hojas (que
dependen del primer nodo, en este caso) devuelven el valor true. En general, pueden
devolver distintos valores, pero con una parte del esqueleto común. Por ejemplo, si
un nodo tiene dos ramificaciones que devuelven [zero, zero] y [zero, suc(X)], la parte
construida que se podrı́a subir al nodo es [zero, Y ].
Otra optimización consiste en eliminar cierta información redundante de algunas
cláusulas como se propone en [Han95b]. Por ejemplo, en la traducción de leq, las dos
cláusulas del predicado leq 1 2 llevan como primer argumento suc(X). La variable
X es relevante para el cómputo que se va realizar, sin embargo, no es necesaria la
expresión completa suc(X) (la constructora suc no se utiliza). Esto sugiere que en
la llamada a leq 1 2 que se hace en el cuerpo de la segunda cláusula de leq 1 se
puede prescindir de dicha constructora y pasar sólo la variable X. En el nombre
del predicado concatenaremos además, el nombre de la constructora inútil que se
eliminado para dejar constancia de este hecho. La segunda cláusula de leq 1 queda:
leq_1(A,B,H,Cin,Cout) :- unifyHnfs(A,suc(X),Cin,Cout1),
hnf(B,HB,Cout1,Cout2),
leq_1_2_suc(X,HB,H,Cout2,Cout).
y las cláusulas para leq 1 2:
leq_1_2_suc(X,B,false,Cin,Cout) :- unifyHnfs(B,zero,Cin,Cout).
leq_1_2_suc(X,B,H,Cin,Cout) :- unifyHnfs(B,suc(Y),Cin,Cout1),
leq(X,Y,H,Cout1,Cout).
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
101
Eliminando estas constructoras inútiles de los argumentos, las unificaciones son menos costosas y se gana en eficiencia.
La última optimización que se hace es en presencia de restricciones de la forma
f (t1 , ..., tn )3b, con f función de aridad (de programa) n (la expresión f (t1 , ..., tn ) es
una llamada a función) y b ∈ {true, f alse}. En este caso, en vez de generar el equal o
el notEqual correspondiente, directamente se genera la llamada f (t1 , ..., tn , b, Cin, Cout),
que es la que producirı́a la resolución de la restricción (se ahorran llamadas intermedias). Además es una llamada orientada, con el valor b, es decir, se anticipa el
resultado que debe producir la evaluación de la función, lo que permite anticipar el
fallo, en caso de que éste efectivamente vaya a producirse.
Teniendo en cuenta todas las optimizaciones que acabamos de ver, el código que produce el sistema para las funciones leq y below es el siguiente:
% leq
leq(A,B,H,Cin,Cout):hnf(A,HA,Cin,Cout1),
leq_1’(HA,B,H,Cout1,Cout).
leq_1(A,B,true,Cin,Cout):unifyHnfs(A,zero,Cin,Cout).
leq_1(A,B,H,Cin,Cout):unifyHnfs(A,suc(X),Cin,Cout1),
hnf(B,HB,Cout1,Cout2),
leq_1_2_suc(X,HB,H,Cout2,Cout).
leq_1_2_suc’(X,B,false,Cin,Cout):unifyHnfs(B,zero,Cin,Cout).
leq_1_2_suc(X,B,H,Cin,Cout):unifyHnfs(B,suc(Y),Cin,Cout1),
leq(X,Y,H,Cout1,Cout).
% below
below(A,B,true,Cin,Cout):hnf(B,[],Cin,Cout).
below(A,B,H,Cin,Cout):hnf(A,HA,Cint,Cout1),
below_1(HA,B,H,Cout1,Cout).
below_1(A,B,true,Cin,Cout):unifyHnfs(A,zero,Cin,Cout).
below_1(A,B,H,Cin,Cout):unifyHnfs(A,suc(X),Cin,Cout1),
hnf(B,:(Y,Ys),Cout1,Cout2),
hnf(Y,HY,Cout2,Cout3),
below_1_2_suc_2.1_:(X,HY,Ys,H,Cout3,Cout).
CAPÍTULO 3. MECANISMO DE CÓMPUTO
102
below_1_2_suc_2.1_:(X,Y,H,false,Cin,Cout):unifyHnfs(Y,zero,Cin,Cout).
below_1_2_suc_2.1_:(X,Y,Ys,false,Cin,Cout):unifyHnfs(Y,suc(Z),Cin,Cout1),
leq(X,Z,false,Cout1,Cout).
below_1_2_suc_2.1_:(X,Y,Ys,H,Cin,Cout):unifyHnfs(Y,suc(Z),Cin,Cout1),
leq(X,Z,true,Cout1,Cout2),
below(suc(X),Ys,H,Cout2,Cout).
En el apéndice H se muestra el programa Prolog que se ha implementado en T OY para
la genecación de código y que lleva a cabo todas las optimizaciones que se han presentado.
3.10.6.
Código para las funciones primitivas y apply
El código de las funciones primitivas no se genera en tiempo de compilación, sino que
ha sido escrito manualmente y se encuentra en el archivo primitives.pl. No obstante, el
formato de dicho código se ajusta al del resto de las funciones. Por ejemplo para la función
+, se tiene el predicado:
primitiveFunct(+, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
+(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is HX + HY; errPrim).
El tipo se anota con la cláusula primitiveF unct (para distinguirla de las funciones de
usuario) y los argumentos tienen el significado que se explicó en 3.4. Esta función necesita una f.n.c. para cada uno de sus argumentos (en este caso f.n.c. y forma normal son
sinónimos) y es lo primero que hace el predicado. A continuación comprueba que ambos
argumentos son números (podrı́an ser variables) antes de utilizar el predicado de suma de
Prolog10 . Si esto es ası́ devuelve efectivamente la suma de argumentos y en otro caso llama
al predicado errP rim que muestra el mensaje:
RUNTIME ERROR: Variables are not allowed in arithmetical operations.
(/cflpr. should be active to do this)
El código que acabamos de presentar es el que corresponde al sistema sin restricciones sobre los reales. Cuando se incorporan las restricciones este código cambia (archivo
primitvesClpr.pl) como se verá en 3.15. Las funciones de igualdad y desigualdad también
tienen su tipo correspondiente en primitives.pl:
primitiveFunct(==, 2, 2, (A -> (A -> bool)), bool).
primitiveFunct(/=, 2, 2, (A -> (A -> bool)), bool).
Sin embargo, su código es especial y se encuentra en el archivo toycomm.pl.
En el archivo primitives.pl también se encuentra la información sobre los operadores
infijos primitivos necesaria para el análisis sintáctico de objetivos y para la salida de
respuestas:
10
El predicado − > es el if then else de Prolog; C− > P1 ; P2 tiene la lectura: si se satisface la condición
C entonces se llama al predicado P1 , en otro caso a P2 .
3.10. GENERACIÓN DE CÓDIGO PARA LAS FUNCIONES
103
primInfix(^, noasoc, 90).
infix(**, noasoc, 90).
primInfix(/, noasoc, 80).
primInfix(*, left, 80).
primInfix(+, left, 70).
primInfix(-, left, 70).
primInfix(<, noasoc, 50).
primInfix(<=, noasoc, 50).
primInfix(>, noasoc, 50).
primInfix(>=, noasoc, 50).
primInfix(==, noasoc, 20).
primInfix(/=, noasoc, 20).
primInfix(:, right, 12).
primInfix(’,’, right, 12).
Estas cláusulas se han generado a partir de las cláusulas inf ix que se vieron en 3.5.2,
excepto las dos últimas, que corresponden a las listas y a las tuplas, y no tienen declaración
explı́cita visible al usuario (no aparecen en el archivo basic.toy).
Para la función apply según la transformación a primer orden que se estudió en 3.5.1
se producen unas reglas a las que se puede aplicar el algoritmo de generación de código.
Sin embargo, para esta función no se genera explı́citamente el árbol definicional, sino que
se genera directamente el código correspondiente. El predicado de entrada reduce a f.n.c.
la expresión funcional que se va a aplicar y es siempre fijo:
apply(F, X, H, Cin, Cout):hnf(F, HF, Cin, Cout1),
apply_1(HF, X, H, Cout1, Cout).
El resto de predicados depende de las constructoras y funciones del programa y las
primitivas. Por ejemplo, para la constructora de listas ‘:’/2 se generan dos cláusulas:
apply_1(:, X, :(X), Cin, Cin).
apply_1(:(X), Y, :(X, Y), Cin, Cin).
Estas dos cláusulas corresponden a las constructoras ‘:0 ’ y ‘:1 ’ de la signatura extendida
(véase 3.5). Prolog admite construcciones con el mismo functor y distintas aridades, por
lo que representamos ambas con el mismo sı́mbolo ‘:’.
Para la primitiva ‘+’ tendremos:
apply_1(+, X, +(X), Cin, Cin).
apply_1(+(X), Y, H, Cin, Cout):+(X, Y, H, Cin, Cout).
Según las ideas expuestas en 3.5.1 la primera cláusula que corresponde a la aplicación
parcial de la constructora ‘+’ a un argumento y produce otra constructora. Por el contrario, la segunda cláusula corresponde a la aplicación total de la constructora y produce
una llamada a la función correspondiente. Como antes, utilizamos el mismo sı́mbolo para
representar constructoras de distintas aridades. Obsérvese que se utiliza el mismo nombre
de constructora con distintas aridades (Prolog los distingue).
Para la función map, en la que nos apoyamos para introducir el orden superior, se
producirán las cláusulas:
CAPÍTULO 3. MECANISMO DE CÓMPUTO
104
apply_1(map, X, map(X), Cin, Cout).
apply_1(map(X), Y, H, Cin, Cout):map(X, Y, H, Cout, Cout).
3.11.
Igualdad estricta (==)
En este apartado tratamos otra de las operaciones fundamentales del sistema: la resolución de restricciones de igualdad estricta. Para resolver una restricción de la forma
A == B, T OY trata de estrechar ambos miembros a formas normales unificables. Si no
aparecen sı́mbolos de función ni en A ni en B, es decir, si A y B son formas normales la
resolución de la igualdad estricta consiste en unificar A y B. En el caso de que aparezcan
llamadas a función en alguno de los miembros habrá que evaluar (siempre perezosamente) estas llamadas para resolver la restricción y esta es la diferencia fundamental entre la
igualdad estricta y la unificación de los lenguajes lógicos, en los que no hay llamadas a
función (y no se necesitan reducciones). Por otro lado, en Prolog es habitual prescindir
del occurs-check de variables a la hora de unificar debido al elevado coste que conlleva.
Por ejemplo, en Prolog la unificación X = [X] tiene éxito produciendo una ligadura cı́clica
X = [[[...[X]...]]]. En T OY la restricción análoga X == [X] falla porque X aparece en el
lado derecho (realmente en la cáscara del lado derecho, como veremos).
En el mecanismo operacional que guı́a la resolución de estas restricciones están involucrados dos conceptos que vamos a exponer antes de abordar en detalle dicho mecanismo.
El primero de ellos es la cáscara o esqueleto de un término, que es otro término en el que se
ha reemplazado cada llamada a función más externa por una variable nueva. Formalmente podemos dar una definición de cáscara sobre la estructura de los términos de nuestro
lenguaje:
casc(X) = X, para toda variable X
casc(c(e1 , ..., en )) = c(casc(e1 ), ..., casc(en )), si c ∈ DC n o c ∈ F S m con n ≤ m
casc(f (e1 , ..., en )) = X, siendo X una variable nueva, si f ∈ F S n
(en segundo caso cubre también el caso de funciones aplicadas parcialmente, que son
constructoras a todos los efectos según vimos en 3.5).
La cáscara de un término es otro término que recoge la parte construida del original.
Por ejemplo, si a, b, c son sı́mbolos de constructora y f sı́mbolo de función, casc(c(a, Y )) =
c(a, Y ) (Y variable), casc(c(f (a), b) = c(X, b), casc(f (f (a))) = X (X variable nueva en
los dos último casos). En T OY no existe un predicado especı́fico para construir la cáscara
de un término. Hay algunos predicados como el de occurs-check (predicado occursN ot),
que hacen un estudio de la estructura del término y simultáneamente generan su cáscara.
En general, el recorrido de la estructura de un término es costoso (en tiempo), por lo que
en nuestro sistema se aprovechan estos recorridos para producir otro tipo de información
que será de utilidad posteriormente. En los apartados siguientes se verá cómo y donde se
construyen las cáscaras.
El otro concepto que utilizaremos es el de frontera de dos términos que podemos definir
como la cáscara común de ambos términos. Por ejemplo, tomando a, b, c, f como antes, la
frontera de c(a, a) y c(a, f (b)) es c(a, X) (X variable nueva). Los términos c(a, b) y b no
tienen frontera (su frontera es vacı́a). Realmente, lo que nos interesa en el sistema no es
tanto la frontera de los términos en sı́, como el conjunto de igualdades que se desprende
de la construcción de la misma, y que son las que tendremos que resolver para resolver
la original. En el primero de los ejemplos anteriores, para resolver la igualdad c(a, b) ==
3.11. IGUALDAD ESTRICTA (==)
105
c(a, f (b)), la igualdad que queda pendiente tras calcular la frontera es a == f (b). Como
es lógico, cuando los términos no tienen frontera como en el segundo ejemplo, no queda
ninguna restricción pendiente porque, de hecho, la igualdad estricta entre ellos no puede
resolverse (produce fallo). Como veremos, este último ejemplo ilustra la razón de ser del
cálculo de fronteras: anticipar el fallo. A diferencia de las cáscaras, T OY sı́ que incorpora
un predicado especı́fico para el cálculo de fronteras (3.11.2).
Para la resolución de igualdades estrictas genéricas (entre expresiones cualesquiera)
T OY incorpora el predicado equal. Este predicado hace uso de otros auxiliares, que estudiaremos primero:
occursN ot: hace el occurs-check de una variable en un término y extrae la cáscara
de dicho término.
binding: liga una variable a una f.n.c.
eqF rontier: produce la lista de igualdades que quedan por resolver después de hacer
la frontera de dos términos.
equalHnf : resuelve igualdades entre f.n.c.’s
3.11.1.
El occurs-check
El predicado occursN ot es el responsable de garantizar que una variable no aparece (no
tiene ocurrencias) en la cáscara de un término. Es importante destacar el hecho de que la
variable puede aparecer en el término siempre que sea dentro de una llamada a función, y
por tanto no en la cáscara. Por ejemplo, si f es un sı́mbolo de función y c de constructora,
y se quiere resolver la restricción X == c(f (X)) el occurs-check tiene éxito al estudiar las
apariciones de X sobre el término c(f (X)). Aparentemente esta forma de operar puede
inducir un error si por ejemplo, la función f es la identidad, ya que el término anterior,
una vez evaluada f , serı́a c(X) y claramente X aparece en él. Sin embargo, esta situación
no se produce porque occursN ot tiene una funcionalidad “extra”: devuelve la cáscara de
dicho término y genera una lista de igualdades “pendientes”. Como veremos después de
estudiar el código para este predicado, al resolver este tipo de igualdades se detectará la
anomalı́a anterior.
La especificación de occursN ot es la siguiente:
occursN ot(X, T, ShT, LstEqs) ⇔ X es una variable que no aparece en la cáscara del
término T , que se devolverá en ShT . En LstEqs se devuelve la lista de igualdades pendientes.
El código del predicado occursN ot se muestra en la tabla 3.13. Tiene como último
argumento una lista diferencia de la forma L/M en la que se van a recoger las igualdades
que quedan pendientes. Se procede por análisis de casos sobre la estructura del término
que se recibe como segundo argumento. En la primera cláusula, cuando este término es
una variable Y , la variable X no aparece en él siempre que X e Y no sean idénticas11 . La
cáscara de la variable Y es ella misma y no quedan restricciones pendientes de resolución,
por lo que la lista de igualdades pendientes es la misma de entrada, hecho que se representa
mediante la lista diferencia L/L.
La segunda y tercera cláusulas se ocupan del caso en el que el término del segundo
argumento es una llamada a función, que debido al sharing aparece siempre en forma
11
El predicado Prolog T \ == S tiene éxito si T y S son sintácticamente distintos. En particular, para
dos variables X e Y tiene éxito si no son la misma variable
CAPÍTULO 3. MECANISMO DE CÓMPUTO
106
occursNot(X,Y,ShY,L/L):-var(Y),!,X \ ==Y,Y=ShY.
occursNot( ,susp(E,Args,R,S),Z,[Z==susp(E,Args,R,S)|L]/L):-var(S),!.
occursNot(X,susp( , ,R, ),ShR,L/M):-!,occursNot(X,R,ShR,L/M).
occursNot(X,T,ShT,L/M):T=..[Name|Args],
lstOccursNot(X,Args,ShArgs,L/M),
ShT=..[Name|ShArgs].
lstOccursNot( ,[ ],[ ],L/L).
lstOccursNot(X,[Ar|Rest],[ShAr|RSh],L/M):occursNot(X,Ar,ShAr,L/L1),
lstOccursNot(X,Rest,RSh,L1/M).
Cuadro 3.13: Occurs-check
suspendida y puede estar evaluada o no. En el primer caso, cuando la función no está evaluada, produce como cáscara del término una variable nueva Z (tercer argumento) y genera
la igualdad entre esta cáscara y la llamada a la función. En la tercera cláusula, cuando
la función ya ha sido evaluada, simplemente hace una operación de desreferenciación, es
decir, hace el test sobre la f.n.c. a la que se evaluó la función en algún cómputo previo.
La última cláusula corresponde al caso de un término construido. Descompone el
término12 para obtener el functor principal (nombre del sı́mbolo de constructora) y los
argumentos. Después hace el chequeo sobre los argumentos obteniendo la lista de cáscaras
de los mismos y actualizando la lista de igualdades pendientes. Para ello utiliza el predicado lstOccursN ot que hace el mismo chequeo que occursN ot pero sobre una lista de
términos en vez de uno sólo y devuelve, como es natural, una lista de cáscaras. Por último
se reconstruye la cáscara del término original a partir de las obtenidas para los argumentos
y el nombre de la constructora original.
Retomemos ahora el ejemplo anterior: al resolver la igualdad X == c(f (X)) y hacer
el test de ocurrencia de la variable X sobre el término c(f (X)), se obtiene la cáscara c(Y )
(Y variable nueva), y la igualdad pendiente f (X) == Y . Entonces el sistema hace la
unificación X = c(Y ), con lo que la igualdad pendiente se convierte en f (c(Y )) == Y .
Si f es la función identidad al resolver la igualdad anterior y evaluar f (c(Y )) obtenemos
c(Y ) con lo que dicha igualdad se transforma en c(Y ) == Y . Esta restricción falla al
hacer el test de ocurrencia, con lo que obtenemos el efecto esperado. Este comportamiento
es debido a la pereza del sistema que no evalúa la llamada a la función hasta que es
estrictamente necesario.
3.11.2.
El estudio de la frontera
Ya explicamos al principio de la sección que la frontera de dos términos es un nuevo
término que “contiene” la parte construida común de los dos originales. Sin embargo, como
ya comentábamos lo que realmente nos interesa del cómputo de la frontera es anticipar el
12
El predicado Prolog T = ..Ls descompone el término T devolviendo en Ls una lista que tiene como
cabeza el functor principal de T y como resto sus argumentos. Por ejemplo c(a, f (b), d(a)) = ..X produce
la ligadura X = [c, a, f (b), d(a)]. Su uso es reversible, es decir, Y = ..[c, a, f (b), d(a)] produce la ligadura
X = c(a, f (b), a)
3.11. IGUALDAD ESTRICTA (==)
107
fallo con el mı́nimo coste cuando la restricción de igualdad sea efectivamente insatisfactible.
Con el mı́nimo coste en este contexto quiere decir sin evaluar ninguna llamada a función.
La idea anterior se puede ilustrar con el siguiente ejemplo: sea f un sı́mbolo de función y c un sı́mbolo de constructora, y supongamos que queremos resolver la igualdad
c(f (1), 2) == c(0, 3). Si el sistema operase de forma “secuencial”, como la constructora
más externa es la misma en los dos términos, lo que harı́a serı́a resolver las igualdades estrictas entre los argumentos dos a dos y del primero al último. En el ejemplo esto significa
que primero resolverı́a f (1) == 0, para lo cual tendrı́a que evaluar la llamada a f . Si la
restricción anterior se resuelve con éxito después tendrı́a que resolver 2 == 3 que obviamente es insatisfactible y falları́a. La evaluación de la llamada a f puede ser en general un
cómputo costoso, que no es en absoluto necesario en este caso, porque la igualdad inicial
fallará de todos modos. Es más, el cómputo de f (1) puede no terminar (por ejemplo si f
esta definida por la única regla f X = f X). Al hacer el estudio de la frontera el sistema
va a advertir el conflicto de constructoras (entre los segundos argumentos) y fallará sin
más, con lo que se mejora el rendimiento y las propiedades de terminación.
Con este ejemplo queda justificado el buen comportamiento del estudio de la frontera en el caso de restricciones que presentan algún conflicto de constructoras y son por
tanto insatisfactibles. Sin embargo, es deseable que en el caso de igualdades que sı́ son
satisfactibles, este cómputo no suponga un coste adicional. Por ejemplo, supongamos c, d
sı́mbolos de constructora de aridades 1 y 0 respectivamente y f sı́mbolo de función de aridad 1, y supongamos que se debe resolver la restricción c(c(c(c(f X)))) == c(c(c(c Y ))).
El cómputo de la frontera hace el estudio estructural de los términos (por descomposición
= ..) y no detecta ningún conflicto de constructoras ya que ambos términos tienen frontera
c(c(c(c Z))), por lo que ahora el sistema tendrı́a que resolver efectivamente la restricción
de partida. En dicha resolución necesariamente hay que hacer un recorrido de los términos
para llegar a la parte más interna y plantear la igualdad f X == Y . Operando de esta
forma se ha hecho la descomposición de los términos por duplicado. El mismo planteamiento del problema sugiere la solución: hacer un sólo recorrido de los términos en el que
se estudia la existencia de la frontera y simultáneamente ir anotando las igualdades pendientes como se hacı́a en el predicado occursN ot. Si este estudio tiene éxito ya se conocen
las igualdades que han de resolverse para resolver la igualdad original, en otro caso dicha
igualdad es insatisfactible.
Especificación del predicado eqF rontier:
eqF rontier(T 1, T 2, Ls1, Ls2) ⇔ existe la frontera de los términos T 1 y T 2 y Ls1, Ls2
son las listas (diferencia) de términos entre cuyos elementos se deben resolver las igualdades
(dos a dos) para satisfacer la igualdad T 1 == T 2.
El código Prolog correspondiente al predicado eqF rontier es el que aparece en la tabla
3.14. En la primera cláusula se estudia el caso de dos variables, que no tienen frontera y
por tanto se genera una igualdad pendiente en las listas diferencia. En la segunda cláusula,
se hace el test de constructora sobre los dos términos obteniendo, si el test es positivo,
el nombre y los argumentos de dichas constructoras. A continuación se comprueba que
ambos sı́mbolos de constructora coinciden y se hace el estudio de la frontera sobre cada
par de argumentos de las constructoras de partida, mediante el predicado eqF rontierList,
cuyo código aparece en la misma tabla.
Las cláusulas tercera y cuarta tratan el caso de llamadas a función, que como siempre,
aparecen en forma suspendida. Si la llamada ha sido evaluada se hace una desreferenciación, es decir, se estudia la frontera tomando la f.n.c. que ha resultado de la evaluación.
CAPÍTULO 3. MECANISMO DE CÓMPUTO
108
eqFrontier(X,Y,[X | L1]/L1,[Y | L2]/L2):- (var(X);var(Y)),!.
eqFrontier(X,Y,FX,FY) :constructor(X,NameX,ArgsX),
constructor(Y,NameY,ArgsY),!,
NameX==NameY,
eqFrontierList(ArgsX,ArgsY,FX,FY).
eqFrontier(susp(Fun,Args,R,S),Y,FX,FY):!,
(
S==hnf,!,eqFrontier(R,Y,FX,FY)
;
FX = [susp(Fun,Args,R,S) | L1]/L1,
FY = [Y|L2] / L2
).
eqFrontier(X,susp(Fun,Args,R,S),FX,FY):!,
(
S==hnf,!,eqFrontier(X,R,FX,FY)
;
FY = [susp(Fun,Args,R,S) | L1]/L1,
FX = [X | L2] / L2
).
eqFrontierList([ ],[ ],L1/L1,L2/L2).
eqFrontierList([X | Xs],[Y | Ys],LX/MX,LY/MY):eqFrontier(X,Y,LX/L1,LY/L2),
eqFrontierList(Xs,Ys,L1/MX,L2/MY).
Cuadro 3.14: Frontera de dos términos
3.11. IGUALDAD ESTRICTA (==)
109
En caso contrario, se genera una nueva igualdad pendiente que se almacena en las listas
diferencia.
3.11.3.
Ligadura de variables a f.n.c.’s (binding)
La resolución de igualdades en algunos casos se reducirá a una igualdad entre una
variable y una f.n.c., que resolverá el predicado binding, cuya especificación es simple:
binding(X, H, Cin, Cout) ⇔ resuelve la igualdad estricta entre la variable X y la f.n.c.
H, tomando como almacén de entrada Cin y devolviendo Cout como almacén de salida.
binding(X,Y,Cin,Cout):var(Y),
!,
unifyVar(X,Y,Cin,Cout).
binding(X,Y,Cin,Cout):!,
occursNot(X,Y,ShY,Lst),
extractCtr(X,Cin,Cout1,CX),
X=ShY,
propagate(ShY,CX,Cout1,Cout2),
equalList(Lst,Cout2,Cout).
Cuadro 3.15: Igualdad estricta entre una variable y una f.n.c.
En el código, hay que tener en cuenta algunos detalles como se aprecia en la tabla 3.15.
Si la f.n.c. es una variable, la igualdad se resuelve haciendo la unificación de variables
mediante el predicado unif yV ar, para unir las restricciones asociadas a cada una. En
caso de ser una constructora, la operación es algo más compleja y es crı́tico el orden en la
secuencia de operaciones: hay que hacer el test de ocurrencia, lo que produce a su vez una
lista de igualdades Lst que habrá que resolver al final; después, de forma similar a lo que
hacı́amos en el predicado hnf hay que unificar la variable X con la forma normal ShY
(el esqueleto de la f.n.c. Y ), pero antes hay que hacer la extracción de las desigualdades
asociadas a X; después hay que propagar las restricciones y queda, por último, resolver la
lista de igualdades producidas por el test de ocurrencia. Esta resolución se hace mediante
el predicado equalList cuya especificación es:
equalList(Lst1/Lst2, Cin, Cout) ⇔ Lst1 y Lst2 son listas de expresiones de la misma
longitud y todas las igualdades resultantes de emparejar los elementos de ambas son ciertas,
tomando como almacén de entrada Cin y almacén de salida Cout.
El código se presenta en la tabla 3.16 y lo que hace es simplemente llamar al predicado
equal con cada uno de los pares.
Veamos mediante un ejemplo cómo operan en secuencia los predicado que acabamos
de describir. Sean zero y suc las constructoras de naturales y add1 una función definida
por la regla:
add1 X = suc X
Supongamos que queremos resolver el objetivo X /= suc zero, X == suc (add1 Y ). Tras
resolver la primera restricción, el almacén de desigualdades será [X : [suc(zero)]]. La igualdad, tras algunos pasos de cómputo producirá la llamada
CAPÍTULO 3. MECANISMO DE CÓMPUTO
110
equalList([ ]/[ ],Cin,Cin):-!.
equalList([(Z==Y) | L]/M,Cin,Cout):equal(Z,Y,Cin,Cout1),
equalList(L/M,Cout1,Cout).
Cuadro 3.16: Resolución de listas de igualdades
binding(X, c(susp(add1, [Y ], R, S)), [X : [suc(zero)]], Cout) que se resolverá por la segunda cláusula de este modo:
1. la llamada a occursN ot tiene éxito produciendo la cáscara ShY = suc(Z) y la lista
de igualdades pendientes Lst = [Z == susp(add1, [Y ], R, S)],
2. extractCtr deja el almacén de restricciones vacı́o y CX = suc(zero),
3. se hace la ligadura X = suc(Z),
4. la propagación de restricciones genera la desigualdad Z /= zero, que en el almacén
queda Cout2 = [Z : [zero]],
5. por último, se procede a la resolución de la igualdad de Lst que quedó pendiente
Z == susp(add1, [Y ], R, S). Esto se hará mediante la llamada
equal(Z, susp(add1, [Y ], R, S), [Z : [zero]], Cout).
3.11.4.
Igualdad estricta entre formas normales de cabeza (equalHnf )
Las igualdades se reducirán, en algunos casos, a igualdades entre f.n.c.’s, para las que
T OY utiliza el predicado equalHnf . La especificación de este predicado es sencillamente:
equalHnf (X, Y, Cin, Cout) ⇔ la igualdad estricta entre las f.n.c.’s X e Y es satisfactible
tomando como almacén de entrada Cin, y Cout es el almacén que se obtiene al resolver dicha
igualdad.
equalHnf(L,R,Cin,Cout):-var(L),!,binding(L,R,Cin,Cout).
equalHnf(R,L,Cin,Cout):-var(L),!,binding(L,R,Cin,Cout).
equalHnf(R,L,Cin,Cout):eqFrontier(R,L,FR/[ ],FL/[ ]),!,
equalList(FR,FL,Cin,Cout).
Cuadro 3.17: Igualdad entre f.n.c.’s
En la tabla 3.17 se muestra el código correspondiente. Las dos primeras cláusulas estudian la posibilidad de que alguno de los miembros sea variable, en cuyo caso se utilizará el
predicado binding. En otro caso, si ninguna es variable, deben comenzar por sı́mbolos de
constructora, por lo que se lleva a cabo el estudio de la frontera. Este estudio puede provocar fallo, con lo que la igualdad no es satisfactible, o bien, tener éxito devolviendo una
lista de igualdades pendientes. Estas igualdades se resuelven con el predicado equalList,
que a diferencia del que se presentó en 3.11.3 ahora tiene cuatro argumentos (no utiliza
listas diferencia), aunque la funcionalidad es básicamente la misma. El código se muestra
en 3.18.
3.11. IGUALDAD ESTRICTA (==)
111
equalList([ ],[ ],Cin,Cin):-!.
equalList([Ar1 | R1],[Ar2 | R2],Cin,Cout):equal(Ar1,Ar2,Cin,Cout1),
equalList(R1,R2,Cout1,Cout).
Cuadro 3.18: Resolución de listas de igualdades
3.11.5.
El predicado equal
Ahora estamos en disposición de estudiar en detalle el predicado genérico equal de resolución de igualdades entre expresiones cualesquiera. Una primera idea para resolver una
igualdad entre dos expresiones serı́a reducir ambas a f.n.c. y después utilizar el predicado
equalHnf tal y como se muestra en la tabla 3.19.
equal(X,Y,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
equalHnf(HX,HY,Cout,Cout).
Cuadro 3.19: Versión Ingenua de Igualdad Estricta
Esta forma de operar es correcta, sin embargo puede hacerse bastante más eficiente.
T OY utiliza una versión más sofisticada que analiza la estructura de los miembros “in
situ” para orientar el cómputo y reducir el espacio de búsqueda. Además es capaz de
orientar determinados reducciones, es decir, a la hora de evaluar una llamada a una función
se fuerza la forma del resultado que debe devolver, como veremos a continuación. El código
para equal se muestra en la tabla 3.20.
Las dos primeras cláusulas estudian el caso de que uno de los miembros sea una variable,
en cuyo caso, evalúa una f.n.c. para el otro y se invoca al predicado equalHnf . Si uno de
los miembros es una variable, podrı́a utilizarse binding directamente (véase 3.11.3), pero
la evaluación de una f.n.c. para el otro puede ligar esa variable, con lo que, dejará de ser
variable. Por ejemplo, si tenemos la función f definida por la regla f 0 = 0 e intentamos
resolver la restricción X == f X, el sistema utilizará la primera cláusula de equal y
calculará una f.n.c. para f X, que será 0; pero además la variable X se liga al valor 0, con
lo que la igualdad a resolver ahora es 0 == 0 y no se puede utilizar el predicado binding. De
lo que sı́ estamos seguros es de que ambos miembros ahora están en f.n.c. y podemos utilizar
equalHnf . En muchas ocasiones, sin embargo, si un miembro es variable, la reducción a
f.n.c. del otro miembro dejará la variable intacta, por lo que en las dos primeras cláusulas
de equal llaman a equalHnf con la “variable potencial” como primer argumento. De
este modo, si efectivamente se tiene una variable, equalHnf llamará automáticamente a
binding (véase 3.11.4).
Las dos cláusulas siguientes para equal tratan el caso de que uno de los miembros sea
una constructora. En este caso se construye una nueva expresión con el mismo nombre
de constructora y con variables nuevas como argumentos, es decir, se imita la estructura
externa de la constructora inicial. Después se hace una reducción orientada a f.n.c. del otro
miembro: se solicita la evaluación a f.n.c. forzando la forma del resultado. Esta orientación
sirve para anticipar un posible fallo (se poda el árbol de búsqueda) con lo que se incrementa
la eficiencia; además mejora las propiedades de terminación. Después de esto se tiene
112
CAPÍTULO 3. MECANISMO DE CÓMPUTO
equal(L,R,Cin,Cout):-var(L),!,
hnf(R,HR,Cin,Cout1),
equalHnf(L,HR,Cout1,Cout).
equal(R,L,Cin,Cout):-var(L),!,
hnf(R,HR,Cin,Cout1),
equalHnf(L,HR,Cout1,Cout).
equal(L,R,Cin,Cout):-constructor(L,C/N),!,
functor(T,C,N),
hnf(R,T,Cin,Cout1),
eqFrontier(L,T,FL/[ ],FR/[ ]),
equalList(FL,FR,Cout1,Cout).
equal(R,L,Cin,Cout):-constructor(L,C/N),!,
functor(T,C,N),
hnf(R,T,Cin,Cout1),
eqFrontier(L,T,FL/[ ],FR/[ ]),
equalList(FL,FR,Cout1,Cout).
equal(susp( , ,R,S),L,Cin,Cout):-S==hnf,!,
equal(R,L,Cin,Cout).
equal(L,susp( , ,R,S),Cin,Cout):-S==hnf,!,
equal(R,L,Cin,Cout).
equal(L,R,Cin,Cout):hnf(L,HL,Cin,Cout1),
equal(HL,R,Cout1,Cout).
Cuadro 3.20: Igualdad Estricta
3.12. RESTRICCIONES DE DESIGUALDAD (N OT EQU AL)
113
la certeza de que ambos miembros comienzan por constructora: uno de ellos ya tenı́a
esta forma y para el otro se ha forzado en la reducción. Además las constructoras más
externas deben coincidir en ambos miembros, pero de las internas aún no conocemos
nada. Nuevamente se puede intentar anticipar un fallo calculando la frontera (3.11.2). Si
tal fallo no se produce, el cómputo continúa resolviendo la lista de igualdades que ha
dejado pendientes el estudio de la frontera mediante el predicado equalList que vimos en
3.11.3.
Veamos con un ejemplo cómo funciona la orientación. Supongamos el and lógico (paralelo) definido del modo siguiente:
and 0 X = 0
and X 0 = 0
and 1 1 = 1
Si se quiere resolver la restricción 1 == and X Y , el sistema utilizará la tercera
cláusula de equal, y calculará una f.n.c. para and X Y orientada a 1, que producirá una
llamada a and con el resultado instanciado a 1. Entonces en el código generado para
and (optimización de subida de constructoras, 3.10.5), T OY descartará automáticamente
las dos primeras reglas de and ya que el resultado que devuelven no se ajusta al que se
espera y utilizará directamente la tercera (instanciando X e Y a 1). Esta poda del árbol de
búsqueda llega al extremo cuando se intenta resolver una restricción como and X Y == 2.
En este caso el cómputo falla sin intentar ninguna regla para and.
Para ver por qúe mejoran las propiedades de terminación consideremos la función f
definida por la regla f 0 = 0 y otra función loop cuya evaluación produce no terminación
(la definición más sencilla para loop es loop = loop). La restricción f loop == 1 produce
un fallo automático debido a la orientación en el cómputo de una forma normal para
f , mientras que sin esta optimización se producirı́a no terminación (recursión infinita al
intentar reducir loop).
Las dos cláusulas siguientes (quinta y sexta) estudian el caso de que uno de los miembros sea una suspensión evaluada (una f.n.c. “escondida”). Si es ası́ hace una desreferenciación, para acceder a la variable o constructora que se obtuvo de la evaluación y se llama
recursivamente a equal. Esta nueva llamada se resolverá necesariamente por uno de las
cláusulas vistas hasta ahora (no hay nada que evaluar).
La última cláusula, por exclusión (nótese que las anteriores tienen todas el predicado
de corte !) trata el caso de suspensiones no evaluadas. Entonces se reduce una de ellas
a f.n.c. y se llama recursivamente a equal. Esta nueva igualdad se resolverá por una de
las cláusulas anteriores. En realidad, tenemos la seguridad de que ambos miembros son
suspensiones no evaluadas y podrı́amos reducir ambas a f.n.c. antes de la llamada recursiva,
pero no es necesario13 .
3.12.
Restricciones de desigualdad (notEqual)
Para la resolución de desigualdades T OY incorpora el predicado genérico notEqual.
Este predicado utilizará a su vez otros especializados en determinados tipos de desigualdad.
En la exposición de la igualdad hemos comenzado explorando los casos particulares y las
13
Puede darse el caso de que ambas suspensiones sean la misma, con lo que la reducción de una provoca
automáticamente la reducción de la otra.
114
CAPÍTULO 3. MECANISMO DE CÓMPUTO
operaciones auxiliares, para terminar con el caso general. Para la desigualdad, por el
contrario, la exposición será más clara comenzando directamente por el caso general.
La especificación del predicado notEqual es:
notEqual(X, Y, Cin, Cout) ⇔ resuelve la desigualdad entre dos expresiones cualesquiera
X y Y tomando como almacén de entrada Cin y devolviendo como almacén de salida Cout.
A diferencia de la igualdad, en la que es posible hacer un análisis sobre la estructura
de los términos que evite reducciones innecesarias, ahora dicho análisis no ofrece ventajas
claras. Por ello, lo primero que haremos con una desigualdad es evaluar ambos miembros
a f.n.c. y utilizar el predicado equalHnf como se muestra en la tabla 3.21.
notEqual(X,Y,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
notEqualHnf(HX,HY,Cout2,Cout).
Cuadro 3.21: Resolución de desigualdades
La especificación de notEqualHnf es:
notEqualHnf (X, Y, Cin, Cout) ⇔ resuelve la desigualdad entre dos dos f.n.c.’s X y Y ,
tomando como almacén de entrada Cin y devolviendo como almacén de salida Cout.
notEqualHnf(X,Y,Cin,Cout):-var(X),!,notEqualVar(X,Y,Cin,Cout).
notEqualHnf(Y,X,Cin,Cout):-var(X),!,notEqualVar(X,Y,Cin,Cout).
notEqualHnf(R,L,Cin,Cout):eqFrontier(R,L,FR/[ ],FL/[ ]),
!,
notEqualList(FR,FL,Cin,Cout)
;
Cin=Cout.
Cuadro 3.22: Resolución de desigualdades
Y el código se muestra en la tabla 3.2214 . Ahora sı́ se hace un estudio de los miembros.
Si alguno de ellos es una variable (dos primeras cláusulas) se utiliza el predicado especializado notEqualV ar (que veremos más adelante). En otro caso (última cláusula), ambos
serán constructoras y ahora hacemos el análisis de la frontera no con el fin de anticipar
el fallo, sino para anticipar el éxito: si se encuentra alguna colisión de constructoras (segunda parte de la disyunción), la desigualdad se satisface automáticamente y el almacén
permanece invariante. Si no hay tal colisión, la desigualdad debe resolverse utilizando la
lista de restricciones que han quedado pendientes, mediante el predicado notEqualList.
Obsérvese que ahora interpretamos las restricciones pendientes de la frontera, no como
igualdades, sino como desigualdades. Si en la igualdad tenı́amos que resolver todas las
igualdades pendientes, ahora sólo tenemos que resolver alguna desigualdad para satisfacer
la desigualdad original y esto es lo que hace notEqualList.
14
En 3.15 se introducirá una cláusula adicional para este predicado, especı́fica para el tratamiento de
desigualdades entre reales. Por el momento podemos obviar este hecho.
3.12. RESTRICCIONES DE DESIGUALDAD (N OT EQU AL)
115
La especificación de notEqualList es:
notEqualList(Lst1, Lst2, Cin, Cout) ⇔ Lst1 y Lst2 son listas de expresiones de la misma longitud y existe alguna desigualdad cierta entre las que resultan de emparejar los elementos
de ambas, tomando Cin como almacén de entrada Cin y almacén de salida Cout.
Esta operación es indeterminista como muestra la especificación cuando decimos “existe alguna”. El código es una simple cláusula disyuntiva que se muestra en la tabla 3.23. La
lectura podrı́a ser: para resolver una desigualdad entre dos listas resolver la desigualdad
entre el primer par de elementos, o bien, resolver la desigualdad entre los restos de las
listas. Nótese que no hay una cláusula que recoja el caso de listas vacı́as, como es natural.
notEqualList([X|R1],[Y|R2],Cin,Cout):notEqual(X,Y,Cin,Cout)
;
notEqualList(R1,R2,Cin,Cout).
Cuadro 3.23: Elección indeterminista de una desigualdad
El predicado notEqualV ar se especifica como:
notEqualV ar(X, Y, Cin, Cout) ⇔ resuelve la desigualdad entre la variable X y la f.n.c.
Y tomando Cin y Cout como almacenes de entrada y salida respectivamente.
notEqualVar(X,Y,Cin,Cout):var(Y),
!,
X \ ==Y,
addCtr(X,Y,Cin,Cout1),
addCtr(Y,X,Cout1,Cout).
notEqualVar(X,true,Cin,Cout):-!,hnf(X,false,Cin,Cout).
notEqualVar(X,false,Cin,Cout):-!,hnf(X,true,Cin,Cout).
notEqualVar(X,Y,Cin,Cout):occursNot(X,Y,ShY,Lst),
!,
contNotEqual(X,Y,ShY,Lst,Cin,Cout).
notEqualVar( , ,Cin,Cin).
Cuadro 3.24: Elección indeterminista de una desigualdad
El código se muestra en la tabla 3.24 y en este caso son crı́ticos tanto los cortes que
aparecen, como el orden de las cláusulas. En la primera cláusula, si ambos miembros son
variables y son distintas (si son la misma la restricción es insatisfactible), entonces basta
con añadir la restricción al almacén de restricciones con la operación addCtr (3.8.1). Según
se explicó en 3.12, debido a la gestión de los almacenes se debe añadir la restricción sobre
ambas variables (de ahı́ las dos llamadas a addCtr).
Las dos cláusulas siguientes representan la excepción sobre el tipo de los booleanos que
se vio en 3.8.1: si una variable booleana X es distinta de true entonces debe ser f alse y
viceversa. Para hacer la unificación T OY se utiliza el predicado hnf (la segunda parte de
116
CAPÍTULO 3. MECANISMO DE CÓMPUTO
la disyunción de la primera cláusula) para forzar la propagación de restricciones (también
podrı́a utilizarse unif yHnf s).
En la siguiente cláusula se resuelve una desigualdad entre una variable X y una expresión que comienza por constructora (distinta de true y f alse). En este caso se utiliza
occursN ot con un doble fin: para el occurs-check y para descubrir si el segundo miembro
está en forma normal o contiene llamadas a función. Si el occurs-check falla, entonces la
desigualdad se satisface automáticamente. Por ejemplo, la desigualdad X /= suc X se satisface automáticamente por este hecho. En otro caso, se llama al predicado contN otEqual,
que hace uso de la información que ha producido occursN ot para descubrir si el segundo
miembro es o no una forma normal.
La especificación de contN otEqual es:
contN otEqual(X, Y, ShY, Lst, Cin, Cout) ⇔ resuelve la desigualdad entre la variable X
y la expresión Y que comienza por constructora, utilizando la cáscara ShY de Y y la lista de
restricciones pendientes Lst y tomando como almacenes Cin y Cout.
contNotEqual(X, ,ShY,[ ]/[ ],Cin,Cout):!,
addCtr(X,ShY,Cin,Cout).
contNotEqual(X,Y, , ,Cin,Cout):constructor(Y,C/ ,ArgsY),
!,
const(C, , ,Dest),
(
genConstructor(Dest,Z,C1, ),
C \ ==C1,
hnf(X,Z,Cin,Cout)
;
genConstructor(Dest,Z,C,ArgsZ),
hnf(X,Z,Cin,Cout1),
notEqualList(ArgsZ,ArgsY,Cout1,Cout)
).
Cuadro 3.25: Continuación de la resolución de desigualdades
En el código de la tabla 3.25, la primera cláusula estudia el caso en el que la lista
(diferencia) de restricciones pendientes que se ha generado en el occurs-check es vacı́a.
Esto significa que el segundo miembro no contiene llamadas a función y por tanto es una
forma normal. Entonces la desigualdad ya está en forma resuelta y sólo hay que añadirla
al almacén de restricciones.
En la segunda cláusula, cuando el segundo miembro no está en forma normal, la cáscara
y la lista de restricciones pendientes no serán necesarios. Por ejemplo, la desigualdad
X /= [3 + 4], tras varios pasos de cómputo deberá ser resuelta en esta cláusula. Una
primera idea es evaluar una forma normal para el segundo miembro que será [7], con lo
que la desigualdad toma la forma resuelta X /= [7]. Sin embargo, esta forma de proceder
exige la evaluación de todas las llamadas a función de este segundo miembro, que, en
general pueden ser costosas (o incluso no terminar). Nótese, por otro lado, que pueden
encontrarse algunas respuestas sin evaluar ninguna llamada a función. Por ejemplo la
respuesta X /= [ ], con seguridad es correcta. En general, se puede ligar la variable del
primer miembro a cualquier constructora distinta, pero del mismo tipo que la del segundo
3.12. RESTRICCIONES DE DESIGUALDAD (N OT EQU AL)
117
argumento. Para cubrir el resto de posibilidades, también se puede ligar la variable a la
misma constructora que la del segundo miembro y plantear una desigualdad entre alguno
de sus argumentos. Esta es la idea del funcionamiento de este predicado. Veamos primero
como se generan las constructoras.
Para generar constructoras del mismo tipo se utiliza el predicado genConstructor
especificado como:
genConstructor(DestT ype, Cons, N ame, Args) ⇔ Cons es una expresión de tipo DestT ype construida con el nombre de constructora N ame y Args es la lista de argumentos
(variables nuevas) utilizados para su construcción.
El código de genConstructor se muestra en la tabla 3.26. En los hechos const se
almacenó en tiempo de compilación toda la información referente a constructoras (3.5.2).
Ahora podemos recuperar el nombre y la aridad de una constructora de un tipo dado,
con los que construimos la expresión Cons utilizando el predicado f unctor de Prolog. Los
argumentos de Cons son variables nuevas que podemos recuperar en el argumento Args
mediante el predicado Prolog de descomposición = ...
genConstructor(TipDest,Cons,Name,Args):const(Name,Ar, ,TipDest),
functor(Cons,Name,Ar),
Cons=..[Name|Args].
Cuadro 3.26: Generación de una expresión construida de un tipo dado
Volviendo a la segunda cláusula de contN otEqual, lo primero que se hace es una llamada a constructor (3.5.2) para recuperar el nombre y los argumentos de la expresión
construida del segundo miembro. A continuación se determina el tipo de esta constructora mediante una consulta al hecho const que tiene asociado (3.5.2). Y después, se abre
una disyunción que cubre los posibles modos de resolver la desigualdad: el primero es
generar una constructora del mismo tipo, pero con distinto nombre, que se ligará a la variable del primer miembro por medio de hnf para hacer la propagación (puede utilizarse
unif yHnf s); el segundo modo es generar una constructora del mismo nombre, ligarla a
la variable del primer miembro y, a continuación, plantear una desigualdad entre alguna
pareja de argumentos de ambas constructoras (la del segundo miembro y la generada),
mediante el notEqualList (3.12).
En el ejemplo que planteábamos X /= [3 + 4], la constructora del segundo miembro es
’:’/2 y los argumentos 3+4 y [ ]. Por la primera parte de la disyunción, la única constructora
distinta de ’:’, pero del mismo tipo, es [ ], por lo que la primera respuesta del sistema es
X == [ ]. La segunda parte de la disyunción, al resolver las dos desigualdades entre los
dos pares de argumentos produce las respuestas X == [A|B] con B /= 7 (cualquier lista
no vacı́a cuya cabeza sea distinta de 7), y X == [A|B] con B /= [ ] (cualquier lista con
2 o más elementos). Estas tres soluciones cubren la respuesta X /= [7] y se han generado
siguiendo la filosofı́a de la pereza que mantiene el sistema, es decir, intentando hacer el
menor número de reducciones para obtener cada respuesta.
Es fácil ver que este mecanismo afecta a las propiedades de terminación del sistema.
Por ejemplo, con la función f rom definida como f rom X = [X|f rom (X + 1)], el objetivo
Y /= f rom 0 producirá infinitas respuestas (Y = [ ]; Y = [A|B] con B /= 0; Y = [A]...).
Si se intentase reducir a forma normal el segundo miembro se producirı́a no terminación
sin obtener ninguna respuesta.
CAPÍTULO 3. MECANISMO DE CÓMPUTO
118
3.12.1.
Restricciones de desigualdad entre formas normales
En 3.9 se planteó la conveniencia de disponer de un predicado especı́fico para resolver
desigualdades entre formas normales y este es el cometido de notEqualT erm. La especificación es la siguiente:
notEqualT erm(T 1, T 2, Cin, Cout) ⇔ resuelve la desigualdad entre dos formas normales
T 1 y T 2 tomando como almacén de entrada Cin y devolviendo como almacén de salida Cout.
El código se muestra en la tabla 3.27. Una forma normal puede ser una variable o
un término que comienza por constructora. Las dos primeras cláusulas distinguen el caso de que alguno de los miembros sea una variable, en cuyo caso se utiliza el predicado
especializado notEqualV arT erm (obsérvese que en las llamadas a notEqualV arT erm el
primer argumento es siempre una variable), que veremos en breve. En la tercera cláusula
se tiene la seguridad de que ambos miembros comienzan por un sı́mbolo de constructora. Entonces se utiliza el predicado constructor (3.5.2) para descubrir el nombre, aridad
y los argumentos de las constructoras. Si el nombre o la aridad no coinciden (primera
parte de la disyunción), la desigualdad tiene éxito automáticamente y deja intactos los
almacenes de restricciones. En otro caso, utiliza el predicado notEqualT ermList para resolver la desigualdad entre alguna pareja de argumentos de las constructoras, de manera
indeterminista.
notEqualTerm(T1,T2,Cin,Cout):-var(T1),!,notEqualVarTerm(T1,T2,Cin,Cout).
notEqualTerm(T1,T2,Cin,Cout):-var(T2),!,notEqualVarTerm(T2,T1,Cin,Cout).
notEqualTerm(T1,T2,Cin,Cout):constructor(T1,C1/A1,Args1),
constructor(T2,C2/A2,Args2),
(
C1/A1 \ ==C2/A2,!,Cout=Cin
;
notEqualTermList(Args1,Args2,Cin,Cout)
).
notEqualVarTerm(X,Y,Cin,Cout):var(Y),!,X \ ==Y,
addCtr(X,Y,Cin,Cout1),
addCtr(Y,X,Cout1,Cout).
notEqualVarTerm(X,Y,Cin,Cout):-!,addCtr(X,Y,Cin,Cout).
notEqualTermList([X | R1],[Y | R2],Cin,Cout):(
notEqualTerm(X,Y,Cin,Cout)
;
notEqualTermList(R1,R2,Cin,Cout)
).
Cuadro 3.27: Desigualdad entre formas normales
El predicado notEqualV arT erm añade una nueva restricción al almacén utilizando
addCtr (3.8.1). En la primera cláusula, si ambos miembros de la desigualdad son variables
distintas, por nuestro modo de almacenamiento debemos añadir la desigualdad asociándola
3.13. LA FUNCIÓN IGUALDAD (EQF U N )
119
a ambas variables. Si las variables fuesen idénticas, se produce un fallo que se propaga a
notEqualT erm, ya que la desigualdad, obviamente no es cierta. La segunda cláusula, en
el caso de que una sea variable y la otra un término, simplemente añade la restricción al
almacén.
3.13.
La función igualdad (eqF un)
En 2.3.15 ya se justificó la existencia de las funciones igualdad y desigualdad. Aunque
ambas pueden definirse como funciones T OY, se implementan a bajo nivel para conseguir algunas optimizaciones. En el caso de la función igualdad, la definición en sintaxis
T OY que se propuso en 2.3.15 era:
X == Y = true <== X == Y
X == Y = false <== X /= Y
De acuerdo con esta definición la traducción que producirı́a el sistema tendrı́a el aspecto
(omitimos los almacenes de restricciones):
== (X, Y, true) : −equal(X, Y ).
== (X, Y, f alse) : −notEqual(X, Y ).
Consideremos el objetivo (2+2==3+4)==B. Por el contexto, el sistema interpreta que el
primer sı́mbolo de ‘==’ es una llamada a la función igualdad, mientras que el segundo es
una restricción. Entonces generarı́a la llamada == (2 + 2, 3 + 4, B). Por la primera regla de
‘==’ se unificarı́a B con true y se harı́a la llamada equal(2 + 2, 3 + 4). El predicado equal
reducirı́a a f.n.c. ambos argumentos (en este caso una f.n.c. es también una forma normal),
con lo que se plantea la restricción 4 == 7, que produce un fallo. Entonces el sistema
probará con la segunda regla, que unificará B con f alse y llamará a notEqual(2+2, 3+4).
El predicado notEqual nuevamente debe reducir las expresiones 2+2 y 3+4 y ahora obtiene
la respuesta B == f alse. El cómputo descrito es correcto, pero tiene el inconveniente de
que las expresiones 2 + 2 y 3 + 4 se evalúan dos veces.
Evitar todo tipo de reevaluación en el sistema es una tarea compleja, y por otro lado, no
hay garantı́as de que la eficiencia del mismo fuese superior, ya que los propios mecanismos
para conseguirlo tendrı́an un coste computacional. Sin embargo, algunas reevaluaciones
como las del ejemplo anterior pueden evitarse con un coste relativamente pequeño. El
código que vamos a presentar para la función igualdad, en el caso de que los argumentos
sean llamadas a función, evaluará ambas llamadas antes de continuar el cómputo (sin
instanciar la variable B, en nuestro ejemplo). Los resultados obtenidos de estas reducciones
podrán utilizarse para completar el cómputo de evaluación de la función igualdad. La
especificación de la función igualdad es la siguiente:
eqF un(X, Y, H, Cin, Cout) ⇔ H es true si la restricción X == Y es cierta y f alse si la
restricción es falsa15 , tomando como almacenes Cin y Cout.
El código se presenta en la tabla 3.28. Las dos primeras cláusulas operan cuando el
resultado viene dado, en cuyo caso no hay inconveniente en resolver las restricciones correspondientes. De hecho en este caso, el cómputo es equivalente al que se harı́a utilizando
la definición T OY que proponı́amos al principio. El resto de cláusulas cubren el caso
complementario, cuando el resultado sea variable.
15
Las funciones en T OY pueden producir fallo por lo que true y f alse no son los únicos posibles
resultados de una llamada a la función eqF un (puede quedar indefinida).
120
CAPÍTULO 3. MECANISMO DE CÓMPUTO
eqFun(X,Y,H,Cin,Cout):-H==true,!,equal(X,Y,Cin,Cout).
eqFun(X,Y,H,Cin,Cout):-H==false,!,notEqual(X,Y,Cin,Cout).
eqFun(X,Y,H,Cin,Cout):var(X),
!,
(
H=true,equal(X,Y,Cin,Cout)
;
H=false,notEqual(X,Y,Cin,Cout)
).
eqFun(X,Y,H,Cin,Cout):var(Y),
!,
(
H=true,equal(X,Y,Cin,Cout)
;
H=false,notEqual(Y,X,Cin,Cout)
).
eqFun(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
eqFunHnf(HX,HY,H,Cout2,Cout).
Cuadro 3.28: Función igualdad
Las cláusulas tercera y cuarta son operativas cuando uno de los argumentos es variable,
en cuyo caso se estudian los dos posibles resultados de la función mediante una disyunción.
En estas dos cláusulas el cómputo vuelve a ser equivalente al que se harı́a utilizando la
definición T OY de la función igualdad. Nótese que primero se considera el caso de que
la función se evalúe a true por ser más natural; no obstante, serı́a igualmente correcto
estudiar primero el caso complementario.
La última cláusula es la que trata el caso de que ninguno de los argumentos sea variable
(pueden ser llamadas a función o expresiones que comienzan por un sı́mbolo de constructora). En particular, pueden ser ambos llamadas a función y es aquı́ cuando se intentan
evitar las reevaluaciones. Lo primero es reducir ambos miembros a f.n.c. (si alguno de
ellos comienza por constructora, hnf lo dejará intacto por la tercera cláusula). Después se
llama al predicado eqF unHnf cuya especificación es igual que la de eqF un excepto que
sólo opera con f.n.c.’s.
El código de eqF unHnf se presenta en la tabla 3.29. Una llamada a función puede
reducirse a una variable y la primera cláusula considera este caso, en el que, nuevamente
debe ser eqF un el que opere. Además, es posible que la variable resultado H se haya
instanciado en alguna de las reducciones que hace eqF un, por lo que no podemos asegurar
que sea una de las cláusulas tercera o cuarta la que continúe el cómputo, sino que puede
ser alguna de las dos primeras.
En la segunda cláusula de eqF unHnf ya tenemos la garantı́a de que ambos miembros
son expresiones que comienzan por constructora. Entonces hacemos el estudio de la frontera. La segunda parte de la disyunción actúa si (nótese el corte tras el cómputo de la
frontera) hay colisión de constructoras (falla el cálculo de la frontera), entonces la función
3.13. LA FUNCIÓN IGUALDAD (EQF U N )
121
eqFunHnf(X,Y,H,Cin,Cout):(var(X);var(Y)),!,eqFun(X,Y,H,Cin,Cout).
eqFunHnf(X,Y,H,Cin,Cout):eqFrontier(X,Y,FrontierX/[ ],FrontierY/[ ]),
!,
eqFunAnd(FrontierX,FrontierY,H,Cin,Cout)
;
H=false,
Cin=Cout.
Cuadro 3.29: Igualdad de f.n.c.’s
se evalúa a f alse dejando intactos los almacenes. En el caso de que haya frontera común,
se hace una elección indeterminista que responde al siguiente enunciado: “una conjunción
de igualdades se evalúa a true si todas ellas se evalúan a true y a f alse si alguna de ellas se
evalúa a f alse”. El predicado eqF unAnd es el que implementa esta idea. Su especificación
es:
eqF unAnd(Lst1, Lst2, H, Cin, Cout) ⇔ Lst1 y Lst2 son listas de expresiones de la
misma longitud; H es true si todas las parejas de igualdades entre los elementos de ambas
listas se evalúan a true y f alse si alguna de ellas se evalúa a f alse.
eqFunAnd([ ],[ ],true,Cin,Cin):-!.
eqFunAnd([X1 | Rest1],[Y1 | Rest2],H,Cin,Cout):eqFun(X1,Y1,H1,Cin,Cout1),
eqFunAnd 1(Rest1,Rest2,H1,H,Cout1,Cout).
eqFunAnd([ | Rest1],[ | Rest2],false,Cin,Cout):notEqualList(Rest1,Rest2,Cin,Cout).
eqFunAnd 1( , ,false,false,Cin,Cin).
eqFunAnd 1(Rest1,Rest2,true,true,Cin,Cout):equalList(Rest1,Rest2,Cin,Cout).
Cuadro 3.30: Conjunción de igualdades.
En el código, que se presenta en la tabla 3.30 se ha puesto especial cuidado en cubrir
todas las posibilidades sin redundancias. La primera cláusula, cuando ambas listas son
vacı́as, devuelve true automáticamente. En la segunda cláusula, lo primero que hacemos
es evaluar la igualdad entre la primera pareja de argumentos utilizando el predicado eqF un.
A continuación, eqF unAnd 1 hace una distinción de casos sobre el resultado. Si ha sido
f alse, entonces el resultado global es f alse sin necesidad de estudiar el resto de pares. Si
ha sido true, entonces se intentan resolver las igualdades entre el resto de parejas para
obtener como resultado final true, utilizando el predicado equalList (3.11.3), con lo que
se cubre el caso de que todas las igualdades de la conjunción se evalúen a true.
El caso que queda por estudiar es que la igualdad entre alguna de las parejas restantes
(todas menos la primera) se evalúe a f alse, en cuyo caso la conjunción serı́a f alse. Y este
CAPÍTULO 3. MECANISMO DE CÓMPUTO
122
caso es el que trata la tercera cláusula de eqF unAnd, que utiliza el predicado notEqualList
(3.12) sobre los restos de las listas.
Este mecanismo de evaluación de conjunciones también sigue la filosofı́a de la pereza y
tiene buenas propiedades de terminación. Por ejemplo, supongamos una función f definida
por una única regla f 3 = 4 y el objetivo ((f 2, 0) == (3, 1)) == B. Para resolver
esta restricción debe evaluarse una llamada a la función igualdad con los argumentos
(f 2, 0) y (3, 1), que tras varios pasos de cómputo se reducirá a resolver la conjunción
(f 2 == 3) ∧ (0 == 1). La primera condición de esta conjunción produce un fallo en el
cómputo, puesto que f 2 no está definido (esto no quiere decir que la condición sea f alse,
sino que no puede evaluarse). Sin embargo, por la segunda cláusula de eqF unAnd puede
evaluarse la segunda condición a f alse y se computa la respuesta B == f alse.
Si el objetivo fuese ((f 2, 0) == (3, 0)) == B entonces si que se produce un fallo en el
cómputo puesto que ahora el segundo elemento de la conjunción 0 == 0 se evalúa a true,
pero no podemos decir nada del primero f 2 == 3 (es indefinido).
En el ejemplo que proponı́amos al principio de la seccion (2 + 2 == 3 + 4) == B
con el código que hemos presentado para evaluar la función igualdad, no se reevalúan las
expresiones 2 + 2 y 3 + 4, ya que la última cláusula de eqF un se encarga de reducirlas
antes de instanciar la variable B. No obstante, en un objetivo como (X == 3 + 4) == B
se producirá una primera respuesta B == true, X == 7 y, reevaluando la expresión
3 + 4, se obtendrá B == f alse con X /= 7. Aquı́ no ha evitado la reevaluación porque
ello supondrı́a, en general, un análisis minucioso de la estructura de los argumentos que
globamente no repercutirá de forma positiva en la eficiencia del sistema.
3.14.
La función desigualdad (notEqF un)
La función desigualdad sigue la misma filosofı́a que la de igualdad. De hecho su implementación se apoya directamente en la de igualdad. La especificación de notEqF un
es:
notEqF un(X, Y, H, Cin, Cout) ⇔ H es f alse si la restricción X == Y se evalúa a true
y H es f alse si la restricción se evalúa a true, tomando como almacenes Cin y Cout.
notEqFun(X,Y,H,Cin,Cout):- H==true, !, notEqual(X,Y,Cin,Cout).
notEqFun(X,Y,H,Cin,Cout):-H==false,!,equal(X,Y,Cin,Cout).
notEqFun(X,Y,H,Cin,Cout):- eqFun(X,Y,Z,Cin,Cout),negate(Z,H).
negate(true,false) :- !.
negate(false,true).
Cuadro 3.31: Función desigualdad
El código se presenta en la tabla 3.31. Las dos primeras cláusulas son semejantes a
las dos primeras de eqF un, excepto que ahora si el resultado de la función es true debe
satisfacerse una desigualdad y si es f alse una igualdad.
La última cláusula hace uso de la función de igualdad directamente, que se encargará de
hacer las optimizaciones oportunas. Después se niega el resultado (Z) que produce esta
evaluación mediante el predicado negate que se presenta en esta misma tabla. Nótese
3.15. LAS RESTRICCIONES SOBRE LOS NÚMEROS REALES
123
que eqF un siempre devolverá uno de los valores true o f alse (si no se produce fallo),
es decir, nunca devolverá una variable; por este motivo puede colocarse un corte en la
primera cláusula de negate (en este caso, el corte no es indispensable y no supone una
gran optimización).
3.15.
Las restricciones sobre los números reales
La inclusión de restricciones sobre reales en el sistema ([AHL+ 96]) se hace de una
forma natural y es sencilla salvo por algunas cuestiones de carácter técnico. Los problemas fundamentales son debidos a la carencia de algunas operaciones en el interface de
comunicación con el resolutor que ofrece Sisctus.
En Sicstus Prolog puede activarse el resolutor de restricciones sobre reales utilizando
la directiva (véase [Hol95],[Gro96]):
use module(library(clpr)).
A partir de este momento Sicstus está preparado para recibir y resolver restricciones.
Para lanzar restricciones al sistema debe utilizarse una sintaxis especial (véase [Gro96]),
aunque para la exposición que vamos a hacer aquı́ bastará con conocer lo siguiente:
las restricciones deben ir siempre encerradas entre llaves (’{’ y ’}’) y separadas por
comas,
las ecuaciones utilizan ’=’ como sı́mbolo de igualdad,
las desigualdades se notarán con el sı́mbolo ’= \ =’
Por ejemplo, en Sicstus (con el resolutor cargado) puede resolverse el objetivo:
| ?- { X+Y=5, X=\=5 }.
que produce la respuesta:
{X=\=5.0},
{Y=5.0-X}
Una de las ventajas de T OY es que, por comodidad para el ususario, no se utiliza
ninguna sintáxis especial para las restricciones: no utiliza llaves, las ecuaciones se plantean
como igualdades (con el sı́mbolo ’==’) y las desigualdades son como siempre (sı́mbolo
’ /=’). El sistema se encargará de distinguir las restricciones numéricas y de hacer las
llamadas pertinentes al resolutor utilizando su sintaxis. Todas estas llamadas se localizan en las primitivas aritméticas del sistema y por ello el sistema cuenta con un juego
de primitivas especiales para el manejo de restricciones, que se encuentran en el archivo
primitivesClpr.pl.
La activación de restricciones sobre reales en T OY (mediante el comando /cflpr.)
prepara al sistema del modo siguiente:
se carga en memoria el resolutor proporcionado por Sicstus (use module(library(clpr))),
también se carga el nuevo juego de primitivas (primitivesClpr.pl),
se activa un flag que informa al sistema de modo de uso en el que se encuentra,
asertando el hecho clpr active (assert(clpr active).).
CAPÍTULO 3. MECANISMO DE CÓMPUTO
124
Para entender el funcionamiento de las nuevas primitivas, vamos a estudiar los cambios
que han de hacerse en el código de la función ‘<’ (para el resto de funciones se hacen
cambios similares). El código de esta función, trabajando sin restricciones sobre reales y
de acuedo con las ideas que expusimos en 3.10.6, tiene esta forma:
$<(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> (HX<HY,H=true;HX>=HY,H=false); errPrim).
Este código se encuentra en el archivo primitives.pl y es el que se utiliza cuando el sistema
no tiene activas las restricciones. El código de la función “menor” es simple: se evalúa una
forma normal de cabeza para cada uno de los argumentos (que en este caso, serán formas
normales) y se comprueba si ambos resultados son números. Si alguno de ellos no es un
número, no se puede evaluar la función sin el resolutor y se produce un error informando
de este hecho. Si efectivamente son números, se comprueba si el primero es menor que
el segundo, en cuyo caso la función devuelve true y si es mayor o igual que el segundo
devuelve f alse.
Nótese que el segundo caso (cuando la función devuelve f alse) no se resuelve por
exclusión porque para ello habrı́a que colocar un corte tras comprobar que se satisface
la primera alternativa. Después de verificar que ambos son números el código tendrı́a
esta forma: (HX < HY, !, true; H = f alse). Pero este código es incorrecto debido al
indeterminismo, ya que está ignorando la posibilidad de que los argumentos tengan más
de una posible reducción a f.n.c.. Por ejemplo, la función coin (2.3.7) puede reducirse tanto
a 0 como a 1 y en consecuencia, la expresión 0 < coin puede evaluarse tanto a true como
a f alse.
Con las restricciones aritméticas activas, una vez calculada una f.n.c. para cada uno
de los argumentos, debe lanzarse al resolutor la restricción correspondiente. Ası́, el código
(aún no definitivo) se transforma en:
$<(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
(H=true,{HX<HY};H=false,{HX>=HY}).
Obsérvese que ahora no se comprueba que las f.n.c.’s calculadas sean números (de
hecho pueden ser variables). La llamada al resolutor se encargará de manejar la restricción
a partir de este momento. Otro ejemplo puede ser la función ‘+’, que en primitivesClpr.pl
está implementada como (código no definitivo):
+(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = HX + HY}.
Con estas modificación habrı́amos terminado si no fuese por un detalle: el sistema ahora cuenta con restricciones de desigualdad sobre los reales y restricciones de desigualdad
estricta (las que ya tenı́amos). Estos dos tipos de desigualdad son indistinguibles desde el
punto de vista sintáctico (utilizan el mismo sı́mbolo /=); sin embargo, tienen un comportamiento distinto. Las de reales deben ser enviadas al resolutor, mientras que las otras son
gestionadas directamente por T OY (3.12).
3.15. LAS RESTRICCIONES SOBRE LOS NÚMEROS REALES
125
Para ilustrar el problema que se deriva de este solapamiento en las desigualdades, supongamos que lanzamos el objetivo X /= Y, X + Y == 2, X − Y == 0. La solución (única)
a las dos últimas restricciones es X == 1, Y == 1, pero por la primera restricción, este
objetivo es insatisfactible. Sin embargo, como hasta el momento T OY no hace distinción
entre desigualdades estrictas y sobre reales, la primera desigualdad serı́a tratada como se
explicó en 3.12 quedando el almacén [X : [Y ], Y : [X]]. Las dos siguientes restricciones,
de acuerdo con las nueva definiciones de ‘+’ y ‘−’ son enviadas al resolutor haciendo las
llamadas {X + Y = 2} y {X − Y = 0}. El resolutor trata ambas restricciones y produce un éxito haciendo las unificaciones X = 1,0 e Y = 1,0, ya que el resolutor no
cuenta con la información de la primera desigualdad: en ningún momento se ha
hecho la llamada {X = \ = Y }. El resultado de este cómputo producirı́a la respuesta
X /= Y, X = 1,0, Y = 1,0, que obviamente es incorrecta.
Para que el objetivo anterior fuese resuelto correctamente (y provocase fallo), el resolutor deberı́a contar con toda la información referente a las variables X e Y , y en particular
con la restricción X /= Y . La solución serı́a enviar esta restricción (la primera del objetivo) al resolutor, pero T OY no maneja tipos en tiempo de ejecución16 y ante una
restricción X /= Y es incapaz de distinguir a priori si se trata de una desigualdad entre
reales o es una desigualdad más.
El propio planteamiento del problema sugiere la solución: cuando nos encontramos con
la restricción X /= Y , supondremos en principio que se trata de una restricción de desigualdad estricta y la almacenaremos como tal; si en el futuro se plantea alguna restricción
numérica sobre X o sobre Y , entonces la desigualdad anterior será transformada en una
restricción numérica y enviada al resolutor, desapareciendo del almacén de desigualdades.
En el ejemplo anterior, X /= Y se almacena como una desigualdad más, pero cuando se
plantea la primera restricción numérica, X + Y == 2, que involucra a X (en este caso
también a Y ), entonces la desigualdad X /= Y es enviada al resolutor, en el que se delega
la responsabilidad de tratar correctamente toda la información enviada, y en este caso de
detectar el fallo que debe producirse al lanzar la última restricción del objetivo.
La cesión de restricciones al resolutor por parte de los almacenes lleva asociado un
efecto de propagación. Veamos un ejemplo: sea el objetivo X /= Y, X /= Z, Z −3 == V . Las
dos primeras restricciones, al no implicar ninguna operación numérica, son almacenadas
como desigualdades estrictas y tendremos el almacén [X : [Y, Z], Y : [X], Z : [X]]. La
última restricción, sin embargo, si que implica una operación aritmética, y por tanto una
llamada al resolutor ({Z −3 = V }). Entonces la restricción X /= Z debe pasar al resolutor,
pero también debe pasar X /= Y . La explicación: si sobre Z hay una restricción numérica,
todas las desigualdades asociadas a Z deben pasarsele al resolutor, ya que Z es de tipo
numérico; si Z es de tipo numérico y tenemos la restricción Z /= X, entonces el tipo
de X también debe ser numérico y todas las desigualdades de X tienen que pasar al
resolutor, y en particular, X /= Y . Este es el efecto de propagación que mencionábamos
con anterioridad y que, en este ejemplo, hace que todas las restricciones vayan a parar
finalmente al resolutor.
Para hacer esta cesión de restricciones T OY cuenta con el predicado toSolver, cuya
especificación es la siguiente:
16
Cuando T OY analiza el objetivo X /= Y, X + Y == 2, X − Y == 0 y hace chequeo de tipos, es
capaz de inferir que tanto X como Y tienen tipo numérico, pero una vez hecho este análisis y verificada
la corrección no hace ninguna anotación sobre tipos. Esto significa que en tiempo de ejecución sabe que
los tipos son correctos, pero no conoce los tipos concretos. Manejar tipos en tiempo de ejecución supone
arrastrar una gran cantidad de información que afectarı́a notablemente a la eficiencia del sistema.
126
CAPÍTULO 3. MECANISMO DE CÓMPUTO
toSolver(X, Cin, Cout) ⇔ si X es una variable entonces toda desigualdad Y /= Z de Cin
para la que Cin contiene una secuencia (posiblemente unitaria) X /= X1 , X1 /= X2 , ..., Xn /=
Y, Y /= Z es lanzada al resolutor en la forma {Y = \ = Z}; Cout es el resultado de eliminar de
Cin todas las desigualdades que han pasado al resolutor. Si X no es variable deja el almacén
intacto y no pasa nada al resolutor.
La especificación implı́citamente contiene una noción de relación de desigualdades entre
variables: A /= B está relacionado con todas desigualdades de la forma A /= X y B /= Y .
Apoyándonos en esta relación podrı́amos especificar toSolver(X, Cin, Cout) como: si X
es una variable, todas sus restricciones asociadas y todas las que pertenezcan al cierre
transitivo de la relación anterior pasan al resolutor (y Cout es el resultado de eliminarlas
de Cin).
toSolver(X,Cin,Cin):-nonvar(X),!.
toSolver(X,Cin,Cout):extractCtr(X,Cin,Cout1,CX),
passToSolver(X,CX,Cout1,Cout).
passToSolver( ,[ ],Cin,Cin).
passToSolver(X,[Y|R],Cin,Cout):{ X = \ = Y },
(
var(Y),
!,
toSolver(Y,Cin,Cout1),
passToSolver(X,R,Cout1,Cout)
;
passToSolver(X,R,Cin,Cout)
).
Cuadro 3.32: Cesión de desigualdades al resolutor
En la tabla 3.32 se muestra el código para toSolver. La primera cláusula, en caso de que
el argumento no sea variable, deja el almacén intacto y no hace nada más. En la segunda
cláusula, cuando se trata de una variable se extraen del almacén (con eliminación) sus
desigualdades asociadas y se utiliza el predicado auxiliar passT oSolver. Este predicado
lanza al resolutor, una a una, las desigualdades asociadas a la variable, pero además, si
el otro miembro es también una variable, llama recursivamente a toSolver con esta nueva
variable para propagar la cesión de restricciones.
De acuerdo con lo que acabamos de ver, el nuevo código para la función + será:
+(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = HX + HY},
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
Ahora se han incluido llamadas a toSolver para cada uno de los argumentos de la
función y también para el resultado. De esta forma nos aseguramos de que todas las
3.15. LAS RESTRICCIONES SOBRE LOS NÚMEROS REALES
127
restricciones que le corresponden al resolutor, le serán enviadas en algún momento del
cómputo.
Aún queda otro detalle pendiente. Supongamos el objetivo X + Y == 2, X /= 1, Y ==
1; la primera restricción es automáticamente enviada al resolutor y en este caso, como ni
X ni Y tienen desigualdades asociadas, no se pasa ninguna restricción más. La segunda
restricción es tratada por el predicado notEqual y queda en el almacén como X : [1]; y
la tercera es procesada por equal y produce la ligadura Y = 1. El objetivo tendrı́a éxito,
cuando en realidad es insatisfactible. El motivo es que la restricción X /= 1 deberı́a haber
pasado al resolutor, pero no lo ha hecho. Para solucionar este problema necesitaremos el
predicado auxiliar isReal(X), que tiene éxito si en el estado actual del cómputo se puede
afirmar que la variable X es de tipo real (no se utiliza información del inferidor), o lo
que es lo mismo: si X es un número o una variable que tiene restricciones asociadas en el
resolutor.
Con este predicado isReal, al que volveremos más adelante, estamos en disposición de
introducir una nueva cláusula para el predicado notEqualHnf (véase 3.12), especı́fica para
desigualdades entre reales. Esta cláusula se muestra en la tabla 3.33 y es la primera del
predicado notEqualHnf . Lo primero que hace es comprobar el modo de uso del sistema
comprobando el flag clpr active y sólo actúa en el caso de que las restricciones sobre
reales hayan sido activadas. En este caso comprueba si tiene suficiente información para
determinar que alguno de los argumentos es de tipo real y, si es ası́, envı́a la desigualdad
al resolutor (en otro caso esta cláusula fallará y será otra la que trate la desigualdad).
Obsérvese que el almacén de desigualdades permanece invariable.
notEqualHnf(X,Y,Cin,Cin):clpr active,
(isReal(X);isReal(Y)),!,{ X = \ = Y }.
Cuadro 3.33: Desigualdad entre reales
Otro hecho importante es que no se pasan desigualdades del almacén al resolutor. Por
ejemplo, en el objetivo X /= Y, X /= 2 la primera restricción, de acuerdo con lo que hemos
visto, pasa al almacén; la segunda será tratada por la nueva cláusula y pasará al resolutor.
Pero ahora, se sabe que X es de tipo real y podrı́an pasarse al resolutor todas las restricciones que la involucren, y en particular X /= Y . De hecho serı́a correcto (y quizá lo más
coherente con lo hemos visto en este apartado) hacer en esta cláusula llamadas toSolver
con X y con Y para pasar sus desigualdades al resolutor. Pero no es necesario pasar estas
desigualdades al resolutor. La explicación es sutil: tras procesar las dos restricciones del
objetivo X /= Y, X /= 2 el resolutor conoce la restricción X /= 2 pero X /= Y permanece en el almacén. Intuitivamente, esta última desigualdad deberı́a ser también conocida
por el resolutor; de lo contrario se corre el riesgo de que en algún momento el resolutor
unifique X e Y (que serı́a incorrecto). Sin embargo, para que esto ocurra debe lanzársele
alguna restricción aritmética (que no sea una desigualdad) y que involucre a X e Y (y que
provoque la unificación), en cuyo caso la primitiva correspondiente se encargará de enviar
la restricción X /= Y al resolutor y no se podrá hacer tal unificación.
En la tabla 3.34 se muestra el código del predicado isReal. La primera cláusula utiliza el
predicado number de Prolog que tiene éxito si su argumento es un número. La segunda, en
el caso de que el argumento sea variable, hace una proyección de restricciones sobre dicha
CAPÍTULO 3. MECANISMO DE CÓMPUTO
128
isReal(X):-number(X),!.
isReal(X):var(X),
linear:dump([X], ,L),
!,
L \ == [ ].
Cuadro 3.34: Código de isReal
variable utilizando el predicado dump17 . Si esta proyección o conjunto de restricciones en
las que interviene la variable, no es vacı́a, entonces en algún momento anterior del cómputo
se estableció alguna restricción aritmética sobre dicha variable y por tanto, es de tipo real
con seguridad.
Para terminar, describimos brevemente la secuencia de cómputos que hace el sistema en presencia de restricciones, mediante el ejemplo de la figura 3.6. En definitiva, es
un esquema de traducción de restricciones sobre reales en programación lógico funcional
(CLF P (R)) a restricciones en programación lógica (CLP (R), [FHK+ 93, Col90, Coh90,
HJM+ 91, JM94]).
TOY>
X
+
3
hnf
<=
sin 0.5
hnf
X
3
hnf
0.47...
+
al resolutor
{X+3 = H}
al resolutor
{H <= 0.47...}
yes
{X <= -2.52...}
Figura 3.6: Cómputo con restricciones
El presencia del objetivo X+3 <= sin 0.5, se hace una llamada a la función ‘<=’, que
solicita una f.n.c. para los dos argumentos X+3 y sin 0.5. La reducción del segundo produce directamente un número (0.47...) y para el segundo hay que utilizar la función ‘+’. Esta
función evalúa a su vez, una f.n.c. para sus dos argumentos X y 3. A continuación, se introduce una nueva variable H y se envı́a al resolutor la restricción { H = X+3 }. La función
‘<=’ completa su evaluación enviando al resolutor la restricción { H <= 0.47...}. Como
resultado, al final del proceso se obtiene la respuesta X <= -2.52..., que es efectivamente
la solución de la restricción.
17
La llamada dump([X], , L) devuelve en la lista L todas las restricciones sobre reales en las que interviene la variable X mediante proyección. Este predicado no está disponible en la versión 3# 5 de Sicstus
Prolog ([Gro96]) y fue programado por C. Holzbaur a petición nuestra. La versión 3# 6 ([Gro97]) lo incluye
como parte del interface del resolutor.
Capı́tulo 4
De la semántica
4.1.
Introducción
En este capı́tulo presentaremos algunos aspectos semánticos de T OY, como la pereza y
las funciones indeterministas. Nuestro objetivo en este capı́tulo es ofrecer una justificación
formal de algunos algunos aspectos de la semántica del T OY. Nuestro interés principal
se ha centrado en la construcción de un cálculo operacional que se aproxime a la implementación real en lo que se refiere al mecanismo resolución de igualdades y desigualdades
estrictas, y para el cual se puedan probar resultados de corrección y completitud (con
respecto a un cálculo de pruebas). Además, veremos que las desigualdades suponen una
potente herramienta con la que se puede abordar la Estrategia Guiada por la Demanda
que utiliza el sistema para la evaluación de funciones.
No obstante, el cálculo que presentamos no cubre todos los aspectos de la implementación. En concreto, no trata el orden superior ni los tipos, y tampoco las restricciones
sobre los números reales. En cuanto a las igualdades y desigualdades, el cálculo captura
el mecanismo esencial de resolución, pero no refleja todas las optimizaciones que realiza el
sistema real y que estudiamos en el capı́tulo anterior.
En cuanto a los antecedentes, en [GHL+ 96, GHL+ 98] se propone una lógica de reescritura basada en constructoras (CRW L) como fundamento de la programación lógico
funcional. En este modelo la evaluación perezosa y las funciones indeterministas encuentran
una justificación precisa desde el punto de vista semántico. Sobre este trabajo se han
desarrollado algunas extensiones que abarcan caracterı́sticas de orden superior ([GHR97]),
y tipos polimórficos y algebraicos ([AR97a, AR97b]).
El cálculo que presentamos aquı́ es otra extensión1 del marco CRW L, en la que se
incorporan desde un principio las restricciones de desigualdad. Este tipo de restricciones
suponen un recurso importante y tiene interés por sı́ mismo, ya que aparecen en situaciones
muy comunes en el contexto lógico funcional (véase 2.3.8) y podrán presentarse tanto en
los programas como en las respuestas.
En [KLM+ 92] se introducen las desigualdades para una clase restringida de programas
en BABEL ([MR89, MR92]) y se propone una máquina abstracta para implementar el
lenguaje resultante. Dicho trabajo está enfocado fundamentalmente hacia la implementación y las desigualdades carecen de tratamiento teórico. En [AGL94] la aproximación es
más cercana a la que presentaremos aquı́, aunque aún hay muchas diferencias. El marco
1
Es una extensión en cuanto a los cálculos presentados. En [GHL+ 96, GHL+ 98] se presenta también
una semántica de modelos, según la que todo programa tiene un modelo libre, que no ha sido abordada
aquı́.
129
CAPÍTULO 4. DE LA SEMÁNTICA
130
teórico considerado en [AGL94] es el esquema CF LP (X ), que fue concebido para programación lógico funcional con restricciones siguiendo las lı́neas de CLP (X ) (programación
lógica con restricciones, [JL87]). Este marco es muy general en el sentido de que cubre
muchos sistemas de restricciones, pero presenta problemas en la unificación perezosa. En
el contexto de CRW L la unificación recibe un tratamiento adecuado, ası́ como las funciones no deterministas y el sharing. Por otro lado, se podrán demostrar la completitud con
respecto a soluciones generales (en [AGL94] era con respecto a soluciones cerradas).
Nuestro cálculo operacional, como sucede con el de [GHL+ 96, GHL+ 98], implica en
principio una estrategia ingenua para la evaluación de funciones. La riqueza expresiva de
las desigualdades permitirá hacer una transformación de programas de modo que, con el
mismo cálculo dicha evaluación corresponderá a la estrategia guiada por la demanda que
implementa T OY (3.10).
Por otro lado, en nuestro cálculo se maneja la noción de prioridad en las condiciones
de los objetivos, con la que se imponen ciertas restricciones sobre el orden en el que deben
procesarse dichas condiciones. Con ello además de aproximarnos más a la implementación
real de T OY, se evitan algunos test globales (y costosos en una implementación real) que
eran necesarios en la versión de [GHL+ 96, GHL+ 98]. Nuestro cálculo incorpora también la
noción de suspensión, que se hace explı́cita en las propias reglas (condiciones de prioridad
0). La resolución de objetivos se hace de forma perezosa y las suspensiones son condiciones
cuya resolución se bloquea mientras que no sea estrictamente necesaria para continuar el
cómputo. Para procesar las suspensiones se utilizan reglas explı́citas de activación.
Notación: en este capı́tulo, por la frecuencia de uso, utilizaremos el sı́mbolo 6= para
la desigualdad.
4.2.
Preliminares
4.2.1.
Signaturas, términos y c-términos
S
n
Partimos de una signatura Σ = DCS
Σ ∪ DFΣ , donde DCΣ =
n∈IN DCΣ es un conjunto
n
de sı́mbolos de constructora y F SΣ = n∈IN F SΣ es un conjunto de sı́mbolos de función,
todos ellos con su aridad asociada y tal que DCΣ ∩ F SΣ = ∅. Suponemos además un
conjunto enumerable V de sı́mbolos de variable. Definimos T erm como el conjunto de
términos (totales) construidos con sı́mbolos de Σ y V de la manera usual y distinguimos
el subconjunto CT erm de términos (totales) construidos o c-términos, que sólo utilizan
los sı́mbolos de DCΣ y V. Habitualmente omitiremos el subı́ndice Σ.
Normalmente trabajaremos con la signatura extendida Σ⊥ que es el resultado de añadir
el nuevo sı́mbolo de constante (constructora de aridad 0) ⊥ a Σ, que juega el papel de
valor indefinido. Sobre esta nueva signatura pueden construirse los conjuntos T erm⊥ y
CT erm⊥ de términos parciales y c-términos parciales respectivamente (están parcialmente
definidos).
Como notación habitual, utilizaremos letras mayúsculas X, Y, Z... para los sı́mbolos
de variable, c, d... para los de constructora, f, g... para los de función, e para los términos
y t, s, ... para los c-términos. Para destacar la aridad de un sı́mbolo de constructora o
función, en algunos casos utilizaremos la notación l/n, donde l es el sı́mbolo en cuestión y
n es su aridad (l ∈ DC n ∪ F S n ).
Se puede definir un orden de aproximación natural v sobre los términos parciales como
el mı́nimo orden parcial sobre T erm⊥ que satisface:
⊥ v e, para todo e ∈ T erm⊥
4.3. EL CÁLCULO DE PRUEBAS GORC6=
131
e1 v e01 , ..., en v e0n ⇒ l(e1 , ..., en ) v l(e01 , ..., e0n ), para todo l ∈ DC n ∪ F S n y
ei , e0i ∈ T erm⊥ .
En algunas demostraciones utilizaremos el concepto de profundidad de un término que
se define de la manera usual:
prof (⊥)
prof (X)
prof (l)
prof (l(e1 , ..., en )
4.2.2.
=0
= 0, ∀X ∈ V
= 0, ∀l ∈ DC 0 ∪ F S 0
= max{prof (ei )|i = 1..n} + 1,
∀l ∈ DC n ∪ F S n , n ≥ 0
Sustituciones
Definimos el conjunto de sustituciones totales como
Subst = {θ : V → T erm}
Ampliando el rango con la signatura extendida Σ⊥ definimos el conjunto de sustituciones
parciales o simplemente sustituciones como
Subst⊥ = {θ : V → T erm⊥ }
En lo que sigue nos interesará en especial un subconjunto de Subst⊥ , el de c-sustituciones
parciales2 que se define como
CSubst⊥ = {θ : V → CT erm⊥ }
Notaremos por tθ al resultado de aplicar la sustitución θ al término t en el sentido
habitual y por µθ denotaremos la composición de θ con µ tal que t(µθ) = (tµ)θ. Como de
costumbre θ = [X1 /t1 , ..., Xn /tn ] representa la sustitución que cumple Xi θ ≡ ti para todo
i = 1..n y Y θ ≡ Y para todo Y ∈ V − Xi , i = 1..n.
El orden de aproximación v induce un orden natural de aproximación sobre Subst⊥
definido como:
θ v θ0 sii Xθ v Xθ0 para todo X ∈ V
4.3.
El cálculo de pruebas GORC6=
En esta sección introducimos el cálculo GORC6= (Goal Oriented Rewriting Calculus
with disequalities), que es una extensión del cálculo lógico de reescritura GORC presentado en [GHL+ 96, GHL+ 98]. Esté cálculo está orientado a la resolución de objetivos y
es una aproximación a la semántica de T OY desde un nivel abstracto. En la extensión
que presentamos aquı́, además de incluir las reglas oportunas para las desigualdades, hacemos un tratamiento ligeramente distinto de la igualdad estricta, más próximo al modelo
operacional que desarrollaremos en secciones posteriores.
Algunas propiedades de este cálculo que, de alguna manera, han inspirado su construcción son las que siguen:
para la aplicación de una regla sólo se requiere el examen de la estructura más
externa de los términos,
2
Se puede definir también el conjunto de c-sustituciones totales como CSubst = {θ : V → CT erm},
pero estas sustituciones no tienen ninguna relevancia en nuestra teorı́a.
CAPÍTULO 4. DE LA SEMÁNTICA
132
la pereza está capturada,
el sharing está implı́cito.
Fijada una signatura Σ = DC ∪ F S un programa es un conjunto R de reglas de
reescritura condicional de la forma:
l = r <== C
donde:
l ≡ f (t1 , ..., tn ), el lado izquierdo de la regla, es un patrón lineal (las variables tienen
una sola ocurrencia en la tupla (t1 , ..., tn )) con f ∈ F S n , ti ∈ CT erm, ∀i = 1..n.
r ∈ T erm es el cuerpo de la regla, y
C es un conjunto de restricciones (posiblemente vacı́o) de igualdad estricta y desigualdad de la forma e1 3e01 , ..., em 3e0m , con ei , e0i ∈ T erm, ∀i = 1..m y 3 ∈ {==, 6=}.
Cuando C es vacı́o se omitirá el sı́mbolo <==.
Definimos el conjunto de c-instancias parciales de las reglas de un programa R como:
[R]⊥ = {(l = r <== C)θ | (l = r <== C) ∈ R, θ ∈ CSusbst⊥ }.
Dado un programa R el cálculo GORC6= servirá para construir pruebas de restricciones
de igualdad de la forma e == e0 , desigualdades e 6= e0 (en ambos casos con e, e0 ∈ T erm⊥ )
y aproximaciones de la forma e → t (con e ∈ T erm⊥ y t ∈ CT erm⊥ ), con respecto a las
reglas de reescritura de [R]⊥ . Cuando se puede construir una prueba para una condición c
(restricción o aproximación), diremos que la condición c es GORC6= -probable con respecto
a R (o simplemente probable cuando no haya confusión) y lo notaremos como R ` c.
Las reglas del cálculo están guiadas por la forma (sintáctica) de los términos que
aparecen en las condiciones, en particular por el sı́mbolo más externo, que determinará la
regla que se puede aplicar. La forma externa de un término puede ser de tres formas:
X ∈ V, c(e1 , ..., en ) con c ∈ DC n y f (e1 , ..., en ) con f ∈ F S m ; y las reglas recogen todas
las posibles igualdades y desigualdades que pueden presentarse atendiendo a dichas formas.
A continuación presentamos las reglas del cálculo GORC6= y algunas ideas intuitivas
acerca del significado que tienen. Con el fin de reducir el número de reglas, aquı́ y en lo
que sigue, los sı́mbolos == y 6= se consideran simétricos.
CÁLCULO GORC6=
1. e → ⊥
2. X → X
∀X ∈ V
3.
e1 → t1 , ..., en → tn
c(e1 , ..., en ) → c(t1 , ..., tn )
4.
e1 → s1 , ..., en → sn C
f (e1 , ..., en ) → t
5.
f (e) → t t3e0
f (e)3e0
c ∈ CDn ,
s→t
f ∈ F Sn,
ti ∈ CT erm⊥
t 6≡ ⊥,
(f (s1 , ..., sn ) = s <== C) ∈ [R]⊥
t ∈ CT erm⊥ ,
3 ∈ {==, 6=}
4.3. EL CÁLCULO DE PRUEBAS GORC6=
6. X == X
7.
9.
X∈V
e1 == e01 , ..., en == e0n
c(e1 , ..., en ) == c(e01 , ..., e0n )
8. c(e) 6= d(e0 )
133
c ∈ DC n
c ∈ DC n , d ∈ DC m , c 6≡ d
ei 6= e0i
c(e1 , ..., en ) 6= c(e01 , ..., e0n )
c ∈ DC n ,
i ∈ {1..n}
Las reglas (6) a (9) permiten hacer pasos de inferencia para probar restricciones en las
que ambos miembros son formas normales de cabeza. Por ejemplo, una restricción de la
forma c(e1 , ..., en ) == c(e01 , ..., e0n ) (c ∈ DC n ) será probable si lo son todas las restricciones
e1 == e01 , ..., en == e0n y una desigualdad de la forma c(e1 , ..., en ) 6= d(e01 , ..., e0m ) (c ∈
DC n , d ∈ DC m ) se probará automáticamente si c 6≡ d; en otro caso debe ser probable
alguna de las desigualdades ei 6= e0i . Para variables, la única restricción que puede probarse
es X == X.
Cuando aparecen expresiones funcionales en una restricción como f (e)3e0 (f ∈ F S n )
hay que hacer uso de las reglas del programa R. Se aplicará la regla (5), que introduce aproximaciones →. La prueba R ` f (e)3e0 se reduce a R ` f (e) → t, t3e0 , con t ∈ CT erm⊥ .
Ahora, la restricción t3e0 no contiene ningún sı́mbolo de función en su lado izquierdo, ya
que t ∈ CT erm⊥ (si su lado derecho es una expresión funcional se aplicará nuevamente
la regla (5)). Para probar la aproximación f (e) → t, la regla (4) permite hacer un paso
de reescritura con alguna de las c-instancias de [R]⊥ . Los argumentos de llamada deben
reducirse (o aproximarse) a los argumentos de la c-instancia (paso de parámetros), el
cuerpo de la c-instancia debe reducirse a t y finalmente, deben probarse las restricciones
C de la c-instancia.
Las reglas (2) y (3) reducen las pruebas en el caso de aproximaciones entre formas
normales de cabeza, mientras que la regla (1) es la que captura la pereza, como veremos
en el siguiente ejemplo.
Ejemplo 1
Sea Σ = {0/0, s/1, [ ]/0, : /2} ∪ {f /1, coin/0}. Asumimos ‘[ ]’ y ‘:’ como las constructoras de listas, aunque utilizaremos la notación Prolog para representarlas; por ejemplo,
la expresión [0, s(0)|L] representa la lista (0 : (s(0) : L)).
Sea R un programa sobre Σ definido por las reglas siguientes:
f (N ) = [N |f (s(N ))]
coin = 0
coin = s(0)
La función f es similar al f rom de programación funcional pura (f (0) produce la lista
[0, s(0), s(s(0)), ...]) y coin representa los dos posibles resultados de lanzar una moneda al
aire. Una (en general puede haber varias) de las posibles derivaciones formales para probar
la validez del objetivo f (coin) 6= [0, 0] con respecto a GORC6= (R ` f (coin) 6= [0, 0]) es la
siguiente:
CAPÍTULO 4. DE LA SEMÁNTICA
134
(3)
0→0
(1)
s(0) → s(0)
f (s(0)) → ⊥
[s(0)|f (s(0))] → [s(0)|⊥]
(3)
f (s(0)) → [s(0)|⊥]
[0|f (s(0))] → [0, s(0)|⊥]
f (coin) → [0, s(0)|⊥]
f (coin) 6= [0, 0]
(3)
(3)
(3)
(3)
(4)
(4)
(5)
0→0
coin → 0
(3)
(3) 0 → 0
0→0
s(0) → s(0)
(3)
(8)
s(0) 6= 0
[s(0)|⊥] 6= [0]
(9)
[0, s(0)|⊥] 6= [0, 0]
(9)
Este ejemplo muestra la forma de una derivación de una desigualdad que involucra
la función indeterminista coin y una lista potencialmente infinita producida por la función f . La regla de GORC6= que se aplica en cada paso está anotada entre paréntesis.
Como el lado izquierdo de la desigualdad inicial es una expresión funcional, la derivación
comienza aplicando la regla (5) para reducir dicha expresión, tomando como c-término t
la lista [0, s(0)|⊥]. Como resultado se obtiene la aproximación f (coin) → [0, s(0)|⊥] y la
desigualdad [0, s(0)|⊥] 6= [0, 0].
La aproximación se trata en el paso (4) , en el que se toma la c-instancia (f (0) =
[0|f (s(0))]) ∈ [R]⊥ (regla de f con θ = [N/0]) para reescribir f (coin) a [0|f (s(0))] (haciendo la aproximación coin → 0, para la que se tomará a su vez la primera regla de coin,
que es c-instancia de sı́ misma). El sharing está implı́cito en (4) : obsérvese que coin, el
argumento de f , sólo se reduce una vez al evaluar la expresión f (coin), a pesar de que
en la regla de f el argumento tiene dos ocurrencias en el cuerpo. Otro hecho importante
es el siguiente: en los lados derechos de las aproximaciones → se introducen c-términos
(t en (4) y (5)), o bien, provienen de los argumentos de c-instancias de reglas (s1 , ..., sn
en (4)), que son también c-términos, es decir, nunca se va a introducir una expresión que
contenga sı́mbolos de función en el lado derecho de una aproximación, lo que significa que
las aproximaciones con las que trabajará el cálculo siempre tienen c-términos
en el lado derecho.
Obsérvese también que tras varios pasos de derivación (que no detallamos), en el paso
(1) se hace la aproximación f (s(0)) → ⊥. De este modo f (coin) se ha reducido perezosamente a la expresión [0, s(0)|⊥], es decir, se evalúa sólo un fragmento de la estructura
infinita. Esta evaluación produce como resultado una lista cuyos dos primeros elementos
son 0 y s(0), y el resto de elementos permanecen indefinidos o indeterminados. No es
difı́cil apreciar que en un cálculo no perezoso el objetivo que estamos tratando producirı́a
no terminación al intentar reducir f (coin) (intentarı́a generar una estructura infinita).
Incluso sin tratarse de estructuras infinitas, un cálculo perezoso es capaz, en general, de
proporcionar pruebas más breves para los objetivos, puesto que puede dejar indefinidas
aquellas expresiones cuya evaluación no sea estrictamente necesaria. Ası́ pues la pereza,
en un cálculo de pruebas tiene dos consecuencias fundamentales: puede conseguir pruebas
más cortas y permite probar la validez de objetivos que un cálculo impaciente no podrı́a
probar. En un cálculo operacional estas dos virtudes se traducen en mayor eficiencia y
mejores propiedades de terminación respectivamente. La pereza justifica el hecho de que
utilicemos la signatura extendida Σ⊥ y la introducción de la regla (1) en nuestro cálculo,
que permite aproximar cualquier término a ⊥, o lo que es lo mismo, dejarla indefinida.
Por último, nótese que en la desigualdad [0, s(0)|⊥] 6= [0, 0], equivalente a (0 : (s(0) :
⊥)) 6= (0 : (0 : [ ])), en la primera aplicación de la regla (9) se elige de modo indeterminista
la segunda pareja de argumentos de ‘:’ para conseguir la prueba. A continuación, sobre el
resultado (s(0) : ⊥) 6= (0 : [ ]) se toman los primeros argumentos obteniendo s(0) 6= 0, que
se deriva por (8).
En este ejemplo, las reglas de función no contienen restricciones. Si las tuviesen apa-
4.3. EL CÁLCULO DE PRUEBAS GORC6=
135
recerı́an en la derivación (c-instanciadas) cuando se aplica la regla (4) de GORC6= y,
naturalmente habrı́a que probar también su validez.
¥
El marco de [GHL+ 96, GHL+ 98] asume una semántica por call-time choice, siguiendo el
enfoque de [Hus93]. Como se muestra en en estos trabajos y en el apartado 2.3.7 de nuestro
trabajo (ejemplo de double coin), call-time choice es perfectamente compatible con una
semántica no estricta y con la evaluación perezosa, supuesto que se realiza compartición
(sharing) en todas las apariciones de una variable en el lado derecho de una regla .
A continuación demostraremos algunas propiedades que se verifican para este cálculo
y que serán de utilidad para relacionarlo con el cálculo operacional que veremos más
adelante.
Proposición 1 (Transitividad de →) La relación → es transitiva, es decir, si a, b, c ∈
T erm⊥ y a → b y b → c son probables, entonces a → c también es probable.
Demostración
En primer lugar si b ≡ ⊥ sólo puede ser c ≡ ⊥ y a → c ≡ ⊥ es también probable por
la regla (1).
Si b 6≡ ⊥ procedemos por inducción sobre el número de pasos l de la prueba de a → b:
l = 1 Como b 6≡ ⊥ y b ∈ CT erm⊥ tenemos dos posibles pruebas para a → b:
Π ≡ (2) X → X, con a ≡ b ≡ X. Para que b → c sea probable tiene que ser c ≡ X y
entonces también a → c es probable por (2).
Π ≡ (3) d → d, siendo a ≡ b ≡ d ∈ DC 0 (constante). Como d → c es probable tiene
que ser c ≡ d o c ≡ ⊥; en ambos casos a → c es probable por la regla (3) si c ≡ d, o
por (1) si c ≡ ⊥.
l + 1 a tiene que tener una de las dos formas siguientes:
a ≡ d(e1 , ..., em ). Como a → b, entonces tiene que ser b ≡ d(t1 , ..., tm ) y las aproximaciones ei → ti deben ser probables. Como b → c también es probable tiene que
ser c ≡ ⊥ en cuyo caso a → c es probable por (1), o bien c ≡ d(s1 , ..., sm ) y entonces
deben ser probables las aproximaciones ti → si . Por h.i. son probables ei → si y
podemos construir una prueba para a → c con la regla de descomposición (3).
a = f (e1 , ..., em ). La prueba de a → b debe construirse con la regla (4). Entonces debe
existir (f (t1 , ..., tm ) = r <== C) ∈ [R]⊥ tal que ei → ti , C, r → b sean probables.
Como la prueba de r → b tiene menos de l + 1 pasos y b → c es también probable,
por h.i., es probable r → c y podemos reconstruir una prueba para a → c con la
regla (4) de GORC6= .
¥
La igualdad estricta no es transitiva en general, debido al indeterminismo. Es decir, que
a == b y b == c sean GORC6= -probables no implica que a == c sea GORC6= -probable.
Por ejemplo, sean a y c dos constantes (constructoras de aridad 0) distintas, f una función
(indeterminista) definida por las reglas f X = a y f X = c. Podemos probar a == f X
y f X == c, pero no es probable a == c. No obstante, si que podemos demostrar un
resultado “parcial” de transitividad para == que se recoge en la siguiente proposición.
CAPÍTULO 4. DE LA SEMÁNTICA
136
Proposición 2 (Transitividad restringida de ==) Sean a, c ∈ T erm⊥ y b ∈ CT erm⊥ .
Si a == b, b == c son GORC6= -probables, entonces a == c es GORC6= -probable (y
b ∈ CT erm, es decir, b es total).
Antes de demostrar el resultado observemos que en el enunciado se exige la premisa
b ∈ CT erm⊥ (posiblemente parcial) y se concluye b ∈ CT erm (total). Esto, aunque se
verá en la demostración es bastante intuitivo porque no hay ninguna regla en GORC6=
que permita probar una igualdad con ⊥ en alguno de sus miembros.
Demostración
Por inducción sobre la longitud l (número de pasos) de la prueba Π de a == b:
l = 1 Si la prueba de a == b puede hacerse en un paso sólo pueden ocurrir dos cosas:
a ≡ b ≡ X ∈ V o a ≡ b ≡ d ∈ DC 0 . Entonces la prueba de a == c es idéntica a la de
a == b por ser a ≡ b. Y en ambos casos b es total.
l + 1 Hacemos una distinción de casos sobre la forma de a:
Si a ≡ d(e1 , ..., en ), con d ∈ DC n , como b ∈ CT erm⊥ y a == b es probable, entonces
tiene que ser b ≡ d(t1 , ..., tn ) con ti ∈ CT erm⊥ y la prueba de a == b será de la
forma:
Π1 ...Πn
d(e1 , ..., en ) == d(t1 , ..., tn )
siendo Πi prueba de ei == ti .
Puesto que b == c es probable, hay dos posibilidades para c:
• c ≡ d(e0 ), con lo que la prueba b == c será
Π01 ...Π0n
d(t1 , ..., tn ) == d(e01 , ..., e0n )
con Π0i prueba de ti == e0i .
Tenemos pruebas de longitud ≤ l para ei == ti y son probables las igualdades
ti == e0i con ti ∈ CT erm⊥ , luego por h.i., existen pruebas Π00i para ei == e0i y
ti ∈ CT erm. Entonces b ∈ CT erm y se puede reconstruir la prueba de a == c
que será
Π001 ...Π00n
d(e1 , ..., en ) == d(e01 , ..., e0n )
• c ≡ f (e0 ), luego la prueba de b == c es de la forma
Π1 Π2
f (e0 ) == d(t)
con Π01 prueba de f (e0 ) → r, Π02 prueba de r == d(t) y r ∈ CT erm⊥ .
En estas condiciones, r debe ser de la forma r ≡ d(r1 , ..., rn ), ri ∈ CT erm⊥ y
Π02 debe contener pruebas de ri == ti . Tenemos pruebas de longitud < l + 1
de ei == ti y también pruebas de ri == ti , con ti ∈ CT erm, luego por h.i.,
ti ∈ CT erm y debe haber pruebas Πi para ei == ti . Entonces b ∈ CT erm y se
puede reconstruir una prueba para a == c, que tendrá la forma:
4.3. EL CÁLCULO DE PRUEBAS GORC6=
137
Π1 Π001 ...Π00n
d(e) == f (e0 )
Si a ≡ f (e) la prueba a == b es de la forma
Π1 Π2
f (e) == b
con Π1 prueba de f (e) → r y Π2 prueba de r == b. Como Π2 es de longitud < l + 1,
b == c es probable y b ∈ CT erm⊥ , podemos aplicar la h.i., con lo que tenemos que
b ∈ CT erm y a == c probable.
¥
4.3.1.
Caracterización de == en función de →
En el cálculo GORC que se presenta en [GHL+ 96, GHL+ 98] la única regla que aparece
para la igualdad es (en nuestra notación):
a→t b→t
a == b
t ∈ CT erm
En este cálculo la igualdad entre dos expresiones es probable si es probable que ambas expresiones aproximan al mismo término total. En nuestro cálculo la prueba es “más
directa”, analizando la estructura sintáctica (realmente el sı́mbolo más externo) de las
expresiones. La justificación de este hecho es que posteriormente presentaremos un cálculo
operacional que tendremos que relacionar con GORC6= y nos interesa por tanto, capturar ciertos aspectos operacionales desde un principio para hacer más sencillas algunas
demostraciones.
Sin embargo, esta diferencia no tiene mayor trascendencia desde el punto de vista
semántico, como prueba el lema de caracterización que presentamos en este apartado. Este
lema afirma que una igualdad e == e0 es válida si tanto e y e0 pueden reducirse a c-términos
(valores totalmente definidos) sintácticamente iguales, usando las reglas de reescritura de
[R]⊥ . En particular, la igualdad entre c-términos es una cuestión meramente sintáctica y
en realidad no hay nada que reescribir, como se prueba en el siguiente proposición.
Proposición 3 Sean t, s ∈ CT erm. t == s es GORC6= -probable sii t ≡ s
Demostración
(⇒) Por inducción sobre la profundidad p de t:
p = 0 Debe ser t ≡ X variable o t ≡ c constructora de aridad 0. En el primer caso,
como s ∈ CT erm y t == s es probable tiene que ser s ≡ X y X == X es probable
por la regla (6). En el segundo caso tiene que ser s ≡ c y c == c se demuestra por
la regla (7).
p + 1 Como t ∈ CT erm debe ser t ≡ c(t1 , ..., tn ) de profundidad p + 1 y como t == s
es probable y s ∈ CT erm tiene que ser s ≡ c(s1 , ..., sn ) (regla (7)). En ese caso deben
ser probables ti == si , ∀i ∈ {1..n}. Como la profundidad de ti es < p + 1, por h.i.
ti ≡ si , ∀i ∈ {1..n} y entonces t ≡ s.
CAPÍTULO 4. DE LA SEMÁNTICA
138
(⇐) Se prueba de forma similar, por inducción sobre la profundidad p de t.
¥
Proposición 4 Si t ∈ CT erm y a → t es GORC6= -probable, entonces a == t es GORC6= probable.
Demostración
Por inducción sobre el número de pasos l de la prueba de a → t:
l = 0. Puede ser a ≡ t ≡ X variable o a ≡ t ≡ c sı́mbolo de constructora de aridad 0;
en cualquier caso por la proposición anterior a == t es probable.
l + 1. Hay dos posibilidades:
a ≡ c(s1 , ..., sm ) y t ≡ c(t1 , ..., tm ) con si → ti probable. Como ti ∈ CT erm, por
hipótesis de inducción si == ti es probable y por tanto podemos construir una
prueba de c(s) == c(t) por la regla (7) de GORC6= .
Si a ≡ f (e), como t == t es probable por el lema anterior, entonces mediante la
regla (5) de GORC6= podemos construir la prueba:
f (e) → t, t == t
f (e) == t
¥
Lema 1 (Caracterización de == en términos de →) a == b es GORC6= -probable
sii ∃t ∈ CT erm tal que a → t y b → t son GORC6= -probables.
Demostración
(⇒) Por inducción sobre el número de pasos l de la prueba:
l = 0. Debe ser a ≡ b ≡ X (regla 6) variable o a ≡ b ≡ c constructora de aridad 0
(regla 7). Tomando t ≡ a, en el primer caso a → t y b → t pueden demostrarse por
la regla (2); en el segundo caso pueden probarse en un sólo paso por la (3).
l + 1. A la vista del cálculo GORC6= si la igualdad a == b es probable en más de un
paso, puede tener dos formas:
• Si c(r1 , ..., rn ) == c(s1 , ..., sn ) es probable, entonces deben ser probables las
igualdades r1 == s1 , ..., rn == sn . Por hipótesis de inducción existen t1 , ..., tn ∈
CT erm, tal que todas las aproximaciones ri → ti y si → ti son probables.
Tomando t ≡ c(t1 , ..., tm ) tenemos que c(r1 , ..., rm ) → t y c(s1 , ..., sm ) → t son
probables por la regla (3).
• Si f (e) == e0 es probable debe serlo utilizando la regla (5) del cálculo, por lo
que f (e) → s y s == e0 con s ∈ CT erm⊥ deben ser probables. Por hipótesis
de inducción, como la prueba de s == e0 tiene menos de l + 1 pasos, existe
t ∈ CT erm tal que s → t y e0 → t son probables. Ası́ pues, como f (e) → s y
s → t son probables, por transitividad de → también debe serlo f (e) → t, lo
que completa el resultado para este caso.
4.3. EL CÁLCULO DE PRUEBAS GORC6=
139
(⇒) Es consecuencia de la proposición 4 y de la transitividad de ==: si a → t y b → t
con t ∈ CT erm son probable, por dicha proposición a == t y b == t son probables. Como t ∈ CT erm, por transitividad de ==, tenemos que a == b es también
probable.
¥
Para completar este apartado proponemos también una caracterización sintáctica de
la desigualdad como contrapartida a la proposición 4.
Proposición 5 (Caracterización sintáctica de 6=) Sean t, s ∈ CT erm⊥ , t 6= s es probable en GORC6= ⇔ existe una posición en la que t y s tienen sı́mbolos distintos entre sı́ y
distintos de ⊥.
Demostración
(⇒) por inducción sobre la longitud de la prueba, que sólo puede utilizar las reglas 8 y 9
del cálculo.
(⇐) por inducción sobre la posición en la que colisionan t y s. En dicha posición sólo
puede haber un sı́mbolo de constructora.
¥
4.3.2.
Sustituciones
En este apartado demostraremos varios resultados que relacionan las sustituciones con
el cálculo GORC6= .
Lema 2 (Lema de sustitución) Dados θ, θ0 ∈ CSubst⊥ tal que Xθ0 = tθ y Y θ0 ≡ Y θ
para toda Y 6≡ X, entonces θ0 ≡ [X/t]θ.
Demostración
Tenemos que probar ∀s ∈ T erm⊥ .sθ0 = s[X/t]θ. Razonamos por inducción sobre la
profundidad p de s:
p = 0, debe ser:
s ≡ ⊥, trivial,
s ≡ X, entonces sθ0 = Xθ0 = tθ = X[X/t]θ = s[X/t]θ,
s ≡ Y 6≡ X, entonces sθ0 = Y θ0 = Y [X/t]θ0 = Y [X/t]θ = s[X/t]θ
s ≡ c ∈ DC 0 , entonces sθ ≡ sθ0 ≡ c
p + 1, debe ser s ≡ l(e1 , ..., en ) con l ∈ DC ∪ F S. Entonces sθ0 = l(e1 , ..., en )θ0 =
H.I.
l(e1 θ0 , ..., en θ0 ) = l(e1 [X/t]θ, ..., en [X/t]θ) = l(e1 , ..., en )[X/t]θ.
¥
Proposición 6 (Orden de aproximación y →) Sean a, b ∈ CT erm⊥ . Se tiene a → b
es probable sii a w b.
CAPÍTULO 4. DE LA SEMÁNTICA
140
Demostración
(⇒) Por inducción sobre la longitud l de la prueba Π de a → b:
l = 0 Hay tres posibilidades:
• Π ≡ (1) a → ⊥ y sabemos que ⊥ v a, ∀a ∈ T erm⊥
• Π ≡ (2) X → X, y es cierto X v X (reflexividad)
• Π ≡ (3) c → c, con c ∈ DC 0 y también es cierto c v c
l + 1 Como a ∈ CT erm⊥ tiene que ser
Π ≡ (3)
Π1 ...Πn
c(e1 , ..., en ) → c(t1 , ..., tn )
siendo Πi prueba de ei → ti , i ∈ {1..n}. Por h.i. ei w ti y por la definición de w
tenemos c(e) w c(t).
(⇐) Si a v b debe ser:
• b ≡ ⊥ y tenemos a → ⊥, ∀a ∈ T erm⊥
• b ≡ c(t1 , ..., tn ), entonces tiene que ser a ≡ c(s1 , ..., sn ) con ti v si , ∀i ∈ {1..n}.
Por h.i. si → ti , ∀i ∈ {1..n} y entonces por la regla (3) tenemos que a ≡ c(s) →
c(t) es probable.
¥
El siguiente resultado establece que si se tiene una sustitución θ que permite demostrar
una aproximación de la forma tθ → sθ, entonces tθ y sθ son “casi” iguales. En concreto,
es posible que haya algunas posiciones en las que sθ tenga ⊥ y en las que tθ contenga
cualquier término (también puede ser ⊥). Pero como s, t ∈ CT erm los sı́mbolos ⊥ deben
haber sido introducidos por θ, es decir, θ puede haber reemplazado algunas variables de s
y t por términos que contengan ⊥. Entonces es posible encontrar otra sustitución θ0 que
reemplaze esas variables por términos totales, de modo que sθ sea idéntico a tθ.
Lema 3 (Refinamiento) Sean t, s ∈ CT erm, θ ∈ Subst⊥ , s lineal y var(t) ∩ var(s) = ∅.
Si tθ → sθ es probable, entonces existe θ0 w θ tal que:
a) θ0 = θ[V − var(s)] (θ0 coincide con θ salvo en var(s))
b) tθ0 = sθ0 (además tθ = tθ0 )
Demostración
Antes de demostrar el teorema observemos que de existir la θ0 que postula el teorema,
como sólo difiere de θ en var(s) y var(s) ∩ var(t) = ∅ tenemos que tθ0 = tθ (el paréntesis
de b)).
La prueba se hace por inducción estructural sobre s:
Si s = X variable, como X ∈
6 var(t) podemos definir θ0 ≡ θ[V −{X}] y Xθ0 = tθ. Por
la definición de θ y por la propiedad 6 tenemos Xθ = sθ v tθ = Xθ0 y Y θ = Y θ0 ,
luego θ v θ0 .
4.3. EL CÁLCULO DE PRUEBAS GORC6=
141
Si s = c(s1 , ..., sn ), para que tθ → sθ sea probable en GORC6= , debe ser tθ ≡
c(t1 , ..., tn ) y deben ser probables las aproximaciones ti → si θ. Como var(t)∩var(s) =
∅, debe ser var(ti ) ∩ var(si ) = ∅. Podemos definir sustituciones θi restringidas a las
variables de si :
½
Xθi =
Xθ, si X ∈ var(si )
X, e.o.c.
De este modo tenemos si θi = si θ y ti θi = ti , y hemos conseguido que θi esté en las
hipótesis del teorema con respecto a la aproximación ti θi → si θi . Entonces existe
θi0 w θi tal que θi0 = θi [V − var(si )] y
ti θi0 = si θi0 (≡ ti )
(i)
Como s es lineal, podemos definir θ0 ası́:
½
Xθ0
=
Xθi0 , si X ∈ var(si )
Xθ, e.o.c.
y tenemos θ0 w θ porque Xθ0 = Xθi0 w Xθi = Xθ, si X ∈ var(si ) ⊆ var(s) y
Xθ0 = Xθ e.o.c.. De hecho, θ0 = θ[V − var(s)]. Por otro lado:
(i)
sθ0 = c(s1 θ0 , ..., sn θ0 ) = c(s1 θ10 , ..., sn θn0 ) = c(t1 , ..., tn ) = t = tθ0
¥
A continuación destacamos un hecho que relaciona el orden v con las sustituciones,
que por sı́ mismo no tienen especial relevancia, pero que puede ayudar a comprender mejor
algunos razonamientos posteriores: dado e ∈ T erm⊥ existe otro término que notaremos
ê ∈ T erm y existe una sustitución µ : V → {⊥} tal que êµ ≡ e, de modo que las variables
para las que µ toma el valor ⊥ sean nuevas con respecto a las de e. El término ê es igual
que e excepto en aquellas posiciones donde e tiene ⊥, en las cuales ê tiene variables nuevas.
Si e ≡ ⊥, podemos tomar ê ≡ X y µ ≡ [X/⊥]; si e ≡ X podemos tomar ê ≡ X y µ ≡ []; y
si e ≡ l(e1 , ..., en ), con l ∈ DC ∪ DF podemos construir ê1 , ..., ên y µ1 , ..., µn de modo que
los conjuntos de variables para los que las µi toman el valor ⊥ sean disjuntos entre sı́ (esto
es posible porque para la construcción de los êi podemos utilizar cualquier conjunto de variables; en particular, podemos escogerlos de modo que sean disjuntos dos a dos). Podemos
reconstruir ê ≡ l(ê1 , ..., ên ) tomando µ como la composición de las µi en cualquier orden
puesto que el resultado será el mismo. Por ejemplo, si e ≡ f (c(X, ⊥), d(c(⊥, Y ), Z), ⊥),
podemos tomar ê ≡ f (c(X, A), d(c(B, Y ), Z), C) y µ ≡ {A/⊥, B/⊥, C/⊥}
Si entre dos términos e, e0 ∈ T erm⊥ tenemos la relación e v e0 puede ocurrir que sean
idénticos o que sean iguales salvo en un conjunto de posiciones en las que e0 está “más
definido” que e0 , es decir, en dichas posiciones e tiene el sı́mbolo ⊥, mientras que e0 tiene
cualquier otro término distinto de ⊥.
El siguiente resultado establece una relación entre la estructura de las pruebas y el
orden de aproximación del modo siguiente: si tenemos una prueba para una aproximación o restricción, entonces se pueden encontrar pruebas para todas las aproximaciones o
restricciones cuyos miembros estén más definidos (el lado izquierdo, en el caso de las aproximaciones) en el orden de aproximación v, y además dichas pruebas serán de la misma
longitud y estructura. Que tengan la misma estructura significa que se construyen utilizando las mismas reglas de GORC6= . Por ejemplo, si f ∈ F S, c, d ∈ DC y t ∈ CT erm⊥ ,
CAPÍTULO 4. DE LA SEMÁNTICA
142
y f (X, c(⊥), ⊥) → t es GORC=
6 -probable, entonces existe una prueba estructuralmente
igual de f (X, c(Y ), d) → t. La idea es que si para construir la primera prueba no es necesario conocer los valores concretos de algunos argumentos (los que son ⊥), es porque
esos valores no son “necesarios” para la derivación. Entonces, podemos reemplazar dichos
valores por términos cualesquiera y la aproximación continuará siendo probable replicando
los pasos que se dieron en la original. En el caso de las restricciones el razonamiento es
similar.
Lema 4 (Monotonı́a) Sean e, e0 ∈ T erm, θ, θ0 ∈ Subst⊥ y t ∈ Cterm⊥ . Si θ v θ0 y Π
es una GORC6= -prueba de eθ → t (eθ3e0 respectivamente), entonces existe una GORC6= prueba Π0 de eθ0 → t (eθ0 3e0 resp.) con la misma longitud y estructura que Π (siendo
3 ∈ {==, 6=}).
Antes de demostrar este resultado observemos que en el caso de las restricciones, la sustitución sólo se aplica a uno de los miembros, el resultado es cierto aplicando sustituciones
a ambos miembros. Basta aplicar dos veces el lema:
R ` eθ3e0 θ y θ v θ0 ⇒ R ` eθ0 3e0 θ y θ v θ0 ⇒ R ` eθ0 3e0 θ0
El enunciado, tal y como lo hemos expresado, facilitará la demostración.
Demostración
El enunciado del teorema encierra tres enunciados (para →, == y 6=).
Para el primero de ellos vamos a demostrar un resultado en apariencia más general,
aunque realmente es equivalente. Probaremos que si a v a0 y Π es una prueba de R ` a →
t, entonces existe una prueba Π0 de R ` a0 → t con la misma longitud y estructura que
Π. La monotonı́a de → es una consecuencia inmediata de este resultado ya que si θ v θ0
por la definición de v tenemos que eθ v eθ0 . Identificando eθ con a y eθ0 con a0 estamos
en las hipótesis del enunciado que vamos a demostrar.
En primer lugar observemos que en el caso de t ≡ ⊥, sabemos que para todo b ∈ T erm⊥
hay una prueba de b → ⊥ de longitud 1 utilizando la primera regla de GORC6= . En este
caso Π ≡ Π0 .
Para el caso de t 6≡ ⊥ (por otro lado más interesante), razonamos por inducción sobre
la profundidad p de a:
p = 0 Puede ser:
a ≡ ⊥, en cuyo caso tiene que ser t ≡ ⊥ que ya está estudiado.
Si a ≡ X, como a v a0 tiene que ser a0 ≡ X, ya que en el orden v es maximal (una
variable sólo está relacionada con ella misma y con ⊥ y dos variables distintas son
incomparables). Entonces Π ≡ Π0 ≡ (2)X → X.
a ≡ c ∈ DC 0 , tiene que ser a0 ≡ c y entonces Π ≡ Π0 ≡ (2)c → c.
p + 1 Puede ser:
a ≡ c(a1 , ..., an ), entonces tiene que ser a0 ≡ c(a01 , ..., a0n ) con ai v a0i . El caso t ≡ ⊥
ya está estudiado y la otra posibilidad es t ≡ c(t1 , ..., tn ), con lo que la prueba será de
la forma:
4.3. EL CÁLCULO DE PRUEBAS GORC6=
Π ≡ (3)
143
Π1 ...Πn
c(a1 , ..., an ) → c(t1 , ..., tn )
con Πi prueba de ai → ti . Como ai v a0i , por h.i. existe para cada i = 1..n una prueba
Π0i de a0i → ti con la misma longitud y estructura que Πi . Ası́ podemos reconstruir
la prueba
Π0 ≡ (3)
Π01 ...Π0n
c(a01 , ..., a0n ) → c(t1 , ..., tn )
con la misma longitud y estructura que Π.
a ≡ f (a1 , ..., an ), tiene que ser a0 ≡ f (a01 , ..., a0n ) con ai v a0i . El caso t ≡ ⊥ ya esta
estudiado y si t 6≡ ⊥ será
Π ≡ (4)
Π1 ...Πn ΠC Πs
f (a1 , ..., an ) → t
siendo (f (s1 , ..., sn ) = s <== C) ∈ [R]⊥ , Πi prueba de ai → si , ΠC pruebas de las
restricciones C y Πs prueba de s → t. Como ai v a0i , por h.i. podemos construir
pruebas Π0i de a0i → si con la misma longitud y estructura que Πi . Y podemos
entonces reconstruir una prueba Π0 con la misma longitud y estructura que Π que
tendrá la forma:
Π0 ≡ (4)
Π01 ...Π0n ΠC Πs
a0 ≡ f (a01 , ..., a0n ) → t
Para la monotonı́a de 3 (3 ∈ {==, 6=}) se puede hacer una prueba similar probando
un resultado equivalente al del enunciado: si Π es una prueba de R ` a3e0 y a v a0
entonces se puede construir una prueba Π0 de a0 3e0 con la misma longitud y estructura
que Π.
Razonamos como antes, por inducción sobre la profundidad p de a:
p = 0 Posibles pruebas Π de a3e0 para a de profundidad 0:
No puede ser Π ≡ ⊥3e0 porque en GORC6= no se puede probar nada de la forma
⊥3e0 .
Si Π ≡ (6) X == X, con a ≡ e0 ≡ X, como a v a0 tiene que ser a0 ≡ X y entonces
Π ≡ Π0 .
Si Π ≡ (7) c == c, con a ≡ e0 ≡ c ∈ DC 0 tiene que ser a0 ≡ c y se tiene Π ≡ Π0 .
Si Π ≡ (8) c 6= d(e0 ), con a ≡ e0 ≡ c ∈ DC 0 , tiene que ser a0 ≡ c y tenemos Π0 ≡ Π.
p + 1 a puede ser de dos formas:
a ≡ c(a1 , ..., an ). Como a v a0 será a0 ≡ c(a01 , ..., a0n ) con ai v a0i . La prueba Π de
a3e0 puede tener cuatro formas dependiendo de la forma de e0 :
CAPÍTULO 4. DE LA SEMÁNTICA
144
Π1 Π2
c(a)3f (e0 )
con t ∈ CT erm⊥ , Π1 prueba de f (e0 ) → t, Π2 prueba de t3c(a). Como
t ∈ CT erm, ahora la prueba de t3c(a) tiene que caer en uno de los casos
anteriores, en los que se prueba que existe una prueba Π02 de t3c(a0 ) con la
misma longitud y estructura que Π2 , con lo que podemos reconstruir la prueba
buscada reemplazando Π2 por Π02 .
• e0 ≡ f (e0 ), Π ≡ (5)
Πi
c(a) == c(e0 )
con Πi pruebas de ai == e0i . Por h.i. existen pruebas Π0i con las que podemos
reconstruir la prueba Π0 para a0 == e0 .
• e0 ≡ c(e0 ), Π ≡ (7)
• e0 ≡ d(e0 ), Π ≡ (8) c(a) 6= d(e0 )
con c 6≡ d. Como a0 ≡ c(a), con la misma regla (8) podemos construir Π0 .
Πi
c(a) 6= c(e0 )
siendo Πi prueba de ai 6= ei para algún i. Por h.i. tenemos una prueba Π0i de
a0i 6= ei con la misma longitud y estructura con la que podemos reconstruir la
prueba buscada Π.
• e0 ≡ c(e0 ), Π ≡ (9)
a ≡ f (a1 , ..., an ). Tiene que ser a0 ≡ f (a01 , ..., a0n ). La prueba Π será de la forma:
Π ≡ (5)
Π1 Π2
f (a)3e0
siendo Π1 prueba de f (a) → t, Π2 prueba de t3e0 y t ∈ CT erm⊥ . Por monotonı́a de
→ existe una prueba Π01 de f (a0 ) → t con la misma longitud y estructura que Π1 ,
con la que podemos reconstruir la prueba Π buscada.
¥
4.4.
Cálculo de resolución de objetivos
De forma similar a lo que ocurre en los lenguajes lógicos, hacer un cómputo en el
contexto en el que trabajamos es resolver un objetivo, que a su vez significa obtener
valores (en forma de restricciones) para las variables que aparecen en dicho objetivo, de
modo que al sustituir las variables por los valores calculados, el objetivo obtenido sea
GORC6= -probable.
Nuestra meta ahora es definir un cálculo que refleje algunos aspectos de la semántica
operacional de T OY, es decir, un cálculo de resolución de objetivos. Este cálculo, al igual
que el LN C (Lazy Narrowing Calculus) de [GHL+ 96, GHL+ 98], combina el estrechamiento
perezoso con la resolución de restricciones. No obstante, la versión que presentamos aquı́ es
muy distinta a la de [GHL+ 96, GHL+ 98], no sólo por la incorporación de restricciones de
desigualdad. Una diferencia muy importante es que a cada condición (restricción o aproximación) del objetivo se le asocia una prioridad, de modo que en cada paso de cómputo
sólo pueden procesarse aquellas condiciones que tienen prioridad máxima. Puede haber
varias con prioridad máxima, en cuyo caso la elección es indeterminista (indeterminismo
don’t care).
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
145
La idea que encierra el uso de prioridades es la de secuenciar los cómputos con un fin
fundamental e intuitivamente natural: cuando se aplica una regla de función f (t1 , ..., tn ) =
r <== C para reducir una llamada f (e1 , ..., en ), hacer primero el paso de parámetros
antes de resolver las restricciones C o reducir el cuerpo r, es decir, procesar primero las
aproximaciones e1 → t1 , ..., en → tn . Con ello se consiguen evitar algunos tests globales
y de coste elevado que se hacı́an en [GHL+ 96, GHL+ 98]; por ejemplo, la comprobación
de si un término es un c-término (sin llamadas a función) que se hacı́a con frecuencia en
[GHL+ 96, GHL+ 98], en nuestro cálculo se realiza en muy pocas ocasiones. En particular,
este test solo es necesario para resolver desigualdades; las igualdades pueden resolverse sin
él.
Paralelamente a la introducción de prioridades se introducen de manera explı́cita las
suspensiones o aproximaciones cuya evaluación se retrasa hasta que sea necesaria para
procesar alguna otra condición. Cuando el cómputo puede continuar sin reducir una aproximación de prioridad máxima, dicha aproximación se suspende y deja de ser prioritaria. Si
en el futuro tal reducción debe procesarse, entonces se activa (también de forma explı́cita)
la suspensión.
En esta misma sección presentaremos los resultados de corrección y la completitud de
nuestro cálculo LN C6= con respecto al cálculo de pruebas GORC6= .
4.4.1.
El cálculo LN C6=
Definimos en primer lugar la noción de objetivo y objetivo inicial, y a continuación
presentamos las reglas de LN C6= .
Definicion 1 (Objetivo para el cálculo LN C6= ) Un objetivo es una expresión de la
forma G ≡ ∃U .P 2R2σ2δ, donde:
U es el conjunto de variables existenciales de G o evar(G).
α
P es un multiconjunto de aproximaciones con prioridad asociada de la forma l1 →1
α
t1 , ..., ln →n tn , αi ∈ IN. A las aproximaciones de ı́ndice 0 las llamaremos suspensiones.
α
Definimos el conjunto de variables producidas como pvar(G) = {X|∃(l → r) ∈ P
tal que X ∈ var(r)}. El subconjunto de variables suspendidas es un subconjunto
0
distinguido de variables producidas definido como suspvar = {X|(l → r) ∈ P, X ∈
var(r)}.
R (parte de resolución) es un multiconjunto de restricciones con prioridad asociada
α
de la forma e 3 e0 , con e, e0 ∈ T erm, 3 ∈ {==, 6=} y α ∈ IN.
σ (parte de sustitución) es un conjunto de igualdades de la forma X = t, con X ∈ V
y t ∈ CT erm.
δ (parte de desigualdades) es un conjunto de desigualdades de la forma X 6= t, con
X ∈ V y t ∈ CT erm.
Dada una variable X, el conjunto de desigualdades asociadas a X se define como
δX = {X 6= t|(X 6= t) ∈ δ}.
¥
Definicion 2 (Objetivo inicial para LN C6= ) Un objetivo inicial tiene la forma G ≡
2R22 (esto es P = σ = δ = ∅ y también evar(G) = ∅) donde todas las restricciones de
R tienen ı́ndice 1.
¥
CAPÍTULO 4. DE LA SEMÁNTICA
146
Definicion 3 (Variables Seguras) Definimos el conjunto de variables seguras de un
término (variables que no van a desaparecer debido a las reducciones) como:
svar(X) ≡ {X}
svar(c(e1 , ..., en )) ≡ ∪ni=1 svar(ei ), si c ∈ DC n
svar(f (e)) ≡ ∅, si f ∈ F S n
¥
El conjunto de variables seguras de un término también podrı́a definirse como el conjunto de variables que no desaparece al formar la cáscara del término (véase 3.11), sin
embargo, la definición que hemos hecho aporta una visión más precisa del comportamiento de estas variables en el contexto actual.
Algunas cuestiones acerca de la notación que se usará en las reglas del cálculo son las
siguientes:
Como ya se ha comentado al principio de la sección, las reglas llevan a cabo transformaciones en los objetivos procesando aquellas condiciones de ı́ndice (o prioridad)
máxima. Para simplificar las reglas, este ı́ndice se notará con m en todas ellas.
Si C es un conjunto de restricciones, C n denota el mismo conjunto en el cual todas
las restricciones tienen n como ı́ndice asociado. Lo mismo para las desigualdades δ.
Cuando una igualdad está en forma resuelta, X == t con X ∈ V y t ∈ CT erm
pasará a la parte de sustitución sólo si la variable X es relevante, es decir, si no
ha sido introducida en algún paso de cómputo como variable existencial, sino que
aparecı́a en el objetivo original. Para anotar estas igualdades (y sólo ellas) en la
parte de sustitución introducimos la siguiente notación:
entonces:
Si G ≡ ∃U .P 2R2σ2δ
½
∃U .P 2R2X = t, σ2δ si X ∈
6 evar(G)
(G ] X = t) ≡
∃(U − {X}.P 2R2σ2δ si X ∈ evar(G)
Si X no es exitencial se añade la igualdad (resuelta) a la parte de sustitución y si
es exitencial deja de serlo. En realidad, en las reglas donde se utiliza la notación ]
(Obind,Bind,Imit== y Imit6= ) la variable X se reemplazará por un c-término y desaparecerá de las producciones, la parte de resolución y las desigualdades; para X sólo
quedará a lo sumo una igualdad en σ.
REGLAS DE LN C6=
Reglas para →
m
m
Dcp→ ∃U .c(e) → c(t), P 2R2σ2δ Ã ∃U .e → t, P 2R2σ2δ
m
m , R2σ2δ)[X/c(t)] ] X = c(t)
Obind U .X → c(t), P 2R2σ2δ, δX Ã ∃U .(P 2δX
si X 6∈ suspV ar(G)
m
Ibind ∃Y, U .X → Y, P 2R2σ2δ Ã ∃U .(P 2R)[Y /X]2σ2δ
m
0
Sus ∃U .l(e) → X, P 2R2σ2δ Ã ∃U .l(e) → X, P 2R2σ2δ
si l ∈ DC ∪ DF
m
m+1
m
Nrw ∃U .f (e) → c(t), P 2R2σ2δ Ã ∃Y , U .e → s, s → c(t), P 2C m , R2σ2δ
si f (s) = s <== C es una variante de una regla de R con variables nuevas Y
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
147
Reglas de activación de suspensiones
0
m+2
m+1
Act1 ∃U .f (e) → X, P 2R2σ2δ Ã ∃Y , U .e → s, s → X, P 2C m+1 , R2σ2δ
m
m
si (X 3 e0 ) ∈ R o (X → c(t)) ∈ P ) y
f (s) = s <== C es una variante de una regla de R con variables nuevas Y
0
m+1
Act2 ∃X, U .c(e) → X, P 2R2σ2δ Ã ∃Y , U .e → Y , (P 2R)[X/c(Y )]2σ2δ
m
m
si (X 3 e0 ) ∈ R o (X → d(t)) ∈ P , y Y variables nuevas
Reglas para == y 6=
m
m+1
m
Eval ∃U .P 2f (e) 3 e0 , R2σ2δ Ã ∃Y, U .f (e) → Y, P 2Y 3 e0 , R2σ2δ
si 3 ∈ {==, 6=} y Y variable nueva
Reglas para ==
m
m
Dcp== ∃U .P 2c(e) == c(e0 ), R2σ2δ Ã ∃U .P 2e == e0 , R2σ2δ
m
Id ∃U .P 2X == X, R2σ2δ Ã ∃U .P 2R2σ2δ
si X 6∈ suspvar(G)
m
m , R2σ2δ)[X/Y ] ] X = Y
Bind ∃U .P 2X == Y, R2σ2δ, δX Ã ∃U .(P 2δX
si X 6≡ Y y X, Y 6∈ suspvar(G)
m
m
m , Y == e, R2σ2δ)[X/c(Y )] ] X =
Imit== ∃U .P 2X == c(e), R2σ2δ, δX Ã ∃Y , U .(P 2δX
c(Y )
si X 6∈ suspvar(G), X 6∈ svar(c(e)), Y variables nuevas
Reglas para 6=
m
Clash ∃U .P 2c(e) 6= d(e0 ), R2σ2δ Ã ∃U .P 2R2σ2δ
si c 6≡ d
m
Store ∃U .P 2X 6= t, R2σ2δ Ã ∃U .P 2R2σ2δ, X 6= t
si X 6∈ suspvar(G), t ∈ CT erm, suspvar(G) ∩ var(t) = ∅
m
m
Dcp6= ∃U .P 2c(e) 6= c(e0 ), R2σ2δ Ã ∃U .P 2ei 6= e0i , R2σ2δ
si i ∈ {1, ..., n}
m
Imit6= ∃U .P 2X 6= c(e), R2σ2δ, δX Ã elegir uno de G1 , G2 donde
m , R2σ2δ)[X/d(Y )] ] X = d(Y ), siendo d ∈ DC, d 6≡ c
G1 ≡ ∃U , Y .(P 2δX
m
m , R2σ2δ)[X/c(Y )] ] X = c(Y )
G2 ≡ ∃U , Y .(P 2Yi 6= ei , δX
si X 6∈ suspvar(G), (c(e) 6∈ CT erm∨var(c(e))∩suspvar(G) 6= ∅), Y variables nuevas
Reglas de fallo
m
Fail1 ∃U .c(e) → d(e0 ), P 2R2σ2δ Ã F AIL,
m
Fail2 ∃U .P 2c(e) == d(e0 ), R2σ2δ Ã F AIL,
m
Fail3 ∃U .P 2X == e, R2σ2δ Ã F AIL,
si c 6≡ d
si c 6≡ d
si X 6≡ e, X ∈ svar(e)
CAPÍTULO 4. DE LA SEMÁNTICA
148
m
Fail4 ∃U .P 2X 6= X, R2σ2δ Ã F AIL
m
Fail5 ∃U .P 2c 6= c, R2σ2δ Ã F AIL
¥
Como puede apreciarse hemos separado las reglas que tratan las aproximaciones de
las reglas para resolver restricciones. Entre las de aproximaciones, la primera es una regla
sencilla de descomposición, la segunda y la tercera son de ligadura de variables. En Obind,
cuando una variable X se liga a un c-término, es posible que algunas de las desigualdades
en forma resuelta de δ dejen de estar en dicha forma. Esto puede ocurrirle sólo a las
desigualdades de δX (las que tienen X en uno de los miembros) y por eso δX pasa a la
parte de resolución.
La cuarta regla suspende todas aquellas aproximaciones que tienen una variable en su
lado derecho y permanecerán suspendidas hasta que dicha variable sea necesitada por el
cómputo. Esta necesidad es detectada en las dos reglas de activación y se produce cuando
la variable en cuestión aparece en alguno de los miembros de una restricción. Nótese que
no sólo se suspenden las reducciones de funciones, sino también las aproximaciones de
constructoras.
La primera regla de activación y Nrw son reglas de estrechamiento que utilizan reglas
de reescritura de [R]⊥ . En ambas, las prioridades juegan un papel fundamental para hacer
el paso de argumentos en primer lugar. En Act1 los ı́ndices son una unidad mayores que
en Nrw para asegurar que la reducción del cuerpo de la función se procese antes que la
condición que ha provocado la activación de la suspensión.
En Act2 , en presencia de una suspensión de la forma c(e) → X que ha despertado, se
procede por imitación, es decir, se liga X con un término c(Y ) que recoge la estructura
más externa de c(e) y se plantean las aproximaciones pertinentes. De este modo no es
necesario comprobar si c(e) es o no un c-término.
La regla Eval es la contrapartida operacional a la regla (5) de GORC6= . La aproximam+1
ción f (e) → Y se suspenderá automáticamente. De hecho, podrı́a suspenderse en esta
regla, pero hemos preferido agrupar todas las suspensiones en la misma regla.
Para la resolución de igualdades entre formas normales de cabeza, Dcp== es una regla
de descomposición, y tanto Id como Bind son de ligadura de variables. En esta última
obsérvese que también δX pasa a la parte de resolución por el mismo motivo que en
Obind. La última regla, Imit== , nuevamente evita comprobar que el segundo miembro
es un c-término y procede por imitación.
Para las desigualdades entre formas normales de cabeza, Clash corresponde a la regla
(9) de GORC6= y Dcp6= a la (8). En Store se detecta la presencia de una desigualdad en
forma resuelta y se pasa a δ; en este caso, sı́ es necesario hacer el test de que el segundo
miembro sea un c-término. La regla Imit6= es un elección indeterminista (indeterminismo
don’t care) para resolver una desigualdad de la forma X 6= c(e). En este caso se puede
proceder de dos formas, del mismo modo que hace T OY: ligando X con una constructora
d(Y ) distinta de c, o bien imitando la estructura y planteando la desigualdad entre algún
par de argumentos.
Sobre las reglas de fallo, la única que merece algún comentario es Fail3 , que corresponde
al occurs-check: una desigualdad entre una variable y un término cuya cáscara contiene a
dicha variable, automáticamente produce un fallo.
El cálculo LN C6= que acabamos de presentar está guiado en gran medida por la estructura sintáctica más externa de los términos (igual que GORC6= ), aunque contiene cierto
grado de indeterminismo en Imit6= . Debemos probar la bondad de los pasos de cómputo
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
149
que pueden hacerse con este cálculo, con independencia de la elección de la regla que se
aplica, en el caso de que haya varias posibles. Y esto es lo que veremos de manera más
formal con los resultados de corrección y completitud.
Veamos ahora un ejemplo de cómputo con LN C6= .
Ejemplo 2
Consideremos un programa R con las funciones f y coin definidas como en el ejemplo
de la sección anterior. Una solución para el objetivo f (coin) 6= [X|Xs] puede ser calculada
del siguiente modo:
1
Eval
2f (coin) 6= [X|Xs]22 Ã
2
1
Sus
0
1
Act
∃Z1 .f (coin) → Z1 2Z1 6= [X|Xs]22 Ã
∃Z1 .f (coin) → Z1 2Z1 6= [X|Xs]22 Ã1
3
2
1
Sus,Sus
0
1
Act
∃Z1 , Z2 .coin → Z2 , [Z2 |f (s(Z2 ))] → Z1 2Z1 6= [X|Xs]22
0
Ã
∃Z1 , Z2 .coin → Z2 , [Z2 |f (s(Z2 ))] → Z1 2Z1 6= [X|Xs]22 Ã2
0
3
1
2
Ibind
∃Z2 , Z3 , Z4 .coin → Z2 , Z2 → Z3 , f (s(Z2 )) → Z4 2[Z3 |Z4 ] 6= [X|Xs]22 Ã
0
2
1
0
0
1
0
0
∃Z2 , Z4 .coin → Z2 , f (s(Z2 )) → Z4 2[Z2 |Z4 ] 6= [X|Xs]22 Ã
Sus
Dcp6=
∃Z2 , Z4 .coin → Z2 , f (s(Z2 )) → Z4 2[Z2 |Z4 ] 6= [X|Xs]22 Ã
1
∃Z2 , Z4 .coin → Z2 , f (s(Z2 )) → Z4 2Z2 6= X22 Ã
3
1
0
Act1
Obind
∃Z2 , Z4 .Z2 → 0, f (s(Z2 )) → Z4 2Z2 6= X22 Ã
0
1
Store
∃Z4 .f (s(0)) → Z4 20 6= X22 Ã
0
∃Z4 .f (s(0)) → Z4 222X 6= 0
Al final del cómputo hemos obtenido en σ la forma resuelta X 6= 0, que es una solución
al objetivo inicial. Más adelante definiremos con precisión forma resuelta y solución. Por
el momento es suficiente observar que si en el objetivo f (coin) 6= [X|Xs] reemplazamos X
por cualquier valor distinto de 0 se verifica la desigualdad, supuesto que coin se reduce a
0 (que es una de las posibilidades). Otras respuestas (hay infinitas) que pueden derivarse
son X 6= s 0 o Xs = [ ].
¥
Nuestro interés se centra ahora en aquellos objetivos que son alcanzables a partir de un
objetivo inicial mediante derivaciones en LN C6= . La definición que hemos dado de objetivo
para LN C6= establece la forma sintáctica de los objetivos, sin embargo, en la construcción
del cálculo se ha puesto especial cuidado en que estos objetivos mantengan invariantes unas
propiedades que serán fundamentales posteriormente para la demostración de corrección
y completitud. La siguiente definición de objetivo admisible captura estas propiedades.
Notación: En lo sucesivo, para abreviar utilizaremos con frecuencia la notación e[X]
(X ∈ V) para expresar X ∈ var(e). No hay posibilidad de confusión con las sustituciones
que son de la forma [X/t].
Definicion 4 (Objetivo admisible) Un objetivo G ≡ ∃U .P 2R2σ2δ es admisible si
cumple las condiciones siguientes:
CAPÍTULO 4. DE LA SEMÁNTICA
150
0
Suspensiones. Si l → r ∈ P , entonces r ∈ V (todas las suspensiones son de la
0
forma l → X).
α
Aciclicidad Consideremos la relación X À Y ⇔ ∃(l → r) ∈ P ∧ l[X] ∧ r[Y ]. Que
P sea acı́clico significa que el cierre transitivo de À es irreflexivo (no existe X tal
∗
que X À X).
α
α
Linealidad. Si P ≡ l1 →1 t1 , ..., ln →n tn la tupla (t1 , ..., tn ) debe ser lineal.
Producción.
• pvar(G) ⊆ evar(G);
• pvar(G) ∩ var(σ) = ∅. Además, si (X = t) ∈ σ entonces X sólo tiene esta
ocurrencia en todo el objetivo G;
• pvar(G) ∩ var(δ) = ∅.
Orden.
α
a) Sea a → b[Z] ∈ P con α 6= 0. Entonces:
β
a.1) Si c[Z] → d ∈ P , entonces α > β.
β
a.2) Si (c 3 d)[Z] ∈ R, entonces α > β.
0
α
b) Si a → Z, c → d ∈ P , entonces var(a) ∩ var(d) = ∅
¥
En primer lugar observemos que un objetivo inicial es admisible. Las propiedades de
aciclicidad, linealidad y producción son propiedades que ya se proponı́an en el cálculo
LN C de [GHL+ 96, GHL+ 98].
Podemos relajar la definición de aciclicidad y enunciar una versión más débil sin pérdida
α
de generalidad: si l → r ∈ P entonces var(l) ∩ var(r) = ∅. No hay ninguna pérdida de
información porque con esta definición más la condición de orden se tiene la versión fuerte
del enunciado: supongamos que en P hay un ciclo, es decir, existe una variable X tal
que X À X. Por la versión débil de aciclicidad no puede existir una regla de la forma
α
l[X] → r[X], luego el ciclo debe producirse por una secuencia (obviamente finita puesto
α
α
αm
que P es finito) de la forma l1 [X] →1 r1 [Y1 ], l2 [Y1 ] →2 r2 [Y2 ], ..., lm [Ym−1 ] →
rm [X]. Ahora
α
0
bien, si α1 6= 0 no puede ser α2 = 0 porque se tendrı́a l2 [Y1 ] → r2 [Y2 ], l1 [X] →1 r1 [Y1 ] ∈
P , pero por la propiedad de orden (b), Y1 no puede aparecer en l2 y en r1 . De hecho,
se tiene αi > 0 para todo i = 1..m, y por la condición de orden (a.1) tendrı́amos la
cadena α1 > α2 > ... > αm > α1 que claramente es una contradicción; y si α1 = 0,
nuevamente por la propiedad de orden (b), X no puede aparecer en ningún lado derecho
de las aproximaciones, por lo que la primera y última aproximaciones de la secuencia de
partida no pueden aparecer en P .
Por lo que acabamos de exponer, en el lema siguiente de preservación de admisibilidad trabajaremos con la versión débil de aciclicidad, que es más sencilla de tratar. Este
resultado será fundamental para las demostraciones de corrección y completitud.
Lema 5 (Preservación de la admisibilidad) Si G es un objetivo admisible y G Ã G0
entonces G0 también es admisible.
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
151
Demostración
Vamos a dividir la demostración de este resultado en tres partes:
Si G es un objetivo admisible y G Ã G0 entonces G0 verifica la propiedad de
suspensiones.
Si G es un objetivo admisible y G Ã G0 entonces G0 verifica la propiedad de orden.
Si G es un objetivo admisible y G Ã G0 entonces G0 verifica las propiedades de
aciclicidad, linealidad y producción
El lema será un corolario de estos dos resultados parciales.
Parte1
La propiedad de suspensiones es fácil de demostrar. Obsérvese que las suspensiones que
0
introduce Sus son de la forma l(e) → X y esta es la única regla que introduce suspensiones.
La única posibilidad para que una suspensión deje de tener una variable en su lado
derecho es por aplicación de una sustitución en las reglas del cálculo. Estudiemos las reglas
que aplican sustituciones:
En Obind,Imit== , Imit=
6 y Bind se comprueba expresamente que la variable que se
sustituye no está suspendida (X 6∈ suspvar(G)).
En Ibind se sustituye una variable por otra, con lo que las suspensiones seguirán
teniendo una variable en el lado derecho.
Y en Act2 , la variable X que se sustituye está suspendida (no puede haber otra
suspensión con X en el lado derecho por linealidad), pero en G0 desaparece la suspensión
(de hecho desaparece la variable X).
Parte 2
Procedemos por análisis de casos sobre la regla aplicada. En lo sucesivo suponemos
G ≡ ∃U .P 2R2σ2δ y
G0 ≡ ∃U 0 .P 0 2R0 2σ 0 2δ 0
β
α
Dcp→ a.1) Supongamos u1 ≡ a → b[Z], u2 ≡ c[Z] → d ∈ P 0 . Pueden darse los casos
siguientes:
Si u1 , u2 ∈ P , por hipótesis α > β.
Si ninguna aparecı́a en P , sino que han sido introducidas por la regla, deben
m
m
ser de la forma ei → ti . En P tenı́amos c(e) → t y por aciclicidad var(c(e)) ∩
var(c(t)) = ∅, que implica var(ei ) ∩ var(tj ) = ∅, ∀i, j ∈ {1..n}, pero entonces
u1 y u2 no pueden tener la forma que suponı́amos al principio y llegamos a una
contradicción.
m
Si u1 es nueva, u1 ≡ ei → ti [Z], con α = m, y u2 ∈ P , entonces en P tenı́amos
m
c(e) → c(t)[Z] y por hipótesis α = m > β.
m
Si u2 es nueva, u2 ≡ ei [Z] → ti , con β = m, y u1 ∈ P , entonces en P tenı́amos
m
c(e)[Z] → c(t) y por hipótesis α > β = m, pero entonces no podrı́amos aplicar
la regla Dcp→ .
α
β
a.2) Suponemos u1 ≡ a → b[Z] ∈ P 0 , r ≡ (c 3 d)[Z] ∈ R0 .
Si u1 ∈ P , como también r ∈ R, por hipótesis α > β.
m
m
Si u1 es nueva, u1 ≡ ei → ti [Z], con α = m, en P tenı́amos c(e) → c(t)[Z] y en
β
R, (c 3 d)[Z], y por hipótesis α = m > β.
CAPÍTULO 4. DE LA SEMÁNTICA
152
b) Como los conjuntos de variables en expresiones a izda. y dcha. de → no se
modifican y tampoco lo hacen R y δ, por hipótesis se verifica el resultado.
Obind Sea θ ≡ [X/c(t)].
β
α
a.1) Supongamos u1 ≡ aθ → bθ[Z], u2 ≡ cθ[Z] → dθ ∈ P 0 . Como en esta regla no
m
β
α
se introducen producciones nuevas tenemos X → c(t), a → b, c → d ∈ P . Supongamos Z 6∈ var(b), como Z ∈ var(bθ) debe ser Z ∈ var(c(t)) y X ∈ var(b),
pero serı́a α > m y no se podrı́a aplicar esta regla. Por lo tanto Z ∈ var(b). Por
m
α
m
α
linealidad Z 6∈ var(c(t)), luego Z ∈ var(c). Entonces tenemos X → c(t), a →
β
b[Z], c[Z] → d ∈ P y por hipótesis α > β.
a.2) Igual que antes debe ser Z ∈ var(b), Z 6∈ var(c(t)) y tenemos X → c(t), a →
β
b[Z] ∈ P . Si (cθ 3 dθ)[Z] ∈ R0 , puede provenir de R o de una desigualdad de
δX .
β
En el primer caso serı́a (c 3 d)[Z] ∈ R y por hipótesis α > β.
En el segundo caso como Z ∈ var(b), Z es producida y por las condiciones de
admisibilidad Z 6∈ var(c 6= d). Deberı́a ser introducida en esta desigualdad por
θ. Pero Z 6∈ var(c(t)), por lo que este segundo caso no puede darse.
0
α
b) Supongamos aθ → Zθ, cθ → dθ ∈ P 0 . Como Dcp→ no introduce suspensiones
m
0
α
ni producciones nuevas tenemos X → c(t), a → Z, c → d ∈ P y por hipótesis
var(a) ∩ var(d) = ∅ (1). Por linealidad var(c(t)) ∩ var(d) = ∅ (2). No puede
ser X ∈ var(d) porque serı́a α > m, por lo que dθ ≡ d (3). Por otro lado
var(aθ) ⊆ var(a) ∪ var(c(t)) (4). De este modo tenemos:
(3)
(4)
var(aθ)∩var(dθ) = var(aθ)∩var(d) = (var(a)∪var(c(t))∩var(d) = (var(a)∩
(1)(2)
var(d)) ∪ (var(c(t)) ∩ var(d)) = ∅
Ibind Sea θ ≡ [Y /X].
β
α
m
α
a.1) Supongamos aθ → bθ[Z], cθ[Z] → dθ ∈ P 0 . En P debemos tener X → Y, a →
β
b, c → d. Por linealidad Y 6∈ var(b), entonces bθ = b y debe ser Z ∈ b. Si
Z 6∈ var(c) deberı́a aparecer en cθ por la sustitución, lo que implicarı́a Z ≡ X.
m
α
En P tendrı́amos Z → Y, a → b[Z] y serı́a α > m y no se podrı́a aplicar la regla.
β
α
Debe ser por lo tanto Z ∈ var(c) por lo que en P tenemos a → b[Z], c[Z] → d
y por hipótesis α > β.
β
α
a.2) Si aθ → bθ[Z] ∈ P 0 , cθ 3 d ∈ R0 . Igual que antes, por linealidad Z ∈ var(b).
β
Si Z 6∈ var(c 3 d), como antes serı́a Z ≡ X y llegarı́amos a contradicción, por
β
β
α
lo que Z ∈ var(c 3 d). En P tenemos por tanto a → b[Z], c[Z] → d y por
hipótesis α > β.
0
α
m
0
α
b) Si aθ → Zθ, cθ → dθ ∈ P 0 en P tendremos X → Y, a → Z, c → d. Por
linealidad Y 6∈ var(b), luego bθ = b. No puede ser X ∈ var(b), ya que serı́a
α > m, por tanto X 6∈ var(b). Por otro lado var(aθ) ⊆ var(a) ∪ {X} y por
hipótesis var(a) ∩ var(d) = ∅. Ası́ tenemos var(aθ) ∩ var(d) = ∅.
Sus a.1) Trivial.
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
153
a.2) Trivial.
b) Lo único que hay que probar es que la nueva suspensión introducida verifica el
0
α
teorema. Supongamos que no es cierto, es decir, l(e)[Y ] → X, a → b[Y ] ∈ P 0 ,
m
α
entonces en P tenı́amos l(e)[Y ] → X, a → b[Y ] y por hipótesis serı́a α > m y
no se podrı́a aplicar la regla.
β
α
Nrw a.1) Sean u1 ≡ aθ → b[Z], u2 ≡ c[Z] → d ∈ P 0 . Analicemos las posibles situaciones:
m+1
Si u1 es nueva, u1 ≡ ei → si [Z] (α = m + 1), como todas las variables de s
m+1
son nuevas, y en particular Z, no puede ser u2 ≡ ej [Z] → sj , y el resto de
condiciones tienen prioridad menor que m + 1.
m
m+1
Si u1 ≡ s → c(t)[Z], con m = α, u2 podrı́a ser u2 ≡ ei [Z] → ti , pero
m
en P tendrı́amos f (e)[Z] → c(t)[Z], que claramente incumple la condición de
β
aciclicidad. La otra posibilidad es u2 ≡ c[Z] → d ∈ P , entonces en P tenı́amos
β
m
f (e) → c(t)[Z], c[Z] → d y por hipótesis m = α > β.
α
m+1
m
Si u1 ≡ a → b[Z] ∈ P , puede ser u2 ≡ ei [Z] → si , en P tendrı́amos f (e)[Z] →
α
c(t), a → b[Z], serı́a α > m y no se podrı́a aplicar la regla. Tampoco puede ser
m
u2 ≡ s[Z] → c(t) porque las variables de s son todas nuevas y Z ∈ var(ei ). Y
β
si fuese u2 ≡ c[Z] → d ∈ P , entonces u1 , u2 ∈ P y por hipótesis α > β.
β
m+1
a.2) Supongamos u1 ≡ ei → si [Z] ∈ P 0 , r ≡ (c 3 d)[Z] ∈ R0 (α = m + 1). Como
las variables de s son nuevas Z debe ser nueva y debe ser r ∈ C m con lo que
β = m y m + 1 = α > β = m.
β
m
Si u1 ≡ s → c(t)[Z] ∈ P 0 , r ≡ (c 3 d)[Z] ∈ R0 , como Z ∈ var(c(t)), Z no es
β
m
nueva, por lo que r ∈ R. En P tenı́amos f (e) → c(t)[Z] y en R, (c 3 d)[Z] y
por hipótesis m = α > β.
β
α
Si u1 ≡ a → b[Z] ∈ P y r ≡ (b 3 c)[Z] ∈ R0 , como las variables de C m son
todas nuevas tiene que ser r ∈ R y por hipótesis α > β.
0
0
b) Si e → [Z] ∈ P 0 tiene que ser e → [Z] ∈ P . Por hipótesis, como var(f (e)) ∩
var(c(t)) = ∅ y como var(s) son todas nuevas, var(f (e)) ∩ var(s) = ∅. El resto
de lados derechos ya aparecı́an en P y por hipótesis cumplen la condición.
β
α
Act1 a.1) Sean u1 ≡ a → b[Z], u2 ≡ c[Z] → d ∈ P 0 .
m+2
m+2
Si fuese u1 ≡ ei → si [Z] no podrı́a ser u2 ≡ ej [Z] → sj porque las variables de s son todas nuevas y no pueden aparecer en ej . Para cualquier otra
posibilidad de u2 , con seguridad la prioridad será menor que m + 2.
m+1
m+2
Si u1 ≡ s → X no puede ser u2 ≡ ei [Z] → si porque en P tendrı́amos
β
m
f (e)[Z] → Z que incumple la linealidad y si u2 ≡ c[Z] → d ∈ P , como por
hipótesis β < m, será β < m + 1.
α
m+2
Si u1 ≡ a → b[Z] ∈ P no puede ser u2 ≡ ei [Z] → si porque en P tendrı́amos
α
0
a → b[Z], f (e)[Z] → X, que incumple la parte b) de la hipótesis. Tampoco
m+1
puede ser u2 ≡ s[Z] → X porque las variables de s son todas nuevas y no
β
pueden, por tanto aparecer en b. Y si u2 ≡ c[Z] → d ∈ P , como u1 , u2 ∈ G, por
hipótesis α > β.
CAPÍTULO 4. DE LA SEMÁNTICA
154
β
α
a.2) Supongamos u1 ≡ a → b[Z] ∈ P 0 , r ≡ (c 3 d)[Z] ∈ R0 .
m+2
Si u1 ≡ ei → si [Z] (α = m + 2), Z serı́a nueva y sólo puede ser r ∈ C m+1
(β = m + 1), con lo que α > β.
m+1
Si u1 ≡ s → X, siendo X ≡ Z y α = m + 1 no puede ser r ∈ C m+1 porque
m
las variables de C m+1 son todas nuevas y por tanto X 6∈ C m+1 . Si r ≡ X 3 e0
β
con β = m, tenemos m + 1 = α > β. Si r ≡ c 3 d[X] ∈ R, tiene que ser
m + 1 = α > β para poder aplicar la regla.
α
Si u1 ≡ a → b[Z] ∈ P , debe ser α < m. Para poder aplicar la regla no puede ser
m
r ∈ C m+1 porque las variables de C m+1 son todas nuevas. Si (r ≡ X 3 e0 )[Z],
β
entonces u1 ∈ P y r ∈ R y por hipótesis α > β = m. Si r ≡ (c 3 d)[Z] ∈ R,
como antes, por hipótesis α > β.
0
α
b) Supongamos u ≡ a → Z, r ≡ c → d ∈ P 0 . Como la regla no introduce suspensiones nuevas tiene que ser u ∈ P .
Si u ∈ G, por hipótesis var(a) ∩ var(r) = ∅.
m+2
Si u ≡ ei → si , como las variables de si son nuevas var(a) ∩ var(si ) = ∅.
m+1
Si u ≡ s → X. Por la parte b) de la hipótesis var(a) ∩ {X} = ∅ porque X
aparece en un lado derecho de una producción.
Act2 Sea θ ≡ [X/c(Y )]
β
α
a.1) Suponemos u1 ≡ aθ → bθ[Z], u2 ≡ cθ[Z] → dθ ∈ P 0 .
m+1
0
No puede ser u1 ≡ ei → Yi (Z ≡ Yi ), porque en P tendrı́amos c(e)[Yi ] → X y
como Yi es nueva no podı́a aparecer en P .
α
Si u1 proviene de una producción en P de la forma a → b sabemos por linealidad
que X 6∈ var(b), lo que implica bθ = b y debe ser Z ∈ var(b). Para u2 tenemos
dos casos:
m+1
No puede ser u2 ≡ ei [Z] → Yi porque serı́a Z ∈ var(c(e)) y en G tendrı́amos
var(c(e)) ∩ var(b) 6= ∅, lo que contradice hipótesis b).
0
α
β
Si u2 proviene de una producción de P tenemos c(e) → X, a → b[Z], c → d ∈ P .
Tiene que ser Z ∈ var(c), porque de lo contrario Z aparecerı́a en cθ debido a la
sustitución θ, lo que implicarı́a Z = Yi para algún i y Z serı́a nueva, pero esto
no puede ser porque ya aparecı́a en G (en b). Por lo tanto Z ∈ var(c) y en P
β
α
tenemos a → b[Z], c[Z] → d, y por hipótesis α > β.
β
α
a.2) Supongamos u ≡ aθ → bθ[Z], r ≡ (cθ 3 dθ)[Z]. Como antes, u proviene de una
α
α
producción de P , a → b y debe ser Z ∈ var(b). Luego en P tenemos a → b[Z].
β
En R debı́amos tener la restricción c 3 d y Z tiene que aparecer en ella, porque
de lo contrario, la habrı́a introducido la sustitución θ; luego serı́a Z = Yi para
algún i nueva, pero Z ya aparece en P luego no ha sido introducida por θ.
β
α
Luego en P tenı́amos a → b[Z] y en R (c 3 d)[Z], y por hipótesis tenemos
α > β.
0
α
b) Supongamos u ≡ aθ → Zθ, r ≡ cθ → dθ ∈ P 0 . Como la regla no introduce
0
suspensiones nuevas debe ser a → Z ∈ P . Para r tenemos dos opciones:
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
155
α
0
0
α
Si c → d ∈ P . Entonces en P tenemos c(e) → X, a → Z, c → d ∈ P . Por
hipótesis tenemos que var(a) ∩ var(d) = ∅ (1). Por linealidad X 6∈ var(d) luego
dθ = d (2) y Yi 6∈ var(d), ∀i = 1..n (3). Y por último, var(aθ) ⊆ var(a) ∪
{Yi }i=1..n (4). Uniendo resultados:
⊆
(2)
var(aθ) ∩ var(dθ) = var(aθ) ∩ var(d) (4) (var(a) ∪ {Yi }i=1..n ) ∩ var(d) =
(1)(3)
(var(a) ∩ var(d)) ∪ ({Yi }i=1..n ∩ var(d)) = ∅.
Si r ha sido introducida por la regla, entonces no es afectada por θ y r ≡
0
m+1
0
ei → Yi ∈ P 0 , en P tenı́amos a → Z, c(e) → X. Como Yi es nueva, para
0
que Yi ∈ var(a) debe ser X ∈ var(a), pero entonces en P tendrı́amos a[X] →
0
Z, c(e) → X que contradice la hipótesis Luego r no puede haber sido introducida
por la regla.
m+1
Eval a.1) La única → nueva es f (e) → Z y como Y es nueva no puede aparecer en
α
m+1
ninguna otra producción. Tampoco podemos tener a → b[Z], f (e)[Z] → Y ∈
m
α
P 0 porque en P tendrı́amos a → b[Z], (f (e) 3 e0 )[Z], serı́a α > m y no se podrı́a
aplicar la regla.
a.2) El único par de producción y restricción que cumple las hipótesis del teorema
m
m+1
y que no aparecı́a en P es f (e) → Y, Y 3 e0 y se cumple m + 1 > m.
b) La única producción nueva que se ha introducido tiene una variable nueva en
su lado derecho que, no puede por tanto, aparecer en ningún lado izquierdo de
una suspensión.
Dcp== a.1) Trivial porque las producciones y las suspensiones no cambian.
a.2) Trivial porque se reemplaza una restricción por otra con el mismo ı́ndice y las
mismas variables.
b) Ídem que a).
Id a.1) Trivial porque las producciones y las suspensiones no cambian.
a.2) Trivial porque todas las restricciones de P 0 aparecı́an también en P .
b) Ídem que a).
Bind Sea θ ≡ [X/t]
β
α
a.1) Supongamos u1 ≡ aθ → bθ[Z], u2 ≡ cθ[Z] → dθ ∈ P 0 , entonces en P debı́amos
α
β
tener a → b, c → c. Tiene que ser Z ∈ var(b) porque de lo contrario la habrı́a
introducido θ y serı́a Z ∈ var(t) y X ∈ var(b), pero entonces en P tendrı́amos
α
m
a → b[X], X == t y no se podrı́a aplicar la regla porque por hipótesis α >
m. Por tanto Z ∈ var(b). Por otro lado debe ser Z ∈ var(c), ya que de otro
α
modo igual que antes serı́a X ∈ var(c) y Z ∈ var(t) y tendrı́amos en P a →
m
b[Z], X == t[Z] y no se podrı́a aplicar la regla porque por hipótesis α > m.
β
α
Luego Z ∈ var(c) y en P tenemos a → b[Z], c[Z] → d y por hipótesis α > β.
α
β
a.2) Supongamos u ≡ aθ → bθ[Z] ∈ P 0 , r ≡ (cθ 3 dθ)[Z] ∈ P 0 . Como antes tiene
que ser Z ∈ var(b). Casos para r:
CAPÍTULO 4. DE LA SEMÁNTICA
156
β
β
Si r proviene de R, r ≡ c 3 d debe ser Z ∈ var(c 3 d) ya que de lo contrario
β
α
m
serı́a X ∈ var(c 3 d y Z ∈ var(t) y en P tendrı́amos a → b[Z], X == t[Z],
β
serı́a α > m y no se podrı́a aplicar la regla. Por tanto Z ∈ var(c 3 d), en P
β
α
tenemos a → b[Z], c 3 d y por hipótesis α > β.
Si r proviene de δX , como Z ∈ var(b), Z es producida y no puede por tanto aparecer en δ y en particular no puede aparecer en δX . Entonces tendrı́a
que haber sido introducida por θ, siendo Z ∈ var(t), pero esto como antes es
contradictorio porque serı́a α > m.
0
α
b) Supongamos aθ → Zθ, cθ → dθ ∈ P 0 . No puede ser Z ≡ X porque Z ∈ pvar(G)
y X 6∈ pvar(G), luego Zθ ≡ Z. Por otro lado, no puede ser X ∈ var(d)
α
m
porque en P tendrı́amos c → d[X], X == t y serı́a α > m, luego dθ ≡ d (1).
Por la misma razón d no puede contener variables que aparezcan en t, luego
var(d) ∩ var(t) = ∅ (2). Tenemos por hipótesis var(a) ∩ var(d) = ∅ (3), y
también sabemos que var(aθ) ⊆ var(a) ∪ var(t) (4), luego:
=
(4)
(1)
var(aθ) ∩ var(dθ) = var(aθ) ∩ var(d) ⊆ (var(a) ∪ var(t) ∩ var(d) ( var(a) ∩
(2)(3)
var(d)) ∪ (var(t) ∩ var(d)) = ∅.
Imit== Sea θ ≡ [X/c(Y )]
β
α
α
β
a.1) Supongamos u1 ≡ aθ → bθ[Z], u2 ≡ cθ[Z] → dθ. En P tendremos a → b, c → d.
Tiene que ser Z ∈ var(b) porque de lo contrario habrı́a sido introducida por θ,
serı́a X ∈ var(b) y por hipótesis tendrı́amos α > m y no se podrı́a aplicar la
regla, luego Z ∈ var(b). Por otro lado, si Z 6∈ var(c), serı́a introducida por θ,
pero θ sólo introduce variables nuevas y Z ∈ var(b), luego Z ∈ var(c). Ası́, en
β
α
P tenemos a → b[Z], c[Z] → d y por hipótesis α > β.
β
α
a.2) Supongamos u ≡ aθ → b ∈ P 0 , r ≡ (cθ 3 dθ)[Z] ∈ R0 . Igual que antes tiene que
ser Z ∈ var(b). Para r tenemos tres posibilidades:
β
β
Si proviene de R, en R tenemos c == d. Tiene que ser Z ∈ var(c → d) porque
θ sólo introduce variables nuevas y Z ya aparecı́a en var(b), por lo que en G
α
β
tenemos a → b[Z], (c → d)[Z] y por hipótesis es α > β.
m
Si es de la forma r ≡ Yi θ == ei θ, tiene que ser Z ∈ var(ei ) ya que θ sólo
α
introduce variables nuevas. Pero entonces en P tenemos a → b[Z] y en R,
m
X == c(e)[Z] y por hipótesis serı́a α > m y no podrı́amos aplicar la regla,
luego esta opción no es posible.
Si r proviene de una desigualdad de δX , Z no puede aparecer en dicha desigualdad porque Z ∈ pvar(G) y δ no contiene variables producidas. Además,
no puede haberla introducido θ que sólo introduce variables nuevas.
0
α
b) Supongamos s ≡ aθ → Zθ, cθ → dθ ∈ P 0 . Como la regla no introduce sus0
α
pensiones ni producciones nuevas en P tenemos a → b, c → d. Como X 6∈
pvar(G) no puede aparecer en ningún lado derecho de una producción, luego Zθ ≡ Z y dθ ≡ d (1). Por otro lado, como las variables Y son nuevas,
{Yi }i=1..n ∩ var(d) = ∅ (2), sabemos var(aθ) ⊆ var(a) ∪ {Yi }i=1..n (3) y por
hipótesis var(a) ∩ var(d) = ∅ (4). Uniendo resultados:
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
157
(3)
(1)
var(aθ) ∩ var(dθ) = var(aθ) ∩ var(d) ⊆ (var(a) ∪ {Yi }i=1..n ) ∩ var(d) =
(2)(4)
(var(a) ∩ var(d)) ∪ ({Yi }i=1..n ∩ var(d)) = ∅.
Clash a.1) Trivial porque las producciones y suspensiones no cambian.
a.2) Trivial por lo mismo y porque desaparece una restricción.
b) Ídem que a.1)
Store a.1) Ídem que antes.
a.2) Ídem.
b) Ídem.
Dcp6= a.1) Ídem.
a.2) Trivial porque las variables de la restricción nueva que aparece son un subconjunto de las variables de la que desaparece.
b) Ídem.
Imit6= Sea θ ≡ [X/d(Y )], siendo d ∈ DC n . En este caso nos da igual que sea d = c o no; lo
que nos interesa es que θ reemplaza X por una expresión que sólo contiene variables
nuevas.
β
α
a.1) Supongamos u1 ≡ aθ → bθ[Z], u2 ≡ cθ[Z] → dθ. Como X 6∈ pvar(G), X 6∈
var(b) y bθ = b, luego Z ∈ var(b). Como θ sólo introduce variables nuevas y
α
Z ∈ var(b) (no es nueva) tiene que ser Z ∈ var(c). Ası́ en P tenemos a →
β
b[Z], c → c y por hipótesis α > β.
β
α
a.2) Supongamos u ≡ aθ → bθ[Z] ∈ P 0 , r ≡ (cθ 3 d)[Z] ∈ R0 . Igual que antes
Z ∈ var(b). Casos para r:
β
Si r proviene de una restricción de R, c 3 d, como θ sólo introduce variables
β
β
α
nuevas, tiene que ser Z ∈ var(c 3 d). En G tendremos a → b[Z], (c 3 d)[Z] y
por hipótesis α > β.
Si r viene de una desigualdad de δX , como Z ∈ pvar(G), dicha desigualdad no
puede contener a Z por lo que tendrı́a que haber sido introducida por θ, pero
θ sólo introduce variables nuevas y Z ya aparecı́a en G.
m
Si r es de la forma Yi θ 6= ei θ, tiene que ser Z ∈ var(ei ), ya que θ sólo introduce
α
m
variables nuevas. Pero entonces en P tendrı́amos a → b[Z], X 6= c(e)[Z] y por
hipótesis serı́a α > m y no podrı́amos aplicar la regla.
b) Análogo a Imit== .
Parte 3
Procedemos también por análisis de casos.
Dcp→ La aciclicidad y la linealidad deben cumplirse en G0 porque de lo contrario, trivialmente no se verificarı́an en G. La producción se cumple porque los conjuntos de
variables producidas y existenciales no cambian y tampoco lo hacen σ y δ.
CAPÍTULO 4. DE LA SEMÁNTICA
158
α
Obind Aciclicidad: X 6∈ suspvar(G) por hipótesis y si existiese l → r[X] ∈ P con α > 1
serı́a α > m y no serı́a aplicable la regla, por lo tanto X 6∈ pvar(G). Entonces
los lados derechos de las reglas permanecen invariables. Las únicas variables que se
pueden introducir en los lados izquierdos son las de c(t), que no pueden aparecen en
ningún lado derecho de por linealidad de G.
Linealidad: se preserva porque los lados derechos de P no cambian.
Producción: tenemos las inclusiones evar(G0 ) ⊂ evar(G) y pvar(G0 ) ⊂ pvar(G0 )
y por hipótesis pvar(G) ⊆ evar(G) luego pvar(G0 ) ⊆ evar(G0 ). Por otro lado las
únicas variables que se introducen en σ son las de c(t) y la propia X, y como los
lados derechos de P no cambian y no contienen variables de c(t) (linealidad) ni a X
es claro que la nueva σ 0 no contiene variables producidas. En δ las únicas nuevas son
las de c(t) que no son producidas.
Ibind Aciclicidad: Y no es producida por linealidad de G, luego los lados derechos de las
producciones no cambian. Si en G0 se incumpliese la aciclicidad serı́a por haber introducido X en un lado izquierdo de una producción cuyo lado derecho ya contuviese
α
X. Esta producción en G tendrı́a la forma l[Y ] → r[X] y por el lema de orden
tendrı́a que ser α = 0 ya que de lo contrario serı́a α > m. Pero el propio lema de
orden afirma que en esta situación Y no podrı́a aparecer en ningún lado derecho en
G, hecho que es obviamente false. Luego en G0 se verifica la aciclicidad.
Linealidad: trivial porque los lados derechos de P no cambian por la aplicación de
la regla.
Producción: tenemos pvar(G0 ) ⊂ pvar(G) y evar(G0 ) ⊂ evar(G0 ) (desaparece la
Y en ambos casos). Por hipótesis pvar(G) ⊆ evar(G) luego pvar(G0 ) ⊂ evar(G0 ).
Por otro lado σ y δ no cambian (y las variables producidas disminuyen) luego no
contienen variables producidas.
Sus Trivial.
m+1
Nrw Aciclicidad: las producciones nuevas ei → si tienen todas las variables de la derecha
m
nuevas, por lo que son acı́clicas y a s → c(t) le ocurre lo mismo en el lado izquierdo.
El resto de producciones no cambia.
Linealidad: los nuevos lados derechos son s1 , ..., sn que provienen de una regla del
programa de la forma f (s1 , ..., sn ) = s <== C en la que, por definición de programa
la tupla s1 , ...., sn es lineal. Además las variables de esta tupla son nuevas y no pueden
aparecer en ningún lado derecho que estuviese en G.
Producción: las nuevas variables producidas que se introducen son las de s que están
entre las nuevas Y que introduce la regla, que a su vez son existenciales en G0 .
σ y δ no cambian y las variables producidas nuevas no pueden aparecer en ellas
precisamente porque son nuevas.
m+2
Act1 Aciclicidad: las producciones nuevas ei → si tienen todas las variables de la derecha
m+1
nuevas, por lo que son acı́clicas y a s → X le ocurre lo mismo en el lado izquierdo.
El resto de producciones no cambia.
Linealidad: ı́dem que en Nrw.
Producción: ı́dem que en Nrw.
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
159
Act2 Sea θ = [X/c(Y )].
Aciclicidad: por linealidad de G, X no puede aparecer en ningún lado derecho de
P , luego los lados derechos permanecen invariables por θ. Las variables que puede
haber introducido θ en los lados izquierdos son las de Y que son nuevas, por lo
que la aciclicidad se preserva en las aproximaciones de P al aplicar θ. Todas las
aproximaciones nuevas que se introducen tienen una variable nueva Yi en su lado
derecho, por lo que también son acı́clicas.
Linealidad: como los lados derechos de P no cambian y las producciones nuevas son
m+1
de la forma ei → Yi con Yi variable nueva (Yi 6= Yj para todo i 6= j) la linealidad
se preserva.
Producción: las nuevas variables producidas que se introducen son las de Y que son
claramente existenciales en G0 . La variable X ya no es existencial en G0 , pero es que
de hecho ya no aparece en P debido a la sustitución θ que se aplica, y claramente no
aparece en los lados derechos de las nuevas producciones que son variables nuevas,
luego pvar(G0 ) ⊆ evar(G0 ). Por otro lado σ y δ no cambian y las nuevas variables
producidas claramente no les afectan, luego pvar(G0 )∩var(σ) = pvar(G0 )∩var(δ) =
∅.
m+1
[Eval La única producción nueva que se introduce es f (e) → Y que tiene una variable
nueva como lado derecho (que no puede aparecer en f (e), por lo que se preservan la
aciclicidad y la linealidad. Esta nueva variable producida es también existencial en
G por lo que pvar(G0 ) ⊆ evar(G0 ), y como σ y δ no cambian y contienen a la nueva
variable tenemos pvar(G0 ) ∩ var(σ) = pvar(G0 ) ∩ var(δ) = ∅.
Dcp== ,Id Triviales porque no cambian ni las producciones, ni las variables existenciales, ni σ
ni δ.
Bind Y no puede aparecer en ningún lado derecho de P por la propiedad de orden (a.2),
luego los los lados derechos de P no cambian y claramente se preserva la linealidad.
Por la misma razón X no puede aparecer en ningún lado derecho y como es la única
variable que se puede haber introducido en los lados izquierdos la aciclicidad se
conserva.
Las variables producidas y las existenciales no cambian luego pvar(G0 ) ⊆ evar(G0 ).
Por otro lado, en δ sólo puede haberse introducido la variable Y y en σ se introducen
X e Y , pero ninguna de ambas era producida y los lados derechos de P no han
cambiado, luego pvar(G0 ) ∩ var(σ) = pvar(G0 ) ∩ var(δ) = ∅.
Imit== X no puede aparecer en ningún lado derecho de P por el lema de orden luego los
lados derechos de P no cambian y se preserva la linealidad. En los lados izquierdos eventualmente pueden haberse introducido variables nuevas que no afectan a la
aciclicidad.
Como el conjunto de variables producidas permanece intacto y el de las existenciales
conserva las que tenı́a es claro que pvar(G0 ) ⊆ evar(G0 ). En δ a lo sumo pueden haberse introducido variables nuevas y en σ además puede haberse introducido
X, pero ninguna de todas estas variables es producida, luego pvar(G0 ) ∩ var(σ) =
pvar(G0 ) ∩ var(δ) = ∅.
Clash Ídem que EDC, ID.
CAPÍTULO 4. DE LA SEMÁNTICA
160
Store P no cambia, luego la linealidad y la aciclicidad son claras. Tanto X como var(t) no
pueden ser ninguna producidas por la propiedad de orden (a.2) y son las únicas que se
introducen en δ. Además σ no cambia, luego pvar(G0 )∩var(σ) = pvar(G0 )∩var(δ) =
∅. Como los conjuntos de variables producidas y existenciales no cambian tenemos
pvar(G0 ) ⊆ evar(G0 ) por hipótesis.
Dcp6= Ídem que EDC, ID.
Imit6= X no es producida porque por el lema de orden no puede aparecer en ninguna
producción de P de ı́ndice > 0 y por hipótesis X 6∈ suspvar(G), luego los lados
derechos de P no están afectados por la sustitución y la linealidad se preserva.
Por otro lado, en las dos alternativas que ofrece esta regla X se sustituye por una
expresión que sólo contiene las variables nuevas Y , que pueden introducirse en los
lados izquierdos de P pero no en los derechos que no cambian, luego la aciclicidad
también se preserva.
Las demostraciones de pvar(G0 ) ⊆ evar(G0 ) y pvar(G0 ) ∩ var(σ) = pvar(G0 ) ∩
var(δ) = ∅ son las mismas que en Imit== .
¥
En las dos definiciones siguientes se precisan los conceptos de forma resuelta y de
solución, en las que nos apoyaremos para concretar los resultados corrección y completitud.
Definicion 5 (Forma resuelta) Decimos que un objetivo admisible G ≡ ∃U .P 2R2σ2δ
es (o está en) una forma resuelta si se cumple:
R = ∅ (todas las restricciones han sido resueltas)
α
∀(l → t) ∈ P , α = 0 (todas las producciones que quedan son suspensiones).
¥
Nótese que en las formas resueltas pueden haber suspensiones. Esto quiere decir que
al final de un cómputo puede haber producciones que no se han procesado debido a la
pereza. Tales suspensiones son ahora inútiles.
Definicion 6 (Solución) Decimos que θ ∈ Subst⊥ es una GORC6= -solución (simplemente solución) para un objetivo G ≡ ∃U .P 2R2δ2σ y lo notaremos por θ ∈ Sol(G)
si:
Xθ ∈ CT erm, ∀X ∈ V − evar(G),
R ` P θ, Rθ, δθ, y
σθ es un conjunto de identidades.
¥
El primer requisito, Xθ ∈ CT erm, ∀X ∈ V − evar(G), es para tener garantizado que
ninguna variable de las que aparecı́a en el objetivo inicial toma el valor ⊥ (todas toman
valores finitos y totales). Las variables existenciales que ha introducido el cómputo pueden
tomar cualquier valor.
Los otros dos requisitos relacionan la noción de solución con el cálculo GORC6= . Al
aplicar θ a las condiciones de P y R, las condiciones resultantes deben ser probables en el
cálculo GORC6= . Para las desigualdades de δ debe ocurrir lo mismo.
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
161
Las igualdades de σ afectadas de θ, no sólo deben ser GORC6= -probables, sino que de
hecho, serán un conjunto de identidades de c-términos. Obsérvese que todas las variables
nuevas que introduce el cálculo LN C6= están cuantificadas existencialmente. En σ sólo se
añaden igualdades de la forma X = t siendo X una variable no existencial y t ∈ CT erm.
Es decir, X es una variable que aparecı́a en el objetivo inicial y este tipo de variables
toman valores finitos y totales (primer requisito). Por eso las igualdades σθ deben ser
identidades entre c-términos.
4.4.2.
Corrección
En este apartado demostraremos que el cálculo LN C6= es correcto con respecto a
GORC6= , es decir, que si θ0 es solución de un objetivo G0 que se derivado de otro G
entonces existe θ que es solución de G y que coincide con θ0 al menos sobre las variables
no existenciales. No podemos afirmar que una solución de G0 sea también solución de
G porque las reglas del cálculo pueden introducir y eliminar variables existenciales. Lo
interesante es que las variables del objetivo inicial toman los mismos valores mediante una
u otra sustitución.
A continuación presentamos el enunciado formal y su demostración, que se apoya en
el lema de sustituciones y las condiciones de admisibilidad de objetivos.
Teorema 1 (Corrección) El cálculo de resolución de objetivos es correcto con respecto
a GORC6= en el sentido siguiente:
Si G es un objetivo admisible y G Ã G0 se verifica:
si G0 ≡ F AIL entonces Sol(G) = ∅,
si θ0 ∈ Sol(G0 ), entonces existe θ ∈ Sol(G) con θ = θ0 [V − (evar(G) ∪ evar(G0 ))]
Demostración
Probaremos que el enunciado es cierto para cada una de las reglas del cálculo.
Dcp→ Si θ0 ∈ sol(G0 ) entonces ei θ0 → ti θ0 es probable. Tomando θ ≡ θ0 podemos reconstruir
una prueba para c(e)θ → c(t)θ utilizando la regla 3 del cálculo de pruebas GORC6= .
El resto de producciones no cambia.
Obind Si θ0 ∈ Sol(G0 ) entonces (P, δX , R, δ)[X/c(t)]θ0 es probable y σ[X/c(t)]θ0 es un conjunto de identidades. Como X ∈ evar(G) y X 6∈ var(c(t)) por aciclicidad, podemos
definir θ como Xθ ≡ c(t)θ0 y Zθ ≡ Zθ0 , ∀Z 6≡ Y . De este modo, por el lema de sustitución tenemos θ ≡ [X/c(t)]θ0 , luego (P, δX , R, δ)[X/c(t)]θ0 = (P, δX , R, δ)θ con lo
que para esta parte del objetivo las pruebas son las mismas tanto en G como en G0 .
m
Para la producción X → c(t) tenemos: Xθ = c(t)θ0 por definición de θ, pero además
como X 6∈ var(c(t)) tenemos c(t)θ0 = c(t)θ. Ası́ (X → c(t))θ ≡ c(t)θ → c(t)θ que
obviamente es probable. Por último σθ = σθ0 es un conjunto de identidades, por lo
que θ es la sustitución que postula el teorema.
Ibind En primer lugar observemos que como Y ∈ pvar(G) entonces Y 6∈ var(σ) ∪ var(δ),
es decir, σ y δ no se ven afectadas por la sustitución [Y /X]. Como Y ∈ pvar(G) y
pvar(G) ⊆ evar(G) podemos definir θ como Y θ = Xθ0 y Zθ = Zθ0 , ∀Z 6≡ Y . Por
el lema de sustitución se prueba que (P, R, δ)θ = (P, R, δ)[Y /X]θ0 y Xθ → Y θ ≡
Xθ → Xθ0 ≡ Xθ → Xθ que es obviamente probable. Es decir, las pruebas en G son
idénticas a las pruebas en G0 . Además σθ = σθ0 es un conjunto de identidades.
162
CAPÍTULO 4. DE LA SEMÁNTICA
Sus En este caso tomando θ ≡ θ0 el resultado es trivial.
Nrw De nuevo tomando θ ≡ θ0 , basta advertir que como (e → s, s → c(t), C)θ0 es probable,
podemos reconstruir una prueba de (f (e) → c(t))θ con la regla 4 del cálculo GORC6= .
Act1 Análogo a Nrw, pero ahora en vez de c(t) tenemos X.
Act2 Nótese que como X ∈ pvar(G) entonces X 6∈ σ ∪ δ, es decir, la sustitución [X/c(Y )]
deja invariantes σ y δ. De estos hecho se sigue que (P 2R)[X/c(Y )]2σ2δ es idéntico
a (P 2R2σ2δ)[X/c(Y )].
Dado que X ∈ pvar(G) ⊆ evar(G), podemos definir θ como Xθ ≡ c(Y )θ0 y
Zθ ≡ Zθ0 , ∀Z 6≡ X y por el lema de sustitución tendremos θ = [X/c(Y )]θ0 . Luego (P 2R2σ2δ)[X/c(Y )]θ0 es idéntico a (P 2R2σ2δ)θ. Como la primera parte es
probable por hipótesis, la segunda también lo es. Y por otro lado σθ = σ[X/c(Y )]θ0
es un conjunto de identidades.
Nos queda por ver que existe una prueba para la aproximación (c(e) → X)θ. Como
θ = [X/c(Y )]θ0 tenemos que (c(e) → X)θ ≡ (c(e) → X)[X/c(Y )]θ0 , que a su vez es
idéntico a (c(e) → c(Y )θ0 ya que X 6∈ var(c(e)) por linealidad de G. Ahora bien,
para (e → Y ))θ0 existen pruebas por hipótesis, y con ellas se puede construir una
para (c(e) → X)θ utilizando la regla 3 del cálculo GORC6= .
Eval Tomando θ ≡ θ0 tenemos que todas las pruebas de Gθ están presentes en G0 θ0 , excepto la de (f (e) → e0 )θ que puede reconstruirse con las pruebas (f (e) → Y, Y 3e0 )θ0
que tenemos en G0 y la regla 5 del cálculo GORC6=
Dcp== Trivial tomando θ ≡ θ0 y utilizando la regla 7 del cálculo GORC6= .
Id Tomando θ ≡ θ0 , como G0 θ contiene todas las pruebas de Gθ excepto la de Xθ == Xθ
que obviamente se puede probar, tenemos el resultado.
Imit== Definimos θ como Xθ ≡ c(Y ) y Zθ ≡ Zθ0 , ∀Z 6≡ X, con lo que tenemos θ ≡
[X/c(Y )]θ0 por el lema de sustitución. De este modo todas las pruebas de Gθ están
entre las de G0 [X/c(Y )]θ0 excepto la de (X == c(e))θ. Ahora bien, (X == c(e))θ ≡
(X == c(e))[X/c(Y )]θ0 ≡ (c(Y ) == c(e))θ0 , y se puede construir una prueba para
esta última restricción con las pruebas de (Y == e)θ0 que tenemos en G0 utilizando
la regla 7 del cálculo GORC6= .
Clash Tomando θ ≡ θ0 tenemos que todas las pruebas de G aparecen en G0 , excepto la de
(c(e) 6= d(e0 ))θ que es trivialmente probable por la regla 8 del cálculo GORC6= . σθ
es un conjunto de identidades por hipótesis.
Store Trivial porque para el cálculo GORC6= son idénticos G y G0 .
Dcp6= Tomando θ ≡ θ0 , si en G tenemos una prueba para (ei 6= e0i )θ podemos construir una
prueba para (c(e) 6= c(e0 ))θ utilizando la regla 9 del cálculo GORC6= .
Imit6= Haremos la demostración para las dos alternativas de la regla. En el primer caso
definimos θ como Xθ ≡ d(Y )θ0 y Zθ ≡ Zθ0 , ∀Z 6≡ X. Por el lema de sustitución
tenemos θ ≡ [X/d(Y )]θ0 . En G0 [X/d(Y )]θ0 están presentes todas las pruebas que hay
que hacer en G, excepto la de (X 6= c(e))θ0 ≡ (d(Y ) 6= c(e))θ que puede construirse
con la regla 8 del cálculo GORC6= (c 6≡ d).
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
163
Para la otra alternativa podemos tomar θ como Xθ ≡ c(Y )θ0 y Zθ ≡ Zθ0 , ∀Z 6≡ X.
Por el lema de sustitución tenemos θ ≡ [X/c(Y )]θ0 . Las pruebas de Gθ están entre
las de G0 [X/c(Y )]θ0 , excepto la de (X 6= c(e))θ que puede construirse con la de
(Yi 6= ei )[X/c(Y ]θ0 y la regla 9 del cálculo GORC6= .
¥
4.4.3.
Completitud
Ahora nos interesa demostrar que el cálculo LN C6= es capaz de derivar formas resueltas
para un objetivo inicial. Además las soluciones del objetivo inicial no deben perderse en
los pasos de derivación. En otras palabras, dado un objetivo inicial y una solución para
él, se puede derivar una forma resuelta para la que existe una solución que coincide con
la primera, al menos sobre las variables del objetivo inicial. Este es la idea del resultado
de completitud que formalizaremos más tarde. Este resultado será una consecuencia (casi)
inmediata del lema de progreso que veremos a continuación.
El lema de progreso utiliza las nociones de testigo y orden de testigos cuyas definiciones
son variantes de las de [GHL+ 96, GHL+ 98]:
Definicion 7 (Testigo) Sea R un programa y sea G ≡ ∃U .P 2R2σ2δ un objetivo para
el cálculo LN C6= y θ ∈ Sol(G). Un testigo para θ es un multiconjunto que contiene una
β
α
GORC6= -prueba para cada una de las condiciones (l → t) ∈ P θ, (e 3 e0 ) ∈ Rθ y δθ.
¥
Definicion 8 (Orden de testigos (/)) Dado un programa R, si M ≡ {{Π1 , ..., Πn }} y
M0 ≡ {{Π01 , ..., Π0m }} son multiconjuntos de GORC6= -pruebas de condiciones (producciones y restricciones), definimos:
M / M0 sii {{|Π1 |, ..., |Πn |}} ≺ {{|Π01 |, ..., |Π0m |}}
donde |Π| es la longitud (número de pasos de inferencia) de Π y ≺ es la extensión para
multiconjuntos del orden habitual sobre IN ([DM79]).
¥
En ([GHL+ 96, GHL+ 98]) el este orden de testigos bastaba para probar el lema de
progreso que se presentaba. En nuestro cálculo debemos extender este orden debido a que
en las reglas Sus y Store el testigo de un objetivo y su transformado son exactamente
el mismo. No obstante, en Sus disminuye el número de producciones no suspendidas (se
suspende una de ellas) y en Store se pasa una desigualdad a la parte de desigualdades
resueltas, es decir, disminuye el número de restricciones en la parte de resolución R. Es
obvio que tanto el número de producciones, como el de restricciones, es finito en un objetivo
admisible. De acuerdo con lo anterior es posible definir un orden lexicográfico (y por tanto
bien fundado) entre parejas de testigos y objetivos:
Definicion 9 (Orden de objetivos y testigos (/)) Sea R un programa y
0
G ≡ ∃U .P 2R2σ2δ y G0 ≡ ∃U .P 0 2R0 2σ 0 2δ 0 dos objetivos admisibles tales que G Ã G0 .
Sean θ ∈ Sol(G), θ0 ∈ Sol(G0 ), M un testigo para θ y M0 un testigo para θ0 . Entonces
(M, G)/(M0 , G0 ) sii
M / M0 , o bien
α
α
α
α
M = M0 y Card({l → r|(l → r) ∈ P, α 6= 0}) < Card({l → r|(l → r) ∈ P, α 6= 0}),
o bien
CAPÍTULO 4. DE LA SEMÁNTICA
164
α
α
α
α
M = M0 , Card({l → r|(l → r) ∈ P, α 6= 0}) = Card({l → r|(l → r) ∈ P, α 6= 0}) y
Card(R0 ) < Card(R).
donde Card denota el cardinal de un conjunto.
¥
Lema 6 (Progreso) Si G ≡ ∃U .P 2R2σ2δ es un objetivo admisible, entonces:
Si G no es una forma resuelta, entonces existe alguna transformación de LN C6=
aplicable a G.
Si M es un testigo de θ ∈ Sol(G) y T es cualquier transformación aplicable a G,
0
entonces existe G0 ≡ ∃U .P 0 2R0 2σ 0 2δ 0 , una sustitución θ0 y un testigo M0 tal que:
i) G Ã G0 mediante la transformación T ,
ii) M0 es un testigo de θ0 ∈ Sol(G),
iii) (M, G)/(M0 , G0 ),
iv) θ = θ0 [V − (evar(G) ∪ evar(G0 ))].
¥
Demostración
Demostraremos cada parte del teorema por separado.
Parte 1
Si G es admisible y no esta en forma resuelta, entonces debe existir alguna condición
(producción o restricción) de prioridad máxima m > 0 en G. Tomemos una cualquiera de
m
m
ellas, que tendrá la forma e → t, o bien e 3 e0 .
m
Supongamos que es de la forma e → t y analicemos los posibles casos:
e≡X
t ≡ Y , Ibind (i.e., se puede aplicar Ibind)
t ≡ c(t)
X 6∈ suspvar(G), Obind
0
X ∈ suspvar(G), existe l((e0 ) → X ∈ P
l ∈ DC n , Act2
l ∈ F S n , Act1
e ≡ c(e)
t ≡ Y , Sus
t ≡ c(t), Dcp→
t ≡ d(t), con c 6≡ d, Fail1
e ≡ f (e)
t ≡ Y , Sus
t ≡ c(t), Nrw
m
Supongamos ahora que la condición es de la forma e 3 e0 :
e ≡ f (e), Eval
e ≡ c(e)
e0 ≡ c(e0 )
3 ≡==, Dcp==
3 ≡6=, Dcp6=
0
e ≡ d(e0 ) con c 6≡ d
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
165
3 ≡==, Fail2
3 ≡6=, Clash
m
m
Los casos que quedan son a) X 3 Y y b) X 3 c(e).
m
a) X 3 Y
X, Y 6∈ pvar(G)
X≡Y
3 ≡==, Id
3 ≡6=, Fail4
X 6≡ Y
3 ≡==, Bind
3 ≡6=, Store
0
X ∈ pvar(G), entonces existe l(e00 ) → X ∈ P
l ∈ DC n , Act2
l ∈ F S n , Act1
b) X3c(e), puede ser:
X == c(e)
0
X ∈ pvar(G), entonces existe l(e00 ) → X ∈ P
l ∈ DC n , Act2
l ∈ F S n , Act1
X 6∈ pvar(G)
X ∈ svar(c(e)), Fail3
X 6∈ svar(c(e)), Imit==
X 6= c(e)
0
X ∈ pvar(G), entonces existe l(e00 ) → X ∈ P
l ∈ DC n , Act2
l ∈ F S n , Act1
X 6∈ pvar(G)
c(e) ∈ CT erm ∧ var(c(e)) = ∅, Store
c(e) 6∈ CT erm ∨ var(c(e)) 6= ∅, Imit6=
Parte 2
Procedemos por análisis de casos sobre la regla T aplicable a G.
Dcp→ Si M es un testigo de θ, debe contener una prueba Π0 de c(e)θ → c(t)θ que será de
la forma:
Π0 ≡ (3)
Π1 ...Πn ΠC ΠB
f (e) → c(t)
con (f (s) = s <== C) ∈ [R]⊥ , Πi prueba de ei θ → si θ, ΠC pruebas de C y ΠB
prueba de s → c(t)θ.
Podemos tomar θ0 ≡ θ y M será el resultado de reemplazar Π0 por Π1 ...Πn ΠC , ΠB .
Obind M debe contener una prueba Π0 de Xθ → c(t)θ. Por el lema de refinamiento existe
θ0 w θ tal que Xθ0 = c(t)θ0 con θ0 = θ[V − var(c(t))]. θ0 es un posible candidato ya
que coincide con θ salvo en var(c(t)) ⊆ pvar(G) ⊆ evar(G).
CAPÍTULO 4. DE LA SEMÁNTICA
166
Por otro lado, [X/c(t)]θ0 w θ: X[X/c(t)]θ0 = c(t)θ0 = Xθ0 w Xθ y para toda Y 6≡ X
Y [X/c(t)]θ0 = Y θ0 w θ.
α
Entonces, si l → r ∈ P existirá una demostración de lθ → rθ y por monotonı́a existirá en G0 una demostración con la misma longitud y estructura de l[X/c(t)]θ0 → rθ.
Ahora bien, X 6∈ pvar(G) por el lema de orden y X 6∈ susV ar(G) por hipótesis (X no aparece en ningún lado derecho); entonces X 6∈ var(r) y por otra parte
α
var(c(t)) ∩ var(r) = ∅ por linealidad. Entonces r[X/c(t)]θ0 ≡ rθ. Si e 3 e0 existirá una demostración de eθ3e0 θ y por monotonı́a (aplicando dos veces el teorema)
existirá una demostración de e[X/c(t)]θ0 3e0 [X/c(t)]θ0 . Lo mismo ocurre con las desigualdades de δX y como δ y σ no contienen no tienen variables producidas y θ0
sólo difiere de θ en var(c(t)) ⊂ pvar(G) tenemos (δ, σ)θ0 = (δ, σ)θ (son las mismas
demostraciones). Y por último Xθ0 = c(t)θ0 es obviamente una identidad.
Por lo tanto M0 es el resultado de eliminar la prueba Π0 de M, y se satisfacen todas
las condiciones del teorema.
Ibind M debe contener una prueba Π0 de Xθ → Y θ. Por el lema de refinamiento existe
θ0 w θ tal que Xθ0 = Y θ00 y θ0 = θ[V − {Y }], es decir. Como en OB se prueba que
[Y /X]θ0 w θ.
α
Si l → r ∈ P , entonces M contendrá una prueba de lθ → rθ y por monotonı́a existe
una prueba con la misma longitud y estructura de l[Y /X]θ0 → rθ. Por linealidad
Y 6∈ var(r) y como θ0 ≡ θ[V − {Y }], tenemos que r[Y /X]θ0 = rθ.
α
Si e 3 e0 ∈ R, M contendrá una prueba de eθ3e0 θ y por monotonı́a existe una
prueba de e[Y /X]θ0 3e0 [Y /X]θ0 con la misma longitud y estructura. Para la parte
resuelta y las desigualdades tenemos σθ = σ[Y /X]θ0 y δθ = δ[Y /X]θ0 .
Ası́, M0 es el resultado de eliminar la prueba Π0 de M.
α
Sus Podemos tomar θ0 ≡ θ0 y tenemos M ≡ M0 , pero ahora Card({l → r|α 6= 0}) <
α
Card({l → r|α 6= 0}) ya que se ha suspendido una de las producciones de G.
Nrw M debe contener una prueba Π0 de f (e)θ → c(t)θ que tendrá la forma
Π0 ≡ (4)
Π1 ...Πn ΠC ΠB
f (e) → c(t)
con (f (s) = s <== C) ∈ [R]⊥ Πi prueba de ei θ → si θ, ΠC pruebas de C y ΠB
prueba de s → c(t)θ.
Podemos tomar θ0 ≡ θ y M será el resultado de reemplazar Π0 por Π1 ...P in ΠC , ΠB .
Act1 Análoga a Nrw.
Act2 M debe contener una prueba Π0 de c(e)θ → Xθ. Dicha prueba tiene que utilizar la
regla 3 de GORC6= y debe ser Xθ ≡ c(t). La prueba será:
Π1 ...Πn
c(e)θ → c(t)
4.4. CÁLCULO DE RESOLUCIÓN DE OBJETIVOS
167
con Πi prueba de ei θ → ti , i = 1..n.
Definimos θ0 sobre las variables nuevas (existenciales) Yi θ0 ≡ ti y Zθ0 ≡ Zθ, ∀Z 6∈
{Yi }i=1..n . De este modo θ0 está en las hipótesis del teorema.
Además tenemos la equivalencia: Xθ ≡ c(Y )θ0 ≡ X[X/c(Y )]θ0 , es decir, todas las
apariciones de X en (P, R) son reemplazadas por el mismo término por θ y por
[X/c(Y )]θ0 . Para el resto de variables ocurre lo mismo por definición de θ0 . Ası́, para
P , R y δ tenemos las mismas pruebas con θ y con [X/c(Y )]θ0 .
Por otro lado tenemos c(e)θ0 = c(e)θ, ya que c(e) no contiene ninguna variable de
Y . Además c(Y )θ0 = c(t) = Xθ, luego (c(e) → X)θ es idéntico a (c(e) → c(Y ))θ0 ,
de donde se sigue que el testigo M0 buscado es el el resultado de sustituir en M la
prueba Π0 por Π1 ...Πn .
Eval M debe contener una prueba de la forma
Π0 ≡ (5)
Π1 Π2
f (e)θ3e0 θ
con Π1 prueba de f (e)θ → t, Π2 prueba de t3e0 θ y t ∈ CT erm⊥ .
Podemos tomar Y θ0 ≡ t (Y ∈ evar(G0 )) y Zθ0 ≡ Zθ, ∀Z 6≡ Y . M0 es el resultado de
reemplazar Π0 por Π1 Π2 .
Dcp== En M debe haber una prueba Π0 de la forma
Π0 ≡ (7)
Π1 ...Πn
c(e)θ == c(e0 )θ
con Πi prueba de ei θ == e0i θ. Podemos tomar θ0 ≡ θ y M0 será el resultado de
reemplazar Π0 por Π1 ...Πn en M.
Id En M debe haber una prueba de Xθ == Xθ. Podemos tomar θ0 ≡ θ; M0 es el
resultado de eliminar dicha prueba de M.
Bind En M debe haber una prueba de Xθ == Y θ, luego Xθ = Y θ. Podemos tomar
θ0 ≡ θ. De este modo tenemos que [X/Y ]θ = θ: Xθ = Y θ = X[X/Y ]θ y para todo
Z 6≡ X tenemos Zθ = Z[X/Y ]θ. Por lo tanto sθ = s[X/Y ]θ, ∀s ∈ CT erm⊥ y en M0
aparecen todas las pruebas de M excepto la de Xθ == Y θ. Por otro lado Xθ = Y θ
es una identidad.
Imit== M debe contener una prueba Π0 de Xθ == c(e)θ, luego tiene que ser Xθ ≡ c(t).
Entonces será
Π0 ≡ (7)
Π1 ...Πn
c(e)θ == c(t) ≡ Xθ
con Πi prueba de ei θ == ti .
Podemos definir θ0 como Yi θ0 ≡ ti y Zθ0 ≡ Zθ, ∀Z 6≡ Yi , de modo que cumple
las condiciones del teorema ya que las Yi son todas variables nuevas. Se cumple
θ = [X/c(Y )]θ0 : Xθ = c(t) = c(Y )θ0 = X[X/c(Y )]θ0 y para todo Z 6≡ X tenemos
Zθ = Z[X/c(Y )]θ0 .
Por lo tanto M0 es el resultado de reemplazar Π0 por Π1 ...Πn y el resto de pruebas
queda igual. Además Xθ0 = c(Y )θ0 es una identidad.
¥
CAPÍTULO 4. DE LA SEMÁNTICA
168
Clash En M debe haber una prueba de c(e)θ 6= d(e0 )θ. Podemos tomar θ0 ≡ θ; M0 es el
resultado de eliminar dicha prueba de M.
Store En este caso podemos tomar θ0 ≡ θ y tenemos M = M0 . El número de producciones
no suspendidas permanece invariable, pero el número de desigualdades en la parte
de resolución ha disminuido.
Dcp6= En M debe haber una prueba Π0 de la forma
Π0 ≡ (9)
Π1 ...Πn
c(e)θ 6= c(e0 )θ
siendo Πj prueba de ej θ == e0j θ, j = 1..n. Entre estas pruebas debe haber una Πi
de ei θ == e0i θ. Podemos tomar θ0 ≡ θ y M0 será el resultado de reemplazar Π0 por
Πi en M, con lo que M0 / M.
Imit6= M debe contener una prueba Π0 de Xθ 6= c(e)θ. Esta prueba tiene que utilizar una
de las reglas 8 ó 9 de GORC6= .
a) Si utiliza la regla 8, será Xθ = d(t) con c 6≡ d (d ∈ DC). Entonces la regla Imit6=
permite derivar el objetivo G1 . Definimos θ0 sobre las variables nuevas (existenciales)
Yi θ0 ≡ ti y Zθ0 ≡ Zθ, ∀Z 6∈ {Yi }i=1..n . De este modo θ0 está en las hipótesis del
teorema (nótese que X puede ser o no existencial, pero en cualquier caso Xθ = Xθ0
por definición de θ0 ). Tenemos la equivalencia: Xθ ≡ d(t) ≡ X[X/d(Y )]θ0 , y para el
resto de variables Z de G tenemos Zθ = Zθ0 . Luego para P , X 6= c(e), R y delta)
se tienen las mismas pruebas por aplicación de θ o θ0 , por lo que el testigo M0 es el
resultado de eliminar la prueba Π0 de M, con lo que M0 / M.
Por otro lado, σθ = σ[X/d(Y )]θ0 es un conjunto de identidades y si a σ se le ha
añadido la sustitución X = d(Y ) es porque X 6∈ evar(G), luego Xθ ∈ CT erm por
la definición de solución, y por otro lado, (X = d(Y ))θ0 es equivalente a Xθ = d(t),
que es una identidad (entre términos totales).
b) Si utiliza la regla 9 debe ser Xθ ≡ c(t) y la prueba Π0 tendrá la forma
Π0 ≡ (9)
Πi
c(t) 6= c(e)θ
siendo Πi una prueba de ti 6= ei para algún i = 1..n (c ∈ DC n ). Entonces Imit6=
permite derivar el objetivo G2 . Ahora definimos θ0 como Yi θ0 ≡ ti para el i concreto
que nos proporciona la prueba y Zθ0 ≡ Zθ, ∀Z 6≡ Yi . Como antes tenemos Xθ ≡
X[X/c(Y )]θ0 y el resto de variables son afectadas igual por θ que por θ0 . Luego para
P , X 6= c(e), R y δ) se tienen las mismas pruebas por aplicación de θ o θ0 y el testigo
M0 que buscamos es el resultado de reemplazar Π0 por Πi en M.
Con σ ocurre exactamente lo mismo que antes.
¥
Teorema 2 (Completitud) Sea R un programa, G un objetivo inicial y θ ∈ Sol(G).
Entonces existe una forma resuelta G0 ≡ ∃U .P 22σ2δ y θ0 tal que:
G Ã G0 ,
4.5. ESTRATEGIA DDS
169
θ0 ∈ Sol(G0 ), y
θ = θ0 [var(G)]
¥
Demostración
En virtud del anterior lema de progreso es posible construir una derivación
G ≡ G0 Ã G1 Ã G2 Ã ...
tal que existen θ ≡ θ0 , θ1 , θ2 ... y M = M0 , M1 , M2 , ... tales que θi ∈ Sol(Gi ), θi+1 =
θi [V − (evar(Gi ) ∪ evar(Gi+1 ))], Mi es un testigo de θi y Mi+1 /Mi , para todo i = 1....
Puesto que θ ∈ Sol(G) (el objetivo tiene solución) y / es un orden bien fundado, la
derivación anterior debe ser finita y terminar en n pasos con una forma resuelta G0 ≡
∃U .P 22σ2δ. Además como evar(G) = ∅ y θi = θi+1 [V − (evar(Gi ) ∪ evar(Gi+1 ))], es
fácil ver que θn = θ[var(G)].
¥
4.5.
Estrategia DDS
El cálculo LN C6= que hemos presentado es un marco general para conseguir resultados
de corrección y completitud. Además, imponiendo prioridades sobre las condiciones de los
objetivos, hemos obtenido una aproximación a una implementación real avalada por el
marco teórico. Sin embargo, la evaluación de funciones en este cálculo se llevarı́a a cabo
de forma ingenua en la supuesta implementación.
Nuestro objetivo ahora es conseguir reflejar la Estrategia Guiada por la Demanda, cuyas
ideas fundamentales se estudiaron en la introducción de 3.10. En la implementación real
se utilizan mecanismos de control (en ejecución) para evaluar f.n.c.’s que, en definitiva, son
el fundamento de la estrategia. Ahora, en un marco abstracto, no disponemos de ningún
mecanismo de control. Sin embargo, disponemos del poder expresivo de las desigualdades
y esto bastará.
Obsérvese que para resolver una desigualdad de la forma X 6= c(e), la regla Imit6=
del cálculo LN C6= es capaz de introducir una constructora d 6≡ c con el fin de forzar un
conflicto y evitar la evaluación de ninguna expresión de e. Además, el cálculo puede resolver
desigualdades entre una variable y una f.n.c. en un sólo paso, mediante las reglas Store
y Imit6= . En realidad, la filosofı́a de la pereza está también presente en la resolución de
desigualdades y sugiere la siguiente idea: para conseguir una f.n.c. de una expresión
se puede resolver una desigualdad entre entre dicha expresión y una variable
nueva, de tal modo que las reglas del cálculo reducirán la expresión sólo hasta que se
pueda resolver la desigualdad, es decir, la reducirán a f.n.c..
Esta idea se plasma en una transformación de las reglas de función. Consideremos la
funciones leq y add estudiadas en 3.10.2, que en la notación de este capı́tulo (primer orden)
tendrán la forma (para abreviar, representamos las constructoras de naturales zero y suc
como 0 y s respectivamente):
leq(0, Y )
= true
add(0, Y )
=Y
leq(s(X), 0)
= f alse
add(s(X), Y ) = s(add(X, Y ))
leq(s(X), s(Y )) = leq(X, Y )
De acuerdo con la idea anterior, las reglas de leq se transforman en las siguientes:
CAPÍTULO 4. DE LA SEMÁNTICA
170
leq(A, B)
= leq(A, B) <== A 6= U
leq1 (0, Y )
= true
leq1 (s(X), 0)
= f alse
leq1 (s(X), s(Y )) = leq(X, Y )
Ahora hay sólo una regla para leq. Esta regla computa una f.n.c. para el primer argumento y llama a leq1 que está definida exactamente igual que la antigua leq. Resolvamos
el objetivo leq(add(0, s(0)), add(s(0), s(0))) == B utilizando esta nueva definición de leq:
1
2leq(add(0, s(0)), add(s(0), s(0))) == B22
Eval,Sus
Ã
0
1
∃Z1 .leq(add(0, s(0)), add(s(0), s(0))) → Z1 2Z1 == B
0
Act1 ,Sus,Sus,Sus
Ã
0
2
0
1
∃Z1 , Z2 , U.add(0, s(0)) → Z2 , add(s(0), s(0)) → Z3 , leq1 (Z2 , Z3 ) → Z1 2Z2 6= U, Z1 ==
Act
B22 Ã1
4
4
3
0
2
0
∃Z1 , Z2 , Z3 , Z4 , U ,0 → 0, s(0) → Z4 , Z4 → Z2 , add(s(0), s(0)) → Z3 , leq1 (Z2 , Z3 ) → Z1 2Z2 6=
1
U, Z1 == B22
Dcp→ ,Sus
Ã
0
3
0
2
0
∃Z1 , Z2 , Z3 , Z4 , U.s(0) → Z4 , Z4 → Z2 , add(s(0), s(0)) → Z3 , leq1 (Z2 , Z3 ) → Z1 2Z2 6=
1
Ibind
U, Z1 == B22 Ã
0
0
0
2
1
∃Z1 , Z3 , Z4 , U.s(0) → Z4 , add(s(0), s(0)) → Z3 , leq1 (Z4 , Z3 ) → Z1 2Z4 6= U, Z1 == B22
0
0
0
Act2 ,Sus
1
∃Z1 , Z3 , U, Z5 ,0 → Z5 , add(s(0), s(0)) → Z3 , leq1 (s(Z5 ), Z3 ) → Z1 2Z1 == B22 Ã
....
Ahora, antes de aplicar cualquier regla de leq1 (la antigua leq) se ha evaluado la f.n.c.
s(Z5 ) para el primer argumento. Después, la primera regla de leq1 fallará automáticamente
y los pasos de cómputo que se hicieron sobre add(0, s(0)) no se han perdido, sino que se
reutilizan para probar otras reglas de leq1 .
Hemos conseguido nuestro propósito, pero también hemos introducido un efecto no
deseado: tenemos que resolver desigualdades que no aparecı́an en el programa inicial. Esto,
en general, puede suponer una sobrecarga en el almacén de restricciones δ y, de hecho,
no estamos realmente interesados en resolver tales desigualdades; sólo las utilizamos para
calcular f.n.c.’s. Pero este problema puede ser resuelto fácilmente introduciendo una nueva
regla en el cálculo LN C6= :
m
Elm6= ∃Y, U .P 2e 6= Y, R2σ2δ Ã ∃U .P 2R2σ2δ
si Y no aparece en ningún otro sitio en G y (e ≡ c(e) o e ≡ X 6∈ suspvar(G)).
El cometido de esta regla es deshacerse de este tipo de desigualdades introducidas con
el único objeto de evaluar f.n.c.’s. La corrección se preserva trivialmente: la variable Y es
existencial y si el objetivo derivado tiene una solución θ se puede construir una solución
para el objetivo original dándole una valor apropiado a Y . La completitud tampoco se ve
alterada, ya que esta derivación elimina una restricción y la misma solución del objetivo
original es también solución del derivado.
Una vez explorada la idea general de la transformación, quedan dos cuestiones pendientes:
Debemos dar una definición formal de las transformaciones y demostrar que un programa y su transformado son equivalentes, es decir, que la transformación preserva
la semántica.
Ã
4.5. ESTRATEGIA DDS
171
El ejemplo que hemos utilizado muestra cómo hacer la transformación considerando
el primer argumento de leq, pero es fácil ver que en el último paso de la derivación
el problema de la reevaluación persiste para las dos últimas reglas de leq1 (segundo
argumento). Necesitamos un algoritmo que lleve a cabo una transformación completa
sobre las funciones y que capture la Estrategia Guiada por la Demanda en toda su
extensión (tal y como se presentó en 3.10).
Los dos apartados siguientes resuelven ambas cuestiones.
4.5.1.
Transformación de programas
En esta sección utilizaremos la noción de posición en un término que vimos en 3.10.1,
ası́ como las de posición demandada y uniformemente demandada. Además necesitaremos
el orden de subsumción (≤) sobre CT erm⊥ definido como:
s ≤ t sii ∃θ ∈ CSubst⊥ tal que sθ ≡ t
Definicion 10 (Transformación de conjuntos de reglas) Sea R un programa sobre
Σ, f una función y
S ⊆ Rf un subconjunto de reglas de f de la forma
f (t1 ) = r1 <== C1
···
f (tn ) = rn <== Cn
f (s) un patrón compatible con S, i.e., un c-término lineal que satisface que
f (s) ≤ f (ti ), para todo i = 1..n.
u ∈ V P (f (s)) una posición uniformemente demandada por S.
El conjunto transformado de S usando f (s) y u se define sobre la signatura Σ ∪ {fu }
como S T = {R} ∪ Su , siendo
R ≡ f (s) = fu (s) <== X 6= U
donde X es la variable que tiene s en la posición u y U es una variable nueva.
Su ≡ { fu (t1 ) = r1 <== C1
...
fu (tn ) = rn <== Cn }
¥
La transformación para la función leq se puede hacer tomando el patrón leq(A, B) y
la posición 1. Pero también podemos tomar un subconjunto S de reglas de leq que sólo
contenga las dos últimas reglas de leq, considerando el patrón leq(s(A), B) y la posición
2, con lo que obtendremos las reglas:
leq(0, Y )
= true
(the first rule does not change)
leq(s(A), B)
= leq2 (s(A), B) <== B 6= U
leq2 (s(A), 0)
= f alse
leq2 (s(A), s(B)) = leq(A, B)
CAPÍTULO 4. DE LA SEMÁNTICA
172
La transformación de un conjunto de reglas S de un programa R produce un nuevo
conjunto de reglas S T . Podemos definir un nuevo programa R = (R − S) ∪ S T , en el que se
han reemplazado las reglas de S por otras nuevas. Nuestro objetivo ahora es demostrar que
ambos programas son semánticamente equivalentes, es decir, las pruebas que se pueden
hacer con R también se pueden hacer con R0 . Y al contrario, pero con una salvedad
razonable: en R0 podremos probar aproximaciones de la forma fu (e) → t que no tienen
sentido en R porque no existe el sı́mbolo fu en su signatura asociada.
Para probar este resultado necesitamos una hipótesis adicional: la signatura debe contener al menos dos sı́mbolos distintos de constructora. Desde el punto de vista semántico
esta condición garantiza que las desigualdades introducidas por la transformación pueden
ser probadas utilizando la regla (8) del cálculo GORC6= (conflicto de constructoras), tomando una c-instancia apropiada de la regla R producida por la transformación. Pero en
la práctica, esta condición no es necesaria porque esas desigualdades no se van a resolver
en realidad, sino que serán eliminadas por la nueva regla Elm6= que hemos introducido.
Lema 7 (Equivalencia Semántica por Transformaciones) Sea Σ una signatura en
la que al menos hay dos sı́mbolos de constructora, R un programa sobre Σ, f una función
de R y S ⊆ Rf ; si R0 = (R − S) ∪ S T se tiene:
a) R ` e → t sii R0 ` e → t
b) R ` e3e0 sii R0 ` e3e0
siendo t ∈ CT ermΣ⊥ , 3 ∈ {==, 6=} y e, e0 ∈ T ermΣ⊥ .
¥
Demostración
Razonemos las dos implicaciones de la parte a).
(⇒) Suponemos R ` e → t y razonamos por inducción sobre la longitud l de la prueba:
l = 0. La única prueba posible es R ` e → ⊥ por la regla (1) de GORC6= y es obvio
que R0 ` e → ⊥.
l + 1. Supongamos que R ` e → t es probable en l + 1 pasos. Entonces t 6≡ ⊥ y debe
ser aplicable alguna de las reglas (2) (3) ó (4) del cálculo GORC6= .
Si se puede aplicar (2) ó (3) es claro que se puede replicar el paso de prueba utilizando
el programa R0 , ya que estas dos reglas no utilizan ninguna regla de [R]⊥ . La prueba
de partida se reduce mediante esta regla a otras de la forma R ` P1 , ..., Pk , donde
cada Pi es de longitud ≤ l + 1. Por hipótesis de inducción tenemos R0 ` P1 , ..., Pk y
con lo que se puede reconstruir la prueba R0 ` e → t.
Si se puede aplicar (4) debe ser e ≡ f (e). Supongamos por tanto, que R ` f (e) → t
es probable en l + 1 pasos. Entonces debe existir una regla en R
Q ≡ f (u) = u <== C
y µ ∈ CSubst⊥ tal que la regla (4) de GORC6= sea aplicable utilizando la c-instancia
Qµ y reducir la prueba a R ` e → uµ, Cµ, uµ → t.
Todas estas pruebas son de longitud ≤ l + 1 y por hipótesis de inducción tenemos
R0 ` e → uµ, Cµ, uµ → t
(ii)
4.5. ESTRATEGIA DDS
173
Si la regla Q 6∈ S, entonces en R0 también existe esa misma regla Q para f y
podemos tomar la misma c-instancia Qµ. La prueba R0 ` f (e) → t se puede reducir
a R0 ` e → uµ, Cµ, uµ → t, que es probable por (ii).
El caso más interesante es cuando la regla Q ∈ S. Observemos que la nueva regla R
para f se definió como:
R ≡ f (s1 , ..., sn ) = fp (s1 , ..., sn ) <== X 6= U
de modo que s ≤ r para toda regla (f (r) = r <== C) ∈ S, y en particular para
Q, de donde se sigue que existe θ ∈ CSubst⊥ tal que sθ = u. Como X en R ocupa
una posición demandada por una constructora c, Xθ forzosamente tiene que ser de
la forma Xθ = c(a).
Por otro lado como la variable U no aparece en s y por hipótesis nuestra signatura
cuenta al menos con dos sı́mbolos de constructora, existe d ∈ DC, d 6≡ c y se puede
definir θ0 ∈ CSubst⊥ como U θ0 ≡ d(b) y V θ0 ≡ V θ para toda V 6≡ U . De este modo
en [R]⊥ tendremos la c-instancia:
Rθ0 µ ≡ f (u)µ = fp (u)µ <== c(a)µ 6= d(b)µ
Utilizando esta c-instancia, la prueba de R0 ` f (e) → t se reduce por la regla (4) de
GORC6= a
(iii)
R0 ` e → uµ, fp (uµ) → t, c(a)µ 6= d(b)µ
| {z } | {z } |
{z
}
(I)
(II)
(III)
Razonemos la existencia de pruebas para (I), (II) y (III).
(I): por (ii).
(II): R ` f (uµ) → t es reducible, utilizando Qµ, a R ` uµ → uµ, Cµ, uµ → t.
Como Q ∈ S, en R0 existe una regla Q0 ≡ fp (u) = u <== C y se puede utilizar la
c-instancia Q0 µ para reducir la prueba a R ` uµ → uµ, Cµ, uµ → t, cuyas pruebas
son todas de longitud < l + 1, y por hipótesis de inducción serán todas probables.
(III): la desigualdad c(a) 6= d(b) es claramente probable por la regla (8) de GORC6= .
(⇐) . Suponemos R0 ` e → t. Como antes procedemos por inducción sobre el número de
pasos l de la prueba:
l = 0. Tiene que ser t ≡ ⊥ y es claro que R ` e → ⊥ es probable por la regla (1) de
GORC6= .
l + 1. Debe ser t 6≡ ⊥ y tiene que ser aplicable una de las reglas (2), (3) ó (4) de
GORC6= . El razonamiento para (2) y (3) es el mismo que en la otra implicación.
Para (4), supongamos que R0 ` f (e) → t es probable en l + 1 pasos utilizando la
regla (4) de GORC6= , de donde se sigue que debe existir una c-instancia de una regla
Q de f que permita aplicar la regla (4).
Si
Q ≡ f (u) = u <== C
CAPÍTULO 4. DE LA SEMÁNTICA
174
no es la regla nueva que introduce la transformación para la función f , entonces
Q ∈ R. La prueba R0 ` f (e) → t es reducible utilizando una c-instancia Qµ a
R0 ` e → uµ, Cµ, uµ → t, en la que cada prueba tiene longitud ≤ l + 1. La misma
c-instancia Qµ permite aplicar la regla (4) de GORC6= y reducir R ` f (e) → t a
R ` e → uµ, Cµ, uµ → t, que son probables por hipótesis de inducción.
La otra posibilidad es que Q sea la nueva regla que introduce la transformación para
f , es decir,
Q ≡ f (u) = fp (u) <== U 6= X
Entonces la prueba de R0 ` f (e) → t utiliza una c-instancia Qµ y es reducible a
R0 ` e → uµ, Xµ 6= U µ, fp (uµ) → t cuyas pruebas tienen todas longitud ≤ l + 1.
Por hipótesis de inducción tenemos, en particular
R ` e → uµ
(iv)
A su vez, la prueba de R0 ` fp (uµ) → t utilizará una c-instancia Q0 η, siendo
Q0 ≡ fp (v) = v <== C
una regla de R0 , y será reducible a R0 ` uµ → vη, Cη, vη → t, cuyas pruebas tienen
todas longitud ≤ l + 1, porque R0 ` fp (uµ) → t ya tenı́a menos de l + 1 pasos. Por
hipótesis de inducción tenemos
R ` uµ → vη , Cη , vη → t
| {z } |{z} | {z }
(I)
(II)
(v)
(III)
De (iv) y la parte (I) de (v), por transitividad de → se deduce
R ` e → vη
(vi)
Uniendo los resultados (vi) y las partes (II) y (III) de (v) tenemos
R ` e → vη, Cη, vη → t
(vii)
En R existe una regla para f idéntica a Q0 , excepto en el nombre de función que en
R0 es fp , de la que podemos tomar una c-instancia por medio de η y reconstruir una
prueba para R ` f (e) → t utilizando (vii) y la regla (4) de GORC6=
La parte b) es una consecuencia inmediata de a): los pasos de ambas pruebas
R ` e3e0 y R0 ` e3e0 son idénticos, excepto aquellos en los que se prueban aproximaciones de la forma f (e) → t, y i) garantiza que se pueden probar exactamente las
mismas aproximaciones de este tipo usando las reglas de uno u otro programa R o
R0 . En general estas pruebas no tendrán la misma longitud ni estructura.
¥
4.5. ESTRATEGIA DDS
4.5.2.
175
Algoritmo de transformación
En esta sección se muestra un algoritmo de transformación de funciones que trabaja de
forma similar al que presentamos en 3.10.2 para la construcción de arboles definicionales.
Para transformar programas completos, únicamente habrá que aplicar este algoritmo a
cada función del programa. Llamaremos trans a la función de transformación que toma
un conjunto de reglas S y un patrón f (s) compatible con S, y devuelve el conjunto
transformado de reglas de S.
La llamada inicial será de la forma trans(Rf , f (X)) y una llamada genérica será trans(S, f (s)).
Algoritmo:
Si S es un conjunto unitario o vacı́o devolver S.
En otro caso, aplicar una de las siguientes alternativas (sólo una es aplicable):
Alguna posición de V P (f (s)) es uniformemente demandada en S.
Sea u la menor en el orden lexicográfico de esas posiciones.
Sea S T = {R} ∪ Su el conjunto transformado de S usando f (s) y u.
Sean c1 , ..., cn las constructoras que ocupan la posición u en los lados izquierdos de
las reglas de S. Sobre Su hacemos la siguiente partición:
sea Su1 el conjunto de reglas de Su que demandan c1 en la posición u.
...
sea Sun el conjunto de reglas de Su que demandan cn en la posición u.
Sea X la variable de la posición u en f (s). Para cada constructora ci construimos el
patrón
pi = fu (s)[X/ci (Y1 , ..., Ym )]
donde Y1 , ..., Ym son variables nuevas.
Devolver: {R} ∪ trans(Su1 , p1 ) ∪ ... ∪ trans(Sun , pn )
Alguna posición de V P (f (s)) es demandada, pero ninguna es uniformemente demandada.
Sean u1 , ..., uk las posiciones demandadas. Hacemos una partición del conjunto de
reglas S del siguiente modo:
Sea Su1 el conjunto de reglas de Rf que demandan la posición u1 , Q1 = S − Su1 .
Sea Su2 el conjunto de reglas de Q1 que demandan la posición u2 , Q2 = Q1 − Su2 .
...
Sea Suk el conjunto de reglas de Qk−1 que demandan la posición uk .
Y sea S0 el conjunto de reglas de S que no demandan ninguna posición.
Devolver: S0 ∪ trans(Su1 , f (s)) ∪ ... ∪ trans(Sun , f (s))
Ninguna posición de V P (f (s)) es demandada.
En este caso sencillamente devolver: S
¥
Utilizando este algoritmo, el conjunto de reglas transformadas para la función leq es:
leq(A, B)
= leq1 (A, B) <== A 6= U
leq1 (0, B)
leq1 (s(A), B)
= true
= leq12 (s(A), B) <== B 6= U
leq12 (s(A), 0)
= f alse
leq12 (s(A), s(B)) = leq(A, B)
176
CAPÍTULO 4. DE LA SEMÁNTICA
Lema 8 (Corrección del algoritmo de Transformación) Sea Σ una signatura en la
que al menos hay dos sı́mbolos de constructora, R un programa sobre Σ y f una función
de R. Entonces:
a) El algoritmo de transformación es terminante con la llamada trans(Rf , f (X)).
b) El programa R0 = (R − Rf ) ∪ trans(Rf , f (X)) es semánticamente equivalente a R
en el sentido del lema de equivalencia semántica (7).
Demostración
La demostración que proponemos realmente prueba un resultado más general. En el
enunciado la llamada inicial es trans(Rf , f (X)), pero el algoritmo funciona exactamente
igual con una llamada de la forma trans(Rf , f (s)) siendo f (s) ≤ f (t) para toda (f (t) =
t <== C) ∈ Rf , que es un caso más general. Probaremos el resultado considerando una
llamada inicial de esta forma.
Para probar la parte a) lo primero es probar que cuando el algoritmo solicita un
conjunto transformado S T tal conjunto es calculable de acuerdo con la definición de
conjunto transformado. Para ello utilizaremos el invariante siguiente: todas las llamadas
trans(V 0 , f (v)) que genera el algoritmo cumplen la condición f (v) ≤ f (t) para toda regla
(f (t) = t <== C) ∈ V. Veamos que efectivamente esta condición es invariante:
En la primera llamada trans(Rf , f (v)) esta condición es cierta por hipótesis. Supongamos que después de m llamadas recursivas esta condición es cierta para trans(V, f (v)).
Si el algoritmo aplica la primera alternativa es porque en V P (f (v)) hay alguna posición
uniformemente demandada por las reglas de V y entonces se puede construir el conjunto
V T usando f (v) y la menor de las posiciones uniformemente demandada u. Por
construcción, los conjuntos Vui de la partición que genera el algoritmo cumplen la condición
de que si W ∈ Vui entonces W tiene la constructora ci en la posición u. Por otro lado el
patrón pi sólo difiere de p en que tiene ci en la posición u, de donde se sigue que pi ≤ f (t)
para toda (f (t) = t <== C) ∈ Vui y por tanto las llamadas trans(Vui , pi ) verifican el
invariante.
Si se aplica la segunda alternativa es obvio que se mantiene el invariante porque las
llamadas que se generan son de la forma trans(Vui , f (v)) donde Vui ⊆ V y el patrón no
cambia. Y si se aplica la tercera alternativa, no se generan llamadas recursivas.
Con esto hemos probado que el algoritmo “no se bloquea”. Para probar que termina
tenemos que demostrar que las llamadas que produce recursivamente van disminuyendo en
complejidad. Para ello definimos la complejidad de una llamada trans(V, f (v) como
el par
(|V|, N CP (lhs(V)) − N CP (f (v))
siendo |V| el cardinal del conjunto V, N CP (lhs(V)) el número de posiciones de constructora en los lados izquierdos de las reglas de V y N CP (f (v)) el número de posiciones de
constructora en f (v). El orden sobre las complejidades es el orden lexicográfico usual.
Ahora debemos probar que esta complejidad disminuye en cada llamada.
En las llamadas que genera la primera alternativa el número de constructoras de los
patrones pi se ha incrementado en 1 (se añade la constructora ci ), luego el segundo elemento del par de complejidad disminuye. El primer elemento puede no modificarse (si
todas las reglas demandan la misma constructora) pero en ningún caso se incrementa.
4.5. ESTRATEGIA DDS
177
En las llamadas que genera la segunda alternativa, puesto que no hay ninguna posición
uniformemente demandada, la partición debe producir más de un conjunto por lo que el
cardinal de los conjuntos que se transforman es estrictamente menor que el del conjunto
de llamada, por lo que los primeros elementos del par disminuyen. Y la tercera alternativa
no produce llamadas.
Para la parte b) probaremos el siguiente resultado (más general): En las hipótesis del
enunciado, dado S ⊆ Rf y f (s) tal que f (s) ≤ f (t) para toda (f (t) = t <== C) ∈ S
R y (R − S) ∪ trans(S, f (s))
son semánticamente equivalentes.
Razonamos por inducción sobre el número de pasos l (número de llamadas a trans)
que necesita el algoritmo para terminar:
l = 0 Entonces el algoritmo ha utilizado la tercera alternativa que no modifica el programa y por tanto la semántica se preserva.
l+1
Si el algoritmo utiliza la primera alternativa, podemos definir R0 ≡ (R − S) ∪ S T que
sabemos es semánticamente equivalente a R por el lema de equivalencia semántica. La
transformación que hace el algoritmo sobre cada uno de los conjuntos de la partición de
S debe tener menos de l + 1 pasos, por lo que tenemos la secuencia de equivalencias
S
R0 ≡ (R − S) ∪ {R} ∪ ( Sui ),
por h.i. es equivalente a
R00 ≡ (R0 − Su1 ) ∪ trans(Su1 , p1 ),
por h.i. es equivalente a
R000 ≡ (R00 − Su2 ) ∪ trans(Su2 , p2 ),
por h.i. es equivalente a
...
Rk ≡ (Rk−1 − Suk−1 ) ∪ trans(Suk−1 , pk−1 )
por h.i. es equivalente a
Rk+1 ≡ (Rk − Suk ) ∪ trans(Suk , pk )
Y ahora, deshaciendo la secuencia tenemos
Rk+1
≡ (Rk − Suk ) ∪ trans(Suk , pk ),
sustituyendo Rk
≡ (Rk−1 − (Suk−1 ∪ Suk )) ∪ trans(Suk , pk ) ∪ trans(Suk−1 , pk−1 ),
sustituyendo Rk−1
...
≡ (R − S) ∪ {R} ∪ trans(Su1 , p1 ) ∪ ...trans(Suk , pk )
≡ (R − S) ∪ trans(S, f (s))
Luego Rk+1 ≡ (R − S) ∪ trans(S, f (s)) es equivalente a R0 que a su vez es equivalente
a R.
Si el algoritmo aplica la segunda alternativa podemos hacer un razonamiento similar
aplicando la hipótesis de inducción sobre la transformación que se hace sobre cada uno de
los conjuntos de la partición y obtenemos la secuencia de equivalencias:
CAPÍTULO 4. DE LA SEMÁNTICA
178
R ≡ (R − S0 ) ∪ S0
(≡ R)
por h.i. es equivalente a
R0 ≡ (R − Su1 ) ∪ trans(Su1 , f (s))
por h.i. es equivalente a
R00 ≡ (R0 − Su2 ) ∪ trans(Su2 , f (s))
por h.i. es equivalente a
...
Rk−1 ≡ (Rk−2 − Suk−1 ) ∪ trans(Suk−1 , f (s))
por h.i. es equivalente a
Rk ≡ (Rk−1 − Suk ) ∪ trans(Suk , f (s))
Y haciendo las sustituciones como antes obtenemos
Rk ≡ (Rk−1 − Suk ) ∪ trans(Suk , f (s))
≡ (Rk−2 − (Suk ∪ Suk−1 )) ∪ trans(Suk−1 , f (s)) ∪ trans(Suk , f (s))
...
≡ (R − (Su1 ∪ ... ∪ Suk )) ∪ trans(Su1 , f (s)) ∪ ... ∪ trans(Suk , f (s))
≡ (R − S) ∪ S0 ∪ trans(Su1 , f (s)) ∪ ... ∪ trans(Suk , f (s))
≡ (R − S) ∪ trans(S, f (s))
Luego Rk ≡ (R − S) ∪ trans(S, f (s)) es equivalente semánticamente a R.
¥
Capı́tulo 5
Conclusiones y trabajo futuro
El lenguaje T OY tiene muchos puntos en común con los paradigmas en los que se ha
inspirado y las principales virtudes de la programación funcional y lógica están presentes
en él. Pero además, aporta elementos que no aparecen en los otros dos estilos, como son
las variables lógicas de orden superior o las funciones indeterministas.
Las restricciones sobre reales, encajan perfectamente en el mecanismo de ejecución
del sistema y su introducción es sencilla, salvo por problemas esencialmente técnicos. La
programación lógico funcional con este tipo de restricciones ha resultado ser un contexto
muy rico desde el punto de vista expresivo, ya que conjuga la potencia de las funciones
con la de las restricciones. Esto no puede hacerse en Prolog porque no hay funciones y en
otros lenguajes funcionales como Haskell no se permiten estas restricciones.
Es importante destacar que T OY es un lenguaje declarativamente puro, en el sentido
de que todo programa T OY se ajusta al marco semántico desarrollado en la sección 4.31 .
En el lenguaje no existen recursos ajenos a la semántica. Es un hecho bien conocido (y
desafortunado) que tal situación no se da en Prolog, que dispone de un amplio repertorio
de recursos no ajustados a la semántica habitual de los programas lógicos ([Apt90]). Tal
es el caso de los predicados metalógicos (descomposición de términos, reconocimiento de
variables, etc), el corte o los predicados assert y retract de modificación dinámica del
programa. En T OY no existen tales predicados (o funciones) y en realidad, no son necesarios. De hecho, T OY es probablemente más rico que Prolog desde el punto de vista
expresivo, aún en ausencia de tales predicados, debido a las funciones (véase 2.6.2).
La construcción de este sistema, como es natural, ha planteado problemas técnicos que
han incrementado nuestro conocimiento acerca de las implementaciones y que, en general,
se han resuelto apropiadamente. Pero también ha suscitado problemas de carácter más
teórico que deben tenerse en cuenta en el diseño de los cálculos formales y sus pruebas
de corrección y completitud. Tal es el caso de las funciones indeterministas. No obstante,
algunos de estos problemas como el las variables de orden superior y las respuestas mal
tipadas, aún siguen abiertos.
El sistema también ha ayudado a diseñar el cálculo operacional que presentamos en
el último capı́tulo. En muchos casos, durante la construcción de dicho cálculo nos hemos
preguntado: “¿cómo lo hace T OY?”. Esta pregunta es natural desde el momento en que
pretendı́amos reflejar algunos aspectos operacionales del sistema, pero también es cierto
que se tenı́a la confianza suficiente para saber que la implementación realmente estaba
haciendo lo correcto.
1
Suponiéndolo ampliado a orden superior en la lı́nea de [G94] y conteniendo tipos en la lı́nea de [AR97b]
179
180
CAPÍTULO 5. CONCLUSIONES Y TRABAJO FUTURO
A modo de autocrı́tica, debemos añadir que el sistema no incluye ningún tipo de
depurador y en algunos casos no resulta sencillo explicar la secuencia de cómputos que se
realiza, incluso conociendo la traducción en profundidad. También es cierto que este tipo
de cómputos normalmente no es trivial.
El primer capı́tulo de este trabajo puede servir no sólo como manual de usuario del
sistema T OY, sino también como introducción al paradigma lógico funcional con restricciones. Pero las aportaciones principales de este documento residen en:
La descripción detallada y exhaustiva del mecanismo operacional de T OY (capı́tulo
2). Esta descripción posiblemente sirva de base para el desarrollo de futuras versiones
y mejoras en el sistema.
El desarrollo de un marco teórico que, aunque incompleto con respecto a la implementación, recoge muchas de las caracterı́sticas esenciales del lenguaje. En particular,
se ha dado una justificación formal de las desigualdades y de Estrategia Guiada por
la Demanda que utiliza el sistema. Previsiblemente este marco servirá como fundamento para fomalizar algunas optimizaciones de la implementación real, ası́ como
para construir nuevos marcos semánticos.
Como trabajo futuro, sobre la implementación actual se pueden incluir nuevas posibilidades como las construcciones where y let de los lenguajes funcionales, operaciones de
entrada/salida, nuevas restricciones (dominios finitos) y también se pueden realizar más
optimizaciones de código.
Uno de nuestros intereses fundamentales es el estudio de la negación en el contexto
lógico funcional, sobre el que hay algunos trabajos relacionados ([M94, M96]). En programación lógica éste es un tema ampliamente estudiado ([AB94, Kun87]). Prolog implementa
la negación como fallo finito ([Cla78]), es decir, la negación de un objetivo tiene éxito si
el objetivo (sin negar) tiene un árbol de búsqueda finito y en el que todas las ramas son
fallidas. Este tipo de negación sólo es completo para objetivos cerrados (sin variables). Para objetivos con variables también se han realizado investigaciones como [Gin91], pero en
nuestro marco las aproximaciones que más nos interesan son las de negación constructiva
([Cha88, Stu91]) y algunas basadas en transformación de programas ([BMP+ 90]).
Por otro lado nuestro lenguaje incluye funciones, pero no tiene sentido (en principio,
al menos) negar una función. Para las funciones el enfoque apropiado parece el de analizar
aquellos valores del dominio para los cuales la función no está definida. Este conjunto de
valores no es computable en general, pero sı́ es posible construir aproximaciones del mismo
que, posiblemente aporten resultados interesantes (tanto teóricos como prácticos).
Apéndice A
Gramática de T OY .
Este apéndice contiene las reglas básicas de la gramática del lenguaje T OY. Adoptaremos las siguientes convenciones de notación:
1. Los sı́mbolos no terminales aparecen en tipografı́a standard.
2. Los terminales (reservados) aparecen encerrados en cajas .
3. Las barras verticales (0 |0 ) se utilizan para separar diferentes alternativas.
4. Los items entre ‘[’ y ‘]’ son opcionales.
5. La notación (item)∗ representa cero o más apariciones item.
6. La notación (item)+ representa una o más ocurrencias de item.
7. Los Tokens aparecerán en cursiva.
Dentro de los Tokens distinguimos las siguientes clases:
• consym,funsym: Identificadores que comienzan por una letra minúscula seguida de
cualquier número de letras, dı́gitos, apóstrofe (0 ) o subrayado ( ). Por ejemplo, f,
reverse o nat son nombres válidos de constructora y función. Hay algunas palabras
reservadas del lenguaje, que no se pueden utilizar como identificadores. Son:
data,
if,
in,
type,
then,
subtype,
infixl,
else,
int,
infixr,
include,
real,
infix,
where,
char,
primitive,
let,
bool
Los identificadores let, where, in y subtype corresponden a construcciones no implementadas aún, pero están reservadas para futuras versiones del sistema.
181
APÉNDICE A. GRAMÁTICA DE T OY .
182
• varsym: Los identificadores que comienzan por una letra mayúscula seguida de cualquier número de letras, dı́gitos, apóstrofe (0 ) o subrayado ( ). Se admiten variables
anónimas, que deberán comenzar con un subrayado ( ) (recuerdese que dos variables
anómimas con el mismo nombre son variables distintas). Por ejemplo,
I don0 t Know W hat T o Do, X, N othing, , anything
son nombres de variable válidos, pero
M r.M r , 9Days, waiting f or you
no lo son.
• funopsym: Los operadores infijos se escriben utilizando uno o más de los siguientes
sı́mbolos:
! #
&
∗
\
+
− .
< = > ? @ ˆ
|
Los sı́mbolos
%
$
:
también se admiten, pero no en la primera posición del nombre. Los nombres de
operador
<−
::
=
..
:
∗
+
− /
<==
/∗ ˆ
: − −>
son reservados.
• conopsym: Los peradores infijos de constructora siguen las mismas reglas que funopsym, pero deben comenzar siempre con el sı́mbolo : .
Se admiten dos tipos de comentarios:
• de lı́nea, que comienzan con el carácter % y terminan con la lı́nea.
• delimitados, que comienzan con el sı́mbolo \∗ y terminan con ∗\ , ambos reservados. Los comentarios delimitados se pueden anidar y son útiles, por ejemplo, cuando
se quiere eliminar una función del programa sin borrar su código.
183
GRAMÁTICA BÁSICA
program
topdecl
( { topdecl } )+
−→
−→
|
|
|
|
|
prims
prim
decls
decl
−→
−→
−→
−→
|
|
operdecl
data typeLhs = constrs
type typeLhs = type
−→
datatype declaration
type alias declaration
operdecl
primitive prims :: type
decls
include string
prim ( , prim )∗
fun
decl ( ; decl)∗
priority declaration
primitive declaration
value declarations
external files
multiple bindings
primitive binding
multiple declarations
typedecl
funrule
clause
function type declaration
function rule
Prolog clause
precedence and grouping
no grouping
left grouping
right grouping
operator list
−→
|
|
oplist
top-level declarations
infix integer oplist
infixl integer oplist
infixr integer oplist
op ( , op )∗
EXPRESIONES DE TIPO
typeLhs
constrs
constr
type
ctype
atype
typesym ( varsym)∗
−→
−→
−→
constr ( | constr
|
type conop type
con ( atype )∗
ctype ( − > ctype )∗
|
|
typesym ( atype )∗
atype
|
|
varsym
( )
−→
−→
−→
|
|
typedecl
)∗
−→
[ type ]
( type ( , type)∗ )
fun ( , fun)∗ :: type
after data or type
multiple constructors
data declaration right-hand side
infix constructor
prefix constructor
function type
type
datatype
simple type
type variable
unit type
list type
tuple type
function’s type
APÉNDICE A. GRAMÁTICA DE T OY .
184
REGLAS DE FUNCIÓN Y CLÁUSULAS
funrule
ruleLhs
−→
−→
conditionrule
condition
clause
−→
−→
−→
|
ruleLhs = exp [conditionrule] function rule
function rule left-hand side
pat funop pat
rule for infix operator
∗
fun ( apat )
<== condition
exp ( , exp)∗
conditional expresions
ruleLhs : − condition
Prolog Clause ( : − mandatory )
NOMBRES
fun
con
op
funop
conop
−→
function name
|
funsym
( funopsym )
|
consym
( conopsym )
|
funop
conop
|
funopsym
0 funsym
|
conopsym
0 consym
−→
−→
−→
0
−→
0
function operator
constructor name
infix constructor operator
infix operators
infix function
infix constructor
infix function
binary operator
function as a binary operator
infix constructor
binary constructor
constructor as a infix constructor
185
EXPRESIONES
exp
−→
|
|
opExp
−→
|
pfxExp
appExp
atomic
−→
−→
−→
opExp op opExp
pfxExp
[ − ] appExp
( atomic )+
|
|
|
|
|
|
|
varsym
fun
con
integer
real
()
exp
( atomic op )
|
( op atomic )
|
|
list
if exp then exp else exp
if exp then exp
opExp
expression
if then else expression
if then expression
operator expresion
( exp ( , exp
list
infix operator or constructor
prefix expression
prefix expression
function application
atomic expression
variable
function name
constructor name
integer number
real number
unit
parenthesised expression
left-section
right-section
)∗
)
[ [ exp ( , exp )∗ ] ]
tuple
list
list
enumerated list
[ exp | list ]
Prolog list
−→
|
APÉNDICE A. GRAMÁTICA DE T OY .
186
PATRONES
pat
apat
−→
|
pat conop pat
( apat )+
|
|
|
|
|
|
varsym
con
fun
integer
real
()
( pat op )
|
( op pat )
−→
|
|
listapat
patterns
constructor operator
application
application pattern
variable
constructor
function
integer number
real number
unit
left-section
( pat ( , exp
listapat
right-section
)∗
)
[ [ apat ( , apat )∗ ] ]
tuple
list
list of patterns
enumerated list
[ apat | listapat ]
Prolog list
−→
|
Apéndice B
Declaración de primitivas (archivo
basic.toy)
/*** THIS FILE DEFINES THE PREDEFINED FUNCTIONS AND TYPES OF THE SYSTEM ***/
data bool = true | false
% primitive functions source code can be found in ’primitives.pl’ where
% its actual type is also declared.
% Type definitons in this file are just informative
% unary integer and real functions
primitive uminus, % unary minus operator
abs
% absolute value
:: real -> real
% unary real functions
primitive sqrt,
ln,exp, % natural logarithm and exponential
sin,cos,tan,cot,
asin,acos,atan,acot,
sinh,cosh,tanh,coth,
asinh,acosh,atanh,acoth
:: real -> real
% binary arithmetic operators and functions for reals and integers
primitive (+),(-),(*),min,max :: real -> real -> real
% binary real functions
primitive (/),
(**),log
% Exponentiation and logarithm
:: real -> real -> real
187
188
APÉNDICE B. DECLARACIÓN DE PRIMITIVAS (ARCHIVO BASIC.TOY)
% integer powers
primitive (^) :: real -> int -> real
% binary integer functions
primitive div, mod, gcd :: int -> int -> int
% rounding and truncating functions
primitive round,trunc,floor,ceiling :: real -> int
% integer to real conversion
primitive toReal
:: int -> real
% relational operators
primitive (<),(<=),(>),(>=) :: real -> real -> bool
% equality and disequality functions
primitive (==),(/=) :: A -> A -> bool
% infix operator precedences
infix 90 ^,**
infix 80 /
infixl 80 *
infixl 70 +,infix
infix
50 < ,<=,>,>=
20 ==, /=
% ’if_then_else’ and ’if_then’ functions are equivalent to the ’sugar syntax’
% functions if .. then .. else and if .. then, but useful as partial functions
if_then_else :: bool -> A -> A -> A
if_then_else true X Y = X
if_then_else false X Y = Y
if_then :: bool -> A -> A
if_then true X = X
% ’flip’ function is necessary for syntax sections management .
flip :: (A -> B -> C) -> B -> A -> C
flip F X Y = F Y X
Apéndice C
Funciones de uso común (archivo
misc.toy)
% misc.toy: a collection of useful functions and type declarations,
% most of them taken from Gofer’s prelude
% type alias for strings
type string = [char]
infixl
infixr
infixr
infixr
infixr
infixr
90
90
50
40
40
30
!!
.
++
//
‘and‘,/\
‘or‘,\/
%
%
%
%
%
%
nth-element selector
function composition
concatenation of lists
non-deterministic choice
parallel and sequential conjunction
parallel and sequential disjunction
% boolean functions
and,or,(/\),(\/) :: bool -> bool -> bool
not :: bool -> bool
% Parallel and
false ‘and‘ _ = false
_ ‘and‘ false = false
true ‘and‘ true = true
% Parallel or
true ‘or‘
_ = true
_ ‘or‘ true = true
false ‘or‘ false = false
189
190
APÉNDICE C. FUNCIONES DE USO COMÚN (ARCHIVO MISC.TOY)
% Sequential and
false /\ _ = false
true /\ X = X
% Sequential or
true \/ X = true
false \/ X = X
% Negation
not true = false
not false = true
andL, orL ,orL’
:: [bool] -> bool
andL
= foldr (/\) true
orL
= foldr or false
orL’
= foldr (\/) false
% orL’ is ’stricter’, but more deterministic, than orL
any, any’,all
:: (A -> bool) -> [A] -> bool
any P
= orL . (map P)
any’ P
= orL’ . (map P)
% any’ is ’stricter’, but more deterministic, than any
all P
= andL . (map P)
undefined :: A
undefined = if false then undefined
% (nf X) is the identity, restricted to finite and totally defined values
% Operationally, (nf X) forces the computation of a normal form for X,
% if it exists.
nf X = X <== X==X
% (hnf X) is the identity, restricted to not undefined values.
% Operationally, (hnf X) forces the computation of a head normal form for X,
% if it exists.
hnf X = X <== X /= _
% (strict F) is the restriction of F to finite, totally defined arguments.
% Operationally, it forces the evaluation to nf of the argument before applying F
strict F X = F X <== X==X
% (strict’ F) is the restriction of F to not undefined arguments.
% Operationally, it forces the evaluation to hnf of the argument before applying F
strict’ F X = F X <== X /= _
191
% mapping a function through a list
map:: (A -> B) -> [A] -> [B]
map F [] = []
map F [X|Xs] = [(F X)|(map F Xs)]
%% Function composition
(.) :: (B -> C) -> (A -> B) -> (A -> C)
(F . G) X = F (G X)
%% List concatenation
(++) :: [A] -> [A] -> [A]
[] ++ Ys = Ys
[X|Xs] ++ Ys = [X|Xs ++ Ys]
%% Xs!!N is the Nth-element of Xs
(!!) :: [A] -> int -> A
[X|Xs] !! N = if N==0 then X else Xs !! (N-1)
iterate :: (A -> A) -> A -> [A]
iterate F X = [X|iterate F (F X)]
repeat :: A -> [A]
repeat X = [X|repeat X]
copy :: int -> A -> [A]
copy N X = take N (repeat X)
filter
filter L []
filter P [X|Xs]
if P X then
else
%%
%%
%%
%%
%%
%%
%%
%%
%%
%%
%%
:: (A -> bool) -> [A] -> [A]
= []
=
[X|filter P Xs]
filter P Xs
Fold primitives: The foldl and scanl functions, variants foldl1 and
scanl1 for non-empty lists, and strict variants foldl’ scanl’ describe
common patterns of recursion over lists. Informally:
foldl F
a [x1, x2, ..., xn] = F
(...(f (f a x1) x2)...) xn
= (...((a ‘f‘ x1) ‘f‘ x2)...) ‘f‘ xn
etc...
The functions foldr, scanr and variants foldr1, scanr1 are duals of these
functions:
e.g. foldr F
a Xs
= foldl (flip f) a (reverse Xs ) for finite lists Xs
foldl
foldl
foldl
F Z []
F Z [X|Xs]
:: (A -> B -> A) -> A -> [B] -> A
= Z
= foldl F (F Z X) Xs
.
192
APÉNDICE C. FUNCIONES DE USO COMÚN (ARCHIVO MISC.TOY)
foldl1
foldl1 F [X|Xs]
:: (A -> A -> A) -> [A] -> A
= foldl F X Xs
foldl’
:: (A -> B -> A) -> A -> [B] -> A
foldl’ F A []
= A
foldl’ F A [X|Xs] = strict (foldl’ F) (F A X) Xs
scanl
scanl
scanl
F Q []
F Q [X|Xs]
scanl1
scanl1 F [X|Xs]
:: (A -> B -> A) -> A -> [B] -> [A]
= [Q]
= [Q|scanl F (F Q X) Xs]
:: (A -> A -> A) ->
= scanl F X Xs
[A]
->
[A]
scanl’
:: (A -> B -> A) -> A -> [B] -> [A]
scanl’ F Q []
= [Q]
scanl’ F Q [X|Xs] = [Q|strict (scanl’ F) (F Q X) Xs]
foldr
foldr
foldr
F Z []
F Z [X|Xs]
:: (A -> B -> B) -> B ->
= Z
= F X (foldr F Z Xs)
foldr1
:: (A -> A -> A) -> [A]
foldr1 F [X]
= X
foldr1 F [X,Y|Xs] = F X (foldr1 F [Y|Xs])
scanr
::
scanr F Q0 []
=
scanr F Q0 [X|Xs] =
%where
auxForScanr F X Ys =
[A]
-> B
-> A
(A -> B -> B) -> B -> [A] ->
[Q0]
auxForScanr F X (scanr F Q0 Xs)
[B]
[F X (head Ys)|Ys]
scanr1
:: (A -> A -> A) -> [A] -> [A]
scanr1 F [X]
= [X]
scanr1 F [X,Y|Xs] = auxForScanr F X (scanr1 F [Y|Xs])
%% List breaking functions:
%%
%%
take n Xs
returns the first n elements of Xs
%%
drop n Xs
returns the remaining elements of Xs
%%
splitAt n Xs
= (take n Xs , drop n Xs )
%%
%%
takeWhile P Xs
returns the longest initial segment of Xs
%%
elements satisfy p
%%
dropWhile P Xs
returns the remaining portion of the list
%%
span P Xs
= (takeWhile P Xs , dropWhile P Xs )
whose
193
%%
%%
%%
takeUntil P Xs
returns the list of elements upto and including the
first element of Xs
which satisfies p
take :: int -> [A] -> [A]
take N [] = []
take N [X|Xs] = if N==0 then [] else [X|take (N-1) Xs]
drop
drop N []
drop N [X|Xs]
:: int
->
[A]
->
[A]
= []
= if N==0 then [X|Xs] else drop (N-1) Xs
splitAt
splitAt N []
splitAt N [X|Xs]
:: int -> [A] -> ( [A] , [A] )
= ([],[])
= if N==0
then ([], [X|Xs])
else auxForSplitAt X (splitAt (N-1) Xs)
%where
auxForSplitAt X (Xs,Ys) = ([X|Xs],Ys)
takeWhile
takeWhile P []
takeWhile P [X|Xs]
:: (A -> bool) -> [A] -> [A]
= []
= if P X then [X| takeWhile P Xs] else []
takeUntil
takeUntil P []
takeUntil P [X|Xs]
:: (A -> bool) -> [A] -> [A]
= []
= if P X then [X] else [X| takeUntil P Xs]
dropWhile
dropWhile P []
dropWhile P [X|Xs]
:: (A -> bool) -> [A] -> [A]
= []
= if P X then dropWhile P Xs else [X|Xs]
span, break
span P []
span P [X|Xs]
:: (A -> bool) -> [A] -> ( [A] , [A] )
= ([],[])
= if P X
then auxForSpan X (span P Xs)
else ([], [X|Xs])
auxForSpan X (Xs,Ys) = ([X|Xs],Ys) % Identical to auxForSplitAt
break P
= span (not . P)
APÉNDICE C. FUNCIONES DE USO COMÚN (ARCHIVO MISC.TOY)
194
zipWith
zipWith Z []
Bs
zipWith Z [A|As] []
zipWith Z [A|As] [B|Bs]
::
=
=
=
(A->B->C) -> [A]->[B]->[C]
[]
[]
[Z A B | zipWith Z As Bs]
zip
zip Xs Ys
%where
mkpair
mkpair X Y
:: [A]->[B]->[(A,B)]
= zipWith mkpair Xs Ys
unzip
unzip
unzip
:: [(A,B)] -> ([A],[B])
= ([],[])
= auxForUnzip X Y (unzip XsYs)
:: A -> B ->(A,B)
= (X,Y)
[]
[(X,Y)|XsYs]
auxForUnzip X Y (Xs,Ys)
= ([X|Xs],[Y|Ys])
until
until P F X
:: (A -> bool) -> (A -> A) -> A -> A
= if P X then X else until P F (F X)
until’
until’ P F
:: (A -> bool) -> (A -> A) -> A -> [A]
= (takeUntil P) . (iterate F)
%% Standard combinators:
%% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %%
const
const K X
:: A -> B -> A
= K
id
id
:: A -> A
= X
X
% non-deterministic choice
(//)
:: A -> A -> A
X // _
= X
_ // Y
= Y
curry
curry F A B
:: ((A,B) -> C ) -> A -> B -> C
= F (A,B)
uncurry
:: (A -> B -> C ) -> (A,B)
uncurry F (A,B) = F A B
fst
fst (X,Y)
:: (A,B)
= X
-> A
-> C
195
snd
snd (X,Y)
:: (A,B)
= Y
fst3
fst3 (X,Y,Z)
:: (A,B,C)
= X
-> A
snd3
snd3 (Y,X,Z)
:: (A,B,C)
= X
-> B
thd3
thd3 (Y,Z,X)
:: (A,B,C)
= X
-> C
subtract
subtract
-> B
:: real -> real-> real
= flip (-)
even, odd :: int -> bool
even X
= (X ‘mod‘ 2) == 0
odd
= not . even
lcm
lcm X Y
:: int -> int -> int
= if ((X==0) \/ (Y == 0)) then 0
else abs ((X ‘div‘ (gcd X Y)) * Y)
%%%% Standard list processing functions:
%% %% %% %% %% %%
head
head [X|_]
:: [A] -> A
= X
last
last [X]
last [_,Y|Xs]
:: [A] -> A
= X
= last [Y|Xs]
tail
tail [_|Xs]
:: [A] -> [A]
= Xs
init
init [X]
init [X,Y|Xs]
:: [A] -> [A]
= []
= [X|init [Y|Xs]]
nub
nub []
nub [X|Xs]
:: [A] -> [A]
%% remove duplicates from list
= []
= [X| nub (filter (X /=) Xs)]
length
length []
length [_|Xs]
:: [A] -> int
= 0
= 1 + length Xs
196
APÉNDICE C. FUNCIONES DE USO COMÚN (ARCHIVO MISC.TOY)
size
size
:: [A] -> int
= length . nub
reverse
reverse
:: [A] -> [A]
= foldl (flip (:)) []
member,notMember :: A -> [A] -> bool
member
= any’ . (==)
notMember
= all . (/=)
%% reverse elements of list
%% test for membership in list
%% test for non-membership
concat
concat
:: [[A]] -> [A]
= foldr (++) []
%% concatenate list of lists
transpose
transpose
:: [[A]] -> [[A]]
= foldr
auxForTranspose
[]
%% transpose list of lists
%where
auxForTranspose Xs Xss = zipWith (:) Xs (Xss ++ repeat [])
%% (\\) is used to remove the first occurrence of each element in the second
%% list from the first list. It is a kind of inverse of (++) in the sense
%% that (xs ++ ys) \\ xs = ys for any finite list xs of proper values xs.
infix 50 \\
(\\)
(\\)
%where
[]
‘del‘ Y
[X|Xs] ‘del‘ Y
:: [A] -> [A] -> [A]
= foldl del
= []
= if X == Y then Xs
else [X|Xs] ‘del‘ Y
Apéndice D
Archivo toycomm.pl
/*
Created:
Modified:
Autor:
Description:
3-6-96
18.6.97
Paco & Jaime
*** STORES VERSION *** This module is neccesary for executing
programs in Toy. The system loads it automatically at the
begining. It contains all the common predicates to all
toy-programs.
/***************
%:-dynamic hnf/4.
CODE FOR HNF
***************/
/*
% Para poner el contador de hnf’s hay que inicializar el contador con la llamada
% bb_put(user:hnfCont,0) y activar este predicado. Despues de la ejecucion del
% objetivo se puede recurerar el contador con bb_get(user:hnfCont,C).
hnf(E,H):bb_get(user:hnfCont,CH),
CH1 is CH+1,
bb_put(user:hnfCont,CH1),
fail.
*/
197
APÉNDICE D. ARCHIVO TOYCOMM.PL
198
hnf(E,H,Cin,Cout):var(E),
!,
(
var(H),
!,
H=E,
Cin=Cout
;
extractCtr(E,Cin,Cout1,CE),
H=E,
propagate(H,CE,Cout1,Cout)
).
hnf(’$$susp’(Fun,Args,R,S),H,Cin,Cout):!,
(S==hnf,!,hnf(R,H,Cin,Cout)
;
H=R,
S=hnf,
hnf_susp(Fun,Args,H,Cin,Cout)
).
hnf(T,H,Cin,Cin):-H=T.
unifyHnfs(H,L,Cin,Cout):var(H),
!,
extractCtr(H,Cin,Cout1,CH),
H=L,
propagate(H,CH,Cout1,Cout).
unifyHnfs(H,H,Cin,Cin).
/***************
CODE FOR EQUAL
equal(L,R,Cin,Cout):var(L),
!,
hnf(R,HR,Cin,Cout1),
equalHnf(L,HR,Cout1,Cout).
***************/
199
equal(R,L,Cin,Cout):var(L),
!,
hnf(R,HR,Cin,Cout1),
%hnf(L,HL,CC1,CV1,CC2,CV2),
equalHnf(L,HR,Cout1,Cout).
equal(L,R,Cin,Cout):constructor(L,C/N),
!,
functor(T,C,N),
hnf(R,T,Cin,Cout1),
eqFrontier(L,T,FL/[],FR/[]),
equalList(FL,FR,Cout1,Cout).
equal(R,L,Cin,Cout):constructor(L,C/N),
!,
functor(T,C,N),
hnf(R,T,Cin,Cout1),
eqFrontier(L,T,FL/[],FR/[]),
equalList(FL,FR,Cout1,Cout).
% Both are suspended forms, but we don’t know if they are solved or not.
equal(’$$susp’(_,_,R,S),L,Cin,Cout):S==hnf,
!,
equal(R,L,Cin,Cout).
equal(L,’$$susp’(_,_,R,S),Cin,Cout):S==hnf,
!,
equal(R,L,Cin,Cout).
equal(L,R,Cin,Cout):hnf(L,HL,Cin,Cout1),
equal(HL,R,Cout1,Cout).
equalHnf(L,R,Cin,Cout):-var(L),!,binding(L,R,Cin,Cout).
equalHnf(R,L,Cin,Cout):-var(L),!,binding(L,R,Cin,Cout).
equalHnf(R,L,Cin,Cout):eqFrontier(R,L,FR/[],FL/[]),!,
equalList(FR,FL,Cin,Cout).
APÉNDICE D. ARCHIVO TOYCOMM.PL
200
/***************
CODE FOR BINDING
***************/
binding(X,Y,Cin,Cout):var(Y),
!,
unifyVar(X,Y,Cin,Cout).
binding(X,Y,Cin,Cout):!,
occursNot(X,Y,ShY,Lst),
extractCtr(X,Cin,Cout1,CX),
X=ShY,
propagate(ShY,CX,Cout1,Cout2),
equalList(Lst,Cout2,Cout).
% It may be improved because propagate has the information of ShY is a hnf and
% all elements in CX are hnf’s
/***************
CODE FOR NOT EQUAL
***************/
notEqual(X,Y,Cin,Cout):hnf(X,HX,Cin,Cout1),hnf(Y,HY,Cout1,Cout2),
notEqualHnf(HX,HY,Cout2,Cout).
% First of all, we check if the solver is activated. In such case the
% the disequality constraint is over real numbers, we send it to the the solver
% and forget it.
% &clpr
notEqualHnf(X,Y,Cin,Cin):clpr_active,
(isReal(X);isReal(Y)),!,{X=\=Y}.
notEqualHnf(X,Y,Cin,Cout):-var(X),!,notEqualVar(X,Y,Cin,Cout).
notEqualHnf(Y,X,Cin,Cout):-var(X),!,notEqualVar(X,Y,Cin,Cout).
201
notEqualHnf(R,L,Cin,Cout):eqFrontier(R,L,FR/[],FL/[]),!,
notEqualList(FR,FL,Cin,Cout)
; % eqFrontier fallo
Cin=Cout.
% If Y is a variable (both are variables), we simply put the constraint into
% the store
notEqualVar(X,Y,Cin,Cout):var(Y),
!,
X\==Y,
addCtr(X,Y,Cin,Cout1),
addCtr(Y,X,Cout1,Cout).
% These two clauses allow to bind X=false in presence of a contraint of the form
% X/=true (and X=true in presence of X/=false) without do anything more
notEqualVar(X,true,Cin,Cout):-!,hnf(X,false,Cin,Cout).
notEqualVar(X,false,Cin,Cout):-!,hnf(X,true,Cin,Cout).
% In othrer case we make an occursNot in order to discover if it is a hnf or if
% it has function calls (this is an artificial use of occursNot)
notEqualVar(X,Y,Cin,Cout):occursNot(X,Y,ShY,Lst),
!,
contNotEqual(X,Y,ShY,Lst,Cin,Cout).
% It occursNot fail, then they are distinct automatically
notEqualVar(_X,_Y,Cin,Cin).
% If the list produced by occursNot unifies with []/[], that is because Y is a
% hnf, and we only add the constraint
contNotEqual(X,_,ShY,[]/[],Cin,Cout):!,
addCtr(X,ShY,Cin,Cout).
202
APÉNDICE D. ARCHIVO TOYCOMM.PL
% Y is not a hnf: we generate constructor symbols of the same type ensuring that
% the disequality es satified
contNotEqual(X,Y,_,_,Cin,Cout):constructor(Y,C/_N,ArgsY),
!,
const(C,_,_,Dest),
(genConstructor(Dest,Z,C1,_ArgsZ), % const with <>name and the same
% destination type
C\==C1,
hnf(X,Z,Cin,Cout)
;
genConstructor(Dest,Z,C,ArgsZ),
% The same name and <>’s Args
hnf(X,Z,Cin,Cout1),
notEqualList(ArgsZ,ArgsY,Cout1,Cout)).
/***************
CODE FOR FUNCTION ==
***************/
’$$eqFun’(X,Y,H,Cin,Cout):-H==true,!,equal(X,Y,Cin,Cout).
’$$eqFun’(X,Y,H,Cin,Cout):-H==false,!,notEqual(X,Y,Cin,Cout).
’$$eqFun’(X,Y,H,Cin,Cout):var(X),!,
(H=true,equal(X,Y,Cin,Cout)
;
H=false,notEqual(X,Y,Cin,Cout)).
’$$eqFun’(X,Y,H,Cin,Cout):var(Y),!,
(H=true,equal(X,Y,Cin,Cout)
;
H=false,notEqual(Y,X,Cin,Cout)).
’$$eqFun’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
eqFunHnf(HX,HY,H,Cout2,Cout).
eqFunHnf(X,Y,H,Cin,Cout):(var(X);var(Y)),!,’$$eqFun’(X,Y,H,Cin,Cout).
eqFunHnf(X,Y,H,Cin,Cout):eqFrontier(X,Y,FrontierX/[],FrontierY/[]),!,
eqFunAnd(FrontierX,FrontierY,H,Cin,Cout)
; % eqFrontier fallo
H=false,
Cin=Cout.
203
eqFunAnd([],[],true,Cin,Cin):-!.
% Non deterministic choice: a conjunction of equalities is true if all of them
% are true and it is false if it is false one of them
eqFunAnd([X1|Rest1],[Y1|Rest2],H,Cin,Cout):’$$eqFun’(X1,Y1,H1,Cin,Cout1),
eqFunAnd_1(Rest1,Rest2,H1,H,Cout1,Cout).
eqFunAnd([_|Rest1],[_|Rest2],false,Cin,Cout):notEqualList(Rest1,Rest2,Cin,Cout).
eqFunAnd_1(_,_,false,false,Cin,Cin).
eqFunAnd_1(Rest1,Rest2,true,true,Cin,Cout):equalList(Rest1,Rest2,Cin,Cout).
/***************
CODE FOR FUNCTION /=
***************/
’$$notEqFun’(X,Y,H,Cin,Cout):-H==true,!,notEqual(X,Y,Cin,Cout).
’$$notEqFun’(X,Y,H,Cin,Cout):-H==false,!,equal(X,Y,Cin,Cout).
%{paco}29-11-96
’$$notEqFun’(X,Y,H,Cin,Cout):’$$eqFun’(X,Y,Z,Cin,Cout),
negate(Z,H).
negate(true,false) :- !.
negate(false,true).
/***************
CODE FOR OCCURS CHECK
***************/
occursNot(X,Y,ShY,L/L):-var(Y),!,X\==Y,Y=ShY.
occursNot(_,’$$susp’(E,Args,R,S),Z,[Z==’$$susp’(E,Args,R,S)|L]/L):-var(S),!.
occursNot(X,’$$susp’(_,_,R,_),ShR,L/M):-!,occursNot(X,R,ShR,L/M).
occursNot(X,T,ShT,L/M):T=..[Name|Args],
lstOccursNot(X,Args,ShArgs,L/M),
ShT=..[Name|ShArgs].
APÉNDICE D. ARCHIVO TOYCOMM.PL
204
lstOccursNot(_,[],[],L/L).
lstOccursNot(X,[Ar|Rest],[ShAr|RSh],L/M):occursNot(X,Ar,ShAr,L/L1),
lstOccursNot(X,Rest,RSh,L1/M).
/***************
AUXILIAR CODE
***************/
equalList([],[],Cin,Cin):-!.
equalList([Ar1|R1],[Ar2|R2],Cin,Cout):equal(Ar1,Ar2,Cin,Cout1),
equalList(R1,R2,Cout1,Cout).
equalList([]/[],Cin,Cin):-!.
equalList([(Z==Y)|L]/M,Cin,Cout):equal(Z,Y,Cin,Cout1),
equalList(L/M,Cout1,Cout).
% Indeterministic choice for doing a pair false with independence of the rest
notEqualList([X|R1],[Y|R2],Cin,Cout):notEqual(X,Y,Cin,Cout)
;
notEqualList(R1,R2,Cin,Cout).
%
%
%
%
Frontier of two terms. The predicate eqFrontier(T1,T2,L1/R1,L2/R2) extract the
shell of common constructors of T1 and T2. In the differece lists it puts the
common part. If the shell contains some distinct constructor for the terms,
eqFrontier fails automatically
eqFrontier(X,Y,[X|L1]/L1,[Y|L2]/L2) :(var(X);var(Y)),!.
eqFrontier(X,Y,FX,FY) :constructor(X,NameX,ArgsX),
constructor(Y,NameY,ArgsY),!,
NameX==NameY,
eqFrontierList(ArgsX,ArgsY,FX,FY).
205
eqFrontier(’$$susp’(Fun,Args,R,S),Y,FX,FY) :!,
(S==hnf,!,
eqFrontier(R,Y,FX,FY)
;
FX = [’$$susp’(Fun,Args,R,S)|L1]/L1,
FY = [Y|L2] / L2
).
eqFrontier(X,’$$susp’(Fun,Args,R,S),FX,FY) :!,
(S==hnf,!,
eqFrontier(X,R,FX,FY)
;
FY = [’$$susp’(Fun,Args,R,S)|L1]/L1,
FX = [X|L2] / L2
).
eqFrontierList([],[],L1/L1,L2/L2).
eqFrontierList([X|Xs],[Y|Ys],LX/MX,LY/MY) :eqFrontier(X,Y,LX/L1,LY/L2),
eqFrontierList(Xs,Ys,L1/MX,L2/MY).
% We have two constructor (one of arity two and the other of arity three). The
% firts one simply check if what we pass is a hnf and then it returns its name
% and arity. The second one returns the list of arguments two.
constructor(C,C/0):-number(C),!.
constructor(T,C/N):functor(T,C,N),
!,
(const(C,_,_,_),!
;
funct(C,Ar,_,_,_),!,N<Ar).
constructor(C,C/0,[]):-number(C),!.
constructor(T,C/N,Args):functor(T,C,N),
!,
(const(C,_,_,_),!
;
funct(C,Ar,_,_,_),!,N<Ar), % Arreglado
T=..[_|Args].
206
APÉNDICE D. ARCHIVO TOYCOMM.PL
genConstructor(TipDest,Cons,Name,Args):const(Name,Ar,_,TipDest),
% We look for the same destination type
functor(Cons,Name,Ar),
% build the term
Cons=..[Name|Args].
% extract the arguments (new vars.)
% STORE DEAL
propagate(_,[],Cin,Cin):-!.
propagate(Y,[C|R],Cin,Cout):(
var(C), % We don’t need to solve Y /= C,
% because C is a variable and it already had,
% the disequality C /= Y.
% Notice that C can not be Y
!,
Cout1=Cin
;
notEqualTerm(Y,C,Cin,Cout1)),
propagate(Y,R,Cout1,Cout).
%
%
%
%
notEqualTerm: notEqual especialized for terms only with contructors
Because of eficience, we don’t make any kind of occur-check, and then
notEqualTerm(X,s(X)), instead of be trivially satisfiable as in the
notEqual(X,s(X)) case, inserts the constraint X /= s(X)
notEqualTerm(T1,T2,Cin,Cout):var(T1),
!,
notEqualVarTerm(T1,T2,Cin,Cout).
notEqualTerm(T1,T2,Cin,Cout):var(T2),
!,
notEqualVarTerm(T2,T1,Cin,Cout).
notEqualTerm(T1,T2,Cin,Cout):constructor(T1,C1/A1,Args1),
constructor(T2,C2/A2,Args2),
(C1/A1\==C2/A2,!,Cout=Cin
;
notEqualTermList(Args1,Args2,Cin,Cout)).
207
notEqualVarTerm(X,Y,Cin,Cout):var(Y),
!,
X\==Y,
addCtr(X,Y,Cin,Cout1),
addCtr(Y,X,Cout1,Cout).
notEqualVarTerm(X,Y,Cin,Cout):!,
addCtr(X,Y,Cin,Cout).
notEqualTermList([X|R1],[Y|R2],Cin,Cout):(notEqualTerm(X,Y,Cin,Cout)
;
notEqualTermList(R1,R2,Cin,Cout)).
% CONSTRAINT HANDLING
% Insertion of a new constraint Var/=Term
addCtr(X,Term,[],[X:[Term]]):-!.
addCtr(X,Term,[Y:Ctr|R],[Y:[Term|Ctr]|R]):X==Y,
!.
addCtr(X,Term,[F|R],[F|R1]):addCtr(X,Term,R,R1).
% Extraction of constraints asociated to a variable. We extract the constraints
% asociated from the store and return them in the last argument
extractCtr(_,[],[],[]):-!.
extractCtr(X,[V:W|R],R,W):X==V,
!.
extractCtr(X,[V:W|R],[V:W|R1],L):extractCtr(X,R,R1,L).
208
APÉNDICE D. ARCHIVO TOYCOMM.PL
% Unification on two vars X Y: if they are not the same var, nothing. Else we
% unify them and do the union of their constraints
unifyVar(X,Y,Cin,Cout):X==Y,
!,
Cout=Cin.
unifyVar(X,Y,Cin,Cout):extractTwoCtr(X,Y,Cin,Cout1,CX,CY),
X=Y,
!,
update(X,CX,CY,Cout1,Cout).
% extracting constraints of two vars X Y in CX CY. This form of operation in
% inify is equivalent to perform two extracCtr, but improves the way, because
% we have got both Stores in one pass
extractTwoCtr(_,_,[],[],[],[]).
extractTwoCtr(X,Y,[Z:CZ|R],Cout,CX,CY):(
X==Z,
!,
CX=CZ,
extractCtr(Y,R,Cout,CY)
;
Y==Z,
!,
CY=CZ,
extractCtr(X,R,Cout,CX)
).
extractTwoCtr(X,Y,[Ctr|R],[Ctr|R1],CX,CY):-extractTwoCtr(X,Y,R,R1,CX,CY).
% Union of constraints asociated to vars X Y checking that beteween these
% constraints is not X/=Y
update(Y,[],CY,Cin,Cout):insertCtrs(Y,CY,Cin,Cout).
update(Y,[T|Ts],CY,Cin,Cout):Y\==T,
% comprobacion de no existe una restriccion X/=Y
!,
update(Y,Ts,[T|CY],Cin,Cout).
insertCtrs(_,[],Cin,Cin):-!.
% esta clausula solo sirve para no meter
% listas de restricciones vacias
insertCtrs(Y,CY,Cin,[Y:CY|Cin]).
209
% REAL CONSTRAINTS
% isReal(X) success iff X is a number or X is a var affected by some constraint
isReal(X):-number(X),!.
isReal(X):var(X),
linear:dump([X],_,L),
!,
L\==[].
% CONVERSION OF DISEQUALITY CONSTRAINTS (SYNTACTIC) TO REAL CONSTRAINTS
% THIS CODE IS ONLY USED WHEN THE SYSTEM IS RUNNING WITH REALS
/*
Disequality constraints beteween vars are dealt as syntactic ones. They are stored as an
*/
toSolver(X,Cin,Cin):-nonvar(X),!.
toSolver(X,Cin,Cout):extractCtr(X,Cin,Cout1,CX),
passToSolver(X,CX,Cout1,Cout).
passToSolver(_,[],Cin,Cin).
passToSolver(X,[Y|R],Cin,Cout):{X=\=Y},
(
var(Y),
!,
toSolver(Y,Cin,Cout1),
passToSolver(X,R,Cout1,Cout)
;
passToSolver(X,R,Cin,Cout)
).
210
APÉNDICE D. ARCHIVO TOYCOMM.PL
Apéndice E
Primitivas sin restricciones
aritméticas (archivo primitives.pl)
/*
This module contains the code for primitives. This functions haves a
direct tranlation into Prolog. Cin and Cout are the stores of disequality
constraints and must be placed as in the following examples. Before the Prolog
operation the hnf predicate must be called for each one argument.
Types for aritmethic functions are defined in a quite ad-hoc way here
in order to allow (a very limited) overloading of arithmetic operations.
The idea is the following: we represent the types ’int’ and ’real’
by the terms ’num(int)’ and ’num(real)’, and we use ’num(A)’ for
achieving overloading, when desired.
*/
/*
Nota: Los tipos de las primitivas se toman de aqui (no del standard) para
poder forzar el tipo de algunas funciones como / o sqrt y siempre devuelvan
real (num(float))
P.e. el + respeta la declaracion + :: num(A) -> num(A) -> num(A), lo que quiere
decir que si los dos argumentos son int el resultado es int y si alguno de los
dos es float el resultado es float.
En / tenemos / :: num(A) -> num(
*/
/***************
primInfix(/,
primInfix(*,
primInfix(+,
primInfix(-,
CODE PRIMITIVE FUNCTIONS
left, 90).
right, 90).
left, 50).
left, 50).
211
***************/
212APÉNDICE E. PRIMITIVAS SIN RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVES.PL)
primInfix(^, noasoc, 98).
primInfix(**, noasoc, 98).
primInfix(<, noasoc, 30).
primInfix(<=, noasoc, 30).
primInfix(>, noasoc, 30).
primInfix(>=, noasoc, 30).
primInfix(==, noasoc, 10).
primInfix(/=, noasoc, 10).
primInfix(:,right,15).
primInfix(’,’,right,12).
primitiveFunct(==, 2, 2, (A -> (A -> bool)), bool).
primitiveFunct(/=, 2, 2, (A -> (A -> bool)), bool).
% Funciones unarias para enteros y reales
primitiveFunct(uminus, 1, 1, (num(A) -> num(A)), num(A)).
’$uminus’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is -HX; errPrim.
primitiveFunct(abs, 1, 1, (num(A) -> num(A)), num(A)).
’$abs’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is abs(HX); errPrim.
% Funciones reales unarias
primitiveFunct(sqrt, 1, 1, (num(_A) -> num(float)), num(float)).
’$sqrt’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is sqrt(HX); errPrim.
primitiveFunct(ln, 1, 1, (num(float) -> num(float)), num(float)).
’$ln’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is log(HX); errPrim.
primitiveFunct(exp, 1, 1, (num(float) -> num(float)), num(float)).
’$exp’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is exp(HX); errPrim.
213
primitiveFunct(sin, 1, 1, (num(float) -> num(float)), num(float)).
’$sin’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is sin(HX); errPrim.
primitiveFunct(cos, 1, 1, (num(float) -> num(float)), num(float)).
’$cos’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is cos(HX); errPrim.
primitiveFunct(tan, 1, 1, (num(float) -> num(float)), num(float)).
’$tan’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is tan(HX); errPrim.
primitiveFunct(cot, 1, 1, (num(float) -> num(float)), num(float)).
’$cot’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is cot(HX); errPrim.
primitiveFunct(asin, 1, 1, (num(float) -> num(float)), num(float)).
’$asin’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is asin(HX); errPrim.
primitiveFunct(acos, 1, 1, (num(float) -> num(float)), num(float)).
’$acos’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is acos(HX); errPrim.
primitiveFunct(atan, 1, 1, (num(float) -> num(float)), num(float)).
’$atan’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is atan(HX); errPrim.
primitiveFunct(acot, 1, 1, (num(float) -> num(float)), num(float)).
’$acot’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is acot(HX); errPrim.
primitiveFunct(sinh, 1, 1, (num(float) -> num(float)), num(float)).
214APÉNDICE E. PRIMITIVAS SIN RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVES.PL)
’$sinh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is sinh(HX); errPrim.
primitiveFunct(cosh, 1, 1, (num(float) -> num(float)), num(float)).
’$cosh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is cosh(HX); errPrim.
primitiveFunct(tanh, 1, 1, (num(float) -> num(float)), num(float)).
’$tanh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is tanh(HX); errPrim.
primitiveFunct(coth, 1, 1, (num(float) -> num(float)), num(float)).
’$coth’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is coth(HX); errPrim.
primitiveFunct(asinh, 1, 1, (num(float) -> num(float)), num(float)).
’$asinh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is asinh(HX); errPrim.
primitiveFunct(acosh, 1, 1, (num(float) -> num(float)), num(float)).
’$acosh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is acosh(HX); errPrim.
primitiveFunct(atanh, 1, 1, (num(float) -> num(float)), num(float)).
’$atanh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is atanh(HX); errPrim.
primitiveFunct(acoth, 1, 1, (num(float) -> num(float)), num(float)).
’$acoth’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
number(HX) -> H is acoth(HX); errPrim.
% operadores y funciones aritmeticos binarias para enteros y reales
215
primitiveFunct(+, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
$+(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is HX + HY; errPrim).
primitiveFunct(-, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
$-(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is HX - HY; errPrim).
primitiveFunct(*, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
$*(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is HX * HY; errPrim).
primitiveFunct(min, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
’$min’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is min(HX,HY); errPrim).
primitiveFunct(max, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
’$max’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is max(HX,HY); errPrim).
% funciones reales binarias
primitiveFunct(/, 2, 2, (num(A) -> (num(A) -> num(float))), num(float)).
$/(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is HX / HY; errPrim).
primitiveFunct(’**’, 2, 2, (num(_A) -> (num(float) -> num(float))), num(float)).
’$**’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
216APÉNDICE E. PRIMITIVAS SIN RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVES.PL)
(number(HX),number(HY) -> H is exp(HX,HY); errPrim).
primitiveFunct(log, 2, 2, (num(float) -> (num(float) -> num(float))), num(float)).
’$log’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is log(HX,HY); errPrim).
% potencia con exponente natural
primitiveFunct(^, 2, 2, (num(A) -> (num(int) -> num(A))), num(A)).
$^(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY), HY >= 0 -> H is exp(HX,HY); errPrim).
% HY >= 0 En otro caso, se podria sacar mensaje, e incluso abortar
primitiveFunct(div, 2, 2, (num(int) -> (num(int) -> num(int))), num(int)).
’$div’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is float(HX // HY); errPrim).
primitiveFunct(mod, 2, 2, (num(int) -> (num(int) -> num(int))), num(int)).
’$mod’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is float(HX mod HY); errPrim).
primitiveFunct(gcd, 2, 2, (num(int) -> (num(int) -> num(int))), num(int)).
’$gcd’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> H is float(gcd(HX,HY)); errPrim).
primitiveFunct(round, 1, 1, (num(_A) -> num(int)), num(int)).
’$round’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
(number(HX) -> H is round(HX); errPrim).
217
primitiveFunct(trunc, 1, 1, (num(_A) -> num(int)), num(int)).
’$trunc’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
(number(HX) -> H is float(integer(HX)); errPrim).
primitiveFunct(floor, 1, 1, (num(_A) -> num(int)), num(int)).
’$floor’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
(number(HX) -> H is floor(HX); errPrim).
primitiveFunct(ceiling, 1, 1, (num(_A) -> num(int)), num(int)).
’$ceiling’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
(number(HX) -> H is ceiling(HX); errPrim).
%Conversion de enteros a reales
primitiveFunct(toReal, 1, 1, (num(_A) -> num(float)), num(float)).
’$toReal’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout),
(number(HX) -> H=HX; errPrim).
primitiveFunct(<, 2, 2, (num(A) -> (num(A) -> bool)), bool).
$<(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> (HX<HY,H=true;HX>=HY,H=false); errPrim).
primitiveFunct(>, 2, 2, (num(A) -> (num(A) -> bool)), bool).
$>(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> (HX>HY,H=true;HX=<HY,H=false); errPrim).
primitiveFunct(<=, 2, 2, (num(A) -> (num(A) -> bool)), bool).
$<=(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> (HX=<HY,H=true;HX>HY,H=false); errPrim).
218APÉNDICE E. PRIMITIVAS SIN RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVES.PL)
primitiveFunct(>=, 2, 2, (num(A) -> (num(A) -> bool)), bool).
$>=(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout),
(number(HX),number(HY) -> (HX>=HY,H=true;HX<HY,H=false); errPrim).
errPrim:nl,
write(’RUNTIME ERROR: Variables are not allowed in arithmetical operations. (/cfl
nl,
!,
fail.
Apéndice F
Primitivas con restricciones
aritméticas (archivo
primitivesClpr.pl)
/***************
CODE PRIMITIVE FUNCTIONS
***************/
primInfix(/, left, 90).
primInfix(*, right, 90).
primInfix(+, left, 50).
primInfix(-, left, 50).
primInfix(^, noasoc, 98).
primInfix(**, noasoc, 98).
primInfix(<, noasoc, 30).
primInfix(<=, noasoc, 30).
primInfix(>, noasoc, 30).
primInfix(>=, noasoc, 30).
primInfix(==, noasoc, 10).
primInfix(/=, noasoc, 10).
primInfix(:,right,15).
primInfix(’,’,right,12).
primitiveFunct(==, 2, 2, (A -> (A -> bool)), bool).
primitiveFunct(/=, 2, 2, (A -> (A -> bool)), bool).
% Funciones unarias para enteros y reales
primitiveFunct(uminus, 1, 1, (num(A) -> num(A)), num(A)).
’$uminus’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
{H = -HX},
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
219
220APÉNDICE F. PRIMITIVAS CON RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVESCLP
primitiveFunct(abs, 1, 1, (num(A) -> num(A)), num(A)).
’$abs’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
{H = abs(HX)},
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
% Funciones reales unarias
primitiveFunct(sqrt, 1, 1, (num(_A) -> num(float)), num(float)).
’$sqrt’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
{H = pow(HX,1/2)},
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(ln, 1, 1, (num(float) -> num(float)), num(float)).
’$ln’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),
H is log(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(exp, 1, 1, (num(float) -> num(float)), num(float)).
’$exp’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
{H = exp(HX)},
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(sin, 1, 1, (num(float) -> num(float)), num(float)).
’$sin’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
{H = sin(HX)},
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(cos, 1, 1, (num(float) -> num(float)), num(float)).
’$cos’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
{H = cos(HX)},
221
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(tan, 1, 1, (num(float) -> num(float)), num(float)).
’$tan’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
{H = tan(HX)},
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(cot, 1, 1, (num(float) -> num(float)), num(float)).
’$cot’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is cot(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(asin, 1, 1, (num(float) -> num(float)), num(float)).
’$asin’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is asin(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(acos, 1, 1, (num(float) -> num(float)), num(float)).
’$acos’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is acos(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(atan, 1, 1, (num(float) -> num(float)), num(float)).
’$atan’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is atan(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(acot, 1, 1, (num(float) -> num(float)), num(float)).
’$acot’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is acot(HX),
222APÉNDICE F. PRIMITIVAS CON RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVESCLP
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(sinh, 1, 1, (num(float) -> num(float)), num(float)).
’$sinh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is sinh(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(cosh, 1, 1, (num(float) -> num(float)), num(float)).
’$cosh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is cosh(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(tanh, 1, 1, (num(float) -> num(float)), num(float)).
’$tanh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is tanh(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(coth, 1, 1, (num(float) -> num(float)), num(float)).
’$coth’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is coth(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(asinh, 1, 1, (num(float) -> num(float)), num(float)).
’$asinh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is asinh(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(acosh, 1, 1, (num(float) -> num(float)), num(float)).
’$acosh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is acosh(HX),
223
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(atanh, 1, 1, (num(float) -> num(float)), num(float)).
’$atanh’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is atanh(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(acoth, 1, 1, (num(float) -> num(float)), num(float)).
’$acoth’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is acoth(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
% operadores y funciones aritmeticos binarias para enteros y reales
primitiveFunct(+, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
$+(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = HX + HY},
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(-, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
$-(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = HX - HY},
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(*, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
$*(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = HX * HY},
toSolver(HX,Cout2,Cout3),
224APÉNDICE F. PRIMITIVAS CON RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVESCLP
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(min, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
’$min’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = min(HX,HY)},
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(max, 2, 2, (num(A) -> (num(A) -> num(A))), num(A)).
’$max’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = max(HX,HY)},
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
% funciones reales binarias
primitiveFunct(/, 2, 2, (num(A) -> (num(A) -> num(float))), num(float)).
$/(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = HX / HY},
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(’**’, 2, 2, (num(_A) -> (num(float) -> num(float))), num(float)).
’$**’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{H = exp(HX,HY)},
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(log, 2, 2, (num(float) -> (num(float) -> num(float))), num(float)).
’$log’(X,Y,H,Cin,Cout):-
225
hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
number(HX),number(HY),H is log(HX,HY),
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
% potencia con exponente natural
primitiveFunct(^, 2, 2, (num(A) -> (num(int) -> num(A))), num(A)).
$^(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
{HY >= 0}, % En otro caso, se podria sacar mensaje, e incluso abortar
{H = exp(HX,HY)},
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(div, 2, 2, (num(int) -> (num(int) -> num(int))), num(int)).
’$div’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
number(HX),number(HY),H is float(HX // HY),
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(mod, 2, 2, (num(int) -> (num(int) -> num(int))), num(int)).
’$mod’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
number(HX),number(HY),H is float(HX mod HY),
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(gcd, 2, 2, (num(int) -> (num(int) -> num(int))), num(int)).
’$gcd’(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
number(HX),number(HY),H is float(gcd(HX,HY)),
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
226APÉNDICE F. PRIMITIVAS CON RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVESCLP
toSolver(H,Cout4,Cout).
primitiveFunct(round, 1, 1, (num(_A) -> num(int)), num(int)).
’$round’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is round(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(trunc, 1, 1, (num(_A) -> num(int)), num(int)).
’$trunc’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is float(integer(HX)),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(floor, 1, 1, (num(_A) -> num(int)), num(int)).
’$floor’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is floor(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
primitiveFunct(ceiling, 1, 1, (num(_A) -> num(int)), num(int)).
’$ceiling’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H is ceiling(HX),
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
%Conversion de enteros a reales
primitiveFunct(toReal, 1, 1, (num(_A) -> num(float)), num(float)).
’$toReal’(X,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
number(HX),H=HX,
toSolver(HX,Cout1,Cout2),
toSolver(H,Cout2,Cout).
% Operadores relacionales.
primitiveFunct(<, 2, 2, (num(A) -> (num(A) -> bool)), bool).
227
$<(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
(H=true,{HX<HY};H=false,{HX>=HY}),
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(>, 2, 2, (num(A) -> (num(A) -> bool)), bool).
$>(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
(H=true,{HX>HY};H=false,{HX=<HY}),
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(<=, 2, 2, (num(A) -> (num(A) -> bool)), bool).
$<=(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
(H=true,{HX=<HY};H=false,{HX>HY}),
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
primitiveFunct(>=, 2, 2, (num(A) -> (num(A) -> bool)), bool).
$>=(X,Y,H,Cin,Cout):hnf(X,HX,Cin,Cout1),
hnf(Y,HY,Cout1,Cout2),
(H=true,{HX>=HY};H=false,{HX<HY}),
toSolver(HX,Cout2,Cout3),
toSolver(HY,Cout3,Cout4),
toSolver(H,Cout4,Cout).
228APÉNDICE F. PRIMITIVAS CON RESTRICCIONES ARITMÉTICAS (ARCHIVO PRIMITIVESCLP
Apéndice G
Construcción del arbol definicional
/************************************************************************
LLAMADA AL MODULO:
arbol(
+Fun
% funcion definida en el formato que devuelve el
% analisis sintactico pero con las variables como
% vars reales y no salida del a. sintactico. Este
% formato se genera y chequea semanticamente en
% el modulo tradfun.ari
-Patron % Devuelve el patron generico de llamada. Sirve
% unicamente con fines de depuracion
-Dds
% Devuelve el arbol dds asociado a la funcion de
% entrada.
************************************************************************/
/************************************************************************/
% CALCULO DEL ARBOL DEFINITORIO.
/************************************************************************/
arbol(fun(Nombre,ArP,Reglas,_),Patron,Dds):functor(Patron,Nombre,ArP),
% formamos el patron de llamada
posicionesPatron(0,ArP,Vpos),
% calculo de posiciones del patron
dds(Patron,Reglas,Vpos,Dds).
% formamos el arbol.
% devuelve una la lista de posiciones correspondiente a un patron no
% instanciado, e.d.,[[1],[2],...,[N]], si el patron tiene N argumentos,
% [] si tiene 0 argumentos.
posicionesPatron(N,N,[]):-!.
posicionesPatron(N,M,[[N1]|R]):229
APÉNDICE G. CONSTRUCCIÓN DEL ARBOL DEFINICIONAL
230
N1 is N+1,
posicionesPatron(N1,M,R).
/************************************************************************/
% CALCULO DE POSICIONES DEMANDADAS
/************************************************************************/
%
%
%
%
%
%
%
se le pasa la lista de reglas asociadas a una funcion, la lista de
posiciones de las variables en el patron y devuelve la lista de pares
regla/listaPC , donde listaPC esta formada por pares Posicion/constructora
donde constructora tiene la estructura Nombre/Aridad
Una posicion es en particular una lista de naturales.
Vpos es la lista de posibles posiciones demandadas (corresponden a las
posiciones de variables en el patron)
demandadas([rule(Cab,Cpo,Restr,_,Lin)|Rreglas],Vpos,
[(rule(Cab,Cpo,Restr,_,Lin),Dem)|Rdemandadas]):demanda(Cab,Vpos,Dem),
!,
demandadas(Rreglas,Vpos,Rdemandadas).
demandadas([],_,[]).
% devuelve la lista de pares (posicion,constructora/aridad) que demanda
% la regla de cabeza Cab.
demanda(Cab,[Pos|Rpos],[(Pos,C)|Dem]):consEnPos(Cab,Pos,C),
% miramos si hay una constructora
!,
% C en esa posicion
demanda(Cab,Rpos,Dem).
demanda(Cab,[_|Rpos],Dem):demanda(Cab,Rpos,Dem).
demanda(_,[],[]).
%
%
%
%
mira si en la posicion que se le pasa hay una constructora y la devuelve
en Nombre, junto con la aridad con la que aparece. Hay que tener en cuenta
que una funcion aplicada parcialmente tambien es una constructora.
Un numero tambien se considera constructora
consEnPos(Cab,[P],Nombre/Aridad):!,
231
arg(P,Cab,Arg),
\+ var(Arg),
(
esNumero(Arg,Nombre/Aridad),!
;
functor(Arg,Nombre,Aridad),
(
cdata(Nombre,_,_,_),!
;
fun(Nombre,ArP,_,_),!,Aridad<ArP
;
primitive(Nombre,_,_),!,ftype(Nombre,ArP,_,_),Aridad<ArP
)
).
consEnPos(Cab,[P|R],Nombre):-arg(P,Cab,Arg),consEnPos(Arg,R,Nombre).
esNumero(N,N/0):-number(N).
/************************************************************************/
% DDS
/************************************************************************/
% algoritmo propio dds. Predicado ppal
dds(Patron,Reglas,Vpos,Dds):demandadas(Reglas,Vpos,Dem),
% calculamos las pos demandadas en Dem
(
% una alternativa dependiendo
(noHayDem(Dem),!,hazTry(Patron,Reglas,Dds));
(uniformDem(Dem,Vpos,Pos),!,hazCase(Patron,Dem,Pos,Dds));
(hazOr(Patron,Dem,Dds))
).
/*
en Dem tenemos una lista de la forma:
[(
rule(Cab,Cpo,Rest,_,Linea),
[(Posicion,Constructora/Aridad)....]
),....
]
*/
232
APÉNDICE G. CONSTRUCCIÓN DEL ARBOL DEFINICIONAL
% Recorre la lista de pares Reglas,PosDem y tiene exito si PosDem=[], para
% todas las reglas.
noHayDem([]).
noHayDem([(_,[])|Xs]):-noHayDem(Xs).
%
%
%
%
devuelve en Pos la primera posicion unif demandada en caso de existir
falla en otro caso.
hacemos un recorrido por todas las pos de Vpos hasta encontrar una que
este demandada por todas las reglas y fallamos si no la encotramos.
% uniformDem(_,[],_):-!,fail.
uniformDem(Dem,[P|_],P):perteneceAtodas(P,Dem),!.
uniformDem(Dem,[_|Resto],PosUD):uniformDem(Dem,Resto,PosUD).
% P es una lista de enteros que representa una posicion,
% Dem es una lista de pares regla/listaPC, donde listaPc es a su vez una
% lista de pares posicion/constructora
perteneceAtodas(_,[]).
perteneceAtodas(Pos,[(_,ListaPc)|Dem]):pertenece(Pos,ListaPc),
perteneceAtodas(Pos,Dem).
pertenece(Pos,[(P,_)|R]):(P==Pos,!);
(pertenece(Pos,R)).
/***************************************************************************
CONSTRUCCION DEL CASE
***************************************************************************/
%
%
%
%
tenemos una pos uniformemente demandada Pos y desarrollamos el case
primero asociamos las reglas con la constructora que demandan
QQ es una lista de la forma
[(Constructora/Aridad,[Reglas que la demandan en Pos]),...]
233
hazCase(Patron,Dem,Pos,Dds):asocConsReglas(Dem,Pos,[],QQ),
construyeCase(Patron,Pos,QQ,Dds).
%
%
%
%
%
%
asocConsReglas(lista pares regla/listaPc ,Posicion,In,Out),
listaPc esta formada por pares Posicion/constructora
busca el nodo
correspondiente a la posicion Pos en cada regla, y lo inserta en la lista
In de modo que al final en out tengamos una lista de pares
constructora/lista de reglas que tienen esa constructora en la pos Pos.
asocConsReglas([],_,L,L).
asocConsReglas([(Regla,ListaPc)|Rdem],PosUD,Asoc,Rasoc):buscaConsDem(ListaPc,PosUD,Cons),
% buscamos la cons=Nom/Ar asoc
insertaRegla(Regla,Cons,Asoc,Asoc1),
% la insertamos al final por
% mantener el orden en la trad
asocConsReglas(Rdem,PosUD,Asoc1,Rasoc).
buscaConsDem([(Pos,Cons)|_],PosUD,Cons):-Pos==PosUD,!.
buscaConsDem([_|Resto],PosUD,Cons):-buscaConsDem(Resto,PosUD,Cons).
% busca en la lista de reglas la constructora C. Si la encuentra, a~
nade la
% nueva regla a la lista asociada a C, y si no, crea un nuevo par al final.
insertaRegla(Regla,Cons,[],[(Cons,[Regla])]). % nuevo par al final
insertaRegla(Regla,Cons,[(Cons,[L1|R1])|R],[(Cons,[L1|R11])|R]):!,
insertaFinal(Regla,R1,R11). % mantenemos el orden de las reglas
% por conservar el orden en la traducc
insertaRegla(Regla,Cons,[L|R],[L|R1]):insertaRegla(Regla,Cons,R,R1).
% inserta un elemento al final de una lista.
insertaFinal(Regla,[],[Regla]).
insertaFinal(Regla,[L|R],[L|R1]):-insertaFinal(Regla,R,R1).
% construye la estructura case(Pos,Patron,ListaCasos), donde lista de casos
% esta formada por pares Constructora/lista de reglas
234
APÉNDICE G. CONSTRUCCIÓN DEL ARBOL DEFINICIONAL
construyeCase(Patron,Pos,Asociaciones,case(Pos,Patron,ListaCasos)):hazListaCasos(Patron,Pos,Asociaciones,ListaCasos).
% devuelve la lista de ddt asociados a un case en forma de pares
% (constructora, ddt asociado).
hazListaCasos(_,_,[],[]).
hazListaCasos(Patron,Pos,[(C/Ar,Reglas)|RestoAsoc],[(C,Caso)|RestoCasos]):nuevoPatron(Patron,Pos,C/Ar,NP),
% las posiciones son deducibles. Esto es bastante mejorable
posVarsPatron(NP,Vpos),
dds(NP,Reglas,Vpos,Caso),
hazListaCasos(Patron,Pos,RestoAsoc,RestoCasos).
% genera un nuevo Patron a partir de uno dado, cambiando la variable de la
% posicion Pos por la constructora Cons.
nuevoPatron(Patron,[],C/Ar,NArg):var(Patron),
functor(NArg,C,Ar).
nuevoPatron(Patron,[Pos|R],Cons,NP):arg(Pos,Patron,Arg),
nuevoPatron(Arg,R,Cons,Narg),
cambiaArg(Patron,Pos,Narg,NP).
% cambiaArg es como el argrep de Arity
cambiaArg(Patron,Pos,Narg,NP):Patron=..[Nom|Args],
sustituyeArg(Pos,Args,Narg,Nargs),
NP=..[Nom|Nargs].
sustituyeArg(1,[_|R],Narg,[Narg|R]).
sustituyeArg(N,[Ar|R],Narg,[Ar|R1]):-N1 is N-1,sustituyeArg(N1,R,Narg,R1).
% devuelve la lista de posiciones de las variables de Patron. Una posicion
% es una lista de naturales. devuelve [[]] si Patron es variable y [] si
% el patron no tiene variables
235
posVarsPatron(P,[[]]):-var(P),!.
posVarsPatron(P,Vpos):P=..[_|Args],
(Args==[],!,Vpos=[];
listaPosVarsPatron(Args,1,Vpos)).
listaPosVarsPatron([],_,[]).
listaPosVarsPatron([Ar|R],Nar,VposT):posVarsPatron(Ar,Vpos),
encab(Nar,Vpos,VposAr),
N1 is Nar+1,
listaPosVarsPatron(R,N1,R1),
concat(VposAr,R1,VposT).
encab(_,[],[]).
encab(N,[L|R],[NL|NR]):-encabLst(N,L,NL),encab(N,R,NR).
encabLst(N,[],[N]).
encabLst(N,[E|R],[N,E|R]).
concat([],L,L).
concat([X|R],L,[X|Ls]):-concat(R,L,Ls).
/***************************************************************************
CONSTRUCCION DEL OR
***************************************************************************/
% construye un termino de la forma or(lista opciones).
% le pasamos el patron, Vpos y la lista de pares regla/pos demandadas por ella
hazOr(Patron,Dem,or(LAlts)):%
%
%
%
hacemos una particion del cto de reglas por la posicion que demandan
primero extraemos todas las que o no demandan ninguna posicion o su
cabeza no casa con el patron. Todas ellas las metemos en Reduce.
En Dem1 queda el resto de reglas
sacaReduce(Patron,Dem,Dem1,Reduce),
APÉNDICE G. CONSTRUCCIÓN DEL ARBOL DEFINICIONAL
236
%
%
%
%
%
%
%
Con el resto de Reglas hacemos una particion por las posiciones que
demandan. En Particion tenemos una lista de pares (Pos,reglas que la
demandan). Realmente aparecen las reglas cuya primera posicion
demandada es Pos; esto es equivalente por el orden que tenemos a
tomar primero todas las que demandan la primera, luego todas las que
demandan la segunda...(siempre dentro de las posibles demandadas) y es
mas eficiente.
hazParticion(Patron,Dem1,Particion),
% y ahora... generamos los arboles correspondientes a cada alternativa
% y a Reduce.
hazListaAlternativas(Patron,Particion,Reduce,LAlts).
% saca Reduce(Patron,In,Out1,Out2). En In le pasamos todas las reglas con las
% posiciones que demandan. En out2 devuelve las que no demandan nada o las que
% tienen una cabeza que no casa con el patron. En Out1 devolvemos las restantes
sacaReduce(_,[],[],[]).
sacaReduce(Patron,[(Rule,LstPosDem)|R],[(Rule,LstPosDem)|R1],Reduce):casa(Rule,Patron),
LstPosDem\==[],
!,
sacaReduce(Patron,R,R1,Reduce).
sacaReduce(Patron,[(Rule,_)|R],R1,[Rule|Reduce]):sacaReduce(Patron,R,R1,Reduce).
casa(rule(Cab,_,_,_,_),Patron):- \+ (\+ Cab=Patron).
/*
% comprueba que son unificables pero no les unifica.
casa(rule(Cab,_,_,_,_),Patron):- \+ nocasa(Cab,Patron).
%&& guarreria con corte.
nocasa(T1,T2):-T1=T2,!,fail.
nocasa(_,_).
*/
% hazParticion(Patron,In,Out). Le pasamos en In1 todas las reglas que
% demandan alguna posicion y cuya cabeza casa con el patron.En Out devolvemos
% la lista de pares Posicion/reglas que la demandan en primer lugar.
hazParticion(_,[],[]).
237
hazParticion(Patron,[(Rule,[(Pos,Cons)|Rpos])|R],
[(Pos,[(Rule,[(Pos,Cons)|Rpos])|RDem])|Part]):extraeReglasPos(Pos,R,Resto,RDem),
hazParticion(Patron,Resto,Part).
% extraeReglasPos(Pos,Demanda,Resto,Out). Recorre la lista de Demanda y devuelve
% en Out las que demandan Pos como primera poscion. Deja en Resto las restantes
extraeReglasPos(_,[],[],[]).
extraeReglasPos(Pos,[(Rule,[(Pos,Cons)|Rpos])|R],R1,
[(Rule,[(Pos,Cons)|Rpos])|RDemPos]):!,
extraeReglasPos(Pos,R,R1,RDemPos).
extraeReglasPos(Pos,[Par|R],[Par|R1],DemPos):extraeReglasPos(Pos,R,R1,DemPos).
% hace los casos que tiene en particion y luego hace el try que tiene en Reduce
hazListaAlternativas(_,[],[],[]).
hazListaAlternativas(Patron,[],Reduce,[Dds]):hazTry(Patron,Reduce,Dds).
hazListaAlternativas(Patron,[(Pos,Reglas)|R],Reduce,[DdsP|DdsR]):hazCase(Patron,Reglas,Pos,DdsP),
hazListaAlternativas(Patron,R,Reduce,DdsR).
/************************************************************************/
% CONSTRUCCION DEL TRY
/************************************************************************/
hazTry(Patron,Reglas,try(Patron,L)):hazListaTry(Patron,Reglas,L).
238
APÉNDICE G. CONSTRUCCIÓN DEL ARBOL DEFINICIONAL
hazListaTry(_,[],[]).
hazListaTry(Patron,[rule(Cab,Cpo,Rest,_,_)|Rr],[si(Rest,Cpo)|Rrsi]):Cab=Patron,
hazListaTry(Patron,Rr,Rrsi).
/************************************************************************/
% PREDICADOS PARA DEPURACION
/************************************************************************/
%% DIBUJITO DEL ARBOL
% Saca en pantalla una representacion "legible" (eso creo) del arbol.
% Hay que pasarle una llamada generica o patron inicial de la funcion y
% el propio arbol.
pinta(Patron,Dds):nl,write(’Patron: ’),write(Patron),nl,nl,esc(Dds,0).
esc(case(Pos,P,L),N):-!,
tab(N),write(’** case ’),write(Pos),write(’ Patron ’),
write(P),nl,N1 is N+5,escListaCase(L,N1).
esc(or(L),N):-!,
tab(N),write(’** or ’),nl,N1 is N+2,escListaOr(L,N1).
esc(try(Patron,L),N):-tab(N),write(’Patron: ’),write(Patron),
nl,N1 is N+2,escListaTry(L,N1),nl.
escListaCase([],_):-!.
escListaCase([(C,L)|R],N):-!,
tab(N),write(C),write(’: ’),nl,N1 is N+3,esc(L,N1),escListaCase(R,N).
escListaOr([],_):-!.
escListaOr([L|R],N):-
239
tab(N),esc(L,N),escListaOr(R,N).
escListaTry([],_):-!.
escListaTry([si(Con,Cpo)|R],N):-!,
tab(N),write(Cpo),write(’ <= ’),write(Con),nl,escListaTry(R,N).
240
APÉNDICE G. CONSTRUCCIÓN DEL ARBOL DEFINICIONAL
Apéndice H
Generación de código
/************************************************************************
LLAMADA AL MODULO:
generaCodigoFinal(
+Tree
% arbol generado con el modulo dds.pl
+NomFun % nombre de la funcion asociada al arbol
-CodF
% Codigo asociado a la funcion en forma de lista
de pares (Cabeza,cuerpo), donde cuerpo es una lista
de atomos
*************************************************************************/
/************************************************************************/
% GENERACION DE CODIGO.
/************************************************************************
Este modulo funciona con el arbol definitorio como entrada. Genera el codigo
asociado a la funcion que describe el arbol dds y funciona analizando la
estructura del del arbol en profundidad, por lo que la salida que produce
esta solo parcialmente ordenada, en un principio, e.d., todo predicado (func)
que es llamado por otro aparece despues de este, pero dos predicados del mismo
nombre no tienen pq aparecer contiguos. Esto puede provocar problemas en
algunos prolog por lo que al final les ordenaremos. No obstante,
aprovecharemos la propiedad antes citada para hacer el orden completo.
Para "subir las constructoras" y generar un codigo mas eficiente muchos de
los predicados llevan como ultimo argumento la "cascara". Una cascara
asociada a un nodo puede tomar tres tipos de valor:
- un termino formado con una constructora. Significa que todos los
nodos por debajo tienen una cascara comun que es la actual
- on: hasta ahora no hay restricciones sobre la cascara, por lo que
al formar una nueva cascara con otra dada, automaticamente tomara el valor
de la dada.
- off: no hay cascara comun para los nodos que estan por debajo y por
lo tanto no es posible hacer esta optimizacion.
241
APÉNDICE H. GENERACIÓN DE CÓDIGO
242
Tambien se hace un unfolding de los predicados, e.d., si un predicado
p llama a otro q y q solo tine una clausula, q desaparece y la llamada en p
se sutituye por el cuerpo de q.
14-3-93
Ademas se realizan otras dos optimizaciones:
- ** QUITADA (20-6-97) ** Indexacion por el argumento del que acabamos
de hallar una forma normal de cabeza. P.e.
f(X,Y,H):-hnf(Y,HY),f_2(HY,X,H).
el orden de los argumentos se altera en la llamada a f_2 con el fin de
aprovechar la indexacion de Sicstus (por el functor del primer argumento).
- Se quitan las constructoras inutiles. P.e:
f(suc(X),Y,H):-hnf(Y,HY),f_2(HY,X,H).
la constructora suc no se propaga a f_2, porque ya no aporta informacion
en f_2 y nos ahorramos el trabajo de la unificacion.
20-6-96
Cuando una variable se unificaba al unificar la llamada a un predicado con la
cabeza de este, no se comprobaba que dicha unificacion era posible de acuerdo
con el almacen de restricciones. P.e.:
f(X,H,Cin,Cout):-hnf(X,HX,Cin,Cout1),f_1(HX,H,Cout1,Cout).
f_1(a,H,Cin,Cout):- ....
La forma normal de cabeza de X puede ser una variable y se unifica con la
constructora a sin comprobar que esto es posible. Para corregirlo se genera el codigo:
f(X,H,Cin,Cout):-hnf(X,HX,Cin,Cout1),f_1(HX,H,Cout1,Cout).
f_1(X,H,Cin,Cout):-unifyHnfs(X,a,Cin,Cout1),....
El codigo de unifyHnfs esta en toycomm.pl
Con esto desaparece la indexacion.
Otra cosilla: muchos predicados (los que contienen ramas or como el or paralelo)
pueden generar codigo de la forma:
or....
or(A,B,H,Cin,Cout):-hnf(B,HB,Cin,Cout1),unifyHnfs(HB,true,Cout1,Cout).
...
Esto ocurre al hacer unfolding y este codigo es reemplazable (y se reemplaza) por:
or....
or(A,B,H,Cin,Cout):-hnf(B,true,Cin,Cout).
Esto se detecta al hacer los unfolding en las ramas case
243
************************************************************************/
% Devuelve el codigo asociado la funcion NomFun con arbol Tree en CodFun
generaCodigoFinal(Tree,NomFun,CodF):generaCodigo(Tree,NomFun,[],Cod,_),
ordenaCodigo(Cod,CodF).
/* generaCodigo(+Tree,+NomFun,+UltPosDem,-Cod,+-Cascara).
- Tree es un arbol generado por dds.
- NomFun es el nombre de la funcion para la que estamos generando codigo
- UltPosDem es la ultima posicion que ha sido demandada. Inicialmente es
[] y (*** YA NO SIRVE PARA ESTO 20-6-96 *** sirve para hacer la optimizacion de
indexar por el argumento del que acabamos de hallar una hnf
- Cod es la lista de clausulas generadas.
- Cascara es la cascara explicada arriba para optimizar */
/******************************************************************************/
%
RAMAS OR
/******************************************************************************/
% En el caso del or simplemente generamos codigo para cada una de las opciones
% La primera clausula nunca unificara inicialmente con un arbol pero puede
% hacerlo en sucesivas llamadas recursivas.
generaCodigo(or([]),_,_,[],on):-!.
generaCodigo(or([Tree|R]),NomFun,UltPosDem,Cod,C):!,
generaCodigo(Tree,NomFun,UltPosDem,Cod1,C1),
generaCodigo(or(R),NomFun,UltPosDem,Cod2,C2),
hazCascara(C1,C2,C),
concat(Cod1,Cod2,Cod).
/******************************************************************************/
%
RAMAS CASE
/******************************************************************************/
244
APÉNDICE H. GENERACIÓN DE CÓDIGO
/*
generaCodigo(case(...),NomFun,UltPosDem,OutCodigo,OutCascara)
Aqui generamos codigo para un arbol (o subarbol) de la forma case(...). En Nomfun
llevamos el nombre del predicado que vamos a generar. Los sucesivos predicados
tendran el nombre NomFun_Pos_Cons, donde Pos es la posicion demandada que aparece
en el case y Cons es la constructora inutil que hemos quitado (si es que se ha
hecho). En OutCodigo devolvemos el codigo generado. En este predicado se genera
directamente solo una clausula, las demas se generan como resultado de la llamada
a generacodigoCasos. Esta clausula en ppio. tiene la forma Cab:-hnf(...),LLam.
E.d. hacemos la fnc de la pos demandada y llamamos al predicado
de Cabeza NomFun_Pos (Llam).
Despues estudiamos la posibilidad de hacer un unfolding, que sera factible
cuando tengamos un case con un solo caso y ademas este caso sea:
- otro case, o
- un try con una sola alternativa.
En esta situacion en lugar de la llamada Llam colocamos el cuerpo de la unica
clausula cuya cabeza encaja con Llam. Ademas unificamos Llam=CabCaso, simplemente
para unificar las variables que aparecen en ambos y hacer coherente el unfolding.
Si podemos hacer el unfolding tenemos en cuenta que el cuerpo de la clausula
puede comenzar por un unifyHnfs, en cuyo caso nos cargamos este unifyHnfs (ya hace
el trabajo hnf) y hacemos coherentes los almacenes y resto de variables.
Si no tenemos la suerte de poder hacer el unfolding, respetamos las clausulas
generadas en generaCodigoCasos y las dejamos tal cual.
*/
generaCodigo(case(Pos,Patron,ListaCasos),NomFun,UltPosDem,
[(Cab,Cpo)|RestoCod],C):!,
% por si las moscas
Patron=..[_Nom|Args],
% destripamos el patron para sacar los
% argumentos
%construimos la cabeza de la clausula.
construyeCabeza(NomFun,Args,H,UltPosDem,Cons,Var,Cin,Cout,Cab),
% el nombre del predicado de la llamada
hazNombreFun(NomFun,Pos,Cons,NomFunLla),
% la llamada en si
construyeLlamada(NomFunLla,Args,H,Pos,VarPos,HnfPos,Cout2,Cout,Llam),
% vemos si tenemos que hacer alguna unificacion de variable con
245
% constructora, en cuyo caso metemos un unifyHnfs. Luego se hace la
% forma normal de cabeza del argumento demandado y luego... el
% RestoCuerpo
(
var(Var),
!,
Cpo=[unifyHnfs(Var,Cons,Cin,Cout1),
hnf(VarPos,HnfPos,Cout1,Cout2)|RestoCpo]
;
Cpo=[hnf(VarPos,HnfPos,Cin,Cout2)|RestoCpo]
),
% generamos el codigo para los subarboles del case
generaCodigoCasos(ListaCasos,NomFunLla,Pos,[(CabCaso,CpoCaso)|Rcod],C),
(
% aqui vemos si es posible hacer el unfolding
(ListaCasos=[(_,case(_,_,_))];ListaCasos=[(_,try(_,[_]))]),
!,
(
% Si al hacer el unfolding nos va a quedar y unifyHnfs
% y luego un hnf, dejamos solo el hnf, que ya hace todo
% el trabajo
CpoCaso=[unifyHnfs(_,L,_,CoutUnif)|RestoCpoCaso],
!,
% unificacion de variables para hacer coherente la
% eliminacion de unifyHnfs
HnfPos=L,
Cout2=CoutUnif,
RestoCpo=RestoCpoCaso
;
RestoCpo=CpoCaso
% en vez de la llamada ponemos
% el cuerpo de la clausula a la
% que llama
),
RestoCod=Rcod,
Llam=CabCaso
% para unificar variables
;
% no es posible el unfolding: somos respetuosos
RestoCpo=[Llam],
RestoCod=[(CabCaso,CpoCaso)|Rcod]
),
% si por debajo de este nodo tenemos una cascara comun, la ponemos en
% vez de H, si no, nos aguantamos sin subir la constructora
APÉNDICE H. GENERACIÓN DE CÓDIGO
246
((C\==off,
H=C)
;
true
).
/* construyeCabeza: la cabeza de una clausula viene tiene como nombre el nombre
que llevamos construido hasta ahora (con el inicial, las posiciones que se han
ido demandando y las constructoras que se han ido quitando. Reemplazamos la
constructora Cons que se demanda en el patron por una nueva variable Var, que
luego en el cuerpo de la clausula se unificaran mediante un unifyHnfs .
reemplazaArgumentoLista extrae el termino (una constructora) que ocupa una
poscion dada y genera una nueva lista de argumetos en la que se ha sustituido
dicho termino por una nueva variable Var. Ademas en la cabeza aparece como
ultimo argumento H, que es el resultado de la evaluacion de la funcion.
construyeCabeza devuelve ademas Cons que es el functor del termino por el que
se indexa y que servira para concatenar su functor al predicado de llamada y
llevar cuenta asi de las constructoras que se han ido quitando y no haya
ambiguedades en el nombre de los predicados */
% esta primera clausula solo se usa cuando el arbol correspondiente a una
% funcion comienza por case y solo se usa una vez
construyeCabeza(NomFun,Args,H,[],[],[],Cin,Cout,Cab):concat(Args,[H,Cin,Cout],Aux),
Cab=..[NomFun|Aux].
construyeCabeza(NomFun,Args,H,UltPosDem,Cons,Var,Cin,Cout,Cab):reemplazaArgumentoLista(Args,UltPosDem,Var,Cons,Args1),
variablesTermino(Args1,[]/LstVars),
concat(LstVars,[H,Cin,Cout],Aux),
Cab=..[NomFun|Aux].
/* construyeLlamada es parecido a construyeCabeza, salvo que ahora la posicion
que interesa es aquella de la se acaba de hallar una hnf. */
construyeLlamada(NomFunLla,Args,H,Pos,VarPos,HnfPos,Cin,Cout,Llam):reemplazaArgumentoLista(Args,Pos,HnfPos,VarPos,Args1),
variablesTermino(Args1,[]/LstVars),
concat(LstVars,[H,Cin,Cout],Aux),
Llam=..[NomFunLla|Aux].
% Hacemos un recorrido por la lista de casos y devolvemos en una lista el
% codigo que genera cada uno de ellos
247
generaCodigoCasos([],_,_,[],on).
generaCodigoCasos([(_,Tree)|R],NomFun,PosAnt,Cod,C):generaCodigo(Tree,NomFun,PosAnt,CodT,C1),
generaCodigoCasos(R,NomFun,PosAnt,Rcod,C2),
concat(CodT,Rcod,Cod),
hazCascara(C1,C2,C).
% reemplazaArgumentoLista(Lst,Pos,ArgNew,Arg,LstNew)
% Reemplaza en una lista de terminos, la posicion Pos, que contiene una
% termino Arg por un nuevo ArgNew. La nueva lista se devuelve en LstNew.
reemplazaArgumentoLista([Vant|Rar],[1],V,Vant,[V|Rar]):-!.
reemplazaArgumentoLista([Ar|Rar],[1|Rpos],V,Vant,[Nar|Rar]):!,
Ar=..[Nom|Args],
reemplazaArgumentoLista(Args,Rpos,V,Vant,Args1),
Nar=..[Nom|Args1].
reemplazaArgumentoLista([Ar|Rar],[N|Rpos],V,Vant,[Ar|Rar1]):N1 is N-1,
reemplazaArgumentoLista(Rar,[N1|Rpos],V,Vant,Rar1).
% saca la lista de variables distintas de un termino ordenadas por orden de
% aparicion en dicho termino
variablesTermino(T,Vin/Vout):var(T),
!,
insertaVar(T,Vin/Vout).
variablesTermino(T,Vin/Vout):T=..[_|Args],
variablesLista(Args,Vin/Vout).
variablesLista([],Vin/Vin).
variablesLista([C|R],Vin/Vout):variablesTermino(C,Vin/Vout1),
variablesLista(R,Vout1/Vout).
insertaVar(V,[]/[V]).
insertaVar(V,[C|R]/[C|R]):V==C,!.
insertaVar(V,[C|R]/[C|R1]):-
APÉNDICE H. GENERACIÓN DE CÓDIGO
248
insertaVar(V,R/R1).
/******************************************************************************/
%
RAMAS TRY
/******************************************************************************/
% El codigo asociado al try es la union de los codigos asociados a sus
% alternativas.
generaCodigo(try(Patron,Alts),NomFun,UltPosDem,Cod,C):generaCodigoAlts(Patron,Alts,NomFun,UltPosDem,Cod,C).
generaCodigoAlts(_,[],_,_,[],on).
generaCodigoAlts(Patron,[si(Restr,Val)|R],NomFun,UltPosDem,[(Cab,Cpo)|Rcod],C):Patron=..[_|Args],
% constrimos la cabeza de la clausula igual que en el case
construyeCabeza(NomFun,Args,H,UltPosDem,Cons,Var,Cin,Cout,Cab),
% construimos el codigo asociado a las restricciones
hazRestr(Restr,ListaEqs,Cin1,Cout1,compilacion),
(
% si lo que devuelve la funcion es una variable, hacemos su hnf
var(Val),
insertaFinal(hnf(Val,H,Cout1,Cout),ListaEqs,Cpo1)
;
% si no puede ser una constructora, que meteremos en H o una lla
% mada a una funcion que ira al final
Val=..[Nombre|As],
% en cualquier caso suspendemos las posibles llamadas a funcio
% nes que aparezcan por dentro.
meteSuspensionesLista(As,ValSusp,compilacion),
(
% si es una constructora le metemos suspensiones y la
% colocamos como ultimo argumento del predicado
esConstructor(Val,compilacion),
H=..[Nombre|ValSusp],
Cpo1=ListaEqs,
249
Cout=Cout1
;
% si es una funcion colocamos la llamada al final del
% predicado
name(Nombre,NombreN),
%concat(NombreN,Cons,NombreL),
name(NombreFun,[36|NombreN]), % 36 es el ascii de $
concat(ValSusp,[H,Cout1,Cout],Aux),
Llam=..[NombreFun|Aux],
insertaFinal(Llam,ListaEqs,Cpo1)
)
),
% si se necesita se mete por delante un unifyHnfs igual que en el case
(var(Var),!,Cpo=[unifyHnfs(Var,Cons,Cin,Cin1)|Cpo1]
;
Cpo=Cpo1,Cin1=Cin),
generaCodigoAlts(Patron,R,NomFun,UltPosDem,Rcod,C1),
hazCascara(Val,C1,C).
% el ultimo argumento nos indica si estamos en tiempo de ejecucion o de compi
% lacion, puesto que los cdata de comp son const en ej y los ftype son funct
meteSuspensiones(Term,Term,_):-var(Term),!.
meteSuspensiones(Term,TermSusp,Tiempo):Term=..[Nom|Args],
meteSuspensionesLista(Args,ArgsSusp,Tiempo),
(
esConstructor(Term,Tiempo),
% miramos si es Constructora
!,
TermSusp=..[Nom|ArgsSusp]
;
name(Nom,L),name(NomF,[36|L]),
TermSusp=..[’$$susp’,NomF,ArgsSusp,_,_]
).
meteSuspensionesLista([],[],_):-!.
meteSuspensionesLista([L|R],[L1|R1],Tiempo):meteSuspensiones(L,L1,Tiempo),
meteSuspensionesLista(R,R1,Tiempo).
% es constructora si es una constructora o si es una funcion aplicada
250
%
%
%
%
%
APÉNDICE H. GENERACIÓN DE CÓDIGO
parcialmente. El parametro Tiempo indica si la llamda se realiza en tiempo
de compilacion o de ejecucion. En compilacion tenemos cdata(...) y fun(...)
y en ejecucion tenemos cons y funct. En este modulo se usa en tiempo de
compilacion pero cuando se lanzan objetivos, las suspensiones se meten en
tiempo de ejecucion.
esConstructor(Term,_):-number(Term).
esConstructor(Term,Tiempo):functor(Term,Nom,Ar),
(
(Nom==’$eqFun’;Nom==’$notEqFun’),Ar<2
;
Tiempo==compilacion,
(
cdata(Nom,_,_,_),!
;
fun(Nom,ArP,_,_),!,Ar<ArP
;
primitive(Nom,_,_),
!,
ftype(Nom,ArP,_,_),
Ar<ArP
)
;
Tiempo==ejecucion,
(const(Nom,_,_,_),!
;
funct(Nom,ArP,_,_,_),!,Ar<ArP)
).
%
%
%
%
%
Por el momento las unicas restricciones permitidas son las de igualdad y
desigualdad, que se traducen en el predicado equal y notEqual resp.
Se puede optimizar mas aun el codigo. El caso de que uno de los argumentos
sea true se puede generalizar a que sea una constructora cualquiera y se
puede hacer un hnf orientado
hazRestr([],[],Cin,Cin,_):-!.
hazRestr([T1==T2|R],[Restr|Rr],Cin,Cout,Tiempo):!,
meteSuspensiones(T1,T1Susp,Tiempo),
meteSuspensiones(T2,T2Susp,Tiempo),
(
(T1Susp==true;T1Susp==false),
251
nonvar(T2Susp),
T2Susp=..[’$$susp’,NomFun,Args,_,_],
!,
concat(Args,[T1Susp,Cin,Cout1],ArgsFun),
Restr=..[NomFun|ArgsFun]
;
(T2Susp==true;T2Susp==false),
nonvar(T1Susp),
T1Susp=..[’$$susp’,NomFun,Args,_,_],
!,
concat(Args,[T2Susp,Cin,Cout1],ArgsFun),
Restr=..[NomFun|ArgsFun]
;
Restr=equal(T1Susp,T2Susp,Cin,Cout1)
),
hazRestr(R,Rr,Cout1,Cout,Tiempo).
hazRestr([’/=’(T1,T2)|R],[notEqual(T1Susp,T2Susp,Cin,Cout1)|Rr],
Cin,Cout,Tiempo):!,
meteSuspensiones(T1,T1Susp,Tiempo),
meteSuspensiones(T2,T2Susp,Tiempo),
hazRestr(R,Rr,Cout1,Cout,Tiempo).
%
%
%
%
%
%
%
CONSTRUCCION DE LAS CASCARAS
estos predicados sacan la cascara comun a dos terminos, e.d., estudia las
constructoras comunes de los dos terms. El flag on indica que el termino
admite cualquier cascara (depende del otro termino). Aparece como fin de
algunas llamadas recursivas. El flag off indica que en las ramas que hay
por debajo no ha sido posible construir cascara comun y por lo tanto ahora
tampoco
hazCascara(C1,C2,off):-(var(C1);var(C2)),!.
hazCascara(off,_,off):-!.
hazCascara(_,off,off):-!.
hazCascara(C1,on,C):!,
(esConstructor(C1,compilacion),C=C1;
C=off).
hazCascara(on,C1,C):!,
(esConstructor(C1,compilacion),C=C1;
252
APÉNDICE H. GENERACIÓN DE CÓDIGO
C=off).
hazCascara(C1,C2,C):(
esConstructor(C1,compilacion),
esConstructor(C2,compilacion),
C1=..[Nom|Args1],
C2=..[Nom|Args2],
listaCascaras(Args1,Args2,Lc),
!,
C=..[Nom|Lc]
;
C=off
).
listaCascaras([],[],[]):-!.
listaCascaras([L1|R1],[L2|R2],[Lc|Rc]):cascara(L1,L2,Lc),
listaCascaras(R1,R2,Rc).
% es igual que hazCascara, pero si hazCascara devuelve off (no tienen cascara
% comun) aqui devolvemos una nueva variable, pq en este punto ya sabemos que
% tienen cascara comun, y si internamente no la tienen hay que meter una var
cascara(C1,C2,C):hazCascara(C1,C2,Cas),
(
Cas==off,
C=_
;
C=Cas
).
% Concatena NomFun con la lista de numeros de pos separados por .’s y precedida
% por _ Por ejemplo hazNombre(hola_1.2,[3,4,5],Sal) nos devuelve en
% Sal hola_1.2_3.4.5
hazNombreFun(NomFun,Pos,Cons,NomFun1):unePos(Pos,Res),
name(NomFun,NomFunN),
% 95 es el ascii de _
253
(
Cons==[],
!,
Aux=[95|Res]
;
functor(Cons,F,_),
name(F,ConsN),
concat([95|Res],[95|ConsN],Aux)
),
concat(NomFunN,Aux,Aux1),
name(NomFun1,Aux1).
% Transforma una lista en la cadena formada por sus eltos separados por .’s
%&&
unePos([P],S):-!,name(P,S).
unePos([P|R],Sal):name(P,S),
unePos(R,Resto),
concat(S,".",R1),
concat(R1,Resto,Sal).
insertaLst(Var,[],[Var]).
insertaLst(Var,[V|R],[V|R1]):(V==Var,!,R1=R;
insertaLst(Var,R,R1)).
/************************************************************************/
% ORDENACION DEL CODIGO.
ordenaCodigo(Cod,CodOr):ordenaCodigoPares(Cod,[],CodPares),
aplanaCodigo(CodPares,CodOr).
ordenaCodigoPares([],Ac,Ac).
ordenaCodigoPares([(Cab,Cpo)|R],Ac,CodOr):functor(Cab,Nombre,_),
insertaPred((Cab,Cpo),Nombre,Ac,Ac1),
ordenaCodigoPares(R,Ac1,CodOr).
insertaPred((Cab,Cpo),Nombre,[],[(Nombre,[(Cab,Cpo)])]).
insertaPred((Cab,Cpo),Nombre,[(Nombre,L)|R],[(Nombre,L1)|R]):!,
insertaFinal((Cab,Cpo),L,L1).
254
APÉNDICE H. GENERACIÓN DE CÓDIGO
insertaPred(Par,Nombre,[Par2|R],[Par2|R1]):insertaPred(Par,Nombre,R,R1).
aplanaCodigo([],[]).
aplanaCodigo([(_,L)|R],Res):-aplanaCodigo(R,R1),concat(L,R1,Res).
/************************************************************************/
/************************************************************************/
% PREDICADOS PARA DEPURACION
/************************************************************************/
% SALIDA DEL CODIGO A PANTALLA DE FORMA + O - LEGIBLE
sacaCodFuns([]):-!.
sacaCodFuns([CodFun|R]):-sacaCodFunN(CodFun),sacaCodFuns(R).
sacaCodFunN([(Cab,Cpo)|R]):functor(Cab,Nom,_),
write(’% CODIGO PARA LA FUNCION ’),write(Nom),nl,
sacaCod([(Cab,Cpo)|R]).
sacaCod([]):-!.
sacaCod([(Cab,Cpo)|R]):-!,write(Cab),write(’ :- ’),sacaCpo(Cpo),sacaCod(R).
sacaCpo([]):-!,write(’.’),nl.
sacaCpo([C|R]):-!,write(C),(R\==[],write(’, ’);true),sacaCpo(R).
Apéndice I
Salida de respuestas
El siguiente código es un extracto del archivo goals.pl.
/*****************************************************************************
SALIDA DE SOLUCIONES
*****************************************************************************/
/* La salida de la solucion se hace asi (se intenta minimizar, esto esto es no
introducir nuevas variables que no sean estrictamente necesarias para dar la
respuesta): primero mostramos todas las igualdades entre variables del
objetivo, respetando los nombres que les dio el usuario. Luego sacamos en
pantalla las igualdes entre las vars del objetivo los terminos construidos a
los que se han ligado. Notese que una variable no puede ligarse
simultaneamente a otra variable y a un termino construido, por lo que los dos
casos anteriores son disjuntos.
Si una variable no se ha ligado durante la ejecucion del objetivo a otra
variable del objetivo ni a un termino construido de momento no hemos sacado
ninguna informacion sobre ella.
Por ultimo sacamos las restricciones de desigualdad y las restricciones sobre
los reales (si estamos trabajando con el resolutor). Para ello primero
extraemos las variables relevantes: las que aparecen en el objetivo mas las
que aparecen en restricciones no lineales sobre los reales. */
% En vars esta la lista de variables del objetivo con los nombres de usuario y
% en Residuo esta todo el residuo que ha quedado tras la computacion:
% desigualdades entre variables y restricciones sobre los reales.
sacaRespuesta(Vars,Cout,Residuo):current_output(Handle),
% necesitamos el handle de la salida
% donde queremos escribir porque escribe
% atomo funciona asi.
nl,tab(6),write(’yes’),
255
APÉNDICE I. SALIDA DE RESPUESTAS
256
% igualdades entre vars del objetivo. En L llevamos cuenta de las vars
% aparecidas hasta el momento con sus nombres para posteriores
% apariciones
sacaVars(Vars,[]/Terms,[]/L),
%nl,
% igualdades entre vars y terminos
sacaTerms(Handle,Terms,L/L1),
% residuo
sacaRestricciones(Handle,Vars,Cout,Residuo,L1/_).
% SALIDA DE IGUALDADES ENTRE VARIABLES
%
%
%
%
%
%
sacaVars([(Nombre,VarProlog).....],[(Nombre,TermConstruido)],Lin/Lout).
Inicialmente Lin es []. Cuando aparece una variable simplemente la metemos
junto con su nombre en Lout. Cuando aparece otra variable (con otro nombre)
pero que es la misma variable prolog, sacamos una igualdad entre los nombres
Ademas vamos construyendo una lista de variables ligadas a terminos para
sacarlas luego.
sacaVars([],Terms/Terms,L/L).
sacaVars([(Nom,Val)|R],Terms/Terms1,L/L2):var(Val),
!,
(yaEsta(Val,L,Nom1),!,L1=L,sacaIgualdadVars(Nom1,Nom);
L1=[(Val,Nom)|L]),
sacaVars(R,Terms/Terms1,L1/L2).
sacaVars([T|R],Terms/[T|Terms1],L/L1):sacaVars(R,Terms/Terms1,L/L1).
yaEsta(X,[(Y,Nom)|R],Nom1):(X==Y,!,Nom1=Nom;
yaEsta(X,R,Nom1)).
sacaIgualdadVars(X,Y):nl,tab(6),write(X),write(’ == ’),write(Y).
% SALIDA DE IGUALDADES DE VARIABLES Y TERMINOS
% Salida de variables ligadas a terminos
sacaTerms(_,[],L/L).
257
sacaTerms(Handle,[(Nom,Val)|R],L/L2):nl,tab(6),write(Nom),write(’ == ’),
escribeAtomo(Handle,L/L1,Val),
sacaTerms(Handle,R,L1/L2).
% SALIDA DE RESTRICCIONES
% Las restricciones se sacan: primero las desigualdades entre variables, luego
% si estamos trabajando con el resolutor sacamos las restricciones sobre reales
sacaRestricciones(Handle,Vars,Cout,noclpr,L/M):term_variables(Vars,Rel),
sacaDesigs(Handle,Rel,Cout,[],_,L/M).
sacaRestricciones(Handle,Vars,Cout,clpr(Residuo),L/M):extraeVarsRelClpr(Vars,Residuo,Rel),
quitaRedundancias(Cout,Cout1),
sacaDesigs(Handle,Rel,Cout1,[],_,L/M1),
sacaRestrClpr(Rel,Residuo,M1/M).
% SALIDA DE DESIGUALDADES SINTACTICAS
sacaDesigs(_,_,[],_,_,L/L).
sacaDesigs(Handle,Rel,[X:CX|R],Rin,Rout,L/M):(
%var(X),
esRelevante(X,Rel),
!,
sacaDesigsVar(Handle,Rel,X,CX,Rin,Rout1,L/M1),
sacaDesigs(Handle,Rel,R,Rout1,Rout,M1/M)
;
sacaDesigs(Handle,Rel,R,Rin,Rout,L/M)
).
258
APÉNDICE I. SALIDA DE RESPUESTAS
sacaDesigsVar(_,_,_,[],Rin,Rin,L/L).
sacaDesigsVar(Handle,Rel,X,[T|R],Rin,Rout,L/M):(
esRelevante(T,Rel),
insertaDesig(X,T,Rin,Rout1),
!,
nl,tab(12),write(’{ ’),
escribeAtomo(Handle,L/M1,X),
write(’ /= ’),
escribeAtomo(Handle,M1/M2,T),
write(’ }’),
sacaDesigsVar(Handle,Rel,X,R,Rout1,Rout,M2/M)
;
sacaDesigsVar(Handle,Rel,X,R,Rin,Rout,L/M)
).
insertaDesig(X,T,Rin,Rout):X@<T,!,insertaDesig1(X,T,Rin,Rout)
;
insertaDesig1(T,X,Rin,Rout).
insertaDesig1(X,Y,[],[(X,Y)]).
insertaDesig1(X,Y,[(A,B)|R],[(A,B)|R1]):(A\==X;B\==Y),insertaDesig1(X,Y,R,R1).
% SALIDA DE RESTRICCIONES SOBRE LOS REALES
%
%
%
%
%
%
%
Aqui hay un pequenio truco para sacar las restricciones asociadas a los reales
Nosotros queremos que el sistema haga la proyeccion, que se hace con el dump
pero en este momento no funcionaria directamente hacer un dump porque ya no
hay objetivos suspendidos y el "almacen de restricciones" esta vacio. Lo que
hacemos es relanzar al sistema el conjunto de restricciones sobre reales y
hacer entonces el dump. Ademas, como queremos dejar el sistema limpio, este
relanzamiento lo hacemos por medio de un call_residue.
sacaRestrClpr(Relevantes,Residuo,L/M):call_residue(relanzaResiduo(Relevantes,Residuo,L/M),_).
relanzaResiduo(Relevantes,Residuo,L/L2):% relanzamos el residuo
map_call(Residuo),
259
% hacemos una lista con los nombres asociados a las vars relevantes para
% el propio dump de nombre a dichas variables
hazListaNombresVarsRel(Relevantes,NomRel,L/L1),
linear:dump(Relevantes,NomRel,ResClpr),
% aqui insertamos la escritura de las restricciones
escribeRestriccionesClpr(ResClpr,L1/L2).
escribeRestriccionesClpr([],L/L).
escribeRestriccionesClpr([C|R],L/L2):nominaVarsTermino(C,C1,L/L1),
nl,tab(12),write(’{ ’),write(C1),write(’ }’),
escribeRestriccionesClpr(R,L1/L2).
hazListaNombresVarsRel([],[],L/L).
hazListaNombresVarsRel([Var|R],[Nom|RNom],L/L2):buscaInsertaVar(Var,L/L1,Nom),
hazListaNombresVarsRel(R,RNom,L1/L2).
/********** NOMINACION DE VARIABLES DE UN TERMINO
***************/
/* Este predicado da nombre a todas las variables que aparecen en un
termino. Si la variable ha aparecido anteriormente ya tendra un
nombre que es el que se le dara y si no, se le da uno nuevo y se
almacena para posteriores apariciones. Las variables junto con sus
nombres se almacenan en una lista, que se va pasando de una clausula
a otra y se va completando. buscaInsertaVar se ocupa de ver si la
variable tiene nombre (ya ha aparecido), que es el que devolvera. Si
no ha aparecido le da un nombre nuevo y la almacena junto con este
en la lista que se le pasa*/
nominaVarsTermino(X,Nom,L/L1):var(X),
!,
buscaInsertaVar(X,L/L1,Nom).
nominaVarsTermino(T,T1,L/L1):T=..[Nom|Args],
(
Nom==’=\=’,!,Nom1=’/=’
;
APÉNDICE I. SALIDA DE RESPUESTAS
260
Nom==’=’,!,Nom1=’==’
;
Nom1=Nom),
nominaVarsListaTerms(Args,Args1,L/L1),
T1=..[Nom1|Args1].
nominaVarsListaTerms([],[],L/L).
nominaVarsListaTerms([T|R],[T1|R1],L/L2):nominaVarsTermino(T,T1,L/L1),
nominaVarsListaTerms(R,R1,L1/L2).
/*****************************************************************************
EXTRACCION DE VARIABLES RELEVANTES DE LA RESPUESTA
*****************************************************************************/
/* extraeVarsRel(Vars,Residuo,Rel).
- Vars es la lista variables del objetivo y viene de la forma
[(Nombre,Term) ...], donde Nombre es el nombre de la variable y Term el
termino al que se ha ligado una vez que se ha resuelto el objetivo.
- Residuo es la lista de objetivos suspendidos que resulta de la
ejecucion. Para la extraccion de vars relevantes solo nos interesan los
residuos no lineales
- Rel es lo que devuelve el predicado y es una lista de variables
(las relevantes).
Una variable es relevante si aparece en un termino que se ha
ligado a una variable del objetivo (el termino puede ser una variable
prolog) o bien, si aparece en una restriccion no lineal. De la primera
parte se encarga extraeVarsRelObj y de la segunda extraeVarsResiduo */
261
extraeVarsRelClpr(Vars,Residuo,Rel):extraeRestrNonLin(Residuo,NonLin),
term_variables((Vars,NonLin),Rel).
extraeRestrNonLin([],[]).
extraeRestrNonLin([(_-(nonlin:C))|R],[nonlin:C|R1]):!,
extraeRestrNonLin(R,R1).
extraeRestrNonLin([_|R],R1):extraeRestrNonLin(R,R1).
% TERMINOS RELEVANTES
esRelevante(Term,L):-var(Term),!,member(Term,L).
esRelevante(Term,L):Term=..[_|Args],
esRelevanteLst(Args,L).
esRelevanteLst([],_).
esRelevanteLst([Term|R],L):esRelevante(Term,L),
esRelevanteLst(R,L).
% estos predicados sirven para depurar un poco la salida. Lo que hacen es quitar
% repeticiones de restricciones repetidas.
quitaRedundancias([],[]).
quitaRedundancias([V:C|R],[V:C1|R1]):quitaRedundanciasLista(C,C1),
quitaRedundancias(R,R1).
262
APÉNDICE I. SALIDA DE RESPUESTAS
quitaRedundanciasLista([],[]).
quitaRedundanciasLista([L|R],R1):member(L,R),
!,
quitaRedundanciasLista(R,R1).
quitaRedundanciasLista([L|R],[L|R1]):quitaRedundanciasLista(R,R1).
%miembro(X,[Y]):-!,X==Y.
%miembro(X,[L|R]):-(X==L,!;miembro(X,R)).
Bibliografı́a
[AAF+ 98]
E. Albert, M. Alpuente, M. Falaschi, P Julián, and G. Vidal. Improving
control in functional logic program specialization. In To appear in Proc.
SAS’98. Springer LNCS, 1998.
[AB94]
K.R. Apt and R. Bol. Logic programming and negation: A survey.
Journal of Logic Programming, 19&20, 1994.
[AJ93]
J. M. Almendros-Jiménez. Diseño de un sistema de tipos con polimorfismo paramétrico y desarrollo de un inferidor para hobabel. Trabajo
de Investigación de Tercer Ciclo, Dpto. de Informática y Automática,
Universidad Complutense de Madrid, Jun. 1993.
[Ant92]
S. Antoy. Definitional trees. In Proc. of the 3rd International Conference
on Algebraic and Logic Programming, pages 143–157. Springer LNCS
632, 1992.
[Apt90]
K.R Apt. Logic programming. In J van Leeuwen, editor, Handbook of
Theoretical Computer Science, volume B, pages 495–574. Elsevier, 1990.
[AG94]
P Arenas-Sánchez and A. Gil-Luezas. User’s manual for bablog. Technical report, Depto. de Informática y Automática, UCM, October
1994.
[AGL94]
P. Arenas-Sánchez, A. Gil-Luezas, and F.J. López-Fraguas. Combining
lazy narrowing with disequality constraints. In Proc. of the 6th International Symposium on Programming Language Implementation and
Logic Programming, pages 385–399. Springer LNCS 844, 1994.
[AHL+ 96]
P. Arenas-Sánchez, T. Hortalá-González, P. López-Fraguas, and
E. Ullán-Hernández. Real constraints within a functional logic language. In Join Conference on Declarative Programming, APPIA-GULPPRODE’96, pages 451–462, 1996.
[AR97a]
P. Arenas-Sánchez and M. Rodrı́guez-Artalejo. A semantic framework
for functional logic programming with algebraic polymorphic types. In
Proc. CAAP’97. Springer LNCS, 1997.
[AR97b]
P. Arenas-Sánchez and M. Rodrı́guez-Artalejo. A lazy narrowing calculus for functional logic programming with algebraic polimorfic types.
In ILPS’97, pages 53–69. MIT Press, 1997.
263
264
BIBLIOGRAFÍA
[BCM89]
P.G. Bosco, C. Cecchi, and C. Moiso. An extension of wam for kleaf: a wam-based compilation of conditional narrowing. In Proc. Sixth
International Conference on Logic Programming (Lisboa), pages 318–
333. MIT Press, 1989.
[BMP+ 90]
R. Barbuti, P. Mancarella, D. Pedreschi, and F. Turini. A transformational approach to negation in logic programming. Journal of Logic
Programming (8), pages 201–228, 1990.
[Car95]
B. Carlson. Compiling and Executing Finite Domain Constraints. PhD
thesis, Upsala University, 1995.
[CF93]
P.H. Cheong and L. Fribourg. Implementation of narrowing: The prologbased approach. In K.R. Apt, J.W. de Bakker, and J.J.M.M. Rutten,
editors, Logic programming languages: constraints, functions, and objects, pages 1–20. MIT Press, 1993.
[Cha88]
D. Chan. Constructive negation based on the completed database. In
Proc. 5th Conference on Logic Programming & 5th Symposium on Logic
Programming (Seattle), pages 111–125, 1988.
[Che90]
P.H. Cheong. Compiling lazy narrowing into prolog. Technical report
25, LIENS, Paris, 1990. To appear in Journal of New Generation Computing.
[Cla78]
K.L. Clark. Negation as failure. In H. Gallaire and J. Minker, editors,
Logic and Data Bases, pages 293–322. Plenum Press, 1978.
[CM87]
W.F. Clocksin and C.S. Mellish. Programming in Prolog. Springer,
third rev. and ext. edition, 1987.
[Coh90]
J. Cohen. Constraint logic programming languages. Communications
of the ACM, 33(7):52–68, 1990.
[Col90]
A. Colmerauer. An introduction to prolog iii. Communications of the
ACM, 33(7):69–90, 1990.
[CR98]
R. Caballero-Roldán. Parsers lógico funcionales. Trabajo de Investigación de Tercer Ciclo, Dpto. de Sistemas Informáticos y Programación,
Universidad Complutense de Madrid,, Sep. 1998.
[CLS97]
R. Caballero-Roldán, F.J. López-Fraguas, and J. Sánchez-Hernández.
User’s manual for T OY. Technical Report 97/57, Depto. SIP, UCM
Madrid, 1997.
[DM79]
N. Dershowitz and Z. Manna. Proving termination with multiset orderings. Communications of the ACM, 22(8):465–476, 1979.
[DM82]
L. Damas and R. Milner. Principal type-schemes for functional programs. In Proc. 9th Annual Symposium on Principles of Programming
Languages, pages 207–212, 1982.
BIBLIOGRAFÍA
265
[FHK+ 93]
T. Frühwirth, A. Herold, V. Küchenhoff, T. Le Provost, P. Lim, E. Monfroy, and M. Wallace. Constraint logic programming – an informal introduction. Technical report ecrc-93-5, ECRC, 1993.
[Gin91]
M.L. Ginsberg. Negative subgoals with free variables. Journal of Logic
Programming, 11:271–293, 1991.
[GM86]
E. Giovannetti and C. Moiso. A completeness result for e-unification
algorithms based on conditional narrowing. In Proc. Workshop on Foundations of Logic and Functional Programming, pages 157–167. Springer
LNCS 306, 1986.
[G94]
F.C. González Moreno. Programación Lógica de Orden Superior con
Combinadores. PhD thesis, DIA-UCM, Madrid, 1994.
[GHL+ 96]
J.C. González-Moreno, M.T. Hortalá-González, F.J. López-Fraguas,
and M. Rodrı́guez-Artalejo. A rewriting logic for declarative programming. In Proc. ESOP’96, pages 156–172. Springer LNCS 1058, 1996.
[GHL+ 98]
J.C. González-Moreno, T. Hortala-González, F. López-Fraguas, and
M. Rodrı́guez-Artalejo. An approach to declarative programming based
on a rewriting logic. To appear in Journal of Logic Programming, 1998.
[GHR97]
J.C. González-Moreno, M.T. Hortalá-González, and M. Rodrı́guezArtalejo. A higher order rewriting logic for functional logic programming. In ICLP’97, pages 153–167. MIT Press, 1997.
[Gro96]
The Programming Systems Group. SICStus Prolog User’s Manual. Swedish Institute of Computer Science, PO Box 1263. S-164 28 Kista, Sweden, 3# 5 edition, October 1996.
[Gro97]
The Programming Systems Group. SICStus Prolog User’s Manual. Swedish Institute of Computer Science, PO Box 1263. S-164 28 Kista, Sweden, 3# 6 edition, November 1997.
[Han93]
M. Hanus. Analysis of nonlinear constraints in clp(∇). In Proc. Tenth
International Conference on Logic Programming, pages 83–99. MIT
Press, 1993.
[Han94]
M. Hanus. The integration of functions into logic programming: From
theory to practice. Journal of Logic Programming, 19&20:583–628,
1994.
[Han95a]
M. Hanus. Compile-time analysis of nonlinear constraints in clp(∇).
New Generation Computing, 13(2):155–186, 1995.
[Han95b]
M. Hanus. Efficient translation of lazy functional logic programs into
prolog. In Proc. Fifth International Workshop on Logic Program Synthesis and Transformation, pages 252–266. Springer LNCS 1048, 1995.
[He97]
M. Hanus (ed.). Curry: An integrated functional logic language. Available at http://www-i2.informatik.rwth-aachen.de/~hanus/curry,
1997.
BIBLIOGRAFÍA
266
[HFP97]
P. Hudak, J. H. Fasel, and J. Peterson. A Gentle Introduction to Haskell
-version 1.4-, March 1997.
[HLS+ 97]
T. Hortalá-González, F.J. López-Fraguas, J. Sánchez-Hernandez, and
E. Ullán-Hernández. Declarative programming with real constraints.
Technical report, SIP-5997, 1997.
[HJM+ 91]
N. Heintze, J. Jaffar, S. Michaylov, P. Stuckey, and R. Yap. The CLP(R)
Programmer’s Manual, Version 1.1. IBM Thomas J. Watson Research
Center, Yorktown Heights, 1991.
[HKM95]
M. Hanus, H. Kuchen, and J.J. Moreno-Navarro. Curry: A truly functional logic language. In Proc. ILPS’95 Workshop on Visions for the
Future of Logic Programming, 1995.
[Hol95]
C. Holzbaur. Ofai clp(q,r) manual, edition 1.3.3. Technical report,
Austrian Research Institute for Artificial Intelligence, Vienna, 1995.
[Hon92]
H. Hong. Non-linear real constraints in constraint logic programming.
In Proc. of the 3rd International Conference on Algebraic and Logic
Programming, pages 201–212. Springer LNCS 632, 1992.
[Hus92]
H. Hussmann. Nondeterministic algebraic specifications and nonconfluent term rewriting. Journal of Logic Programming, 12:237–255, 1992.
[Hus93]
Hussmann. Non-Determinism in Algebraic Specifications and Algebraic
Programs. Birkhäuser Verlag, 1993.
[JJ97]
M.P Jones and Peterson J.C. Hugs 1.4. the nottingham and yale haskell
user’s system. Technical report, University of Nottingham and University of Yale, April 1997.
[JL87]
J. Jaffar and J.-L. Lassez. Constraint logic programming. In Proc. of
the 14th ACM Symposium on Principles of Programming Languages,
pages 111–119, Munich, 1987.
[JM94]
J. Jaffar and M.J. Maher. Constraint logic programming: A survey.
Journal of Logic Programming, 19&20:503–581, 1994.
[JMS+ 92a]
J. Jaffar, S. Michaylov, P.J. Stuckey, and R.H.C. Yap. An abstract
machine for clp(∇). In Proc. SIGPLAN Conference on Programming
Language Design and Implementation, pages 128–139. SIGPLAN Notices, Vol. 27, No. 7, 1992.
[JMS+ 92b]
J. Jaffar, S. Michaylov, P.J. Stuckey, and R.H.C. Yap. The clp(∇)
language and system. ACM Transactions on Programming Languages
and Systems, 14(3):339–395, 1992.
[Jon]
M.P. Jones. An Introduction to Gofer.
[Jon94]
M.P. Jones. Qualified types: theory and practice. Cambridge University
press, 1994.
BIBLIOGRAFÍA
267
[KLM+ 92]
H. Kuchen, F.J. López-Fraguas, J.J. Moreno-Navarro, and
M. Rodrı́guez-Artalejo. Implementing a lazy functional logic language with disequality constraints.
In Proc. of the 1992 Joint
International Conference and Symposium on Logic Programming. MIT
Press, 1992.
[Kun87]
K. Kunen. Negation in logic programming. Journal of Logic Programming, 4:289–308, 1987.
[L92]
F.J. López Fraguas. A general scheme for constraint functional logic
programming. In Proc. of the 3rd International Conference on Algebraic
and Logic Programming, pages 213–227. Springer LNCS 632, 1992.
[L94]
F.J. López Fraguas. Programación Funcional y Lógica con Restricciones. PhD thesis, DIA-UCM, Madrid, 1994.
[LR91]
F.J. López Fraguas and M. Rodrı́guez-Artalejo. An approach to constraint functional logic programming. Technical report dia/91/4, Universidad Complutense, Madrid, 1991.
[LLR93]
R. Loogen, F. Lopez-Fraguas, and M. Rodrı́guez-Artalejo. A demand
driven computation strategy for lazy narrowing. In Proc. of the 5th
International Symposium on Programming Language Implementation
and Logic Programming, pages 184–200. Springer LNCS 714, 1993.
[Llo94]
J.W. Lloyd. Combining functional and logic programming languages. In
Proc. of the International Logic Programming Symposium, pages 43–57,
1994.
[Llo95]
J.W. Lloyd. Declarative programming in escher. Technical report cstr95-013, University of Bristol, 1995.
[LW91]
R. Loogen and S. Winkler. Dynamic detection of determinism in functional logic languages. In Proc. of the 3rd Int. Symposium on Programming Language Implementation and Logic Programming, pages 335–346.
Springer LNCS 528, 1991. Extended version to appear in Theoretical
Computer Science, 1995.
[LW95]
R. Loogen and S. Winkler. Dynamic detection of determinism in functional logic languages. Theoretical Computer Science 142, pages 59–87,
1995.
[M94]
J.J. Moreno-Navarro. Default rules: An extension of constructive negation for narrowing-based languages. In Proc. Eleventh International
Conference on Logic Programming, pages 535–549. MIT Press, 1994.
[M96]
J.J. Moreno-Navarro. Extending constructive negation for partial functions in lazy functional-logic languages. In Proc. 5th International
Workshop on Extensions of Logic Programming, pages 213–227. Springer LNAI 1050, 1996.
268
BIBLIOGRAFÍA
[MR89]
J.J. Moreno-Navarro and M. Rodrı́guez-Artalejo. Logic programming
with functions and predicates: The language babel. Technical report
dia/89/3, Universidad Complutense, Madrid, 1989.
[MR92]
J.J. Moreno-Navarro and M. Rodrı́guez-Artalejo. Logic programming
with functions and predicates: The language babel. Journal of Logic
Programming, 12:191–223, 1992.
[O’K90]
R.A. O’Keefe. The Craft of Prolog. Cambridge, MIT Press, 1990.
[PH97]
J. Peterson and K. (eds) Hammond. Report on the Programming Language Haskell: a Non-strict, Purely Functional Language -version 1.4-,
January 1997.
[PJ93]
J. Peterson and M.P. Jones. Implementing type classes. In Proc. of
ACM SIGPLAN SYmposium on Programming Language Design and
Implementation (PLDI’93), pages 227–236. ACM SIGPLAN Notices
Vol. 28, No. 6, 1993.
[Red85]
U.S. Reddy. Narrowing as the operational semantics of functional languages. In Proc. IEEE Internat. Symposium on Logic Programming,
pages 138–151, Boston, 1985.
[SS86]
L. Sterling and E. Shapiro. The Art of Prolog. MIT Press, 1986.
[Stu91]
P.J. Stuckey. Constructive negation for constraint logic programming.
In Proc. LICS’91, pages 328–339, 1991.
[V89]
P. Van Hentenryck. Constraint Satisfaction in Logic Programming. MIT
Press, 1989.
[Wad85]
P. Wadler. How to replace failure by a list of successes. In Functional
Programming and Computer Architecture. Springer LNCS 201, 1985.
Descargar