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.