Documentos de Académico
Documentos de Profesional
Documentos de Cultura
vars K N : int .
eq cont-frac(N) = cont-frac-
rec(1, N) .
ceq cont-frac-rec(K, N) = K /
((K * K) + cont-frac-rec((K + 1),
N)) if(K<=N) .
ceq cont-frac-rec(K, N) = 0
if(K>N) .
.....
ARANGO I. F.
Aplicaciones de la Lógica al Desarrollo de Software:
Lenguajes Lógicos y Funcionales.
ISBN:
MATERIA:
Informática 681.3
Formato: Digital
DERECHOS RESERVADOS
2015 respecto a la primera edición, por:
FERNANDO ARANGO ISAZA
Profesor Asociado,
Facultad de Minas,
Universidad nacional de Colombia,
Sede Medellín.
ISBN:
TABLA DE CONTENIDO
Contenido
CAPÍTULO 1 ................................................................................................................................................................ 1
INTRODUCCIÓN. .................................................................................................................................... 2
LA MÁQUINA DE ESTADOS. .................................................................................................................. 3
1.2.1 Vista Interna del Computador .................................................................................................... 3
1.2.2 El Proceso Computacional. ........................................................................................................ 5
1.2.3 La Máquina de Estados. ............................................................................................................. 5
LOS PROGRAMAS DEL COMPUTADOR.................................................................................................... 6
LENGUAJES DE PROGRAMACIÓN ........................................................................................................... 8
EVOLUCIÓN DE LOS LENGUAJES DE PROGRAMACIÓN. ........................................................................... 9
1.5.1 Primera Generación: Lenguaje de Máquina. ............................................................................ 9
1.5.2 Segunda Generación: Lenguaje Ensamblador. ....................................................................... 10
1.5.3 Tercera Generación: Lenguajes Procedurales. ....................................................................... 11
1.5.4 Cuarta Generación: SQL y Lenguajes Declarativos. .............................................................. 14
1.5.5 Quinta Generación: Uso de la Lógica. .................................................................................... 15
RESUMEN DEL CAPÍTULO. ................................................................................................................... 16
CAPÍTULO 2 .............................................................................................................................................................. 19
INTRODUCCIÓN ................................................................................................................................... 20
PARADIGMA DE INSTRUCCIONES. ........................................................................................................ 21
PARADIGMA DE FUNCIONES (O PROCESOS). ........................................................................................ 21
2.3.1 Especificación de Funciones. ................................................................................................... 22
2.3.2 Arquitectura funcional. ............................................................................................................. 24
2.3.3 Proceso de desarrollo. .............................................................................................................. 27
PARADIGMA DE ENTIDADES (O ESTRUCTURACIÓN DE DATOS). .......................................................... 28
2.4.1 Estructuración de los datos en archivos. .................................................................................. 28
2.4.2 Problemas con los Archivos y Aparición de los Gestores de Bases de Datos. ......................... 29
2.4.3 Modelo Relacional. ................................................................................................................... 30
2.4.4 La especificación formal bajo el modelo Relacional. ............................................................... 31
2.4.5 Arquitectura Relacional. .......................................................................................................... 32
PARADIGMA DE OBJETOS. ................................................................................................................... 35
2.5.1 Concepto Clásico de Objeto. .................................................................................................... 36
2.5.2 Relaciones entre objetos. .......................................................................................................... 38
2.5.3 Aplicaciones como Sistemas Dinámicos. .................................................................................. 38
2.5.4 Especificación formal bajo el modelo OO. ............................................................................... 39
2.5.5 Arquitectura Objetual. .............................................................................................................. 39
PARADIGMA DE AGENTES. .................................................................................................................. 41
RESUMEN DEL CAPÍTULO. ................................................................................................................... 42
CAPÍTULO 3 .............................................................................................................................................................. 45
INTRODUCCIÓN ................................................................................................................................... 46
LA LÓGICA COMO UNA DISCIPLINA DE RAZONAMIENTO. ................................................................... 46
3.2.1 Lógica en la antigua Grecia. .................................................................................................... 47
3.2.2 La lógica en la matemática moderna. ...................................................................................... 48
LA LÓGICA COMO UNA DISCIPLINA DE LOS LENGUAJES. .................................................................... 49
3.3.1 Sintaxis. .................................................................................................................................... 49
3.3.2 Semántica. ................................................................................................................................ 51
3.3.3 Inferencia. ................................................................................................................................. 52
LÓGICA DE PROPOSICIONES. ............................................................................................................... 53
3.4.1 Proposiciones. .......................................................................................................................... 53
3.4.2 Sintaxis de la lógica proposicional. .......................................................................................... 53
3.4.3 Semántica.en lógica proposicional. .......................................................................................... 58
3.4.4 Inferencia en lógica proposicional. .......................................................................................... 62
3.4.5 Formas Normales. .................................................................................................................... 67
RESUMEN DEL CAPÍTULO. ................................................................................................................... 70
ii Tabla de Contenido.
1 Dicha variante sintáctica puede considerarse como “azúcar sintáctica” sobre dicha lógica [Bourbaki 50].
PREFACIO viii
óptica particular de cada uno de los lenguajes considerados. De los capítulos del libro
original, se preservaron algunos apartes relativos a la lógica, junto con las figuras y los
ejemplos.
El trabajo, por otro lado, no puede considerarse un producto terminado. Aparte de las
obligadas revisiones a los ejemplos, nuevos ejercicios y nuevos lenguajes, aún faltan
secciones importantes. Principalmente en lo planeado para los tomos II y III que pretenden
recoger los elementos propios de la especificación, verificación y refinamiento del
software, en el marco del uso de FRAMEWORKs de programación, especializados en
soportar la traducción directa a lenguajes procedurales, de especificaciones declarativas.
El texto es auto contenido y se orienta a ser usado como un medio autónomo de aprendizaje
y consulta2. También ha venido siendo utilizado como texto guía en el curso “Lenguajes
Lógicos y Funcionales”, que ha estado a cargo del autor, en el Departamento de Ciencias de
la Computación y de La Decisión (DCCD) de la Facultad de Minas, Universidad Nacional
de Colombia, Sede Medellín.
A continuación se presenta una descripción básica del contenido y objeto de cada capítulo,
señalando los que son fundamentales, y los que pueden ser obviados en una primera lectura.
Los elementos que conforman el curso referido arriba, junto con los talleres y códigos que
lo soportan, se pueden consultar en la intranet del DCCD 3.
La organización del texto es la siguiente:
Los capítulos 1 y 2 presentan una visión general de los paradigmas de
desarrollo del software y de los lenguajes de programación: Estos capítulos
le dan sentido al tópico desarrollado en el texto, y, en particular, permiten
reconocer la importancia de la lógica en el marco general del desarrollo del
software. Ellos están dirigidos a lectores con poca información sobre el
tema y pueden ser obviados por lectores más avanzados
Los capítulos 3 y 4 presentan los elementos de la lógica de proposiciones y
de la lógica de predicados que son relevantes a los desarrollos posteriores.
El capítulo se centra en los aspectos sintácticos y semánticos de las lógicas,
sin profundizar en los temas de consistencia y demostración. Se recomienda
una lectura rápida de repaso a los lectores avanzados
El capítulo 5 presenta la lógica ecuacional como fundamento de los
lenguajes funcionales. Primero, se enfatizan los criterios formativos para los
términos y las reglas que determinan su estructura sintáctica, luego se
presentan los criterios de demostración asociados con el predicado de
igualdad, y por ultimo, se muestran en detalle los elementos propios de una
demostración por derivación ecuacional. Este capítulo es clave en el
contexto de los lenguajes funcionales y no debe ser obviado por lector
alguno. El capítulo 5 presenta,además, los Sistemas de Reescritura de
Términos (SRT) como una forma de automatizar la demostración en Lógica
Ecuacional, que, si bien no alcanza el potencial demostrativo de dicha
lógica, da cuenta de los lenguajes funcionales. Se plantean los conceptos de
substitución particularización, emparejamiento, y reemplazo; junto con la
relación de reescritura entre términos, y la propiedad de Chrch_Roser
(confluencia y terminancia). El discurso, mas allá del la sección 2, es
abstracto, formal y preciso, y su lectura no es necesaria para abordar los
capítulos que siguen.
2Incluye, en adición a las referencias obligadas, un conjunto importante de referencias WWW (principalmente de
Wikipedia), que sirven como vías de acceso a la literatura que expande los tópicos mencionados.
3 Actualmente en desarrollo, puede verse un prototipo con los contenidos del curso en la pagina del autor:
http://xue.medellin.unal.edu.co/farango/
INTRODUCCIÓN x
El capítulo 6 introduce los dos lenguajes funcionales que serán usados en el
resto del texto, los lenguajes MIT SCHEME y MAUDE. Luego de
presentar su origen y contexto histórico, se indica la manera de usar los
intérpretes respectivos, y, por último se muestran los cálculos que pueden
efectuarse con los operadores y tipos nativos que ofrecen. La lectura de este
capítulo es indispensable para quienes desean ejecutar los ejemplos y
programar los ejercicios.
El capítulo 7 presenta los principios y facilidades que ofrecen los lenguajes
para definir nuevos operadores y aplicar mecanismos de selección en los
cálculos. Se asimilan estas facilidades al planteamiento de un conjunto de
aserciones ecuacionales, posiblemente definidas como el consecuente de una
implicación. Se discuten, luego, tópicos relativos a la naturaleza misma de
cada uno de los lenguajes, así: las diferencias entre un lenguaje débilmente
tipado (SCHEME) y un lenguaje fuertemente tipado (MAUDE); los criterios
y modos de controlar la estrategia de evaluación de términos complejos; los
elementos que determinan el medio ambiente de evaluación para los
términos; y por último, los mecanismos de modularización. Este es un
capítulo central en el marco del trabajo.
El capítulo 8 introduce la recursión como elemento fundamental para definir
operadores matemáticos complejos. Se analiza el efecto de la forma del
programa en la eficiencia del proceso de cálculo. La eficiencia se plantea
tanto en términos del espacio de memoria como del tiempo de proceso. Se
introduce la diferencia entre los procesos estables en memoria (iterativos) y
los procesos inestables en memoria (recursivos) como medida de la
eficiencia en el espacio. Se plantean los conceptos básicos de la teoría del
orden como medio para medir la eficiencia del proceso en el tiempo, y se
ilustra con ejemplos prácticos la manera de evaluar y controlar dicha
eficiencia. Por último, se señala e ilustra con ejemplos, la necesidad de
controlar la propagación de los errores causados por inexactitudes en el
cálculo. Este es un capítulo central en el marco del trabajo.
El capítulo 9 presenta los principios y facilidades que ofrecen los lenguajes
para definir nuevos tipos de datos, y en particular, tipos para datos
compuestos. Se señalan e ilustran, con múltiples ejemplos, los elementos
necesarios para definir los nuevos tipos, entre ellos la declaración del tipo,
los constructores y selectores, las invariantes de tipo, los operadores sobre
datos compuestos, etc... Este es un capítulo central en el marco del trabajo.
El capítulo 10 presenta los principios y facilidades que ofrecen los lenguajes
para generalizar los códigos, parametrizándolos por tipo de datos y sus
operaciones asociadas. Se contrasta el enfoque del SCHEME, que se centra
en parametrizar los operadores que se evocan facilitando el paso de
operadores como argumentos de otros operadores; con el enfoque del
MAUDE, que se centra en parametrizar los grupos de tipos o “teorías” que
se usan en otra teoría, facilitando el paso de teorías como argumentos de
otras teorías. Este capitulo es inherentemente complejo y puede obviarse en
una primera lectura del texto.
INTRODUCCIÓN
xi
El capítulo 11 retoma el tema de la lógica, y presenta los elementos
necesarios para darle soporte a los lenguajes lógicos, en particular al
PROLOG. Presenta, primero, el concepto de equivalencia semántica para
las fórmulas de la lógica, y desarrolla los métodos para la normalización de
fórmulas en la lógica proposicional y de predicados. Presenta, luego, el
teorema de la resolución y el método de demostración por reducción al
absurdo, a través de la introducción de resolventes para fórmulas en lógica
clausal. Ilustra el uso del unificador más general para la introducción
correcta de los resolventes en fórmulas de la lógica de predicados. Se
recomienda una lectura rápida de repaso a los lectores avanzados.
El capítulo 12 presenta la resolución SLD como una forma de automatizar la
demostración en lógica clausal para el caso de cláusulas de Horn. Presenta
los elementos del lenguaje PROLOG señalando los elementos clausales que
conforman el programa (hechos y reglas) y los elementos clausales que
conforman los objetivos a ser demostrados en el marco del programa.
Ilustra como la resolución SLD permite demostrar los objetivos por medio
de un proceso de reescritura de los mismos, en el cual existen la “falla” y el
“retroceso”. Este capítulo es clave en el contexto de los lenguajes lógicos y
no debe ser obviado por lector alguno.
El capítulo 13 ilustra las capacidades del PROLOG como un lenguaje
funcional. Muestra la manera de llevar a cabo cálculos aritméticos y de
efectuar operaciones sobre listas, reescribiendo los códigos de los capítulos
correspondientes para los lenguajes funcionales. Este es un capítulo central
en el marco del trabajo.
El capítulo 14 presenta las capacidades del PROLOG como lenguaje de
búsqueda apoyado en la falla y el retroceso del SLD. Muestra, primero, la
capacidad del lenguaje para hacer consultas “inteligentes” sobre bases de
datos, interpretadas como colecciones de hechos, dando soporte a las “Bases
de Datos Deductivas”. Muestra luego la capacidad del lenguaje para
resolver problemas que implican hallar en un espacio de búsqueda los
valores o tuplas de valores que satisfacen una condición dada. Muestra, por
último ejemplos de solución de “acertijos” planteados como reglas de un
programa PROLOG. Este es un capítulo central en el marco del trabajo.
AGRADECIMIENTOS
A Daniel Cabarcas que trabajó en la elaboración de los módulos de clase para el curso
“Lenguajes Declarativos”, y que dieron lugar al libro anterior. A Daniel Cabarcas, David
Cardona, Willington Vega y Jorge Yukio, que, como monitores del curso, contribuyeron a
la definición de ejemplos, casos de estudio y depuración de los códigos. A los estudiantes
de los cursos “Lenguajes Lógicos y Funcionales” y “Lenguajes Declarativos”, que con sus
anotaciones y críticas han contribuido a depurar los códigos y a mejorar las partes oscuras
del texto.
Capítulo 1
Evolución de los Lenguajes de
Programación
Capítulo 1: Evolución de los Lenguajes de Programación
2
Introducción.
A medida que el uso del computador se extiende a casi todas las actividades del mundo
moderno, se requieren cada vez más y mejores programas de aplicación o “software”.
El desarrollo de software, por su parte, requiere de mano de obra especializada y, por lo
general, el esfuerzo requerido para su desarrollo aumenta rápidamente con su complejidad 4.
Es así como, hoy en día, la construcción del software es el cuello de botella en el uso del
computador y el responsable de la mayor parte de los costos. Esta situación ha sido
denominada “crisis del software” [Gibbs 94].
La necesidad de resolver la crisis del software, junto con la importancia económica que se
le adjudica, ha impulsado el rápido desarrollo de una disciplina asociada con el problema
del desarrollo del software5.
Como todas las disciplinas, la disciplina del desarrollo de software ha evolucionado en muy
diversas direcciones. En [Arango 06] se plantea, sin embargo, que en ella se pueden
reconocer tres ejes fundamentales, a saber:
El eje de los paradigmas arquitectónicos, que comprende los diferentes
principios y conceptos que soportan la manera de entender y componer una
pieza de software. Los paradigmas determinan los tipos, o “categorías”, de
elementos que se usan para describir, o “modelar”, el software en sus
diferentes fases de desarrollo
El eje de los lenguajes de programación que comprende los diferentes
principios y conceptos que soportan la estructura y significado de los
elementos usados al especificarle al computador, o “codificar”, una piezas
de software.
El eje de los paradigmas de gestión que recoge los diferentes modos de
proceder para obtener el software. Estos incluyen tanto los modos de
proceder para la definición, planeamiento y control de las fases y etapas en
un proyecto de desarrollo de software, como los modos de proceder en la
construcción de los modelos y códigos asociados con el mismo.
Los cambios históricos en el eje de los lenguajes de programación es examinada de forma
sumaria en este capítulo. La tesis del capítulo es que la tendencia en el desarrollo de los
lenguajes de programación, apunta a la concepción de lenguajes que describen cada vez
más el área de aplicación del software, en contraste con lenguajes que describen el proceso
computacional asociado con la ejecución del software en el computador. En esta tendencia
se destaca la aparición de lenguajes que se apoyan en elementos de una lógica matemática,
permitiendo elaborar programas que constituyen teorías definidas en la lógicas y que al ser
ejecutados se llevan a cabo procesos de demostración.
La evolución de los paradigmas arquitectónicos es examinada en el capítulo siguiente. En
este capítulo se plantea que los lenguajes que se usan durante las fases de análisis, diseño y
La Máquina de Estados.
El “computador” u “ordenador”, es una máquina cuya función básica es la de llevar a cabo
operaciones de almacenamiento, cómputo (y/u ordenamiento), y distribución sobre
elementos de datos.
Desde la óptica de quién usa el computador, la tarea del computador será la de recibir los
datos necesarios para los cómputos, ejecutar los cómputos y devolver (o almacenar y
distribuir) los resultados de la ejecución. Ella es, pues, fundamentalmente una tarea de
transformación de “datos de entrada” en “datos de salida”, o mas simplemente de “datos”
en “resultados”7, que han de ser posteriormente almacenados y distribuidos.
8La unidad de almacenamiento mas usada es el “byte”, que almacena ocho dígitos binarios (u ocho “bits” o “pizcas”) que
equivalen O a un número entre 0 y 255, o dos números consecutivos entre 0 y 15
9 Son memorias de acceso directo (o, Random Access Memory), que pueden “escribirse” y “leerse” y Memorias de sólo
lectura (o, Read Only Memory), que sólo pueden leerse.
10En los computadores modernos, sin embargo, se acelera el acceso a ciertos lugares de esta memoria por medio del uso
de memorias auxiliares muy rápidas (memorias “CACHE”) en las que su contenido se duplica.
11Ya que por un lado algunos de los medios de almacenamiento son removibles e intercambiables, y por el otro, las
computadoras tienden a estar interconectadas formando redes de distribución de datos a nivel planetario.
Capítulo 1: Evolución de los Lenguajes de Programación
5
1.2.2 El Proceso Computacional.
Al llevarse a cabo la transformación definida en un programa se lleva a cabo un “proceso
computacional” (al que con frecuencia nos referiremos simplemente como el “proceso”).
Un proceso computacional se caracteriza por estar compuesto de un conjunto de procesos
más elementales que ocurren en una sucesión parcialmente ordenada en el tiempo. Los
procesos que componen a otro proceso se suceden formando uno o varios “hilos de
ejecución” que ocurren en “paralelo”, y que se unen o dividen en ciertos instantes del
tiempo.
Cada uno de los procesos que componen otro proceso, se compone a su vez, de un conjunto
de procesos aun más elementales, que ocurren, también, en una sucesión parcialmente
ordenada en el tiempo. Esta descomposición, puede extenderse hasta niveles donde los
procesos componentes son operaciones muy elementales, que ocurren al nivel de los
circuitos internos del procesador. Para efectos de nuestra discusión, sin embargo, no
consideraremos niveles de descomposición, mas allá del que descompone los procesos, en
un conjunto finito de operaciones denominadas “operaciones elementales del
procesador” u “operaciones de maquina”.
Lo que hace interesante a las operaciones elementales del procesador, es que para cada una
de ellas se ha definido una cadena finita de dígitos binarios (unos y ceros) denominada
“instrucción del procesador” (o “instrucción de máquina”), que la representa de forma
digital. Una secuencia de instrucciones de máquina, puede entonces, representar una
secuencia de operaciones elementales del procesador, que bien podría constituir un hilo de
ejecución en un proceso. El procesador tiene, además, la capacidad de ejecutar el hilo de
proceso representado por una de tales secuencias, siempre y cuando ella se encuentre
almacenada en la memoria de acceso directo (RAM o ROM).
1.2.3 La Máquina de Estados.
Al irse ejecutando las instrucciones de un programa, se producen, tanto ingresos de datos al
computador, como creación interna de nuevos datos a causa de los cálculos. Estos datos
son colocados y recolocados, en los diferentes lugares de almacenamiento a medida que
aparecen y que se manipulan. El proceso correspondiente a cada instrucción de procesador
produce, en efecto, un cambio específico sobre los datos almacenados. Este cambio es lo
que caracteriza a la instrucción, y lo que la distingue de las demás12.
Así, a medida que el proceso avanza, las memorias del computador atraviesan por una serie
de situaciones discretas o "estados", caracterizados por la información almacenada. Al
iniciarse el proceso, las memorias tienen un estado predeterminado, que va cambiando a
medida que nuevos datos entran y se calculan. Al finalizar el proceso, la sucesión de
estados inducida por el programa, debe haber producido, almacenado y posiblemente
“escrito” en algún dispositivo de salida, los resultados del proceso.
Lo interesante de esta visión de los procesos, es que una manera de caracterizarlos, a
cualquier nivel de descomposición, es caracterizando el cambio en el estado de los
dispositivos de almacenamiento que ellos inducen en la máquina. Tal como se verá más
12De no existir este efecto, la instrucción no llevaría a cabo nada. La única excepción son las instrucciones de salida,
cuyo efecto ocurre en el "exterior" de la máquina.
Capítulo 1: Evolución de los Lenguajes de Programación
6
adelante, la definición de los programas (y de sus partes), se fundamentará, ya sea en
definir de forma explícita los procesos elementales que componen a los procesos macro que
determinan, o en definir de forma implícita el cambio global de estado producido por
dichos procesos macro.
13No implica esto que no pueda utilizar directamente las instrucciones de máquina, sino que utiliza las de la BIOS para
aquellas cosas que ya están programadas allí.
Capítulo 1: Evolución de los Lenguajes de Programación
8
lenguaje muy especializado y lleno de los "tecnicismos", y le que posibilitan el uso de un
lenguaje muy cercano al del contexto del problema.
Aplicaciones: Implementan las operaciones que el usuario usa para darle solución a sus
problemas. Ellos interactúan con el usuario, de formas que pueden ser muy simples o
muy complejas dependiendo de la manera como se hayan implementado las operaciones.
En esta interacción, el usuario solamente hace referencia a los elementos involucrados
en las operaciones (que son los involucrados en el problema), por lo que las aplicaciones
implementan el nivel de comunicación usuario máquina más especializado y el de más
alto nivel posible.
Lenguajes de Programación
Los lenguajes de Programación tienen como objeto, facilitar la elaboración de los
programas que soportan el nivel lingüístico de las Aplicaciones. Son implementados por
medio de programas traductores, que reciben del usuario (o “programador”) la
especificación del programa en el lenguaje de programación, o programa “fuente”, y la
traducen al lenguaje ofrecido para los programas por el SO, o programa “objeto”.
Desde que fue reconocida la importancia de los lenguajes de programación, se ha dedicado
una gran cantidad de esfuerzo e investigación a la concepción de “buenos lenguajes”, y a la
construcción de sus traductores correspondientes. Es así que existen ahora miles de
lenguajes de programación14, que cubren una gama muy amplia de características, y de
formas de abordar el problema. Definir una clasificación para los diversos lenguajes no es,
tampoco, tarea fácil15.
En este texto no se pretende afirmar que un lenguaje o familia de lenguajes es
uniformemente mejor que los demás. Esto es válido no sólo porque el mejor lenguaje para
un propósito no necesariamente es el mejor para otro propósito diferente, sino también
porque cuando aparecen nuevas características que se muestran útiles, tarde o temprano,
son incorporadas en los lenguajes existentes16.
aparecen lenguajes que facilitan elaborar elementos ejecutables que se transmiten por la red y se interpretan por los
“navegadores” (JAVA, PEARL, etc..), al usarse el computador para el control de procesos en tiempo real, aparecen
modelos para la gestión y razonamiento sobre el tiempo; al querer que el computador se comporte como un ser humano,
aparecen modelos para representar el razonamiento, los propósitos, las preferencias y la colaboración.
17 Aunque esto es una función del sistema operativo, vale la pena mencionar que, en computadores antiguos (Vg.
IBM1130) era posible definir cada instrucción con un grupo de 16 palancas e ingresarlas a la máquina presionando un
botón.
18 Tiene una memoria que almacena la dirección de la instrucción que está ejecutando.
19Aumente la dirección de la instrucción en ejecución en el tamaño de la “palabra del procesador” que es un valor
constante y el tamaño de todas las instrucciones de máquina.
Capítulo 1: Evolución de los Lenguajes de Programación
10
Nótese que este tipo de instrucciones de máquina son las que generan una secuencia de
estados en la RAM.
De especial importancia, son las instrucciones de máquina que cambian la dirección de la
instrucción siguiente a ser ejecutada por otra diferente. Ellas le permiten al procesador
ejecutar “saltos” sobre las instrucciones del programa, por lo que ellas no necesariamente se
ejecutan siempre en la secuencia en que aparecen.
Los saltos sobre el programa pueden ser, además, optativos según valores previamente
calculados. Estos “saltos bajo condición” le permiten al programa adaptar la secuencia de
ejecución a los datos del caso en ejecución, ampliando así el conjunto de casos para los
cuales el programa opera correctamente.
Por otro lado, los saltos durante la ejecución de un programa, permiten contar con grupos
de instrucciones que “implementan” servicios específicos que pueden ser utilizados desde
diversos programas y lugares en un programa. A estos grupos de instrucciones se les ha
denominado “subprogramas”. Para utilizar o “evocar” un subprograma basta con saltar a su
grupo de instrucciones desde el lugar donde se requiera el servicio, ejecutar las
instrucciones del subprograma, y “retornar” al lugar original con otro salto hacia atrás20.
Los subprogramas pueden hacer o no parte del programa que los usa y estar incluso “micro
codificados” en memorias de alta velocidad dentro del procesador.
1.5.2 Segunda Generación: Lenguaje Ensamblador.
El primer gran salto adelante sobre el lenguaje de máquina, es la aparición de los programas
“ensambladores” que dan lugar a la programación en “lenguaje ensamblador” (ó
“assembler”).
En un lenguaje ensamblador las componentes de las instrucciones de máquina son
substituidas por símbolos mnemotécnicos21.
Una instrucción de máquina en un procesador de 32 bits, para indicar que se copie el contenido de un
registro del procesador en otro, podría usar una secuencia de dígitos binarios, donde los primeros ocho
dígitos indican que la instrucción es una de mover datos, los segundos ocho dígitos indican cual el
registro fuente de la información, los terceros ocho dígitos indican cual es el registro destino de la
información, y los últimos ocho dígitos no tienen significado alguno. Así, en lenguaje ensamblador esta
instrucción podría lucir de la manera siguiente:
10110000100000000100000000000000
La correspondiente instrucción en lenguaje ensamblador podría lucir de la manera siguiente:
20 Usando la dirección del lugar del programa que llevo a cabo la evocación, que debió haberse almacenado antes del
salto.
21 Ver http://en.wikipedia.org bajo “Assembly Language”.
Capítulo 1: Evolución de los Lenguajes de Programación
11
Algunas de las características más relevantes de un lenguaje ensamblador son las
siguientes:
Los lugares de almacenamiento que usa el programa (en la RAM o dentro
del procesador) pueden ser referidos por medio de rótulos o “nombres de
variable”. Las variables deben ser definidas en el programa antes de ser
utilizadas, asociándolas con un tamaño de memoria definido22. Es
responsabilidad del traductor y de los programas de apoyo la asignación de
direcciones reales para dichos lugares. Esto le evita al programador tener
que relacionarse directamente con la estructura de direcciones físicas de la
máquina.
Un símbolo de operaciones no tienen que representar siempre a una
operación específica del lenguaje de máquina. Es posible que este símbolo
represente una operación compleja que debe llevarse a cabo por con varias
instrucciones de máquina. En este caso el ensamblador puede ya sea llevar a
cabo la substitución de la operación referida por las instrucciones de
máquina que le corresponden (que deben haber sido previamente definidas o
“compiladas”), en cuyo caso el operador constituye una “macro”; o incluir
una evocación al subprograma que le corresponde (que debe haber sido
previamente elaborado), en cuyo caso el operador constituye un
subprograma.
A pesar de ser significativo el avance del lenguaje ensamblador en relación con el lenguaje
de máquina, este sigue estando íntimamente asociado al procesador y a las características
propias del computador objetivo. Por ello los programas en lenguaje ensamblador son poco
transportables entre máquinas y entre versiones del sistema operativo de la máquina.
1.5.3 Tercera Generación: Lenguajes Procedurales.
Con la aparición de los lenguajes de tercera generación, la programación de computadoras
deja de ser un oficio propio de los expertos en el “hardware23” o constructores de máquinas,
y se convierte en un oficio independiente. En efecto, la gran mayoría de lenguajes en uso
actualmente son básicamente lenguajes de tercera generación.
Al igual que los lenguajes de la 1ª y 2ª generación, los lenguajes de la 3ª se orientan a
especificar el proceso computacional, visto como la ejecución de una (o varias) secuencias
de instrucciones que modifican el contenido de la memoria.
Ellos ofrecen, sin embargo, mecanismos nuevos y poderosos para definir los cálculos y
controlar el curso del proceso. En los numerales siguientes introduciremos brevemente
dichos mecanismos.
1.5.3.1 Independencia de la Máquina.
Los lenguajes de tercera generación son concebidos con independencia de la máquina. Así,
Para usarlos el programador no necesita conocer la arquitectura de la máquina, en cuanto a
la estructura de las direcciones de memoria, conexión de periféricos, naturaleza de las
instrucciones del procesador etc...
24 Ver por ejemplo la página web de la ANSI (American National Standars Institute) en http://web.ansi.org.
Capítulo 1: Evolución de los Lenguajes de Programación
13
diferentes conjuntos de instrucciones de máquina requeridos para lleva a cabo cada acción,
y los va incorporando en el programa objeto, a medida que las acciones son referidas en el
programa fuente25.
1.5.3.3 Instrucciones de asignación y de I/O.
El proceso computacional es especificado en los lenguajes de tercera generación, por medio
de secuencias de instrucciones de “asignación” y de “Entrada/Salida” (“I/O”).
Una instrucción de asignación indica la colocación del valor representado por un término
en el lugar de la memoria representado por ya sea por una variable o por una dirección de
memoria (contenida en otra variable).
Una instrucción de Entrada/Salida de datos, indica un intercambio de valores entre los
dispositivos físicos de Entrada/Salida y los lugares de la memoria.
Las instrucciones de asignación y de Entrada/Salida de un programa, se ejecutan en una
secuencia determinada por las instrucciones de “secuenciamiento”. A medida que se
ejecutan dichas instrucciones, la memoria del computador va pasando por una serie de
estados desarrollándose así un proceso computacional.
1.5.3.4 Instrucciones de Secuenciamiento.
Las instrucciones de secuenciamiento determinan la secuencia en que se ejecutan las
instrucciones de asignación y de Entrada/Salida.
Existen en general dos tipos de instrucciones de secuenciamiento, las que ordenan las
instrucciones de forma temporal, y las que indican saltos.
Las instrucciones de asignación y de Entrada/Salida se ordenan, colocándolas una tras otra
separadas por un caracter especial (vg. el caracter “;”, el caracter “.”, o simplemente el
caracter que indica un cambio de línea). El caracter que separa las instrucciones indica
además si ellas se ejecutan en serie o en paralelo. Cuando las instrucciones se ejecutan en
serie, la instrucción que sigue debe esperar a que la anterior termine antes de comenzar a
ejecutarse. Cuando las instrucciones se ejecutan en paralelo, la instrucción que sigue puede
ejecutarse sin esperar a que la anterior haya terminado.
El salto bajo condición del assembler tiene su correspondiente en la mayoría de los
lenguajes de tercera generación (vg. el “if(<condicion>) go to <instruccion>”), e
inicialmente fue la única instrucción de salto disponible.
Las versiones más modernas de los lenguajes de 3ª generación implementan, sin embargo,
las “formas estructuradas de secuenciamiento” (ver Capítulo 2). Estas son básicamente las
siguientes:
Selección: Indican seleccionar entre dos o más grupos alternativos de
instrucciones con base en el valor de uno o varios términos.
Repetición: Indican repetir una y otra vez un grupo de instrucciones
terminando la repetición cuando un término toma un cierto valor al ser
calculado.
25 A los traductores de los lenguajes se les denomina “compiladores” o “intérpretes” según el momento en que incorporan
al programa traducido las instrucciones de máquina correspondientes a los elementos del programa.
Capítulo 1: Evolución de los Lenguajes de Programación
14
Rompimiento: Indican abortar un ciclo de repetición sin ejecutar
completamente el grupo de instrucciones, cuando se cumple una situación
específica en el proceso.
1.5.3.5 Subprogramas y Librerías.
Los lenguajes de tercera generación no sólo acogen el concepto de subprograma, de los
lenguajes de 2ª generación, sino que lo extienden, para estandarizar las formas de
intercambiar datos entre el programa o subprograma que evoca, y el subprograma evocado.
Esta estandarización incluye tanto definir las posibles formas de intercambio de datos (vg.
“por valor” o “por referencia”), como controlar que los datos intercambiados cumplan las
condiciones esperadas por el evocador y el evocado (vg. evitar el intercambio de valores
con tipo incorrecto ya sea rechazando la evocación o transformando los datos
intercambiados).
Una característica importante de los lenguajes de 3ª generación es su facilidad para crear y
utilizar librerías de funciones y tipos definidos, impulsando el intercambio de código entre
programadores y programas. Es así, que al elaborar un programa en un lenguaje de tercera
generación moderno, el programador sólo tiene que escribir un porcentaje reducido del
mismo, estando la mayor parte del programa constituido por código preelaborado.
1.5.4 Cuarta Generación: SQL y Lenguajes Declarativos.
Los lenguajes de 3era generación introdujeron el término o fórmula para indicar o declarar
que el valor resultante de la aplicación de una o varias funciones a datos elementales es
requerido por una instrucción del programa. Lo importante de esta declaración es que el
intérprete es quién se encarga de definir el proceso computacional por medio del cual se
llevan a cabo los cálculos, sin que este proceso esté descrito en el programa. La
importancia de evitar describir este proceso se manifiesta en diversos lenguajes avanzados
de programación.
Los lenguajes que evitan describir de forma explícita el proceso computacional, por medio
de instrucciones de asignación, Entrada/Salida e instrucciones de Secuenciamiento, fueron
denominados como lenguajes de “cuarta generación” [Martin 85]. En estos lenguajes el
programador describe lo QUE desea obtener del programa absteniéndose de definir COMO
debe éste proceder para obtenerlo.
Las hojas de cálculo, tales como LOTUS o EXCEL pueden considerarse como ejemplos de lenguajes de
4ª generación, en cuanto, al definir un programa el usuario usualmente se limita a incluir fórmulas en las
“celdas” y a definir por medio de editores especializados los reportes y las formas de ingreso de datos.
Capítulo 1: Evolución de los Lenguajes de Programación
15
Dado que los gestores de “bases de datos” generalizan las operaciones de organización
acceso y presentación de la información que manipulan, en ellos aparecieron rápidamente
lenguajes especializados, que por limitarse a declarar lo QUE desea obtener, pueden
considerarse de 4ª generación. Entre ellos están lenguajes orientados a describir los datos y
la forma como se organizan y relacionan (o “Data Definition Languages”), lenguajes
especializados en describir consultas sobre los datos (o “Query Languages”) y lenguajes
orientados a describir la forma y contenido de los reportes y pantallas (o “Report
Generation Languages”).
Los lenguajes SQL, RPG, NOMAD y FOCUS que se apoyan en líneas de texto para describir la
estructura y composición de los datos que manipulan. El lenguaje SQL, por su parte, implementa un
modelo de consulta sobre los datos apoyándose en los conceptos de la teoría de conjuntos, el álgebra
relacional y el cálculo de predicados.
Una tendencia actual es la de dotar a los lenguajes de 3ª generación con poderosos medios
ambientes de desarrollo que ofrecen herramientas especializadas para llevar a cabo la
descripción de los diferentes aspectos del programa. En particular es usual que la
definición de interfaces complejos del programa con el usuario, se lleven a cabo por medio
de editores interactivos de menús, formas de dialogo y reportes, que crean una
representación visual del interfaz deseado, dejando al medio ambiente con la
responsabilidad de generar automáticamente el código procedural correspondiente.
El uso de lenguajes especializados para la especificación de los diferentes tipos de
elemento, sin embargo, establece de por sí un fraccionamiento de la especificación
encapsulando las partes en su propio ambiente lingüístico. A la necesidad de usar diversos
lenguajes en la elaboración de una pieza de software se le ha denominado “desacoplo de
impedancia” [Canos 97].
1.5.5 Quinta Generación: Uso de la Lógica.
Si bien en los lenguajes de 4ª generación ya aparece la tendencia a usar fórmulas
matemáticas como medio para describir, de forma declarativa, las tareas que debe ejecutar
el computador. El uso exclusivo de fórmulas, aserciones y ecuaciones y fórmulas bien
formadas de una lógica matemática, como medio para especificar el programa, constituye
un avance significativo, que, aquí incluiremos en la 5ª generación26.
En los lenguajes de 5ª generación se describen las características de los elementos de un
área de aplicación por medio de aserciones (o restricciones) que deben satisfacer. Un
conjunto de datos que describa una configuración particular (o “instancia”) de los
elementos del área de aplicación, puede satisfacer o no las restricciones planteadas.
Cuando la instancia satisface las restricciones planteadas, se denomina un “modelo” de las
restricciones.
26 wikipedia.org/wiki/Fifth-generation_programming_language.
Capítulo 1: Evolución de los Lenguajes de Programación
16
Dado una instancia que es modelo de las restricciones, en un lenguaje de 5ª generación, es
posible resolver automáticamente consultas pertinentes a los elementos de la configuración
particular que describe. La resolución de estas consultas se lleva a cabo, por medio de
procesos automáticos de demostración en el marco de la lógica utilizada.
Una utilidad adicional de estos lenguajes es su capacidad de fundamentar “editores” de
instancias del área de aplicación que garanticen la satisfacción de las restricciones
planteadas.
En los lenguajes “funcionales”27 se describen las propiedades de conjuntos de elementos y
de operadores definidos sobre dichos elementos, por medio de una teoría matemática en
una lógica ecuacional. Un “intérprete” de la teoría así descrita, se usa para llevar a cabo el
cálculo de términos base28 construidos con los operadores definidos.
En los lenguajes “lógicos”29 se describen las propiedades del área de aplicación por medio
de de formas clausales de la lógica de predicados (en particular “cláusulas de Horn”). Si se
cuenta con un “modelo” de dichas fórmulas (los datos que describen el área de aplicación),
es posible someter al intérprete aserciones clausales para que sean verificadas en el modelo.
Estas aserciones pueden contener variables que son valoradas en el proceso de verificación,
convirtiéndose en un medio de consulta.
Es importante anotar que, si bien los programas escritos en lenguajes de 5ª generación, son
teorías en una lógica matemática, no todas las teorías en lógica matemática constituyen
programas. Esto se debe a que los intérpretes de la teoría sólo operan adecuadamente para
teorías que satisfacen ciertas condiciones, en ocasiones bastante restrictivas. Las consultas,
por otra parte, no siempre siguen un proceso eficiente, por lo que los programas no siempre
son “mejores” que los escritos en lenguajes de otras generaciones anteriores.
La tendencia actual es la, primero, de usar estos lenguajes para entender las propiedades del
área de aplicación y crear prototipos de los programas requeridos; y luego, por
“refinamiento” de estos prototipos se obtienen los programas finales requeridos,
posiblemente en lenguajes más eficientes que los de 5ª generación.
El fortalecimiento de los intérpretes de los lenguajes de 5ª generación es, aun, motivo de
estudio e investigación.
27 wikipedia.org/wiki/Functional_programming.
28 Expresiones que no tienen variables.
29 wikipedia.org/wiki/Logic_programming
Capítulo 1: Evolución de los Lenguajes de Programación
17
permiten ingresar los datos al computador y entregar la información resultante, los
diferentes tipos de memoria permiten almacenar los datos y resultados con diferentes
tiempos de permanencia, y los dispositivos de procesamiento se encargan de llevar a cabo
las acciones internas de cómputo y ordenamiento.
A medida que se llevan a cabo el ingreso de datos, los cálculos y los ordenamientos, la
información contenida en la memoria va cambiando con el tiempo en lo que se denomina el
“proceso computacional”. Este proceso se compone de procesos más elementales que
ocurren en una sucesión parcialmente ordenada en el tiempo. Estos procesos más
elementales, a su vez, pueden descomponerse en procesos aún más elementales, y estos
últimos en otros más elementales, hasta llegar a un conjunto finito de operaciones atómicas
denominadas “operaciones de máquina”.
El proceso computacional es, sin embargo definido por el usuario por medio de
“programas”. En efecto, las operaciones más elementales se corresponden con cadenas de
dígitos binarios denominadas “instrucciones de máquina”. Una secuencia de dichas
cadenas representa un proceso computacional y constituye, en consecuencia, un programa
(en “lenguaje de máquina”). Para definir la transformación que efectúa el computador,
basta con “cargar” el programa que la representa en la memoria, por lo que para cambiar de
transformación es suficiente con cambiar el programa utilizado.
Por ser las instrucciones de máquina muy elementales, el lenguaje de máquina no es
apropiado para indicarle al computador que lleve a cabo acción alguna, ni mucho menos
para escribir programas de alta complejidad. Para aliviar estos problemas los computadores
ofrecen programas preelaborados que apoyan al usuario, y a los demás programas, en las
tareas de control de la máquina, asignación de recursos, manipulación de archivos,
elaboración y ejecución de programas, control de dispositivos, etc... Entre ellos están los
que constituyen la BIOS encargados de interactuar con los dispositivos físicos; los que
constituyen el sistema operativo (SO) encargados de las tareas de administración de los
recursos y comunicación con los usuarios; los traductores e intérpretes que se encargan de
traducir programas escritos en lenguajes avanzados al lenguaje de máquina; y, por último,
las aplicaciones de usuario final que son las encargadas de llevar a cabo las tareas que
hacen del computador un dispositivo útil.
Los lenguajes en los que se escriben los programas han evolucionado; desde el lenguaje de
máquina mismo (primera generación) a los lenguajes ensambladores (segunda generación),
que usan símbolos mnemotécnicos para referirse tanto a conjuntos de instrucciones de
máquina (previamente “compiladas”) como a los lugares de la memoria; desde los
lenguajes ensambladores a los lenguajes de tercera generación, que usan elementos de la
matemática para indicar los cálculos y movimientos de los datos en memoria junto con
“instrucciones de control” para definir la secuencia en que se efectúan dichos cálculos y
movimientos; desde los lenguajes de tercera generación a los de cuarta generación, que
pretenden definir los programas describiendo el resultado de los procesos que determinan;
y, por último, desde los de cuarta generación a los de quinta generación, que se
fundamentan el la lógica matemática para describir de forma precisa el resultado de los
procesos y la relación de los datos con los resultados.
Capítulo 1: Evolución de los Lenguajes de Programación
18
En el marco de los lenguajes de quinta generación los programas son teorías definidas en
una lógica matemática, y la ejecución de los programas son procesos de demostración en el
marco de dicha lógica
Capítulo 2
Evolución de los paradigmas
arquitectónicos
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
20
Introducción
En el capítulo anterior se argumentó que la tendencia en el desarrollo de los lenguajes de
programación, apunta a la concepción de lenguajes cada vez más capaces de describir el
resultado del proceso computacional, en contraste con lenguajes orientados a describir el
proceso computacional mismo. En el marco de esta tendencia se destacó el uso de la lógica
matemática como lenguaje de programación, donde los programas son teorías definidas en
una lógica y la ejecución de los programas son procesos de demostración en el marco de
dicha lógica.
Por otro lado, en el marco de los paradigmas de gestión modernos no se prescribe iniciar
un proyecto de desarrollo de software llevando a cabo de inmediato la codificación de los
programas. Se prescribe más bien un “pensar antes de actuar”, para darle una razón de ser
junto con una adecuada modularización (o “arquitectura”) al software, en concordancia con
las necesidades y arquitectura del área de aplicación.
Esto usualmente se traduce en que el proceso de desarrollo se divide en las fases siguientes:
modelamiento del área de aplicación o “análisis”, caracterización y estructuración del
software o “diseño”, codificación del software o “construcción”, verificación del software o
“prueba”, y puesta en operación o “implantación”. Este ciclo de fases suele repetirse de
forma reiterada y cíclica en una secuencia de etapas de desarrollo, que generan de forma
progresiva el software30.
Durante cada una de las fases del proyecto se deben elaborar documentos y modelos: Estos
modelos van desde unos que describen fundamentalmente el área de aplicación hasta otros
que describen completamente el software. La tendencia en este proceso es la de usar
lenguajes similares en todas las fases para lograr que la arquitectura del software coincida
con la arquitectura del problema” [Booch 96]. Así, la diferencia fundamental entre los
modelos de las fases de análisis, diseño y programación, es que en las primeras el modelo
no tiene necesariamente que ser ejecutable, o, de serlo, no necesariamente tiene que ser
eficiente.
Para describir de forma adecuada el área de aplicación debe ser posible, además, incorporar
en la especificación del software, los diferentes tipos de conceptos31 con los que se describe
el área de aplicación en lenguaje natural. A estos “tipos” de conceptos los denominaremos
en adelante “categorías conceptuales”32. Los diferentes tipos de construcción que ofrecen
los lenguajes de programación permiten representar entes de las diferentes categorías. Así,
por ejemplo, las variables permiten representar propiedades, los subprogramas permiten
30 Se uso el término “fase” con el objeto de distinguirlo de las “etapas” del proyecto. Estas últimas constituyen una
división temporal del proyecto orientada a obtener paulatinamente resultados tangibles y verificables: En un desarrollo
“en cascada” las etapas coinciden con las fases, y el software se construye sólo al final del desarrollo; en contraste, en un
desarrollo “ágil”, las fases se llevan a cabo de forma repetitiva y cíclica para obtener el software de forma incremental.
31Consideraremos, por ejemplo los conceptos casa, habitación, silla etc.., como miembro de un tipo de concepto, el tipo
de los “objetos físicos”.
32 Las categorías vistas como un sistema de clasificación de las “cosas”, o de las “diferentes formas de ser” de la tradición
aristotélica, nos permite clasificar los conceptos del lenguaje. Por ejemplo las “categories of being” referidas en
http://en.wikipedia.org/wiki/Category_%28philosophy%29 , son las siguientes: objetos físicos, objetos mentales, clases de
objetos, propiedades, relaciones, espacio y tiempo, aserciones, y eventos.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
21
representar procesos y eventos, las estructuras permiten representar relaciones de
composición entre propiedades, las instrucciones de secuenciamiento permiten representar
relaciones de composición entre funciones y eventos etc..
Al conjunto de categorías conceptuales que se pueden usar en un lenguaje lo
denominaremos el “paradigma arquitectónico”, entendido como el modo de comprender y
modularizar el área de aplicación y el software. En este capítulo examinaremos un
conjunto de paradigmas arquitectónicos vistos en el orden de su aparición histórica. La
progresión de estos paradigmas, en opinión del autor, representa un proceso evolutivo que
va incorporando, cada vez, más categorías conceptuales al lenguaje, sin que la aparición de
un nuevo paradigma substituya o elimine al anterior.
Desde el punto de vista de la lógica, la aparición de nuevos paradigmas impulsa la
necesidad de usar lógicas cada vez más complejas, que den cuenta de dichas categorías de
conceptos y de los procesos de razonamiento que involucran los conceptos de estas
categorías.
La clasificación de las lógicas y el estudio de los lenguajes lógicos que de cada tipo de
lógica se derivan, le da la estructura fundamental a este trabajo. Así, el trabajo se divide en
partes. En cada parte se presenta un tipo de lógica para luego estudiar en profundidad los
lenguajes que, de uno u otra forma, implementan o soportan a dicha lógica.
Paradigma de Instrucciones.
Muchos programas fueron concebidos sin tener en cuenta noción alguna de modularización
o arquitectura. Por lo tanto, el nivel básico de granularidad de los modelos lo constituyen
las instrucciones del lenguaje de programación, en particular los de 3era generación (y
hacia atrás).
Los modelos visuales usados en el marco de este paradigma (v.g. diagramas de la “lógica”
de la aplicación o “flow charts” usados en la programación FORTRAN y COBOL
[McCracken 64]), son representaciones más legibles del ensamblaje de instrucciones del
lenguaje que constituye un programa.
En el marco de este paradigma no se utiliza formalismo lógico alguno, por lo que carece de
interés en el marco de éste trabajo.
Función
Dominio Rango
Para especificar las diferentes operaciones se utilizan lenguajes que van desde el natural,
hasta formalismos lógicos, así:
34En un sentido estricto, el “operador” es un elemento sintáctico y la “función” es el elemento semántico que representa.
Usaremos sin embargo de forma indistinguida los términos “operación” y “función” en el texto que sigue.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
23
El seudo código o “mini especificaciones (IBM)”35 describen las
operaciones usando una mezcla de lenguaje natural y lenguaje de
programación. Se centran principalmente en indicar, la forma como se
repiten secuencian y seleccionan en ejecución, los grupos de instrucciones
elementales que componen la operación.
El seudo código para el programa que calcula la raíz podría ser como sigue sería el siguiente:
Lea x,y,dx
Hasta que |x2-y| dx
cambie x por ((y/x)+x)/2
El cálculo del disponible en una cuenta bancaria, se puede describir por medio de la siguiente
especificación formal explícita:
Una especificación implícita formal por restricciones de la función raíz cuadrada de un número real (que
no facilita el cálculo de la misma), es la siguiente:
35 Ver:
http://publib.boulder.ibm.com/infocenter/rsysarch/v11/index.jsp?topic=%2Fcom.ibm.sa.process.doc%2Ftopics%2Fc_Min
ispecs_what_how_use.html
36 Ver: http://en.wikipedia.org/wiki/Vienna_Development_Method
37 Ver: http://en.wikipedia.org/wiki/Z_notation
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
24
Una especificación explicita formal de la función raíz cuadrada de un número real (que hace posible el
cálculo de la misma), es la siguiente:
F1 F2
F11 F22
38 Ver: http://www.hit.ac.il/staff/leonidm/information-systems/ch64.html
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
25
El diagrama de Flujo de Control (DFL)39, describe la forma como se
secuencian, repiten, y seleccionan para ejecución, las diferentes operaciones
componentes, al llevarse a cabo la ejecución de la operación que componen.
P
P1
?
P11 P12
P2
P21
Una transacción que crea una nueva reserva presupuestal para un gasto, puede especificarse en un
diagrama IPO de de la forma siguiente:
Ítems solicitados
Cliente Buscar
ítems en
almacén
Datos Ítems
Cobro Despachados Cliente
Remisión
Factura
Registro
Cobro pago
Entrega Salida almacén
Ítems
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
27
El proceso de gestión de los trabajos de grado de los estudiantes de ingeniería en la Universidad, puede
describirse por medio de un DFD con “swimlines”, así:
Solución propuesta
Evaluación
de propuestas propuestas de TDG Base de 9 del DTDG
Propuestas
7
Base de Copia Informe
Realiza pre-registros Consulta Formulario Modifica Plan Realiza Evalúa al Entrega copia
Desea conseguir Elabora Plan Elabora el Sustenta el Final del TDG
pre-registro propuestas del Plan 3 registro del Director del del Informe
E
No 1 Aprueba el Plan
Publica la 2 Formulario
Estudia y El Plan de TDG de TDG y Presenta
CAPC
Diligencia el Formulario
formulario de archivado Nombra el Jurado
DE
TDG
nombramiento del Calificador del
Director del TDG
3 finalizado
TDG
b
Informe Final
DTDG
No 7
Estudia y Evalúa la Diligencia el Acta Especial de
No 5
evalúa el El TDG amerita sustentación Acta Especial Calificaciones
JTDG
c Si El Jurado
Informe final sustentación pública del de
Calificador
del TDG TDG Calificaciones Si
aprueba el
Informe Final 4 TDG
evaluado
No
Estudia, aprueba y
e Estudia el Es necesario
CF
modifica acuerdos y
reporte de tomar medidas Si FIN
6 correctivas
reglamentos
TDG
relacionados con TDG
CPC: COORDINADOR DE PROGRAMA CURRICULAR; CAPC: COMITÈ ASESOR DE PROGRAMA CURRICULAR; DE: DIRECTOR DE LA ESCUELA; E: ESTUDIANTE; DTDG: DIRECTOR DEL TDG; JTDG: JURADO CALIFICADOR DEL TDG; GI: GRUPOS DE INVESTIGACIÓN; CF: CONSEJO DE
FACULTAD
La descripción del registro que contiene la información de un gasto en un programa COBOL, se muestra
a continuación:
01 REG-D.
05 CLAVE-D.
10 SOLICITUD-D.
15 LETRAS-SOL-D PIC X(2).
15 DIGITOS-SOL-D PIC X(5).
10 ARTICULO-D PIC 9(10).
05 IMPUTACION-D.
10 TIPO-REC-D PIC 9.
10 SUBP-D PIC 99.
10 RUBRO-D PIC 99.
10 ORD-D PIC 99.
10 PROG-D PIC 99.
05 DEPCIA-D PIC X(5).
05 IND-SOLICITUD-CUMPLIDA-D PIC XX.
88 SOLICITUD-CUMPLIDA-D VALUE "SI".
05 IND-ESTADO-D PIC XX.
43 El uso de este concepto implica que el programa usará operadores de intercambio de información de o hacia los
dispositivos externos al mismo (operadores de “lectura” y “escritura”). Una alternativa a esta opción es la de localizar
datos del programa en varios “medios ambientes” con diferentes grados de permanencia con respecto a la ejecución del
programa (v.g. la variable SESSION del lenguaje PHP contiene valores disponibles durante las diversas ejecuciones del
programa que componen una “sesión”).
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
29
88 CUMPLIDO-D VALUE "CU".
88 CANCELACION-RESERVA-D VALUE "CR".
88 ACTIVO-D VALUE "AC".
05 DESCRIPCION-D PIC X(60).
05 ELEMENTO-D.
10 TIPO-MOV-D PIC 99.
10 DOCUMENTO-D.
15 LETRAS-DOC-D PIC XX.
15 DIGITOS-DOC-D PIC X(5).
10 FECHA-CAUSACION-D.
15 ANO-D PIC 99.
15 MES-D PIC 99.
15 DIA-D PIC 99.
10 FECHA-ASIENTO-D PIC 9(6).
10 VALOR-D PIC 9(12)
44 Ver: http://en.wikipedia.org/wiki/Database
45 Por “Data Base Management System”
46 Ver: http://en.wikipedia.org/wiki/Relational_database_management_system y
http://en.wikipedia.org/wiki/Relational_database
47 Ver: http://en.wikipedia.org/wiki/E._F._Codd
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
31
Las dependencias entre los datos se representan con base en los “atributos
clave” de las tuplas. Así, el valor de la clave es único para cada tupla en una
relación, identificándola de forma unívoca, y, al incluirlo en otra tupla como
“clave foránea” permite representar relaciones de dependencia funcional
entre las tuplas.
La normalización organiza la información entre las tuplas garantizando que
se representen adecuadamente las relaciones de dependencia funcional entre
los datos, se eliminen las redundancias y se eviten las “anomalías” por el
mantenimiento de datos.
El álgebra relacional permite implementar un lenguaje de consulta de tipo
algebraico que, básicamente, constituye una aplicación a las bases de datos
de los operadores de la teoría de conjuntos
El cálculo relacional, es una aplicación del cálculo de predicados al
problema de las consultas sobre las bases de datos. El lenguaje de 4ª
generación “Strucured Query Language” o SQL48 le da soporte tanto al
álgebra relacional como al cálculo relacional.
2.4.4 La especificación formal bajo el modelo Relacional.
En el marco del modelo relacional, las especificaciones de la estructura de los datos
almacenados en archivos, y de las posibles consultas a los mismos, no son otra cosa que
aplicaciones a la informática de los conceptos matemáticos de Tupla, Relación, y
Subconjunto, de la teoría clásica de Conjuntos.
Los conceptos de actualización, o cambio en la información contenida en los archivos,
requiere, sin embargo, de formalismos más elaborados. Entre ellos vale la pena destacar las
nociones de “medio ambiente” para la valoración de las expresiones con variables y el uso
de PRE y POST condiciones para dar cuenta de los cambios en el medio ambiente
inducidos por las instrucciones del programa [Hoare 69]49, [Baber 02]. Las PRE y POST
condiciones están, en efecto, incorporadas en las notaciones VDM [Jones 90] y Z [Woodc
96] referidas antes para el caso de las funciones50.
La especificación formal de una transacción que indica que se aumente la reserva disponible para un
gasto (v.g. una compra), puede expresarse de la forma siguiente:
Sea:
G = { xGasto / x.codigo = AumentoReserva.codigo }
Antes de la transacción:
G = { JGasto }
48 Ver: http://en.wikipedia.org/wiki/SQL
49 Ver: http://en.wikipedia.org/wiki/Hoare_logic
50Cabe, sin embargo, anotar que cada notación usa su propia terminología. Así, para VDM las funciones que generan un
cambio de estado son denominadas “operaciones”; en cambio para Z estas son un tipo especial de “schema”.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
32
J.Reserva = R
| J.CentroCosto |=1
AumentoReserva.Valor + R (J.CentroCosto).Disponible
Después de la transacción:
J.Reserva = AumentoReserva + R
Donde:
“AumentoReserva” es la transacción cuyos datos se entran por pantalla.
“J” Identifica el gasto al que hace referencia la transacción, que debe existir para poder
aumentarle su valor.
“CentroCosto” es la cuenta sobre la que se hace el gasto que debe existir y tener dinero
disponible par el nuevo valor de la reserva.
La especificación de la arquitectura de los datos, bajo el modelo relacional, para una aplicación de
gestión de las compras en una entidad pública, el marco del programa Microsoft Access ®, se muestra
a continuación:
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
33
La especificación de la arquitectura de los datos, bajo el modelo Entidad/Relación, para el caso del
ejemplo anterior se ilustra a continuación:
Paradigma de Objetos.
La descomposición progresiva de funciones del paradigma de funciones no se orienta “per
se” a la identificación de componentes funcionales genéricos reutilizables que puedan ser
utilizados como partes al construir las diferentes funciones de una aplicación. Esto genera
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
36
una tendencia a reprogramar una y otra vez los mismos procesos, generando no solo una
perdida de esfuerzo al elaborar el software, sino también un enorme costo al modificarlo.
La utilización de las “entidades” del paradigma de datos en diferentes programas, permite
vislumbrar la frecuente ocurrencia de procesos básicos asociados a los atributos de dichas
entidades. Esto sugiere la conveniencia de asociar y almacenar la especificación de estos
procesos como parte de la especificación de dichas entidades, para hacerlos más
reutilizables.
La asociación datos/procesos aparece primero en el contexto de los lenguajes de
programación procedural, como la capacidad de definir tipos de datos adicionales a los
ofrecidos por el lenguaje (v.g. el lenguaje ADA).
2.5.1 Concepto Clásico de Objeto.
A la capacidad de definir nuevos tipos se le incorpora la de redefinir los tipos nuevos o los
existentes por medio de la relación de “herencia”, para impulsar el reuso de
especificaciones ya existentes.
A estos tipos susceptibles de herencia se les denominó, entonces, como “clases” y a las
instancias de ellos se les denominó “objetos” en el lenguaje SIMULA 6752 [Ole-Johan 70].
Modernamente se considera la aproximación Orientada Por Objetos como una disciplina
para el desarrollo de Software53, que se aplica de forma uniforme en todas las etapas y fases
del desarrollo [Booch 96]54, [Jacobson 92]55, [Rumbaugh 91]56, [Jacobson 98], [Meyer
98]57.
De [Meyer 98] se pueden extraer los elementos siguientes como condiciones para que una
disciplina de desarrollo pueda ser denominada “orientada por objetos”:
El concepto de Objeto como hilo conductor del método: Todo el proceso de desarrollo
debe fundamentarse en el concepto de objeto como hilo conductor del proceso, sin que
haya un cambio de aproximación cuando se pasar de una etapa o fase a otra diferente.
Los objetos tienen “estado” caracterizado por el valor de una serie de
“atributos” que pueden observarse a través del interfaz.
El estado de los objetos puede ser observado y modificado por medio de una
serie de operaciones denominadas “métodos” que constituyen el “interfaz”
del objeto.
Los objetos son creados y destruidos por métodos especializados a tal fin.
El modelo de clases que presenta las clases con sus atributos y métodos,
junto con sus relaciones de herencia, asociación y uso.
59Es decir, proyectan un punto del dominio de la clase (el estado del objeto antes del evento) a otro punto del mismo
dominio (el estado en que queda el objeto luego del evento).
60 Ver: http://en.wikipedia.org/wiki/Dynamic_logic
61 Ver: http://en.wikipedia.org/wiki/Deontic_logic
62 Ver: http://www.uml.org/
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
40
A continuación se presenta el modelo de clases de una aplicación orientada a la gestión de los trabajos
de grado.
Cronograma
-*Tema
-Semanas
JURADO 2
+Reservar_Propuesta()
-*Franja Horaria 1...n 1 +Registrar_Plan() -Título
+Establecer_Disponibilidad() +Cancelar_Reserva() -Tipologia
-Nro de Personas PROPUESTA
-Objetivos
DIRECTOR 0,1 0,1 0,1 0...n PLAN2
-Recursos Economicos 0...n
DOCENTE -Recursos Fisicos
-/Nro de Reservas
-Categoria
0,1 ESTUDIANTE 2 -/Plan Vigente
-Oficina
-Extension +Publicar_Propuesta()
1 ESTUDIANTE 1
+Nombrar_Director_TDG()
+Nombrar_Jurado_TDG()
INSCRIPCION
1...n 1
1...n
Director Propuesto 0,1 -*Nro 0...n /Solicitudes
-Fecha de Inscripcion
-Condiciones
AUTOR
1 +Registrar_Pre-Registro()
PERSONA
EVENTO PROGRAMA CURRICULAR 0...n 1 1
-*Documento de Identidad
-*Codigo -Nombre -*Nombre GRUPO DE INVESTIGACION
-Actividad -Login -Facultad -*Nombre
-Responsable -Password
-Fecha
1...n 1
+Publicar_Programacion() 1,2
AREA DE INTERES
-*Linea de Profundizacion
1...n 1...n
1...n
Grupo de
Investigación.Publicar_Propuesta Reservar_Propuesta(Documento de
(Titulo, Tipología, Nro de Personas, Identidad, Código Reserva, Nro)
Objetivos, Recursos Económicos, PUBLICADA RESERVADA
Recursos Físicos, Nombre)
Entry: Registrar Fecha Entry: Registrar fecha de reserva
Publicación Do: Calcular fecha de vencimiento
Inicio [Nro_Reservas =1 ] Calcular posición en la cola
/ Estudiante.Cancelar_Reserva(Codigo
Reserva)
Acta Concepto
Acta de Calificación.Elaborar_Acta_Calificación Final.Asignar_Concepto_Final().Concepto =
EN ELABORACIÓN EN ESTUDIO
().Concepto Final = “APROBADO” “APROBADO”
Entry: Asignar Estado Entry: Asignar Estado
FIN
A continuación se presenta el modelo de casos de uso, que representa la interacción de los usuarios
interesados en la evaluación de un trabajo de grado:
Evaluación
del TDG
Nombrar Jurado
Calificador
Ingresar
Disponibilidad Horaria
Director
Escuela
Jurado Estudiante
Calificador
Paradigma de Agentes.
Sin entrar en detalles sobre este paradigma, que pertenece más al campo de la Inteligencia
Artificial, podemos señalar que adiciona al paradigma de los objetos conceptos propios de
las organizaciones humanas de cooperación, tales como, conocimiento, objetivos o
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
42
intenciones, coordinación de tareas con otros agentes, capacidad de viajar, etc. [Nwana
96]63.
63 Ver: http://en.wikipedia.org/wiki/Software_agent
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
43
“relaciones matemáticas” de la teoría de conjuntos (modelo “relacional”). El modelo
relacional se asocia, además, con una serie de restricciones de “normalización” que elimina
problemas existentes en el modelo anterior, tales como la duplicación inconsistente de los
datos del área de aplicación, y la desaparición inadvertida de datos dependientes de otros
datos, por operaciones que manipulan los otros. Para describir la arquitectura de los datos
bajo el modelo relacional, se usa, principalmente el “modelo Entidad-Relación” que asocia
las tuplas de valores en las tablas con las “entidades” del área de aplicación; y para
describir la relación de los datos con los programas se usan lenguajes algebraicos como el
lenguaje SQL.
El “paradigma de objetos”, asocia los datos que describen las entidades del área de
aplicación con los elementos de programa que los manipulan, creando el concepto de
“objeto”. La especificación de las propiedades comunes a grupos de objetos similares
constituyen las “clases”, que se incorporan ya sea en los programas o en los gestores de
“Bases de Objetos”. El paradigma clásico objetual incorpora ,además, una serie de
conceptos y elementos de especificación orientados a ampliar la capacidad de modelo para
describir los elementos del área de aplicación, que, incluidos en las clases, maximizan la
reutilización de las especificaciones; entre ellos, la “herencia” que evita duplicar la
especificaciones en clases similares, el “encapsulamiento” que estandariza la utilización de
los objetos en los programas que los manipulan, el “polimorfismo” que permite la escritura
de código genérico reutilizable en varias clases relacionadas, y las relaciones de
“asociación”, “agregación”, y “uso”, que estandarizan los tipos de interacción entre los
objetos. Una evolución del paradigma clásico, ve los objetos como “procesos dinámicos
comunicados” que ocurren en el dominio de tipos abstractos de datos determinados por las
clases; incorporando la capacidad de especificar algunas de las características de dichos
procesos, entre ellas las condiciones en que se inician y terminan los procesos, los pasos
posibles y obligatorios dentro de los procesos, y las comunicaciones posibles en las
diferentes etapas de los procesos. Dada la complejidad del modelo, la arquitectura objetual
se especifica por medio de una gran diversidad de modelos, que describen diferentes
aspectos de la misma. La notación UML, por ejemplo, incorpora una gran variedad de
primitivas y diagramas, entre los que se destacan, el diagrama de “Clases”, el diagrama de
“Transición de Estados”, el diagrama de “Interacciones”, y el diagrama de “Casos de Uso”.
El “paradigma de agentes”, adiciona al paradigma de los objetos conceptos propios de las
organizaciones humanas de cooperación, tales como, conocimiento, objetivos o
intenciones, coordinación de tareas con otros agentes, capacidad de viajar, etc.
Capítulo 3
Lógica Proposicional
Capítulo 3: Lógica Proposicional
46
Introducción
En el primer capítulo del trabajo se adujo que los lenguajes de programación han
evolucionado de lenguajes orientados a describir en detalle el proceso computacional que
ocurre al ejecutar un programa, a lenguajes orientados a describir las características
relevantes del área de aplicación.
En el marco de esa evolución se destacó el uso de la lógica matemática como lenguaje de
programación. La lógica permite describir las características del área de aplicación con
base en aserciones o restricciones, facilita la construcción de instancias que modelan el área
de aplicación satisfaciendo las restricciones, y, por último, permite obtener respuestas a
consultas relativas a dichas instancias con base en demostraciones soportadas en la lógica.
En el segundo capítulo se mostró que para representar adecuadamente áreas de aplicación
cada vez más complejas, los lenguajes que dan soporte al desarrollo han venido
incorporando tipos de conceptos o “categorías conceptuales” cada vez más cercanas a las
del lenguaje natural. A un conjunto específico de dichas categorías se le denominó
“paradigma arquitectónico”. El software elaborado con base en uno de dichos paradigmas
posee una forma de modularización estrechamente ligada al paradigma, por lo que éste
constituye la base de todo método de desarrollo.
Para que sea posible usar la lógica matemática como un lenguaje de especificación en el
marco de un método moderno de desarrollo es, entonces, necesario que ella pueda dar
cuenta de los conceptos involucrados en los diferentes paradigmas arquitectónicos. A este
efecto se señalaron, en el capítulo anterior, diferentes lógicas como medio para dar soporte
a los diferentes paradigmas.
El enfoque de este trabajo es el de presentar sólo los aspectos de la lógica que soportan
cada uno de los temas de software que se traten. En consecuencia, los aspectos de la lógica
se van presentando de forma paulatina, antecediendo los temas de software, y sin presentar
temas de lógica que sean irrelevantes a los mismos.
El presente tomo, se centra en presentar la manera como la lógica da soporte a los lenguajes
de programación: Se deja para tomos a ser escritos posteriormente, la discusión relativa a
los lenguajes de análisis y diseño.
En este capítulo se introducen, en particular, los conceptos básicos de la lógica de
proposiciones y de la lógica de predicados, centrándose en los aspectos de sintaxis y
semántica. Estos dos aspectos son básicos y necesarios para todas las presentaciones de
lógica que se hacen posteriormente.
69 Se refiere en que por ejemplo, “ hace notar que puede remplazarse la proposición
El algebra boleana puede ser vista como una álgebra de los números enteros “modulo 2” con las operaciones suma y
70
multiplicación de enteros.
71 que ya se vislumbraban en las proposiciones aristotélicas.
Capítulo 3: Lógica Proposicional
49
La Lógica Como una Disciplina de los Lenguajes.
Para sistematizar el razonamiento, la lógica debe apoyarse en una notación simbólica que
permita representar las palabras y frases con las que se especifican los problemas y se
caracterizan los elementos de sus posibles campos de aplicación. En otras palabras toda
lógica debe ser un lenguaje simbólico.
El lenguaje de la lógica debe ser, además, preciso y carente de ambigüedades. Por ello la
lógica debe imponer reglas a la formación e interpretación de sus palabras y frases, que
eliminen las ambigüedades e imprecisiones de la mayoría de los lenguajes simbólicos, y en
particular, las que ocurren en los lenguajes naturales
La teoría de los lenguajes formales [Hopcroft 01], ofrece las condiciones que permiten
lograr dicha precisión y capacidad de interpretación. Desde la óptica de los lenguajes
formales, una lógica no es más que un conjunto de ensamblajes de símbolos que satisface
una serie de condiciones previamente definidas. Estas condiciones son básicamente las
siguientes:
La existencia de una gramática o “sintaxis” precisa para el lenguaje, que
permita distinguir los ensamblajes (o frases del lenguaje) que son correctos o
“bien formados” de los que no lo son.
La existencia de una semántica que permita darle un significado a las frases
bien formadas del lenguaje, para poder, a partir de ellas, tomar las decisiones
que sean de interés.
La existencia de unas reglas de inferencia que permitan obtener (deducir)
frases nuevas con significado conocido (conclusiones), a partir de otras
frases cuyo significado es ya previamente conocido (premisas).
Si adicionalmente se pretende utilizar la lógica como un lenguaje de programación, es
necesario que sus palabras y frases puedan interpretarse de forma automática en el
computador, y que sus reglas de inferencia puedan dirigirse de forma automática a la
obtención de las conclusiones deseadas. Para ello las reglas de inferencia deben determinar
procesos computacionales de demostración, mucho más reglados que los que determinan la
mayoría de las lógicas existentes. En otras palabras no todas las lógicas pueden ser usadas
como un lenguaje de programación.
3.3.1 Sintaxis.
La sintaxis de un lenguaje está constituida por un conjunto de elementos que regulan la
conformación y estructura de sus frases. A una frase que satisface la sintaxis del lenguaje
lógico se le ha denominado “formula bien formada” (en lo que sigue “fbf”).
En primer lugar, el “alfabeto de símbolos” define las unidades básicas de especificación
que pueden ser utilizadas para formar las frases. Estas unidades pueden ser letras, símbolos
especiales, palabras, o figuras que son consideradas elementales e indivisibles. La
especificación del lenguaje debe definir el alfabeto de símbolos, y el intérprete debe señalar
como errado el uso de cualquier símbolo que no pertenezca al alfabeto.
La forma más simple de definir el alfabeto es la de listar los símbolos que lo componen.
Sin embargo cuando los símbolos son ensamblajes básicos de letras u otros componentes
más elementales, el alfabeto de símbolos puede ser infinito. En este caso se deben proveer
reglas que permitan establecer si un ensamblaje constituye o no un símbolo valido del
Capítulo 3: Lógica Proposicional
50
alfabeto. Estas reglas usualmente se dan en la forma de “expresiones regulares” o
“máquinas de estado finitas”, que son básicamente patrones para definir dichos
ensamblajes72.
En segundo lugar, los “criterios formativos” (en lo que sigue “CF”), en general,
determinan las formas válidas de ensamblar frases bien formadas del lenguaje para formar
otras frases bien formadas más complejas. Para ello los CF señalan que símbolos del
alfabeto se usan para unir las frases y que símbolos constituyen las frases bien formadas
más elementales. Los CF permiten entonces, descomponer, de forma progresiva, una frase
del lenguaje en componentes o “subfrases” que pueden a su vez estar compuestas por otras
componentes y así sucesivamente hasta llegar a símbolos del alfabeto. Esta descomposición
puede representarse en forma de árbol, formando lo que denominaremos el “árbol
sintáctico” de la frase.
Las expresiones de la aritmética como “3+4” son fbf de la lógica de predicados que usan los símbolos
del alfabeto (números y operadores), y los ensamblan de formas predefinidas (p.e. el operador “+” tiene
el perfil “<operando> + <operando>”).
El árbol sintáctico de esta fbf es el siguiente:
3 4
ama ama
a
juan luisa luisa juan
3
El carácter recursivo de los criterios formativos, induce a que el conjunto de las fbfs de una
lógica sea, en general, infinito.
Los criterios formativos pueden especificarse por medio de notaciones formales como la de
una gramática en notación de Backus Naur (BNF)73. Bajo ciertas condiciones las
72 Estos patrones definen un lenguaje de símbolos que es usado dentro del lenguaje. Para mayor información sobre estos
elementos el lector debe referirse a [Hopcroft 01]
73 Ver: http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form
Capítulo 3: Lógica Proposicional
51
especificaciones BNF permiten la generación automática de los programas requeridos para
establecer el árbol sintáctico asociado con una frase, y señalar posibles violaciones a los
criterios formativos. A estos programas se les denomina “parsers”74.
En lo que resta del trabajo iremos presentando la gramática de los lenguajes estudiados, por
medio de especificaciones simplificadas en la forma de patrones que, de forma conjunta,
constituyen una especificación de la gramática por medio de expresiones regulares y
gramáticas BNF.
3.3.2 Semántica.
Para asociar un significado a las fbfs de una lógica, es necesario contar con uno o varios
“dominios semánticos” asociados con la lógica, que contendrán los significados. El
significado de cada fbf (que en lo que sigue denominaremos ya sea como su “valor
semántico”, o como el elemento que ella “denota”), es un elemento o una estructura de
elementos en los dominios semánticos. La precisión de un lenguaje lógico estriba, además,
en que cada fbf tenga un significado único.
La manera obtener el significado de una fbf depende de las características propias de cada
lógica y será objeto de estudio en cada caso. Sin embargo, en principio, el significado de
una fbf debe estar completamente determinado por el significado de sus fbfs componentes y
por la manera en que ellas se ensamblan. Para ello es necesario que los símbolos que
componen las frases tengan de antemano un significado dado.
La fbf “3+4” tendrá su valor semántico en el dominio de los números enteros y dicho valor puede ser
establecido o “calculado” con base en el valor semántico de sus símbolos, es decir los enteros que
representan los símbolos “3” y “4” y la función representada por el símbolo “+”.:
La fbf “ama(juan,luisa)ama(luisa,juan)” tendrá su valor semántico en el dominio {verdadero, falso} y
su valor sólo puede ser establecido luego de conocer el valor semántico de “juan” y “luisa” (las personas
a las que refieren dichos nombres), el valor semántico del predicado “ama” (una relación entre personas
que ha sido denominada “ama”), y el valor semántico del conector “” (una función con codominio
{verdadero, falso})
Los símbolos de “constantes” del alfabeto (es decir aquellos cuyo significado no cambia en
el contexto de un cálculo o una demostración ver 4.2.1.2.2), pueden ser símbolos lógicos, o
símbolos no lógicos. El significado de los primeros es inmutable, propio de la lógica, e
independiente del contexto donde se aplique. El significado de los segundos, por su lado,
depende del contexto al que se aplican las fbfs, y debe establecerse como una proyección
arbitraria, de dichos símbolos a elementos o a estructuras de elementos de los dominios
semánticos de la lógica. En lo que sigue, a los dominios semánticos a los que se proyectan
los símbolos no lógicos de la lógica los denominaremos como “dominios de
interpretación” (de los símbolos no lógicos). A la función que proyecta los símbolos no
lógicos la denominaremos una “interpretación” (de los símbolos no lógicos sobre el
dominio semántico). La interpretación de los símbolos no lógicos es, en general, arbitraria
74 El lector interesado debe consultar “Compilers: Principles, Techniques, and Tools” [Ullman 86]
Capítulo 3: Lógica Proposicional
52
por lo que pueden existir múltiples interpretaciones para el conjunto de símbolos no lógicos
de las fbfs.
Si bien los símbolos de la fbf “3+4” tendrán un valor semántico fijo e inmutable (por acuerdo universal),
y por tanto una sola interpretación, los símbolos de la fbf “ama(juan,luisa)” tendrán un valor semántico
que puede ser diferente en diferentes grupos de personas (su contexto), y por tanto múltiples posibles
interpretaciones.
El proceso por el cual se obtiene el valor semántico de una fbf a partir del valor semántico
de los símbolos del alfabeto, lo denominaremos “calculo del valor de la fbf”. Es sin
embargo importante notar que como se verá más adelante, este cálculo no siempre se puede
llevar a cabo por razones prácticas.
En el marco de la lógica de predicados, el cálculo del valor semántico de una fórmula con variables (ver
3.5.2) cuantificadas sobre conjuntos infinitos es prácticamente imposible.
Dado que el término “semántica” del lenguaje natural se entiende de muy diversas formas,
en lo que sigue usaremos el término “valor” para referirnos al valor semántico de una fbf
en general, y la frase “valor de verdad” para referirnos al valor semántico de una fbf cuyo
dominio semántico es el conjunto {verdadero, falso}. El término genérico “semántica”,
por su parte, lo reservaremos para referirnos al conjunto de interpretaciones en las que una
(o varias) fbf de dicho tipo tiene(n) como su valor de verdad a “verdadero”.
3.3.3 Inferencia.
Si bien el valor semántico de una fbf debería poder calcularse a partir del valor semántico
de sus símbolos, este significado no siempre se puede obtener por razones prácticas (ver
4.3).
Para resolver este problema se asocian una serie de “criterios demostrativos” con el
lenguaje lógico. Los criterios demostrativos permiten obtener o “inferir” nuevas fbfs, con
un valor semántico dado, a partir de otras fbfs cuyo valor semántico es conocido
previamente. Desde el punto de vista lingüístico, los criterios demostrativos no son otra
cosa que reglas de transformación que obtienen nuevas fbfs útiles, a partir de otras fbfs
previamente definidas.
De saberse previamente que el valor de verdad, bajo una cierta interpretación, es “verdadero” para las
fórmulas “ama(juan,luisa)” y “XYama(X,Y)ama(Y,X)”, se puede deducir que el valor de verdad de
Capítulo 3: Lógica Proposicional
53
la fbf “ama(luisa,juan)” también es “verdadero” bajo dicha interpretación.
Para poder usar los criterios demostrativos es necesario contar con fbfs “semilla” cuyo
valor de verdad sea conocido posiblemente por observación sobre lo que ocurre en el
dominio de interpretación. En los casos en que este valor de verdad no puede ser
(completamente) observado, el valor de verdad de las fórmulas semilla debe ser asumido
constituyéndose estas fórmulas en los “axiomas de la teoría”.
En la teoría de los números enteros la validez de la ley conmutativa de la suma (X(X+Y= Y+X )
puede ser observada sólo para un conjunto finito de pares de enteros, por lo debe ser asumida
constituyéndose en un axioma a partir del cual se demuestran otras verdades o “teoremas”.
Lógica de Proposiciones.
En la lógica de proposiciones el dominio semántico es el conjunto {0, 1} que puede ser
entendido como los valores de verdad “verdadero” y “falso”75 (en lo que sigue
denominaremos a este dominio como “BOOL”). En otras palabras, la lógica de
proposiciones es la lógica de la verdad o falsedad de los planteamientos que puedan
formarse en un lenguaje cualquiera76, en particular en el lenguaje natural.
3.4.1 Proposiciones.
Una proposición es una afirmación que es “o bien” verdadera “o bien” falsa.
3.4.2 Sintaxis de la lógica proposicional.
En la lógica básica de proposiciones, la sintaxis es extremadamente simple ya que los
símbolos son las letras y unos pocos conectores.
3.4.2.1 Alfabeto
Las proposiciones más obvias, son precisamente las constantes “verdadero” y “falso”, que
denotaremos con las letras V y F respectivamente.
Las “variables proposicionales” serán letras en minúscula con o sin subíndice: {a, a1, a2, a3,
… , b, b1, b2, b3, … p, p1, p2, p3, … , q, q1, q2, q3, …}. Ellas serán usadas para representan las
75 A los que nos referiremos de diversas formas: {ciero, falso},{true, false},{verdadero,falso},{V,F},{T,F} etc..
76 http://en.wikipedia.org/wiki/Propositional_logic
Capítulo 3: Lógica Proposicional
54
proposiciones relativas al área de aplicación que son consideradas elementales en cuanto no
se dividen en proposiciones más simples. A estas proposiciones elementales nos
referiremos en lo que sigue como los “átomos” o “primitivas” de la teoría. Las variables
proposicionales constituyen los símbolos no lógicos (constantes) de la lógica
proposicional77. Vale la pena anotar que la selección de cuales frases del lenguaje
constituyen proposiciones “primitivas” es arbitraria; en consecuencia, la única condición
que en este trabajo se le impone al conjunto de proposiciones primitiva, es que el valor de
verdad dado a una o a varias de las primitivas (en una interpretación) no determine o afecte
el valor de verdad que pueda dársele a otra u otras de la primitivas; es decir, que las
primitivas sean independientes.
Las letras mayúsculas con o sin subíndices, { A, A1, A2, A3, … , B, B1, B2, B3, … P, P1, P2, P3, … , Q, Q1,
Q2, Q3}, junto con los símbolos del alfabeto griego, serán usados para referirnos a las
proposiciones que pueden ser subfórmulas en otra fbf planteada de forma genérica (como
una plantilla). Estos símbolos constituyen meta-variables que refiere a una cualquiera de
las fbfs de un conjunto determinado, sean ellas atómicas o no, (usualmente, todas las
proposiciones posibles). Las metavariables serán usadas para discutir o plantear las
propiedades compartidas por las fbfs que resulten de substituir las meta-variables por una
proposición cualquiera de las del conjunto al que se asocian.
Los símbolos lógicos de la lógica proposicional son los conectores lógicos que se utilizan
para formar proposiciones complejas con base en proposiciones más simples. Los
conectores lógicos que usaremos en lo que sigue son los siguientes78:
: denominado “no” (que ocasionalmente representaremos como NOT)
: denominado “o” (que ocasionalmente representaremos como OR)
, denominado “y” (que ocasionalmente representaremos como AND).
(), denominado “implica” (que ocasionalmente representaremos como
==> o como <==).
, denominado “doble implicación” (que ocasionalmente representaremos
como <==>).
77Al igual que para todos los símbolos no lógicos, el valor de las “variables proposicionales” es constante durante los
procesos de cálculo e inferencia que se lleven a cabo en el marco de una interpretación, pero varía con la interpretación.
78 Es posible escoger como básico al conjunto {, } o al conjunto {, } y considerar los otros como derivados (ver
p.e. )
Capítulo 3: Lógica Proposicional
55
Si F es una fbf entonces también lo es F
Si F y G son fbf entonces también lo son FG, FG, FG (GF), y FG
Ningún otro ensamblaje es una fbf
En gramática BNF:
φ ::= p | (φ) | (φ φ) | (φ φ) | (φ φ) | (φ φ) | (φ φ)
Dónde:
La fbf siguiente:
p((p¬r)r(¬pq))
Puede representarse por medio del árbol sintáctico siguiente:
p
r
Capítulo 3: Lógica Proposicional
56
La estructura de composición de la fbf del ejemplo anterior, puede indicarse de forma explícita por
medio del uso de paréntesis, así:
p(((p(¬r))(r((¬p)q))))
Nótese que el árbol sintáctico de las dos fbfs que siguen es el mismo, bajo el orden de precedencia
anterior:
p(((p(¬r))(r((¬p)q))))
p((p¬r)r(¬pq))
Capítulo 3: Lógica Proposicional
57
Es importante anotar que, a pesar de existir un orden de precedencia que permite usar
menos paréntesis, no es posible dejar de usarlos completamente en una fbf.
El lector debe notar que, a pesar de existir un orden de precedencia que permitió usar menos paréntesis,
no es posible eliminarlos completamente en una fbf sin cambiar su árbol sintáctico. En efecto, el árbol
sintáctico de la fbf anterior sin paréntesis:
p p¬rr¬pq
Es el que se muestra a continuación:
p p ¬ q
r
r ¬
p
El árbol sintáctico cambia también al cambiar el orden de precedencia. Así, la fbf anterior bajo el orden
de precedencia {, , , , }, sería el que se muestra a continuación:
p p
q
¬ ¬
r
p
r
79 En [Aho 2007], el sentido de asociatividad se define sin definir una secuencia de aplicación de los operadores,
indicando que : “We say that the operator + associates to the left, because an operand with plus signs on both sides of it
belongs to the operator to its left.”.
Capítulo 3: Lógica Proposicional
58
de derecha a izquierda (<-) ya que el primero a ser aplicado en una secuencia de conectores
contiguos que preceden una fbf (v.g. P), debe ser el más cercano a la fbf.
pp¬rr¬pq
Es el que se muestra a continuación:
¬ q
p p
r
r ¬
p
80 En este sentido la lógica proposicional es “bivalente”. Es posible, sin embargo, concebir lógicas “trivalentes” o
“multivalentes” y lógicas en que se le da un “grado de certeza” al valor de verdad asociado con una fbf. (ver
http://es.wikipedia.org/wiki/L%C3%B3gica_difusa)
Capítulo 3: Lógica Proposicional
59
Una “interpretación” , en lógica proposicional es una asignación de valor de
verdad para los símbolos no lógicos (variables proposicionales) que aparecen en las
fbfs de una especificación.
: {a, a1, a2, a3, … , b, b1, b2, b3, … p, p1, p2, p3, … , q, q1, q2, q3, …} {F,V}
Si la especificación tiene n variables, para ella existen 2n posibles interpretaciones.
3.4.3.2 Semántica de los conectores.
Como se vio arriba, una fórmula compleja, es una fórmula que ensambla fórmulas menos
complejas por medio de los conectores. El valor de verdad de una fbf compleja será
establecido con base en el valor de verdad de las fbfs que ensambla y del valor semántico
de los conectores.
Los conectores son los símbolos lógicos de la lógica de proposiciones y representan
operadores en el álgebra de Bool. En particular el conector ”” representa al operador “+”
(suma), el conector “” representa al operador “*” (producto), y el conector “” representa
el operador “-“ (cambio de signo).
En el álgebra de Bool, estos conectores están asociados a funciones de dominios y
codominios basados en el dominio BOOL, que pueden representarse de forma explícita por
medio de tablas. En particular los operadores “, , ” representan las funciones siguientes:
P Q P PQ PQ
V V F V V
V F F V F
F V V V F
F F V F F
Los conectores “, ”, por su parte, representan a operadores compuestos, definidos a
partir de los anteriores, así:
FG Es una forma de escribir (F)G
FG Es una forma de escribir ((F)G)((G)F)
En consecuencia los conectores “, ” representan las funciones siguientes:
P Q PQ PQ
V V V V
V F F F
F V V F
F F V V
Capítulo 3: Lógica Proposicional
60
Los conectores “” y “” están también relacionados ya que puede definirse en función
del “” y del “”, así:
FG Es una forma de escribir (FG)
En consecuencia el conjunto mínimo de conectores81 es uno cualquiera de los conjuntos {,
}, {, }, o {, }
Nótese que las tablas refieren a fbfs cualquiera como argumentos de los conectores
(representadas por variables en mayúscula) y no sólo a átomos, en cuanto lo que interesa es
el valor de verdad de la fbf (primitiva o no) a la que se aplique el conector.
Así, la semántica de los conectores (en el sentido expuesto en 3.3.2 ) puede resumirse de la
forma siguiente:
: verdadero cuando su argumento es falso.
: verdadedro sólo si sus dos argumentos son verdaderos.
: verdadero cuando alguno de sus argumentos es verdadero.
: verdadero excepto cuando siendo el primer argumento (antecedente) verdadero,
su segundo argumento (consecuente) es falso.
: verdadero cuando los dos argumentos tienen el mismo valor de verdad.
Definición:
Una fbf F es insatisfacible o una contradicción si NO existe una interpretación
que sea modelo de F
Definición:
Una fbf F es válida o una tautología si toda posible interpretación es modelo de F
Note que si F es válida, entonces F es insatisfacible, y que una fbf F es contingente si no
es válida ni contradictoria
Ley de la separación: P (P Q) Q
Modus tollendo tollen: Q (P Q) P
Modus tollendo ponens: P (P Q) Q
Ley de la simplificación: P Q P
Ley del silog. hipotético: (P Q) (Q R) (P R)
Ley de la exportación: (P Q R) (P (Q R))
Ley de la importación: (P (Q R)) (P Q R)
Ley del absurdo: (P Q) Q P
Ley de la adición: P P Q
Vale la pena justificar el uso de meta-variables en el ejemplo anterior. Para ello basta con
notar que para definir si una fbf es válida o insatisfacible, es suficiente con considerar su
valor de verdad para todas las posibles combinaciones de valores de verdad de sus
variables, AÚN CUANDO ESTAS NO REPRESENTEN ÁTOMOS. Esto es debido a que
el cálculo del valor de verdad de la fbf para cualquiera de las posibles combinaciones de
valores de verdad de sus átomos pasa por hallar el valor de verdad para alguna de las
Capítulo 3: Lógica Proposicional
62
posibles combinaciones de valores de verdad de sus variables (que en todos los casos ya ha
quedado analizada).
En este sentido, las fbfs que son tautologías o contradicciones pueden considerarse como
plantillas que representan familias de fbfs que son tautologías o contradicciones. Estas
familias están compuestas por las fbfs resultantes de substituir cada variable por una fbf
cualquiera, primitiva o no (substituyendo por la misma fbf todas las ocurrencias de cada
variable particular).
Para el caso en que al considerar el valor de verdad de una fbf para todas las posibles
combinaciones de valores de verdad de sus variables, dicha fórmula resulte ser contingente,
ella NO puede ser considerada como una plantilla que represente a una familia de fbfs que
también son contingentes, considerando sus variables como meta-variables. En efecto
siempre queda la posibilidad de que algunas de las combinaciones de valor de verdad de
dichas meta-variables, no ocurran en todas las posibles combinaciones de valores de verdad
de los átomos de las que ellas pudieran estar compuestas (y tal vez dichas combinaciones
fueron las que determinaron que la fbf original fuera contingente).
3.4.3.5 Semántica de un conjunto de fbfs.
Tal como se definió 3.3.2 la semántica de una fbf que tiene como dominio semántico a
BOOL, es el conjunto de interpretaciones de sus símbolos no lógicos, para los que el valor de
verdad de la fórmula es “verdadero”. En el marco de la lógica proposicional, esto se
traduce al conjunto de asignaciones de valores de verdad para los átomos (o variables
proposicionales) de la fbf para las que ella calcula a “verdadero”.
La semántica de una tautología es el todo el conjunto de posibles asignaciones de valores de verdad para
sus átomos.
La semántica de una contradicción es el conjunto vacío.
Cuando una fbf hace parte de un conjunto de fbfs, podemos extender el concepto de
semántica de la fbf al conjunto de interpretaciones de los símbolos no lógicos de todas las
fórmulas que aparecen en el conjunto, para las que el valor de verdad de la fbf es
“verdadero”.
Así, en lo que sigue consideraremos que la semántica de un conjunto de fbfs, es la
intersección de las semánticas extendidas de cada una de las fórmulas que contiene. Esta
semántica no es otra que la semántica de la fbf resultante de conectar todas las del conjunto
con el conector “”.
3.4.4 Inferencia en lógica proposicional.
Si bien el valor de verdad de una fbf en lógica proposicional debe poder obtenerse del
significado de sus átomos, este valor se hace cada vez más difícil de obtener a medida que
crece el número de átomos.
Los criterios demostrativos de la lógica de proposiciones permiten, por su parte, inferir el
significado de fbfs sin necesidad de recurrir al significado de sus átomos. Igualmente
Capítulo 3: Lógica Proposicional
63
dichos criterios permiten “razonar” para inferir nuevas fbfs verdaderas a partir de otras
previamente dadas como tal.
Definición:
82No nos ocuparemos aquí del caso en que F1F2...Fn es una contradicción, ya que de considerarse suficiente que
(F1F2...Fn) G sea una tautolgía para que G sea consecuncia lógica de las Fi, si F1F2...Fn es una contradicción
entonces cualquier fbf seria su consecuencia lógica.
Capítulo 3: Lógica Proposicional
64
Una teoría en una lógica es una secuencia de fbfs, F1,F2, ...,Fn, tales que para cada
fórmula Fi, esta es ya sea un axioma de la teoría o una consecuencia lógica de F1,F2,
...,Fi-1.
En una teoría existen, en efecto, un conjunto de fórmulas que son dadas por verdaderas,
denominadas axiomas, y un conjunto de fórmulas que son consecuencia lógica de los
axiomas, que son llamadas teoremas.
3.4.4.2 Tablas de verdad
Una forma de probar que (F1F2...Fn)G es una tautología, es la de obtener el valor de
verdad de la fórmula para todas las posibles interpretaciones de sus átomos. Esto equivale
a hallar el valor de verdad de la fórmula para 2n interpretaciones, siendo n el número de
variables que involucra.
Cuando n es pequeña, el cálculo de los valores de verdad de la fórmula para todas las
interpretaciones, puede organizarse fácilmente en una tabla de verdad. El proceso, a
grandes rasgos consiste en construir una tabla donde se colocan, en el encabezamiento de
cada columna, de izquierda a derecha, una a una, desde la más simple hacia la más
compleja, las sub-fórmulas que conforman la fórmula (quedando las variables de la fórmula
en las primeras columnas). Luego se llena cada fila iniciando las casillas que corresponden
a las variables con una de las posibles asignaciones de valores de verdad a dichas variables;
y más luego se llenan las casillas que siguen, de izquierda a derecha, con base en la
semántica del conector principal de la fórmula de la columna.
Cuándo las variables de la tabla representan los átomos, ella tiene entonces una fila por
cada una de las interpretaciones más uno (2n+1), y una columna por cada una de las
subfórmulas de la fórmula (una columna para cada variable y una columna para cada
conector). Para más detalles y ejemplos ver sección 1.3.5 de [Grassmann 97].
V V V F V V F F V V
V V F F V F V V F V
V F V F F F F F V V
V F F F F F V V F V
F V V V V V F V V V
F V F V V F V F V V
F F V V V V F V V V
F F F V V F V F V V
Esta proposición posee una propiedad interesante que vale la pena resaltar. Todas las asignaciones
posibles de sus variables, hacen a la proposición verdadera. Luego, P((P¬R)R(¬PQ)) es una
tautología. La importancia de las tautologías radica en su uso a la hora de definir las reglas de inferencia
Capítulo 3: Lógica Proposicional
65
del razonamiento lógico.
En la siguiente tabla de verdad se puede verificar que Q es consecuencia lógica de las proposiciones P y
PQ. Veamos:
P Q PQ P(PQ)
V V V V
V F F F
F V V F
F F V F
En efecto, al comparar la columna bajo P(PQ) con la columna bajo Q, se puede comprobar que Q es
consecuencia lógica de las proposiciones P y PQ; ya que siempre que P(PQ) es verdadero (solo la
primera fila), también lo es Q, que es la consecuencia. Nótese que Q es verdadera para otras
interpretaciones, pero esto es permitido por la definición.
Para ilustrar lo anterior, probaremos mediante una tabla de verdad, que R es consecuencia lógica de las
proposiciones PQ, QR y P.
V V V V v V V F
v V F V F F V F
v F V F v F V F
v F F F v F V F
F V V V v F V F
F V F V F F V F
F F V V v F V F
F F F V v F V F
En la tabla no solo hay una sino tres pruebas de que R es consecuencia lógica de las fórmulas
{PQ,QR,P}. Como en el ejemplo presentado arriba, se puede comparar la columna bajo
(PQ)(QR)P, con la columna bajo R, notando que siempre que la primera es verdadera también lo
es la segunda. Como segunda prueba basta computar la séptima columna que muestra que
((PQ)(QR)P)R es una tautología, lo que corresponde a la segunda definición. Finalmente la
última columna muestra que (PQ)(QR)P¬R es una contradicción.
Nótese que tal como se explicó en el Ejemplo 31¡Error! No se encuentra el origen de la referencia.
una fbf que es verdadera [o falsa] para todas las combinaciones de valor de verdad de sus variables, sean
estas átomos o no, es una tautología [o contradicción].
Premisa1
Premisa2
..
Premisan
_______________________________________
Conclusión
Dónde:
Premisai: Son fbfs en las que las variables, denominadas “meta variables
proposicionales”, representan otras fbfs.
Conclusión Es una fbf en las que en las que las meta variables proposicionales,
representan las mismas fbfs que representaban en las premisas.
Definición:
Un conjunto de reglas de inferencia se denomina un “sistema deductivo”
La tautología
P(PQ)Q
Tiene la forma (F1F2)G , con F1=P, F2=(PQ) y G=Q.
Por tanto una secuencia {F1, F2, F3, F4, ..., Fn} donde Fk tenga la forma FjG para j<k, tiene como un
posible teorema a G.
En este caso la regla de inferencia correspondiente a esta tautología es la siguiente:
A
AB
_______________________________________
B
Dónde: A y B : Son meta variables proposicionales que, representan otras fbfs.
Considere:
el niño cae
si el niño cae entonces el niño llora
_______________________________________
el niño llora
Hay algunos aspectos que vale la pena resaltar de las equivalencias presentadas, dirigidos
hacia nuestro propósito de convertir cualquier fórmula a una “forma normal”.
1. Todas las equivalencias o implicaciones se pueden reemplazar por conjunciones o
disyunciones.
2. Debido a la asociatividad de “” (equivalencias 4), los paréntesis en (PQ)R,
pueden ser omitidos, es decir, podemos escribir PQR. Más en general, podemos
escribir P1P2...Pn sin ambigüedad, donde P1,P2,...,Pn son fórmulas.
P1P2...Pn es verdadero si y solo si, al menos uno de los Pi (1in) es verdadero.
De la misma manera, podemos escribir P1P2...Pn que es verdadero si y solo si
todos los Pi (1in) son verdaderos.
3. Debido a la conmutatividad de “” y de “”, el orden en que aparecen los Pi en
P1P2...Pn o en P1P2...Pn es indiferente.
4. Gracias a las leyes de De Morgan, siempre es posible llevar las negaciones del
exterior hacia adentro, hasta el punto en que tan solo las proposiciones más simples
aparezcan negadas.
3.4.5.2 Forma Normal Conjuntiva (forma “clausal”).
La “forma normal conjuntiva” (FNC) reduce las fbfs a la conjunción de “cláusulas”, por lo
que diremos que las fbfs están en “forma clausal”. La importancia de esta forma normal es
debida a que bajo ciertas condiciones, una fbf en forma clausal constituye una
Capítulo 3: Lógica Proposicional
69
especificación lógica para la que es posible automatizar la demostración que establece, que
otra cláusula sometida como “consulta”, es consecuencia lógica de la especificación.
Definición:
Llamaremos “literal” a un átomo o la negación de un átomo.
Definición:
Llamaremos “cláusula” a una fbf compuesta por literales conectados por “” (una
“disyunción” de literales).
Definición:
Diremos que una fbf está en “forma normal conjuntiva” (o FNC) si y sólo si es
una serie de cláusulas conectadas por “” (una “conjunción de cláusulas).
Por ejemplo, si p, q, y r son átomos, todas las siguientes fórmulas están en forma normal conjuntiva:
(p¬qr)(¬pq)(¬rp)
Teniendo en cuanta que un átomo cualquiera p (ó q) es semánticamente equivalente a pF (ó qF) , p (ó
q) es considerado por si mismo una fbf en forma clausal y por tanto las siguientes fbf también lo son:
pq
F
Para toda fórmula en lógica proposicional, existe una fórmula en FNC, semánticamente
equivalente a ella. Esto no lo probaremos, pero exhibiremos un proceso que permite
encontrar una FNC equivalente para cualquier fórmula.
Basta seguir los siguientes pasos aplicando las equivalencias presentadas en la sección
anterior:
1. Eliminar todas las “” y “”, utilizando las equivalencias 1 y 2.
2. Si la fórmula resultante contiene cualquier subexpresión compuesta negada,
eliminar la negación utilizando las equivalencias 9 y 10.
3. Entonces, se utiliza sucesivamente la equivalencia 5(a) para llevar las “” hacia
adentro y las “” hacia afuera hasta encontrar una FNC.
¬((QP)¬R)
= ¬((¬QP)¬R) Por def. de
= (¬(¬QP)R) Por ley de De Morgan y doble negación
= ((Q¬P)R) Por ley de De Morgan y doble negación
= (QR)(¬PR) Por ley distributiva y conmutativa
(QR)(¬PR) es una FNC, equivalente a ¬((QP)¬R). Nótese que la segunda derivación del
proceso requiere la ley de doble negación pues ¬((¬QP)¬R) = (¬(¬QP)¬(¬R)) que por doble
Capítulo 3: Lógica Proposicional
70
negación es equivalente a (¬(¬QP)R). En ocasiones omitiremos la sustentación, y como en éste caso,
reuniremos varios cambios sucesivos en uno solo, para dar mayor fluidez al discurso, pero es importante
saber exactamente lo que se está haciendo.
(P(QR))S
= (P(¬QR))S Por def. de
= ¬(P(¬QR))S Por def. de
= (¬P¬(¬QR))S Por ley de De Morgan
= (¬P(Q¬R))S Por ley de De Morgan
= ((¬PQ)(¬P¬R))S Por ley distributiva
= ((¬PQ)S)((¬P¬R)S) Por ley distributiva
= (¬PQS)(¬P¬RS) Por ley asociativa.
De donde se puede concluir que (¬PQS)(¬P¬RS), es una FNC para (P(QR))S.
Así como existe una FNC, también existe una forma normal disyuntiva que se define así:
Definición:
Diremos que una fbf está en “forma normal disyuntiva” (o FND) si y sólo si es
una disyunción de conjunciones de literales.
Siguiendo un proceso similar al presentado arriba, para cualquier fórmula se puede
encontrar una fórmula en forma normal disyuntiva equivalente. Queda en manos del lector,
intentarlo.
Ejercicios Propuestos.
3.6.1 De la lógica en general
( ) La lógica es el estudio del pensamiento humano.
( ) La lógica es una sola y es la ciencia de la verdad.
( ) La lógica es un lenguaje.
( ) La lógica tiene que ver con todos los tipos de lenguaje utilizado por los humanos.
( ) La lógica es el estudio de los todos los lenguajes compuestos por ensamblajes de
símbolos.
(…) Todo lenguaje de programación es una lógica.
(…) La lógica puede ser usada como lenguaje de programación.
(…) Toda lógica es un lenguaje de programación.
( ) Una lógica determina cuales ensamblajes de símbolos son correctos en el lenguaje
simbólico que le compete.
( ) La sintaxis de la lógica limita los símbolos que pueden ser usados en un lenguaje
lógico.
( ) El alfabeto de una lógica es en general limitado y el conjunto de fbfs es en general
ilimitado.
( ) El alfabeto de una lógica es en general ilimitado y el conjunto de fbfs es en general
limitado.
( ) Todo ensamblaje de símbolos del alfabeto propio de una lógica es una fbf en dicha
lógica.
( ) La sintaxis de la lógica la componen el alfabeto de símbolos y los criterios formativos
para sus posibles ensamblajes.
( ) La lógica determina un significado para todos los posibles ensamblajes de símbolos.
Capítulo 3: Lógica Proposicional
73
( ) Una fórmula atómica de una lógica es una formula cuyas partes constituyentes no son
formulas bien formadas.
( ) Los símbolos del alfabeto son los átomos de la lógica.
( ) El significado de una fbf depende del significado de los átomos que contiene.
( ) El significado de un ensamblaje correcto puede variar y por tanto es siempre
ambiguo.
( ) Todo ensamblaje de los símbolos de una lógica, que satisfaga los criterios formativos
de la lógica, es un átomo de dicha lógica.
P Q R
P Q R
P Q R
P Q P R Q
P Q P R Q
P Q P R Q
PQ R
PQ R
PQPRQ
(P Q) (P R Q)
PQRPQRS
(P (Q R)) ((P Q) (R S))
3.6.2.4 Prueba.
1- Pruebe que las fbfs del ejemplo Ejemplo 36 son tautologías, y luego expréselas como
reglas de inferencia.
2- Pruebe que las parejas de fbf presentadas en el numeral 3.4.5.1 son equivalencias
semánticas utilizando tablas de verdad:
3.6.2.5 Varios
1. Desarrollar la tabla de verdad para cada una de las definiciones de los conectivos lógicos.
(¬P, PQ, PQ, PQ y PQ).
2. Determinar cuáles de las siguientes son proposiciones atómicas y cuáles no. Si no lo son,
descomponerlas en sus átomos constituyentes y rescribirlas como fórmulas de la lógica
proposicional.
a) “Si la humedad es alta y la temperatura es alta, entonces uno no se siente
confortable.”
Capítulo 3: Lógica Proposicional
76
b) “Todos los artistas están locos.”
c) “Héctor es rico y José es rico y también feliz.”
d) “La colombina que se comió Camila era roja o naranjada.”
e) “Si Héctor está en la piscina, entonces José debe de estar también en la piscina.”
3. Dibujar el árbol sintáctico para la siguiente declaración de la lógica de proposiciones: “Si
no me equivoco, Héctor estaba en la piscina y José no, de manera que Sandra dijo
mentiras.”
4. Construir la tabla de verdad y el árbol sintáctico para las siguientes fórmulas:
(¬P(QP)P)(¬P(P¬Q))
(¬PQ)(P¬Q¬R)(¬P¬R)R
5. 7. Explicar por qué las primeras dos definiciones de consecuencia lógica son
equivalentes. (Pista: la razón yace sobre la definición de implicación y de tautología).
a) Qué relación existe entre las fórmulas (F1F2...Fn)Q y (F1F2...Fn¬Q).
8. Probar las siguientes deducciones:
a) ¬P es consecuencia lógica de (PQ) y ¬Q.
b) PQ es consecuencia lógica de (P(QR)) y Q.
c) R es consecuencia lógica de pQ, PR y QR.
9. Convertir la siguiente argumentación a lógica de proposiciones y demostrar su validez:
“Si Josef esta arrestado es porque es culpable o alguien lo ha calumniado. Por tanto, si Josef
no es culpable, alguien debió haber calumniado a Josef.”
Capítulo 4
Lógica de Predicados
Capítulo 4: Lógica de Predicados.
78
Introducción
Para utilizar proposiciones que se refieran a propiedades de conjuntos (o individuos) y
poder derivar de ellas proposiciones que se refieran a individuos (o conjuntos), se requieren
elementos adicionales a los de la lógica proposicional.
Por ejemplo: de las proposiciones: “Todos los hombres son mortales” y “Juan es hombre”
se debe poder derivar que “Juan es mortal”; y, de las proposiciones “Juan es mortal”; y
“Juan es hombre”, se debe poder derivar que “Algún hombre es mortal”.
Para clarificar que las proposiciones más elementales (o atómicas), son afirmaciones (o
predicas) sobre individuos (u objetos), en la lógica de predicados se distingue lo que se
predica (v.g. un verbo) de los objetos sobre los que se predica (v.g. un sustantivo). De esta
manera el cálculo de predicados permite representar en una lógica las afirmaciones más
comunes del lenguaje natural, así:
Uso de sintagmas verbales como medio para afirmar.
Uso de sintagmas nominales para referirse a los objetos del mundo sobre los que
recae la afirmación.
4.2.1.2 Términos.
Son construcciones sintácticas que representan los objetos sobre los que recae el predicado.
En una lógica tipada cada término debe estar asociado a un tipo.
Se fundamentan en los símbolos siguientes.
4.2.1.2.1 Constantes
Símbolos que permiten hacer referencia a objetos particulares. Las constantes en una
lógica tipada deben estar asociadas con su tipo (ya sea de forma explícita por medio de una
“declaración” o de forma implícita con base en la forma de la constante. En lo que sigue
usaremos símbolos especiales o rótulos que empiezan con minúscula para las constantes.
4.2.1.2.2 Variables.
Capítulo 4: Lógica de Predicados.
80
Símbolos que permiten predicar sobre objetos indeterminados de un tipo para hacer
afirmaciones sobre el tipo. Las variables en una lógica tipada deben ser declaradas y
asociadas con su sort
Usaremos letras mayúsculas para representar las variables, y en algunos casos se indicará
de forma explícita su tipo.
X, Y, Z:Int
X+Y
novio_de(maria)
83En lo que sigue sólo indicaremos de forma explícita que una construcción constituyue un predicado cuando ello no es
obvio del contexto en que se usa.
Capítulo 4: Lógica de Predicados
81
El número de términos que se deben ensamblar con el predicado para definir una
proposición atómica (“aridad” del predicado), que en nuestro caso será constante
para cada predicado84.
Sort asociado a cada uno de los términos ensamblados por el predicado.
Símbolos que forman el predicado y la manera como se disponen en relación a los
términos que ensambla.
Lugar y posición (número de orden) que le corresponde a cada uno de los términos
en el marco de la disposición de los símbolos que forman el predicado.
Distinguiremos dos tipos de plantilla:
Las “plantillas estándar”, que disponen en sucesión los elementos, colocando
primero un símbolo para el predicado, y luego entre “( )” una lista con los términos,
separados por “,”.dándole a cada término una posición fija en la lista.
Las “plantillas infijas” que distribuyen los símbolos del predicado y los términos de
forma arbitraria, fijando para cada término un lugar en el ensamblaje (que en lo que
sigue señalaremos con el símbolo “_”). La plantilla infija determina un número de
orden para cada uno de los lugares (por ejemplo contándolos de izquierda a
derecha), para darle a cada término una posición fija en el ensamblaje.
Los espacios disponibles se consideran ordenados (cada uno corresponde a un entero entre
{1..aridad}). El significado de la proposición está asociado a dichas posiciones, por lo que
dos proposiciones de un predicado sobre los mismos objetos en posiciones diferentes de la
plantilla pueden tener, en general, diferente significado.
4.2.1.4 Cuantificadores.
Son los símbolos “ ” denominado “para todo”, y el símbolo “ ” denominado “existe”.
Se usan en combinación de una o varias variables y una fbf para formar una fbf
“cuantificada”
4.2.2 Criterios Formativos de las fbf en lógica de predicados
Un predicado se ensambla con uno o varios términos de la forma que lo indica su plantilla
para formar una una proposición atómica. Las fbfs, en general, se ensamblan con los
conectores de la lógica de proposiciones de la misma forma que en dicha lógica. Cualquier
84 En general una plantilla con las mismas palabras y distinta aridad puede considerarse asociada con un predicado
diferente.
Capítulo 4: Lógica de Predicados.
82
fbfs pueden además ensamblarse con cuantificadores y variables para formar fbfs
“cuantificadas”.
4.2.2.1 Criterios formativos de una proposición atómica en lógica de predicados.
Una plantilla de predicado en el que cada uno de los símbolos de espacio disponible ha sido
substituido (o “instanciado”) por un término del tipo correspondiente al espacio, es una fbf
atómica en lógica de predicados.
A los términos que instancian los espacios de una plantilla de predicado se les denomina
“argumentos” del predicado.
Restricciones:
En una lógica de predicados tipada, los argumentos deben ser del tipo previamente
asociado al espacio que ocupan en la plantilla.
El orden en que aparecen los argumentos en los espacios de la plantilla es
importante:
Así dos proposiciones definidos sobre la misma plantilla y con los mismos
argumentos son proposiciones diferentes si el orden de los argumentos es diferente.
X ama(X,maria)
Y:Hombre ama(Y,juan)
X:Int ((X+1)>X)
Nótese que una fbf cuantificada es una fbf, y por lo tanto puede ser también cuantificada.
Y X ama(X,Y)
Capítulo 4: Lógica de Predicados
83
X Y:Hombre ama(Y,X)
X Y ama(X,Y)
X:Int Y:Int (X+Y)=(Y+X)
Para simplificar la escritura de las fbf donde se repite el mismo cuantificador sobre
variables del mismo tipo, escribiremos la lista de las variables luego de una sola ocurrencia
del cuantificador.
X,Y:Int (X+Y)=(Y+X)
X
Y
ama ama
a
X maria Y juan
a3
Aunque los ciantificadores deben tener igual prioridad y deben tener un sentido de
asociatividad de derecha a izquierda, nótese que el orden de prioridad con respecto a los
conectores es arbitraria, por lo que podríamos darles otra precedencia a la presentada arriba
(cambiando el significado de la fbf)85.
85En [Hut 04], los cuantificadores tienen la mayor precedencia (junto con la negación). En este trabajo, sin embargo,
adoptaremos la estrategia de las notaciones VDM [Jones 90] y Z [Woodc 96], donde al tener los cuantificadores la mínima
precedencia, se minimiza el uso de paréntesis.
Capítulo 4: Lógica de Predicados
85
X
ama
Y Y juan
ama
X maria
En:
X (ama(X,Y) ama(Y,juan))
Hay dos ocurrencias de Y y una de X
Una variable que ocurre dentro del alcance de dos o más cuantificadores aplicados sobre la
misma variable queda ligada al cuantificador (sobre la variable) mas cercano en el árbol
sintáctico.
En la fbf siguiente, la primera ocurrencia de Y está ligada al Y mientras que la segunda está ligada al
Y.
Y (Y ama(X,Y)) ama(Y,juan)
En la fbf siguiente, las dos ocurrencias de Y son libres y la ocurrencia de X es ligada al X.
X (ama(X,Y) ama(Y,juan))
La primera de las fbf siguientes es abierta, mientras que la segunda es cerrada:
X (ama(X,Y) ama(Y,juan))
Y X (ama(X,Y) ama(Y,juan))
En la fbf siguiente, P(X,Y) refiere a una fbf indeterminada en la que X y Y son variables libres.
X P(X,Y)
Nótese que el cuantificador X liga todas las ocurrencias libres de la variable X en P(X,Y), quedando la
variable Y libre.
X Y ama(X,Y)
(ama(c,c) ama(c,l) ama(c,j) )
(ama(l,c) ama(l,l) ama(l,j) )
(ama(j,c) ama(j,l) ama(j,j) )
X Y ama(Y,X)
(ama(c,c) ama(l,c) ama(j,c) )
(ama(c,l) ama(l,l) ama(j,l) )
(ama(c,j) ama(l,j) ama(j,j) )
X Y ama(X,Y)
(ama(c,c) ama(c,l) ama(c,j) )
(ama(l,c) ama(l,l) ama(l,j) )
(ama(j,c) ama(j,l) ama(j,j) )
X Y ama(Y,X)
(ama(c,c) ama(l,c) ama(j,c) )
(ama(c,l) ama(l,l) ama(j,l) )
(ama(c,j) ama(l,j) ama(j,j) )
Y las interpretaciones siguientes, representadas de forma tabulada (en la que la persona correspondiente
a una fila ama a la persona correspondiente a la columna), serían modelo de las formulas:
La interpretación:
c l j
c x
l x
j x
Es modelo de X Y ama(X,Y)
La interpretación:
c l j
c x
l x
j x
Es modelo de X Y ama(Y,X)
La interpretación:
c l j
c x x x
l
j
Es modelo de X Y ama(X,Y)
La interpretación:
c l j
c x
l x
j x
Capítulo 4: Lógica de Predicados.
90
Es modelo de X Y ama(Y,X)
Nótese que si la variable se asocia explícitamente con un tipo, sólo se tienen en cuenta los
objetos del tipo. En caso contrario se asume que la variable recorre todos los objetos de U.
Una consecuencia de la semántica de una fbf con cuantificadores, es que si ellos cuantifican
una variable definida sobre un conjunto infinito (con un número infinito de objetos como es
el caso de los números enteros), el valor de verdad de la fbf no se puede calcular. En estos
casos el valor de verdad de las fórmulas sólo puede establecerse por demostración.
Todo lo que se puede expresar en una lógica tipada, se puede expresar también en una
lógica atipada usando “predicados de tipo”. Estos predicados se aplican a un sólo
argumento y son ciertos cuando el término es del tipo referido en el predicado. Así, una
lógica tipada se puede considerar como un caso particular de una lógica atipada o una
forma de “azúcar sintáctico” [Bourbaki 50], sobre la lógica atipada (que es la lógica
clásica).
Así, las fórmulas siguientes son semánticamente equivalentes ():
X:T P(X) X (XTP(X))
X:T P(X) X (XTP(X))
Por la semántica del “”, si T es el conjunto vacío { }, entonces se cumple para cualquier
P(X) que:
(X:{ } P(X)) V
Por la semántica del “” si T es el conjunto vacío { }, entonces se cumple para cualquier
P(X) que:
(X:{ } P(X)) F
X Y ...P(X,Y,..)
––––––––––––––––––––––––––––
V1 V2 ...P(fx(V1, V2..,a,b..), fy(V1, V2..,a,b..),...)
Donde V1, V2,.. son variables, a,b.. son constantes y fx(V1, V2..,a,b..), fy(V1, V2..,a,b..)... son
términos del sort de X, Y, etc..
2. Prueba de existencia: De una fbf que tenga un cuantificador universal asociado a una
variable, se deduce la fórmula que remplaza el cuantificador universal por el existencial
para dicha variable. De una fbf donde ocurra un término base se deriva una fbf que
cuantifica existencialmente a una variable que substituye el término base (en cualquiera
de sus ocurrencias).
3. Generalización de predicados a una variable: De una fbf en la que no aparece una cierta
variable como variable libre. Puede deducirse la fbf que tiene un cuantificador sobre la
variable y como alcance del cuantificador a la fórmula.
Cláramente el criterio de particularización se cumple tanto para variables tipadas universalmente o no, y
siempre y cuando la particularización ocurra a un valor del tipo de la variable.
Se deja al lector la tarea de interpretar los otros dos criterios para el caso de variables no tipadas
universalmente, sino sobre un tipo TU .
Por ejemplo, si P, Q, y R son átomos, todas las siguientes fórmulas están en forma normal conjuntiva:
X H(X)M(X)
X Y p(f(X,Y),f(Y,X))
x y z P(x,y)R(z)
Para llevar cualquier fbf a una forma normal prenex equivalente se deben llevar a cabo los
siguientes pasos:
1. Eliminar todas las “” y “”, utilizando las equivalencias 1 y 2 de la lógica
proposicional (ver 3.4.5.1).
Capítulo 4: Lógica de Predicados.
94
2. Si la fórmula resultante contiene cualquier subfórmula compuesta negada, llevar las
negaciones hasta los átomos de la subfórmula, utilizando las equivalencias 9, 10 de
la lógica proposicional y , 12(a) y 12(b), de la lógica de predicados hasta que solo
aparezcan átomos negados.
3. Llevar los cuantificadores hacia afuera utilizando las equivalencias 11 y 13, y si es
necesario renombrando variables para evitar la introducción de ligaduras entre
variables ligadas a cuantificadores diferentes (ver 0 ).
Por último nótese que la fbf que llamamos M o “matriz” en la definición de forma normal
Prenex, al no contener cuantificadores, puede ser llevada a una FNC, siguiendo los pasos
presentados en la sección 3.4.5.2.
Ejercicios Propuestos.
4.7.1 Verdadero o falso?
( ) La lógica de predicados permite deducir afirmaciones sobre miembros de un conjunto a
partir de afirmaciones sobre el conjunto.
86 Entendiendo como instancia la substitución de las ocurrencias de una variable por un elemento de su dominio.
Capítulo 4: Lógica de Predicados.
96
( ) Las proposiciones de la lógica de predicados, son afirmaciones sobre objetos o
conjuntos de objetos.
( ) En la lógica de predicados se distingue lo que se predica de los objetos sobre los que
se predica.
( ) La lógica de predicados permite representar de forma simplificada las afirmaciones del
lenguaje natural.
( ) La lógica de predicados no adiciona nuevos elementos al alfabeto de símbolos.
( ) Los operadores y operandos no hacen parte de la lógica de predicados ya que sólo se
usan en lógicas más avanzadas (v.g. aritmética, álgebra y cálculo).
( ) En la lógica de predicados lo que se predica se representa en la “plantilla” del
predicado y los objetos sobre los que se predica se representan por medio de
términos.
( ) Los términos de la lógica de predicados no tienen relación con los tipos, ya que estos
sólo existen en los lenguajes de programación.
( ) Las plantillas de los predicados no tienen relación con los tipos de los términos.
( ) El orden de los términos dentro de la plantilla del predicado es insustancial en la
lógica de predicados clásica.
( ) Los predicados de la lógica de predicados son átomos que pueden tener como partes
constitutiva a otros átomos de la lógica proposicional.
( ) Las proposiciones atómicas de la lógica de predicados, son plantillas de predicado
instanciadas sobre objetos específicos o indeterminados.
( ) Los cuantificadores sirven para calcular el valor de verdad de los términos.
( ) Los cuantificadores permiten llevar a cabo afirmaciones sobre conjuntos de objetos.
( ) El alcance de un cuantificador sólo puede ser un predicado.
( ) En la lógica de predicados no se usan conectores.
( ) El alcance de un cuantificador puede cambiar según su orden de prioridad relativo a
los conectores.
( ) En una fbf cuantificada, la aparición de una variable en cualquier parte de la fbf, se
denomina “una ocurrencia” de la variable.
( ) Una formula bien formada en lógica de predicados, no puede tener variables libres si
la fbf está dentro del alcance de un cuantificador.
( ) Una ocurrencia de una variable está ligada a un cuantificador cuando este cuantifica
la variable y la ocurrencia está en el alcance del cuantificador.
( ) Una ocurrencia de una variable que está en el alcance de un cuantificador sobre la
misma variable puede declararse como libre en la definición de la variable.
( ) Todas las ocurrencias de una variable en una fbf están ligadas a un mismo
cuantificador o todas son libres.
Capítulo 4: Lógica de Predicados
97
( ) Todas las ocurrencias de una variable en una fbf están ligadas a un mismo
cuantificador.
( ) Si en una formula bien formada hay una ocurrencia de una variable dentro del alcance
de un cuantificador ligado a la variable y otra ocurrencia por fuera del alcance de
dicho cuantificador, entonces ambas ocurrencias corresponderán, a la misma variable
luego de que esta sea asignada.
( ) Los átomos base de la lógica proposicional son plantillas de predicados instanciadas
con variables.
( ) La extensión de los predicados determina el valor de verdad de los átomos base de la
lógica de predicados.
( ) Las tablas de una Base de Datos pueden asimilarse a la extensión de los predicados de
una lógica de predicados.
( ) En cálculo de predicados no tiene sentido hablar de una Interpretación ya que no hay
variables proposicionales.
( ) La interpretación para un conjunto de fbfs en una lógica de predicados es el conjunto
de extensiones de los predicados que involucran.
( ) Sólo puede haber una interpretación para un conjunto de fbfs en una lógica de
predicados.
( ) Para conocer el valor de verdad de una fbf en lógica de predicados es necesario usar
siempre criterios de demostración.
( ) El valor de verdad de una fbf cuantificada con varios cuantificadores consecutivos, no
depende de la posición relativa de dichos cuantificadores.
( ) El valor de verdad de una fbf con un solo cuantificador sobre una variable y una sola
variable no cambia si se cambia la variable por otra diferente.
( ) El valor de verdad de una fbf con varios cuantificadores sobre varias variables no
cambia si se cambia la variable de uno de los cuantificadores y todas sus ocurrencias
en el alcance por otra variable diferente.
( ) Las variables ligadas deben ser asignadas a un valor del dominio antes de calcular la
semántica de una fórmula en lógica de predicados.
( ) Para calcular el valor de verdad de una fbf con variables libres, ellas deben ser
asignadas a una constante específica, además de definir la interpretación.
(...) Si hay dos cuantificadores seguidos sobre la misma variable, no es posible hallar la
semántica de la fbf.
4.7.2 Árboles sintácticos
Elabore el árbol sintáctico de las fórmulas siguientes, para los dos órdenes de precedencia
siguientes:
, , , , ,( / )
( / / ), , , ,
X ama(X,maria) ama(Y,juan)
Capítulo 4: Lógica de Predicados.
98
(X ama(X,maria)) Y ama(Y,juan)
X ((Y ama(X,maria)) ama(Y,juan))
X Y ama(X,maria) ama(Y,juan)
4.7.3 Variables Libres y Ligadas
Indique por medio de líneas las ligaduras entre las variables de las fórmulas y las del
cuantificador, dejando sin conectar las variables libres.
Haga el ejercicio para los dos órdenes de precedencia siguientes:
, , , , ,( / )
( / / ), , , ,
X ama(X,maria) ama(Y,juan)
(X ama(X,Y)) Y ama(Y,juan)
X ((Y ama(X,Y)) ama(Y,juan))
X Y ama(X,maria) ama(Y,X)
4.7.4 Cálculo de valores de verdad.
Asuma que la interpretación de los predicados p(X,Y) y q(X,Y) (donde X, Y toman sus valores
en el dominio {a, b}) está dada por las tablas siguientes (donde cada línea corresponde a un
átomo que toma el valor de verdadero y se asume que lo átomos no representados toman el
valor de falso):
q(x,y):
X Y
a a
b b
p(x,y):
X Y
a b
b a
Halle el valor de verdad de las fórmulas siguientes desarrollando los cuantificadores con
base en los átomos asociados a cada predicado:
X Y (p(X,Y) q(Y,X))
4.7.5 Varios
5. Suponga que q(X) y r(X) representan “X es un número racional y “X es un número real”
respectivamente. Simbolizar las siguientes declaraciones de la lógica de primero orden:
a) “Todo número racional es un número real.”
Capítulo 4: Lógica de Predicados
99
b) “Algunos número reales son números racionales.”
c) “No todo número real es un número racional.”
6. Para la siguiente interpretación sobre el dominio D={1,2}:
Asignación para las constantes a y b.
a b
1 2
Términos.
Como ejemplo informal del uso de predicados en lenguaje natural se presentó en el
capítulo anterior, la frase “Juan ama a María”, señalando que contiene una afirmación
(“ama a”) que recae sobre dos entidades específicas (“Juan” y “María”) de un dominio de
interpretación específico (v.g. “los estudiantes del curso de Lenguajes Declarativos que se
dicta en el salón M3-206”).
En el lenguaje formal presentado, el predicado anterior se tradujo a la fbf
“ama_a(juan,maría)” que separa de forma más precisa la afirmación, de las entidades sobre
las que ella recae. La afirmación en si misma está representada por el símbolo de predicado
“ama_a”, y las entidades sobre las que ella recae están representadas por los símbolos
constantes “juan” y “maría”.
Como ejemplo de la expresividad del lenguaje formal se ilustró el uso de los
cuantificadores y variables para hacer afirmaciones sobre todos los miembros del dominio
de interpretación. Así la afirmación “Todos aman a María” se tradujo a la fbf “X
ama_a(X,maría)”, que se apoya en el símbolo variable “X” para referirse a todos los
miembros del dominio de interpretación.
Si bien las constantes y las variables le permiten a la lógica referirse a las entidades del
dominio de interpretación, ellas no permiten referirse a las relaciones existentes entre
dichas entidades, ni referirse a una entidad que tiene una cierta relación con otras. En
efecto, si entre los estudiantes del curso de Lenguajes Declarativo ocurren relaciones de
Capítulo 5: Lógica Ecuacional
103
noviazgo, habrá seguramente afirmaciones interesantes sobre el novio de alguien o sobre el
carácter general de los noviazgos. Las constantes y las variables son insuficientes para
representar estas afirmaciones en la lógica de predicados y razonar sobre ellas.
Para solucionar este problema la lógica de predicados ofrece una construcción sintáctica
que denominaremos “término”. El término incorpora las constantes y las variables como
términos elementales y permite su agrupación en términos más complejos usando
“operadores”.
5.2.1 Operadores.
Los operadores son símbolos de la lógica de predicados que se utilizan para construir
términos complejos a partir de términos más simples y, en últimas, a partir de las constantes
y variables. A cualquiera de los términos agrupados por un operador en un término, se le
denomina “operando”.
Al igual que ocurre con los predicados, cada operador se asocia a un “perfil” o “notación”.
El perfil de un operador es una plantilla (ver 4.2.1.3.2) que indica lo siguiente:
El sort87 del término resultante de la aplicación del operador.
El número y sort de los operandos.
La posición relativa de cada uno de los operandos con respecto al operador.
En lo que sigue será relevante distinguir entre las plantillas con notación
“prefija” de las plantillas con de notación “infija” (ver 4.2.1.3.2).
Los signos de puntuación que deben aparecer en un término construido con
base en el operador.
Un número de orden de cada operando en la aplicación del operador, que
será usado como medio para hacer referencia a dicho operando (Usualmente
este número de orden no se hace explícito y se asume que corresponde al
orden de aparición del operando recorriendo el término de izquierda a
derecha). Ocasionalmente la plantilla asigna un nombre a cada uno de los
operandos a ser usado como referencia en lugar del número de orden.
Definición:
aridad - Número de términos que agrupa el operador.
Una plantilla de operador puede indicar una aridad variable para términos de un mismo
sort.
novio|a-de(_)
PRECAUCIÓN:
Es usual que el lector confunda los operadores con los predicados, ya que ambos tienen una forma sintáctica similar
(lucen parecidos pero son distintos). Un predicado puede considerarse un operador que al evocarlo representa un
valor de tipo boleano.
Como ejemplo introduciremos el predicado “novios”, aplicable a dos estudiantes, para afirmar que entre ellos existe
una relación de noviazgo.
Nótese que la introducción de un símbolo de la lógica, exige declarar el tipo de construcción a la que corresponde,
por tanto se sabe si un símbolo es de función (o de predicado) porque fue declarado como tal.
_+_
Es la plantilla del operador “+” que indica que el operador separa los operandos.
La aridad del operador “+” es 2.
El operador “+” en el lenguaje LISP usa notación prefija permitiendo una aridad indeterminada.
Así, la plantilla de este operador puede representarse de la forma siguiente:
(+ _ _ _ ...)
novio|a-de(maria)
novio|a-de(x)
novio|a-de(novio|a-de(x))
PRECAUCIÓN:
No debe usarse un predicado en el lugar de un término como operador o como operando. Así, son incorrectas las
instancias siguientes:
novios(maria)
novio|a-de(novios(x,y))
Son instancias del operador “+” con notación infija las siguientes:
1+1
2+1+3
Son instancias del operador “+” del lenguaje LISP las siguientes.
(+ 1 1)
(+ 1 (+ 2 3) 7 (+ 4 5))
1 -
4 7
El uso exclusivo de la notación estandar para las funciones, también evita las
ambiguedades.
Capítulo 5: Lógica Ecuacional
107
El uso exclusivo de la notación “estandar” para las funciones, también evita la aparición de
ambiguedades, ejemplo:
El término 2 * 4 + 7 podría estar asociado a cualquiera de los dos árboles sintácticos que se muestran a
continuación, haciendo ambiguo su significado (en un caso evaluaría a 22 y en el otro a 13).
* +
2 + * 7
4 7 2 4
El término siguiente
3^4^5 * 4 + 7 / -3 + 5
El término 2.4 es ambiguo pudiendo corresponder a los dos árboles sintácticos siguientes:
2.4 2.4
Para evitar la ambigüedad es necesario darle una precedencia a un sentido de asociatividad sobre el otro.
En nuestro caso, al igual que en el lenguaje C, daremos precedencia a la asociatividad “izquierda-
derecha” sobre la asociatividad “derecha-izquierda” seleccionando el árbol de la derecha.
Nótese que, en un operador monádico, un sentido de asociatividad contrario a la dirección que va del
operando al operador carece de sentido, ya que siempre debe aplicarse primero el operador que está más
cerca del operando. Así el término 2.4 debe siempre calcularse así ((((2.4)))).
Los términos (2 + 4) + 7 y 2 + (4 + 7) tienen los dos árboles sintácticos diferentes que se muestan en la
figura. Pero por ser el operador “+” un “operador asociativo”, se conoce que los dos árboles calculan al
mismo valor.
Capítulo 5: Lógica Ecuacional
110
+ +
2 + + 7
4 7 2 4
El ejemplo siguiente lleva a cabo una afirmación sobre el estudiante que tiene la relación de noviazgo
con una estudiante específica de la clase.
es-alto(novio|a-de(maría))
PRECAUCIÓN:
Note que el símbolo “es-alto” es un símbolo de predicado mientras que el símbolo “novio|a-de” es un símbolo de
función. Para ello es necesario que en la definición del alfabeto de símbolos de la lógica hayan sido declarados
como tal.
Los operadores deben ser entonces interpretados por medio de funciones que existen en el
dominio de interpretación. En este sentido los operadores constituyen un medio para
referirse a la función dándole un nombre o “signatura”.88
La asociación de los operadores con las funciones, permite usar los términos para describir
las propiedades de las funciones que ocurren en el dominio de interpretación.
La aserción siguiente lleva a cabo una afirmación sobre la relación de noviazgo (que los novios se
88Es usual que no distingamos entre el nombre de una cosa y la cosa misma, así creemos erróneamente que “sen(x)”es
una función, cuando en realidad es un nombre asignado a una función (un apareamiento entre los reales) que ya existía.
Capítulo 5: Lógica Ecuacional
111
aman) usando la igualdad (vista como referencia al mismo elemento del dominio) el operador que la
representa.
x ( ama_a(x,novio|de(x)) ama_a(novio|de(x),x) )
Lógica Ecuacional
La lógica ecuacional es una rama de la lógica de predicados que se ocupa del razonamiento
sobre las propiedades de las funciones. Para ello se apoya en el predicado de igualdad
aplicado a términos definidos sobre los operadores asociados con dichas funciones.
La lógica ecuacional da soporte formal al razonamiento en matemáticas que se ocupa de las
propiedades de las funciones definidas sobre las entidades matemáticas (v.g. los números).
A continuación se presenta primero el predicado de igualdad, luego los criterios de
demostración que se le asocian, y por último el carácter de las teorías construidas con base
en dicho predicado.
5.3.1 El predicado de igualdad.
El predicado de igualdad permite afirmar que dos términos que pueden ser sintácticamente
diferentes, son iguales o “dan lo mismo” desde el punto de vista semántico. Una razón
obvia para que dos términos puedan considerarse iguales es que su valor semántico sea el
mismo objeto en el dominio de interpretación.
En este punto el lector debe notar que es posible definir una relación de igualdad entre los
objetos del dominio de interpretación, bajo una condición distinta a ser el mismo objeto.
Para ello basta que dos miembros del dominio de interpretación “den lo mismo” desde el
punto de vista de un propósito o criterio específico. Esto permite plantear diversos criterios
de igualdad sobre un mismo dominio con, condiciones de igualdad específicas para cada
criterio. Así, los criterios de igualdad puede verse como una categoría (o dimensión de
clasificación) para los elementos del dominio, en el que las condiciones que determinen la
“igualdad” determinan que elementos pertenecen a cada una de las clases de equivalencia
de la categoría.
Los estudiantes de la clase pueden dividirse en grupos de estudiantes “igualmente estudiosos”, donde un
estudiante de un grupo pueda ser considerado tan estudioso como otro estudiante del mismo grupo, pero
más o menos estudioso que un estudiante de un grupo diferente. Así, en la categoría de clasificación
“estudioso” existen tantas clases de equivalencia como niveles de “estudioso” puedan ser reconocidos.
Si un estudiante desea llevar a cabo un trabajo académico con un compañero “igualmente estudiosos” a
sí mismo, le debe ser indiferente cualquiera de los miembros de la clase de equivalencia a la que
pertenece.
En lo que sigue, sin embargo, el significado del concepto de igualdad es una relación entre
los términos, más bien que una elación entre los objetos del dominio de interpretación,
entendiendo que la igualdad de dos términos (sintácticamante diferentes) equivale a que
tengan igual valor semántico (calculen al mismo objeto).
El predicado de igualdad es fundamental para describir las propiedades de las funciones
que ocurren en el dominio de interpretación.
x y (¬(x=y) ¬(novio|de(x)=novio|de(y))
x y ( x+y = y+x)
x y ...P(x,y,..)
––––––––––––––––––––––––––––
v1 v2 ...P(fx(v1, v2..,a,b..), fy(v1, v2..,a,b..),...)
Donde v1, v2,.. son variables, a,b.. son constantes y fx(v1, v2..,a,b..), fy(v1, v2..,a,b..)...
son términos del sort de x, y, etc..
5.3.3 Teoría en lógica ecuacional.
Tal como se explicó en el capítulo anterior, una teoría es una secuencia de aserciones, que
comienza con unas aserciones básicas denominadas axiomas, y continúa con unas
aserciones derivadas denominadas teoremas; donde cada teorema, tienen la propiedad de
ser consecuencia lógica de las aserciones que le preceden.
En lógica ecuacional las aserciones de una teoría, son planteamientos de igualdad entre
términos formados con los operadores declarados en la teoría. Si la teoría es interpretada
sobre un dominio que satisface los axiomas, estos simplemente expresan las propiedades de
las funciones que representan los operadores involucrados en los axiomas. Además, si los
criterios de demostración son correctos, los teoremas expresan propiedades de dichas
funciones que son “descubiertas” a través de los procesos de demostración.
En dominios de interpretación con un número infinito de objetos, como los números
enteros, fraccionarios, reales, y demás tipos de valores matemáticos, los axiomas con
cuantificadores no pueden ser calculados (y “comprobados”), por lo que deben ser
asumidos como ciertos. Por las mismas razones, la correctitud de los teoremas que
involucren cuantificadores, solo puede comprobarse para un número limitado de casos, así,
sólo es posible demostrar la incorrectitud de un teorema (y por tanto la de los axiomas, o la
de los criterios de demostración que lo soportan), si se halla un caso particula para el que el
teorema no se cumple.
En lo que sigue ilustraremos los elementos de una teoría en lógica Ecuacional, para luego
plantear un proceso de demostración que puede ser automatizado y que le da soporte a los
lenguajes de programación denominados “funcionales”.
5.3.3.1 Elementos de una teoría en Lógica Ecuacional
Una teoría en lógica Ecuacional Multisort está, en consecuencia, constituidas por los
elementos que se describen a continuación.
5.3.3.1.1 Declaración de sorts.
Nombres de los sorts sobre los que se definen los términos de la teoría.
Capítulo 5: Lógica Ecuacional
114
En una teoría que para razonar sobre la aritmética de los enteros se incluirá la siguiente definición de
sort.
sort: Int
Nótese que la constante 0, no se define como tal, sino como el nombre de un operador sin argumentos
con el sort “Int” como codominio.
5.3.3.1.3 Axiomas.
Aserciones básicas sobre las funciones asociadas a los operadores, que son asumidas como
“ciertas” en el marco de la teoría.
(1) X:Int (X + 0 = X)
(2) X:Int (0 + X = X)
(3) X:Int (-X + X = 0)
(4) Y:Int Y:Int Z:Int (X + (Y + Z) = (X + Y) + Z)
Para simplificar la especificación de los axiomas, en lo que sigue se omitirán los cuantificadores
universales, y se introducirá el tipo de las variables cuantificadas por medio de una “declaración de
variable” que antecede a los axiomas que la usan. Así la especificación que sigue es equivalente a la
anterior.
Axiomas:
(0) X, Y, Z : Int
(1) X+0=X
(2) 0+X=X
(3) -X + X = 0
(4) X + (Y + Z) = (X + Y) + Z
Capítulo 5: Lógica Ecuacional
115
5.3.3.2 Demostración en Lógica Ecuacional.
La incorporación de teoremas en la lógica ecuacional se lleva a cabo por un proceso de
demostración apoyado en una “derivación”.
Definición:
Derivación – Una derivación es una secuencia de términos t0,t1,t2,...,tn que
pueden demostrarse iguales por razonamiento ecuacional.
Para simplificar la especificación de la igualdad entre todos los términos de una derivación
usaremos la notación siguiente:
NOTACION:
La expresión t0=t1, representa la aserción siguiente:
v0 v1 v2... vm (t0=t1)
La expresión t0=t1=t2=...=tn, representa las aserciones siguientes:
v0 v1 v2... vm (t0=t1)
v0 v1 v2... vm (t1=t2)
...
v0 v1 v2... vm (tk-1=tk) para todo 1k<n
Donde v0,v1,v2,.. ,vm son las variables involucradas en los términos.
En la teoría asociada a los enteros de los ejemplos anteriores se puede demostrar la aserción siguiente:
- -X = X
Donde la secuencia de términos que forman la derivación se muestra en la primera columna. Los
elementos del razonamiento que permite pasar de un término de la derivación al siguiente se describen
en las columnas siguientes. En la segunda columna se indica el axioma seleccionado, en la tercera la
substitución efectuada a las variables del axioma, y en la cuarta columna la particularización resultante
de dicha substitución. Nótese que uno de los términos de la particularización es idéntico a un
subtérmino del término de la derivación (el subtérmino subrayado), y que en el término siguiente de la
derivación este subtérmino es remplazado por el otro término de la particularización.
Para automatizar la selección del término siguiente en cada paso de la derivación, existen
fundamentalmente dos tipos de enfoque. El primero, es el de concebir mecanismos
robustos de automatización que busquen el camino correcto, examinando las consecuencias
de cada opción, y que desechen las que resulten inadecuadas. El segundo, es el de
simplificar la teoría utilizada para evitar que aparezcan caminos de derivación inadecuados.
El primer enfoque da lugar a la aparición de líneas de investigación dentro del campo
general de la inteligencia artificial, mientras que el segundo enfoque da lugar a la aparición
de los lenguajes funcionales.
Para evitar caminos de derivación inadecuados los lenguajes funcionales optan, primero,
por utilizar sólo axiomas en la derivación (desechando los teoremas) y, segundo, por
utilizarlos en un único sentido (v.g. de izquierda a derecha). Así, en cada paso de la
derivación, se selecciona una terceta axioma/particularización/subtérmino teniendo
solamente en cuenta que el lado izquierdo del axioma escogido, ya particularizado, sea
idéntico al subtérmino seleccionado. El uso exclusivo de axiomas agiliza la selección de la
Capítulo 5: Lógica Ecuacional
119
terceta, y el uso de los axiomas en un solo sentido evita procesos cíclicos triviales inducidos
por la selección repetitiva del mismo axioma.
Los lenguajes funcionales no ofrecen, sin embargo, otros mecanismos automáticos para
evitar que ocurran ciclos no triviales ni caminos de derivación que lleven a resultados no
deseados. Ellos dejan esta responsabilidad al matemático (o programador) quién debe
simplificar por sí mismo la teoría (o programa) para que estos problemas no aparezcan. Es,
entonces, necesario que las ecuaciones (o axiomas) que determinan los procesos de
derivación en un programa de un lenguaje funcional, tengan “buenas” propiedades. Dos
propiedades esenciales, que se les conoce como las asunciones de Church-Rosser89 [Church
36], [Toyama 90], son la terminancia y la confluencia. La terminancia implica que todos
los posibles caminos de derivación llegan eventualmente a un término final del cual ya no
es posible continuar, y la confluencia implica que el término final de todos los caminos es
el mismo.
Lo que sí ofrecen los lenguajes funcionales, es la capacidad de hallar de forma automática
la terceta axioma/particularización/término, a ser aplicada en cada paso del proceso de
derivación. De existir más de una terceta aplicable en el proceso, ellos optan por aplicar
reglas propias de selección, y/o por permitir que se lleven a cabo de forma paralela las
reescrituras definida por las varias tercetas. Esta última alternativa permite que en los
lenguajes funcionales los procesos de ejecución en paralelo se definan de forma natural, sin
necesidad de adicionar elementos sintácticos especializados a tal fin90.
Una consecuencia importante del enfoque asumido por los lenguajes funcionales, es que
ellos no pueden dar cuenta de toda la gama de demostraciones posibles en una teoría.
Como se verá en los capítulos siguientes, esta limitación, si bien es importante, no limita la
potencia expresiva de los lenguajes funcionales en relación con la de los lenguajes
procedurales. Es fácil ilustrar, en efecto, que todo proceso que puede especificarse en un
lenguaje procedural, puede también especificarse, de forma indirecta, como una derivación
en el marco de una teoría descrita en un lenguaje funcional.
Así, si bien los programas escritos en el marco de los lenguajes funcionales son
esencialmente teorías en una lógica ecuacional, y las ejecuciones de dichos programas
pueden asimilarse a demostraciones en las teorías. Su razón de ser, sin embargo, no es la
de demostrar teoremas que den luz sobre las propiedades de los objetos de la teoría, sino la
de llevar a cabo cálculos que obtengan (o construyan) objetos específicos de la teoría
(necesarios a un fin práctico concreto). En consecuencia, las derivaciones de las que se
ocupa el resto del texto, partirán en general de términos base complejos (la “fórmula” a ser
“calculada”) y terminarán en unos términos base más simples (el resultado del cálculo). No
serán tema de este trabajo, las derivaciones que parten de términos con variables (como la
del Ejemplo 27).
La utilidad de estas demostraciones en la vida cuotidiana es obvia, si consideramos que una compra de
1000 * 1 + 500 * 2
Que debe reducirse a un término “más simple” pero equivalente, antes de efectuar el pago.
La especificación presentada en el capítulo anterior para razonar sobre la aritmética de los enteros se
apoya en la signatura definida por los elementos siguientes:
S es el conjunto { int }
91 Si un símbolo de operación aparece en dos familias distintas, se dice que es un operador “sobrecargado”.
92 S* es el conjunto potencia de S
Capítulo 5: Lógica Ecuacional
121
es el conjunto { - , +, 0, s }
La relación entre los conjuntos S y , definida en la signatura, asigna a cada símbolo de operación al
menos un elemento del conjunto S*S, que define su rango, aridad y coaridad, así:
5.6.2 Términos.
Con la Signatura de una Especificación Multisort y un conjunto de variables, se pueden
definir los términos de la especificación. Los términos son, entonces, relativos a las
variables, de tal manera que a cada conjunto de variables le corresponde un conjunto de
términos específico.
Las variables de la signatura se definen con base en un conjunto (finito) X de símbolos de
variable x1,x2,x3,....,xn y una relación denominada tipo entre X y S. Los símbolos de
variable son distintos a los símbolos de la signatura (S,). La relación entre X y S clasifica
las variables asociándolas a un símbolo de sort, así:
X = {Xs | sS}
NOTACION:
Cuando sea necesario hacer referencia a los sorts de las variables, ellas se
representarán por expresiones de la forma x:s,
Donde xXs , sS y x está clasificada en la categoría rotulada por s.
Cuando sea necesario hacer referencia al conjunto de sorts de un conjunto de
variables, estas se representarán por expresiones de la forma x:s.
Donde xX , sS y cada símbolo de s rotula la categoría a la que pertenece el
correspondiente símbolo de x.
Se denominan términos de la signatura (S,) sobre X, a las frases de un lenguaje, (X),
definido sobre el alfabeto {(S,),X} que, al igual que las variables están clasificadas (o
tipadas) por los símbolos de sort, así:
(X) = {,s(X) | sS}
NOTACION:
Cuando sea necesario hacer referencia a los tipos de los términos, estos se
representarán por expresiones de la forma t:s.
Donde t,s(X) , sS y s rotula la categoría a la que pertenece t.
El conjunto de términos tipados de la signatura (X) = {,s(X) | sS} se define por medio
de los criterios formativos siguientes:
Las variables son términos: Xs ,s(X) para sS
Las constantes son términos: ,s ,s(X) para sS
Los símbolos de operación aplicados a términos son términos, así: (t1,...,tn)
,s(X) si s,s y ti,s(i)(X) (siendo s(i)S el elemento situado en la
posición i de s)
Es importante notar que la naturaleza recursiva de los criterios formativos, implica que el
conjunto de términos de una especificación sobre cualquier número de variables, es, en
general infinito.
NOTA: Los criterios formativos de los términos implican el uso de una notación estándar
(ver Ejemplo 5), para representar la aplicación de los símbolos de operación (u
operadores) a los términos que agrega (sus operandos). En lo que sigue, sin
embargo, se asume que la aplicación de los símbolos de operación a los términos
se lleva a cabo con base a la plantilla asociada al operador (si esta plantilla existe).
Para una caracterización informal de la plantilla de un operador el lector debe
referirse a la sección 5.2.1.
Es posible definir un conjunto de términos asociado a la signatura presentada en el Ejemplo 30, con base
en un conjunto de variables, así:
Sea X es el conjunto { X, Y, Z }
Variable Sort
X int
Y int
Z int
Son términos de la especificación los que siguen:
(X) = ,int(X) = { X, Y, Z, 0, X+Y, X+Z, s(0), -X, s(s(0))+ -X,........ }
Capítulo 5: Lógica Ecuacional
123
5.6.3 Ecuaciones.
Con los términos de la especificación se puede definir el conjunto de ecuaciones de la
especificación E. E está compuesto por un conjunto finito de ecuaciones e1,e2,e3,....,em. Las
ecuaciones pueden ser ya sea ecuaciones simples o ecuaciones condicionales.
Las ecuaciones simples son construcciones de la forma:
l=r
Donde l y r son términos en ,s(X), para algún sort (sS).
Las ecuaciones condicionales son expresiones de la forma:
l=r if u1=v1,....un=vn
donde l=r y u1=v1,....un=vn son ecuaciones simples
NOTACION:
Cuando sea necesario hacer referencia a las variables de los términos de una
ecuación esta se representarán por expresiones de la forma (x:s)l=r ó (x)l=r según se
desee hacer o no referencia a los sorts de las variables.
Las ecuaciones simples de una especificación en lógica Ecuacional Multisort representan
predicados de igualdad sobre términos de la especificación. Se asume, además, que el
predicado de igualdad representado, está cuantificado universalmente en todas las variables
que ocurren en los términos de la ecuación. Así, la ecuación (x1,x2,x3,....,xn)l=r representa
la fbf siguiente:
x1 x2 x3 ....xn(l=r)
De igual manera las ecuaciones condicionales son predicados de la forma siguiente:
l=r (u1=v1 . u2=v2... un=vn)
Y se encuentran universalmente cuantificados en todas las variables que ocurren en sus
términos.
La especificación multisort de los dos ejemplos anteriores puede completarse con el conjunto de
ecuaciones E siguiente:
x+0=x
0+x=x
-x + x = 0
x + (y + z) = (x + y) + z
Que corresponden a los axiomas presentados en el Ejemplo 26.
SRT
Tal como se indicó en el capítulo anterior, es posible usar razonamiento ecuacional para
establecer si dos términos de una especificación multisort son equivalentes, sin recurrir a su
significado en el domino de interpretación.
En el marco de los lenguajes funcionales, este razonamiento es automatizado usando las
ecuaciones de la especificación como reglas dirigidas de reescritura, que se aplican de
forma automática al término actual de la derivación para generar el término siguiente.
Desde este punto de vista, una especificación multisort no es más que un conjunto de reglas
de reescritura aplicables a los términos de la especificación, a la que se le denomina
Sistema de Reescritura de Términos o SRT [Terese 2003] [Klint 07].
La aplicación de una ecuación (de una especificación multisort) como una regla de
reescritura sobre un término (de la especificación) para derivar otro término
semánticamente equivalente (en el marco de la especificación), esta definida y regulada por
los conceptos de substitución particularización, emparejamiento, y reemplazo.
En esta sección se presentan formalmente estos conceptos en el marco de la teoría de
lenguajes.
Cuando un término se demuestra semánticamente equivalente a otro derivándolo de él por
aplicación de las ecuaciones de la especificación como reglas dirigidas de reescritura, entre
ellos existe una relación dirigida que se denomina relación de reescritura. Nótese que el
uso de las ecuaciones como reglas dirigidas de reescritura impone limitaciones al
razonamiento93 y, por lo tanto, no todo par de términos que puedan demostrarse
semánticamente equivalente por razonamiento ecuacional, están relacionados por la
relación de reescritura.
En esta sección se define formalmente la relación de reescritura.
Al ser automática la aplicación de las reglas de reescritura el camino que toma una
derivación estará restringido por las reglas que son aplicables en cada paso de la reescritura.
Una forma de evitar que aparezcan caminos indeseables es obligar al SRT a satisfacer las
condiciones de terminancia y confluencia, que imponen condiciones a las reglas aplicables
en cada paso de la derivación.
En esta sección se definen formalmente estos conceptos.
5.7.1 Substitución, Particularización y Emparejamiento.
Una substitución es una función que mapea un conjunto de variables
X={x1:s ,x2:s ,x3:s ,...,xn:sxn} a un conjunto de términos T={t1:st1,t2:st2,t3:st3,...,tm:stm}, y
x1 x2 x3
x + (y + z) = (x + y) + z
Serán particularizaciones válidas las siguientes ecuaciones:
z + (y + x) = (z + y) + x
x + (x + x) = (x + x) + x
(3+a) + (y + 2f) = ((3+a) + y) + 2f
No serán particularizaciones válidas las siguientes ecuaciones:
z + (y + x) = (z + x) + x
x + (x + x) = (y + y) + y
(3+a) + (y + 2f) = ((3+a) + y) + (2+f)
El árbol que se muestra a continuación representa el término M(x, S(A(y, 0))). En cada nodo del árbol se
indica el camino correspondiente.
Capítulo 5: Lógica Ecuacional
127
M
1 2
x S
2.1
A
2.1.1 2.1.2
y 0
Las siguientes referencias denotan los términos indicado:
t| denota el subtérmino M(x, S(A(y, 0)))
t|1 denota el subtérmino x;
t|2 denota el subtérmino S(A(y,0));
t|2.1 denota el subtérmino A(y,0);
t|2.1.1 denota el subtérmino y;
t|2.1.2 denota el subtérmino 0.
Sean t:st y r:sr términos tales que st=sr y sea u una ocurrencia de t. Llamaremos reemplazo,
y escribiremos t[ur], al término que resulta de reemplazar en t, el subtérmino t|u por r.
94Demostración que se deberá hacer por reescritura, convirtiéndose esta condición en la de que exista entre los términos
de las ecuaciones una relación ui*E vi
Capítulo 5: Lógica Ecuacional
128
La relación entre t y t’ es denotada como tE t’ cuando la reescritura se basa en las
ecuaciones de E. La clausura reflexiva y transitiva de la relación de tE t’ es denotada por
t*E t’. Así, se dice que entre dos términos t y s existe la relación t * s, si existen
términos t1, t2, ... tn tales que
t E t1 E t2 ... tn E s.
Con el mismo alfabeto de los ejemplos anteriores, dado el SRT (, R) con R={A(x,0)x}, se puede
afirmar que:
Nótese que si entre los términos t y t’ existe una relación de reescritura tE t’, la aserción
representada por la ecuación t=t’ es consecuencia lógica de la especificación multisort,
debido a que la reescritura es una aplicación correcta del razonamiento ecuacional (ver
5.3.3.2).
En una derivación automatizada el término que sigue a otro es seleccionado por el SRT
entre aquellos que mantiene con él una relación de reescritura. Esto implica que la
automatización de la derivación de un SRT es correcta en el sentido de que toda derivación
en el SRT es una demostración en lógica ecuacional.
De haber más de un candidato a término siguiente, el SRT escoge uno cualquiera de los
candidatos. Las propiedades de confluencia y terminancia hacen que cualquier escogencia
sea adecuada.
5.7.4 Propiedad de Church-Roser: Confluencia y Terminancia.
Un conjunto de ecuaciones E es terminante si no hay secuencias infinitas de reescritura. El
conjunto E es confluente, si el resultado final de reescribir un término es único, en el
sentido de que si existen t*E t1 y t*E t2 con t1 t2 entonces existe un término t’ tal que
t1*E t’ y t2*E t’. Si un conjunto de ecuaciones E es terminante y confluente se dice
que es canónico o que cumple la condición de Church-Rosser [Toyama 90].
Si un conjunto de ecuaciones E es Church-Rosser, todo término t podrá reescribirse hasta
un término final tE que denominaremos su forma normal. Si para dos términos t y t’ se
cumple que tE t’E, entonces se cumple que t = t’.
Para el caso de reescritura con una ecuación condicional (x:s)l=r if u1=v1,....un=vn, se
requiere que (ui)E (vi)E (i= 1,..,n).
Con el mismo alfabeto de los ejemplos anteriores, definamos un SRT (, R) con R={M(x,y) M(y,x),}.
Este SRT no es terminante, pues M(x1,y1) se puede reescribir a M(y1,x1), y este a su vez se puede
reescribir a M(x1,y1), y así infinitamente, sin encontrar un término canónico.
Capítulo 5: Lógica Ecuacional
129
Ejercicios propuestos.
VERDADERO O FALSO?
( ) Los operadores son símbolos de la lógica de predicados que se utilizan para construir
términos complejos a partir de proposiciones simples.
( ) Los términos complejos se usan para agrupar términos más simples usando predicados.
( ) Los predicados pueden considerarse como operadores cuyo sort de término resultante
es de tipo Boleano.
Capítulo 5: Lógica Ecuacional
132
( ) Los términos atómicos son las constantes y las variables.
( ) Existen operadores con número variable de operandos.
( ) La aplicación de un operador a un conjunto de operandos no requiere satisfacer las
restricciones aridad y tipo propias de los predicados.
( ) Los criterios formativos de los términos limitan el conjunto de términos que usan un
operador a ser un conjunto finito.
( ) La naturaleza recursiva de los criterios formativos de los términos permite, en general,
que estos sean de tamaño indeterminado.
( ) La naturaleza recursiva de los criterios formativos de los términos permite, en general,
que el conjunto de ellos sea infinito.
( ) El árbol sintáctico de un término permite visualizar cuales son los operandos de cada
operador.
( ) Los criterios de prioridad y sentido de asociatividad de los operadores sólo son útiles
cuando se presentan operadores con notación infija.
( ) El uso de paréntesis en los términos es suficiente para determinar el árbol sintáctico
aún con operandos de notación infija.
( ) Los criterios de prioridad y sentido de asociatividad de los operadores sólo son
suficientes para representar cualquier término, sin necesidad de usar paréntesis.
( ) La semántica de los operadores son las funciones que representan en el dominio de
interpretación.
( ) La lógica Ecuacional posibilita plantear y razonar acerca de las propiedades de las
funciones que ocurren en el dominio de interpretación.
( ) El predicado de igualdad permite afirmar que dos términos que sintácticamente
diferentes, son iguales desde el punto de vista semántico (v.g. representan el mismo objeto
del dominio de interpretación).
( ) Los criterios demostrativos de la lógica Ecuacional, son la base para definir las reglas
de transformación de las ecuaciones del álgebra.
( ) Si en una fbf de lógica de predicados cuantificada universalmente se particulariza una
variable a un término que tiene otras variables diferentes, estas debe cuantificarse
existencialmente.
( ) Una teoría es una secuencia de términos que pueden demostrarse iguales por
razonamiento Ecuacional.
( ) Una derivación es una secuencia de proposiciones ecuacionales donde cada una es un
axioma o es demostrada como cierta a partir de las proposiciones que la preceden.
d^ c^ b
d^ c^ b
d^ c^ b
- - d
b + c + d + d
b - c - d - d
b - c + d + d
c - -b + c + d – d + c - b
c - -b*c/d + d^ c+ b
b)
Operador Precedencia Asociatividad
_+_ 1 ->
_^_ 2 <-
_−_ 3 <-
_*_ 4 ->!
c)
Operador Precedencia Asociatividad
_+_ 1 ->
_−_ 2 ->
_^_ 3 ->
_*_ 4 ->
d)
DEFINA EL ÁRBOL SINTÁCTICO PARA LA SIGUIENTE DEFINICIÓN DE UNA
VARIABLE EN EL LENGUAJE C.
int *a()[];
TEORIAS
Un campo, se define en matemáticas como un conjunto F dotado de 2 operaciones, la
operación “+” y la operación “*”, que cumplen 7 axiomas conocidos como los axiomas de
campo.
Una teoría en lógica ecuacional para campos es la siguiente:
Capítulo 6: Sistema de Reescritura de Términos (SRT)
136
Conjuntos:
F
Operadores:
_+_ : F F -> F
-_ : F -> F
0 : -> F
_*_ : F F -> F
_-1 : F -> F
1 : -> F
Variables (para todo):
a, b, c : F
Axiomas:
(1) (a+b)+c = a+(b+c)
(2) a+b = b+a
(3) a+0 = a
(4) a+(-a) = 0
(5) (a*b)*c = a*(b*c)
(6) a*b = b*a
(7) a*1 = a
(8) a*a-1 = 1
(9) a*(b+c) = a*b + a*c
Basados en esta teoría, podemos demostrar por ejemplo que a*0=0, construyendo la
siguiente derivación:
a*0 = (a*0)+0 = (a*0)+(a+(-a)) = ((a*0)+a)+(-a) = ((a*0)+(a*1))+(-a) = a*(0+1)+(-a) =
a*(1+0)+(-a) = a*1+(-a) = a+(-a) = 0
a. Seguir la demostración presentada, detallando la sustitución hecha en cada paso.
b. De manera análoga a la presentada en el ejemplo de la sección 5.3.3.2, demostrar
que --a=a.
c. Demostrar que a*(-b) = -(a*b). Se puede hacer uso de los resultados obtenidos
arriba. Aunque existen varias maneras de demostrarlo, una posible demostración
comienza por utilizar los axiomas (3), (4) y (9), el resto lo dejamos al lector.
Análogamente se puede demostrar que (-a)*b = -(a*b).
d. Demostrar que (-a)*(-a) = a*a.
e. Demostrar que (-1)*a = -a.
Resolución.
La resolución sirve para fundamentar un método para demostrar que una fórmula en FNC
(Forma normal Conjuntiva ver 3.4.5.2) de la forma F1F2...Fn¬ G es insatisfacible,
probando con ello que G es consecuencia lógica de las fbfs F1,F2, ...,Fn (ver 3.4.4.1 ).
6.2.1 Resolución en lógica proposicional.
Empezaremos por presentar el método de resolución para la lógica de proposiciones, donde
se comprenderá la esencia del problema.
6.2.1.1 Notación
Ahora, para facilitar la presentación del método, introduciremos algo de notación. Una
fórmula en FNC es de la forma:
(L1,1 L1,2 … L1, i) (L2,1 L2,2 … L2, j) …… (Ln,1 Ln,2 … Ln,m)
Donde los Li,j son literales (un átomo o su negación); será representada en lo que sigue de la
manera siguiente:
{L1,1 , L1,2 ,…, L1, i} , {L2,1 , L2,2,…, L2, j} , …… , {Ln,1 , Ln,2 ,…, Ln,m}
Donde los Li,j son los literales, {Lk,1 , Lk,2 ,…, Lk,j}, son las cláusulas, las “,” que separan los
literales de las cláusulas tepresentan el operador “”, {…} , {…} , …… , {…} es la conjunción
de cláusulas, y las “,” que separan las cláusulas representan el operador “”.
6.2.1.2 Resolventes
Definición:
Capítulo 6: Resolución y demostración automática de teoremas
141
Sea C1, C2 y R cláusulas. A R se le llama resolvente de C1 y C2 si hay un literal
lC1, tal que ¬lC2 y R tiene la forma (C1 – {l}) (C2 – {¬l}) y es consecuencia lógica
de C1 y C2.
Donde: ‘–’ es la operación diferencia para conjuntos y ‘’ la operación unión de conjuntos.
De manera que R es el conjunto formado por los literales de C1 sin l y los literales
de C2 sin ¬l.
{q,¬r,p} , {r,¬p}
Son, una cualquiera de las cláusulas siguientes:
{q, p, ¬p}
{q, ¬r, r}
Donde el lector debe notar que la misma pareja de cláusulas puede tener varios resolventes diferentes.
Es importante tener en cuenta que en el caso de la primera pareja que pueden resolverse tanto por Q
como por R, debe escogerse uno de los dos siendo un error eliminar simultáneamente a ambos. Así:
NO ES RESOLVENTE DE LA PRIMERA CLAUSULA LA SIGUIENTE:
{q}
Esto puede verse más claramente si se considera que por la tabla de verdad asociada al conector , los
dos resolventes de la primera pareja son equivalentes al valor lógico V (verdadero), y este no es
equivalente a q.
Se deja al lector la tarea de comprobar que los resolventes propuestos en el ejemplo anterior son
consecuencia lógica de las cláusulas que lo originan.
Igualmenter se deja al lector la tarea de comprobar si los resolventes “candidatos” que se presentan
mas abajo son realmente consecuencia lógica de las parejas de cláusulas asociadas:
El resolvente de las cláusulas {¬r} y {r} es el valor lógico F (falso). Esto puede verse
claramente si se tiene en cuenta que la fbf {¬r},{r} es semánticamente equivalente a
{¬r,F},{r,F} , cuyo resolvente es obviamente F. En lo que sigue el resolvente de dos
clausulas con un literal y su negación (F) lo denominarémos la cláusula “vacía”.
El siguiente teorema servirá para mostrar que al agregar a un conjunto de cláusulas el
resolvente de dos de sus cláusulas, no se afecta el valor de verdad de la forma clausal. En
lo que resta de ésta sección aparecerán algunas demostraciones, que consideramos
Capítulo 6: Resolución y demostración automática de teoremas
142
importantes, no solo por el esfuerzo de precisar argumentos, sino para evidenciar la
efectividad del método que presentaremos más adelante.
TEOREMA 6-1: Sea G={C1,C2} una fórmula en FNC, representada como conjunto de
cláusulas. Sea R un resolvente de las cláusulas C1,C2. Entonces G’={C1,C2,R} es
equivalente a G.
Demostración: Para probarlo, empezaremos por suponer una interpretación que hace verdadero a G’ y
probaremos que también G es verdadero bajo tal interpretación. Luego supondremos una interpretación que
hace verdadero a G y probaremos que también G’ es verdadero bajo tal interpretación.
1. Sea I una interpretación que hace verdadero a G’. Como G’ es una conjunción de Cláusulas, I hace
verdadera todas las cláusulas de G’. En Particular hace verdadero a C1 y a C2. Por tanto G={C1,C2}
es también verdadero.
2. Sea I una interpretación que hace verdadero a G. De nuevo C1 y C2 son verdaderas bajo I, y tan solo
falta probar que R es verdadero bajo I para probar que G’ es verdadero bajo I. Sabemos que R tiene
la forma (C1–{L})(C2–{¬L}), con LC1 y ¬LC2. Se pueden dar dos casos, L es verdadero bajo I, o
¬L es verdadero bajo I.
Si L es verdadero bajo I: Recordemos que una cláusula es una disyunción de literales, por lo que,
para que una cláusula sea verdadera bajo I, basta que uno de sus literales sea verdadero bajo I.
De éste modo, como C2 es verdadero, existe un literal L’C2 tal que L’ es verdadero bajo I. Pero
como L es verdadero bajo I, ¬L es falso bajo I y por tanto L’ ¬L. De manera L’C2–{¬L}, de
manera que L’(C1–{L})(C2–{¬L}) y por tanto R es verdadero bajo I.
Si ¬L es verdadero bajo I: de manera análoga, existe L’C1–{L} que es verdadero bajo I, y que
hace a R es verdadero bajo I.
Habiendo hecho evidente el caso más simple del Teorema, no es difícil ver el caso más
general: Si G es una fórmula en FNC, representada como conjunto de cláusulas, y R es un
resolvente de dos de sus cláusulas, G es equivalente a G{R}.
6.2.1.3 Demostración por introducción de resolventes.
La idea del método de demostración basado en resolventes, es tomar el conjunto de
cláusulas (que representa la fórmula en FNC) y agregar sucesivamente resolventes, hasta
que se evidencie una contradicción. La contradicción se evidencia, cuando se introduce
como resolvente la cláusula vacía (F), ya que ésta proviene de un par de cláusulas de la
forma {L},{¬L}. La presencia de una pareja de cláusulas de ésta forma hace insatisfacible o
contradictoria la FNC que representa pues L¬L=F y como el conjunto de cláusulas
representa la conjunción de ellas, la presencia de F allí hace falsa toda la expresión para
cualquier interpretación..
Aunque el método tenga sentido, no es evidente que siempre que se enfrente a una FNC
contradictoria se llegue a la introducción de F probando la contradicción, así que lo que
sigue es demostrar éste hecho que se conoce como completitud del método.
6.2.1.4 El teorema de Resolución
Y finalmente:
Res*(G) = Res (G)
n0
n
G = {{¬A,¬B,¬D},{¬C,A},{C},{B},{¬G,D},{G}}
Entonces
Res0(G) = G
Para obtener Res1(G), debemos resolver todas las parejas de cláusulas de G y agregar los resolventes a
G. Entonces
Res1(G) = G{{¬B,¬D,¬C},{¬A,¬D},{¬A,¬B,¬G},{A},{D}}
Para obtener Res2(G), debemos resolver todas las parejas de cláusulas de Res1(G). Sin embargo,
Res1(G) contiene las cláusulas de G que ya hemos resuelto, así que solo falta resolver las cláusulas de G
con las nuevas cláusulas de Res1(G) y las nuevas de Res1(G) entre sí. Entonces
Res2(G)=Res1(G){{¬B,¬D},{¬A,¬B},{¬C,¬D},{¬B,¬G,¬C},{¬A,¬G},{¬B,¬C},{¬D},{¬A}, {¬B,¬G}}
Luego Res3(G), se obtiene de manera análoga:
Res3(G)=Res2(G){{¬G,¬C},{¬C},{¬G},{¬B}, false }.
Al intentar encontrar Res4(G) de forma análoga, nos encontramos con que no se pueden encontrar más
resolventes, de manera que:
Res4(G) = Res3(G).
Finalmente, se puede concluir que
f x1 x2 xr P1,1 x1 ,, xr P1,n1 x1 ,, xr Pk ,1 x1 ,, xr Pk ,nk x1 ,, xr
Es una contradicción.
En adelante omitiremos los cuantificadores y simplemente supondremos que cualquier
variable que aparezca está cuantificada universalmente. Además utilizaremos la notación
de cláusulas y conjuntos de cláusulas introducida en la subsección 140 , de manera que f
aparecerá así:
{{P1,1(X1,…,Xr), , P1,n1(X1,…,Xr)}, , {Pk,1(X1,…,Xr), , Pk,nk(X1,…,Xr)}}
Donde, sin pérdida de generalidad asumiremos que cada claúsula estará en forma prenex
con sus cuantificadores universales a la derecha de la cláusula.
6.2.2.2 Unificación
En la subsección 140 vimos que la clave para la aplicación del principio de resolución es
encontrar un literal dentro de una cláusula que sea complementario a un literal en otra
cláusula. Para cláusulas sin variables, esto es bastante simple. Sin embargo, para cláusulas
que contienen variables, es un poco más complicado. Por ejemplo consideremos las
cláusulas:
C1: {P(X),Q(X)}
C2: {¬P(f(Y)),R(Y)}
A primera vista, no hay ningún literal en C1 que sea complementario a uno en C2. Sin
embargo teniendo en cuenta que las variables de ambas cláusulas están cuantificadas
universalmente, es posible entender que C1 y C2 representan un conjunto de cláusulas sin
variables con todas las posibles particularizaciones de sus variables.
Podemos, entonces, sustituir X por f(a) en C1 y a Y por a en C2, para obtener:
C1’: {P(f(a)),Q(f(a))}
C2’: {¬P(f(a)),R(a)}
Que son instancias de C1 y C2 respectivamente, es decir, son cláusulas que se encontrarían
al expandir todo el cuantificador universal que implícitamente cuantifica ésta cláusula. Al
hacer este reemplazo aparecen P(f(a)) y ¬P(f(a)) que son complementarios entre si. Luego de
C1’ y C2’ podemos obtener el resolvente
C3’: {Q(f(a)),R(a)}
Capítulo 6: Resolución y demostración automática de teoremas
145
Es claro que existe una infinidad de formas diferentes de instanciar C1 y C2 para encontrar
un resolvente. Sin embargo podríamos obtener no uno sino un conjunto de resolventes si
particularizamos a Y por f(Y) en C1, para obtener el conjunto de cláusulas contenido en C1
C1*: {P(f(Y)),Q(f(Y))}
De nuevo, C1* es una instancia de C1, aunque una “más general” que C1’. En esta ocasión
P(f(X)) en C1* es complementario a ¬P(f(Y)) en C2. Luego, podemos obtener el siguiente
resolvente de C1* y C2
C3: {Q(f(Y)),R(Y)}
Que expresa el conjunto (infinito) de resolventes correspondientes a las posibles
particularizaciones de la variable Y . C3 tiene, además, la característica muy especial de
que cualquier otro resolvente que resulte de instancias de C1 y C2 es a su vez una instancia
de C3. A esta sustitución se le llama la “más general”.
Aunque éstos conceptos se pueden formalizar, y el método para encontrar el unificador más
general se puede estructurar para ser aplicado por un computador, preferimos en éste caso
dejar la idea intuitiva y presentar ejemplos, para que el lector adquiera experiencia
haciéndolo.
Para plasmar la idea del unificador “mas general” necesitamos un mínimo de nomenclatura
que nos permita expresar las ideas.
Las sustituciones las representaremos como conjuntos de la forma: ={t1/V1,...,tn/Vn}.
Donde V1,...,Vn son variables distintas entre si y t1,...,tn son términos. Lo que
representa la sustitución de Vi por ti, para cada i=1,..,n.
Dada una expresión E y una sustitución ={t1/V1,...,tn/Vn}, E es la expresión que se
obtiene a partir de E, sustituyendo Vi por ti para cada i=1,...,n.
Dados dos literales L1 y L2, se le llama unificador de L1 y L2 a una sustitución de
variables que hace iguales a L1 y L2. Es decir, es un unificador para L1 y L2 sii
L1=L2.
Algunos ejemplos, para que el lector coincida con la opinión de que la idea intuitiva es bastante simple.
Dados los literales
L1 = P(a,X,f(g(Y))) y L2 = P(Z,f(Z),f(U)):
{a/Z, f(a)/X, g(a)/U, a/Y} es un unificador para L1 y L2.
{a/Z, f(a)/X, g(Y)/U} es el unificador más general para L1 y L2.
La primera sustitución (a/Z) es bastante obvia, pues P(a,.. y P(Z,.. solo pueden ser iguales si Z
Capítulo 6: Resolución y demostración automática de teoremas
146
se reemplaza por a.
Con la primera establecida, nótese que se obtiene L2 = P(a,f(a),f(U)). De manera que la segunda
sustitución (f(a)/X) nuevamente es obvia, pues P(a,X,... y P(a,f(a),.. solo pueden ser iguales si
se sustituye X por f(a).
La tercera sustitución sin embargo nos da un poco más de flexibilidad, pues en éste punto
tenemos: L1 = P(a,f(a),f(g(Y))) y L2 = P(a,f(a),f(U)), de manera que lo que hace falta es que
f(U) sea igual a f(g(Y)). Debe ser claro que la forma más general de conseguirlo es
reemplazando U por g(Y).
Dados los literales
L1 = Q(f(a),g(X)) y L2 = Q(Y,Y):
No existe unificador para L1 y L2.
Procediendo como en el ejemplo anterior, es claro que Y debe reemplazarse por f(a), con lo que
obtenemos L2 = Q(f(a),f(a)). Luego es imposible encontrar una sustitución de variables que
haga iguales a g(X) con f(a).
Dados los literales
L1 = P(f(a,X),y,f(g(Y),a)) y L2 = P(Z,f(Z,U),f(W,a)):
{a/X, f(a,a)/Z, f(f(a,a),U)/Y, g(f(f(a,a),U))/W} es un unificador para L1 y L2.
{f(a,X)/Z, f(f(a,X),U)/Y, g(f(f(a,X),U))/W} es el unificador más general para L1 y L2.
La manera más general de hacer iguales a Z con f(a,X) es haciendo tal sustitución.
Sin embargo al hacer ésta sustitución obtenemos L2 = P(f(a,X),f(f(a,X),U),f(W,a)), de modo que
la siguiente sustitución debe sustituir y con f(f(a,X),U).
El resto del proceso lo dejamos para que el lector lo analice.
Algo que notar es que en los ejemplos presentados, no hay variables comunes entre L1 y L2,
lo que hace más fácil el trabajo pues cada elemento de la sustitución no influye sino a uno
de los dos literales. Sin embargo esto no tendría que ser así. Se debe tener especial cuidado
cuando la particularización de las variables de una cláusula involucra un término con
variables que ya existen en la otra, ya que las variables de igual nombre no necesariamente
deben ser las mismas en el resolvente; considere, por ejemplo, las cláusulas siguientes.
C1: {P(X),Q(X,Y)}
C2: {¬P(f(Y)),R(Y)}
Si particularizamos a x por f(Y) en C1, para obtener el conjunto de cláusulas contenido en C1
C1*: {P(f(Y)),Q(f(Y),Y)}
Habremos introducido una ligadura incorrecta entre las dos variables de Q, que antes
referían objetos diferentes.
Afortunadamente, en lo que sigue podremos independizar las variables entre una cláusula y
otra, incluso cuando sea necesario cambiar el nombre de algunas variables para que esto se
vea explícitamente.
6.2.2.3 Resolventes
Capítulo 6: Resolución y demostración automática de teoremas
147
La manera de generar resolventes y el método para encontrar contradicciones a partir de
cláusulas en lógica de primer orden son idénticos a los mostrados en la sección 142 para la
lógica proposicional, excepto por la unificación.
Sean C1 y C2 dos cláusulas sin variables en común. Sean L1C1 y L2C2 dos literales. Si L1 y
¬L2 tienen un mgu (unificador más general) , entonces la cláusula (C1 – L1) (C2 – L2)
es un resolvente para C1 y C2.
Sean:
C1={P(X),Q(X)} y C2={¬P(a),R(X)}
Nótese que C1 y C2 comparten la variable X, de manera que lo primero es renombrar X en C2 digamos
por Z (simplemente cualquier otra variable que no aparezca en C1), con lo que obtenemos
C1={P(X),Q(X)} y C2={¬P(a),R(Z)}.
Luego, escogiendo L1=P(X)C1 y L2=¬P(a)C2, el lector puede verificar que ={a/X}, es el mgu de
L1 y ¬L2. Entonces:
Ahora es necesario un acto de fe para completar el cuadro. Lo que se demostró para los
resolventes para la lógica de proposiciones es también válido para ésta nueva definición de
resolvente en el marco de la lógica de predicados, así:
1. Se puede agregar un resolvente de dos cláusulas a un conjunto de cláusulas sin
afectar su valor de verdad.
2. Si un conjunto de cláusulas es contradictorio, siempre se podrá encontrar la cláusula
vacía F, agregando resolventes.
Sean:
C1={P(X),Q(X)} y C2={¬P(Y),R(X,Y)}
Nótese que a pesar de que C1 y C2 comparten la variable X, esta variable esta cuantificada
(universalmente) de forma independiente en cada cláusula representando a objetos diferentes. La
cláusula C2 , por su parte, posee dos variables diferentes que por tener cada una su propio cuantificador,
refieren a objetos diferentes.
Si al unificar las dos ocurrencias del predicado P( ) se efectúa la substitución X/Y en C2, sin haber
cambiado previamente los nombres de las variables, se obtiene como resolvente la claúsula siguiente:
Este cambio de variables, aplicado a todas las cláusulas del programa (evitando variables
repetidas entre las cláusulas) es equivalente a utilizar las equivalencias expuestas en la
sección 4.4.2.2, para lograr que todos los cuantificadores que aparecen en la expresión
(F1F2...Fn¬G) se lleven hacia la izquierda, dejando a la derecha una expresión sin
cuantificadores y en forma normal conjuntiva. En el 4.4.2.2 se muestra un caso de este
proceso de transformación que es fácil generalizar.
mortal(juan)
humano(juan)
duerme(maria)
edad(maria, 19)
Los siguientes predicados PROLOG tienen variables dentro de sus argumentos.
mortal(X)
humano(Y)
gusta(juan,X)
El siguiente predicado PROLOG usa la función hermano_de(..) en uno de sus argumentos.
gusta(X,hermano_de(maria))
El lector debe notar que los argumentos de un predicado deben ser términos, por lo que no es válido
usar un predicado como argumento. Así es incorrecto el predicado siguiente:
gusta(X,edad(maria,19))
6.4.3.1 Hechos
Las cláusulas del programa que sólo tienen un literal positivo (lo implicado) se denominan
“hechos”. En otras palabras los hechos tienen un sólo predicado que puede tener o no
variables.
capital(colombia, bogota) .
humano(socrates) .
sumar(X, 0, X) .
derivada(X, X, 1) .
vertical(linea(punto(X, Y), punto(X,Z))) .
6.4.3.2 Reglas
Las cláusulas del programa que tienen, además del literal positivo (lo implicado), tiene
literales negativos (los que implican) se denominan “reglas”.
El literal implicado se denomina la “cabeza” de la regla y la lista de literales que implican
se denomina el “cuerpo” de la regla.
Mortal(X) :- humano(X) .
sumar(X, s(Y), s(Z)) :- sumar(X, Y, Z).
tia(X,Y) :- hermana(X,Z), padreOmadre(Z,Y) .
6.4.4 Consultas
Las consultas se escriben en PROLOG como una secuencia de predicados, separados con
“,” que termina con el símbolo “.” y es precedido por el símbolo “:-“, o por “?-“95 .
:- mortal(socrates), humano(platon) .
:- vertical(linea(punto(1, 2), punto(1, 3))) .
:- sumar(10, 8, RESPUESTA) .
hermana(ana,juan) .
hermana(ana,pedro) .
padreOmadre(jose,ana) .
padreOmadre(jose,juan) .
padreOmadre(jose,pedro) .
padreOmadre(pedro,esteban) .
tia(X,Y) :- hermana(X,Z), padreOmadre(Z,Y) .
Esta es una base de conocimientos que pretende explicar algunas relaciones de parentesco. Por ejemplo la primera
línea expresa que ana es hermana de juan; la tercera que jose es padre o madre de ana; y la última expresa que X es
tía de Y si X es hermana de Z y Z es padre o madre de Y.
Una consulta sería:
:- hermana(ana,juan) .
A lo que un intérprete de PROLOG respondería: “true”. Y obviamente frente a la consulta siguiente:
:- hermana(ana,jose)
:- hermana(ana,X) .
A la que el intérprete de PROLOG respondería: X = juan.
Finalmente a la consulta siguiente:
:- tia(ana,S) .
El intérprete respondería: yes. S = esteban.
Capítulo 6: Resolución y demostración automática de teoremas
154
Procesamiento por Resolución SLD.
El intérprete de PROLOG, utiliza el principio de resolución para probar que el programa
con la adición de la consulta negada es una fbf contradictoria. Sin embargo, gracias a lo
restringido de las cláusulas a las que se enfrenta el intérprete (por ser de Horn), veremos
que para aplicar el principio de resolución no es necesario computar todos los posibles
resolventes del conjunto, sino tan solo unos pocos, muy específicos, que llevan rápidamente
hacia la contradicción.
Lo primero que hay que notar es que la base de conocimientos no debe ser contradictoria en
si misma. De allí que la contradicción debe provenir de la introducción de la consulta
negada. Por ello es tan solo es necesario computar resolventes que provengan de la
consulta.
Recordemos además, que la consulta es una cláusula que tan solo contiene literales
negados, mientras que un hecho es una cláusula que sólo tiene un literal positivo, y una
regla es una cláusula que tiene un sólo literal positivo (la cabeza) seguido de una serie de
literales negados. De esta manera, es posible restringir la obtención de resolventes a
aquellos que se derivan de unificaciones entre un literal de la consulta, y el literal de un
hecho o la cabeza de una regla contenidos en el programa.
El proceso de resolución SLD (“Selective Linear Definite clause resolution96”) consiste, en
efecto, en buscar siempre para el primer literal de la consulta, al que en adelante
denominaremos “meta”, un hecho o la cabeza de una regla del programa que unifiquen con
dicha meta. Ésta búsqueda se hace en orden de arriba hacia abajo a través de las
cláusulas del programa, en lo que se conoce como resolución lineal.
Tras hallar el primer hecho o regla que unifique con la meta de la consulta, se obtiene el
resolvente, y se usa como la nueva consulta substituyendo a la anterior en el proceso
posterior de introducción de resolventes. Nótese que cuando la unificación de la meta se
efectúa con un hecho, la nueva consulta se obtiene de suprimir dicha meta en la consulta
anterior, luego de efectuadas las substituciones de las variables pertinentes a la unificación
en los demás literales de la consulta; en este caso diremos que la meta ha sido satisfecha o
alcanzada. Por otro lado, cuando la unificación de la meta de la consulta se efectúa con la
cabeza de una regla, la nueva consulta se obtiene de sustituir la meta en la consulta
anterior por la cola de la regla, luego de efectuadas las substituciones de las variables
pertinentes a la unificación en los demás literales de la consulta y en la cola de la regla; en
este caso diremos que la meta ha sido substituida por un conjunto de metas que la implican.
El proceso, en consecuencia, parte de una consulta que por supresión e introducción de
metas y substitución de sus variables, se va transformando de forma paulatina hasta llegar a
un éxito o a un fracaso. Se llega a un éxito cuando la supresión de metas conduce a la
consulta vacía, implicando que se ha demostrado la contradicción del programa aumentado
con la consulta negada. Se llega un fracaso cuando no se encuentra en el programa hecho o
cabeza de regla alguna que unifique con la meta de la consulta actual del proceso,
implicando que aún no se ha demostrado la contradicción del programa aumentado.
96http://en.wikipedia.org/wiki/SLD_resolution
Capítulo 6: Resolución y demostración automática de teoremas
155
Al producirse un fracaso el proceso de resolución SLD trata de efectuar un retroceso. El
retroceso consiste en desechar el objetivo que produjo el fracaso, y devolverse al objetivo
anterior, para tratar de resolverlo con un hecho o regla del programa distinto al que se
utilizó para llegar al objetivo fracasado. Para encontrar este nuevo hecho o regla,
simplemente se retoma la búsqueda del hecho o la regla que unifique con la meta del
objetivo sin tener en cuenta, o retractándose, del que se había utilizado anteriormente. De
no hallarse tal nuevo hecho o regla, se procede de nuevo a desechar el objetivo anterior,
tomando el que le antecedía (el anterior del anterior) para tratar de resolverlo con un hecho
o regla diferente a la usada antes. El proceso de desechar objetivos y devolverse al anterior,
se repite una y otra vez hasta que se llegue a un éxito o hasta que se agoten las
posibilidades de moverse hacia atrás en la secuencia de objetivos. De agotarse la secuencia
de objetivos, se considera entonces que el objetivo inicial no era consecuencia lógica del
programa. Esta consideración se conoce como negación de la consulta bajo fracaso.
En los subnumerales que siguen se ilustra el proceso de resolución SLD por medio de un
ejemplo sencillo, y se presentan dos formas de representarlo, una en forma de árbol que
enfatiza los caminos de falla y retroceso, y otra en forma de tabla que enfatiza el proceso
como uno de reescritura de la consulta. Esta última representación, por ser más práctica,
será la que usaremos para ilustrar el proceso asociado a los códigos que se escriben en los
capítulos siguientes.
6.5.1 Búsqueda de la prueba y árboles de búsqueda
Supongamos la siguiente programa PROLOG:
f(a) .
f(b) .
g(a) .
g(b) .
h(b) .
k(Y)
Y = _01
f(_01), g(_01), h(_01)
Figura 6.1. Primer paso en la deducción para la consulta K(Y).
Para satisfacer ésta nueva consulta, el intérprete busca de nuevo un hecho o la cabeza de
una regla que unifique con la meta de la nueva consulta. En éste caso encuentra que
mediante el reemplazo de _01 por a, se puede unificar la meta f(_01), con f(a), para obtener
una nueva consulta, g(a),h(a). Luego, sin necesidad de ningún reemplazo, la primera meta
de la consulta se puede unificar con el hecho g(a), eliminándola para obtener la nueva
consulta h(a). La representación hasta éste punto se puede observar en la Figura 6.2.
k(Y)
Y = _01
f(_01), g(_01), h(_01)
_01 = a
g(a), h(a)
h(a)
Figura 6.2. Paso intermedio en la deducción para la consulta K(Y).
Pero no hay manera de satisfacer la meta de esta última consulta. De manera que el
intérprete reconoce haber cometido un error y verifica si en algún punto existía la
Capítulo 6: Resolución y demostración automática de teoremas
157
posibilidad de unificar con la meta de manera diferente. Esto lo hace regresando un paso en
el camino mostrado en la Figura 6.2, y buscando a través del resto de la base de
conocimientos. En éste caso no existía otra posibilidad para unificar g(a). Así que sube un
peldaño más y allí si encuentra otra posibilidad. Mediante el reemplazo de _01 por b, se
puede unificar la meta f(_01) con f(b), para obtener una nueva consulta g(b),h(b). Este
proceso se conoce como “backtracking” o retroceso.
Luego, y sin necesidad de ningún reemplazo la meta de la consulta se puede unificar con el
hecho g(b), eliminándola para obtener la nueva consulta h(b). Y finalmente la única meta de
esta consulta unificar con el hecho h(b), para obtener la consulta vacía (o la cláusula vacía
en términos del principio de resolución). En la Figura 6.3, se ilustra el proceso completo.
k(Y)
Y = _01
f(_01), g(_01), h(_01)
_01 = a _01 = b
g(a), h(a) g(b), h(b)
h(a) h(b)
f(X)
X = a X = b
97 En los ejemplos de los capítulos siguientes, con frecuencia usaremos una línea para ilustrar más de un paso.
Capítulo 6: Resolución y demostración automática de teoremas
159
1 6 Y/Y´ f(Y), g(Y), h(Y)
2 1 a/Y g(a), h(a)
3 3 h(a)
4 ¿ Falla ya que no hay unificación posible
Para completar la descripción del proceso, vale la pena anotar que en el paso 1 se usó la
cláusula 6 del programa, pero luego de cambiarle la varible Y por Y´para evitar la colisión
de variables entre dicha cláusula y el objetivo; además, luego de la falla en el paso 4,
aparece de nuevo el paso 2 significando que hubo un retroceso para cambiar la claúsula del
programa con la que se había efectuado la resolución; esto es, al fallar el proceso con la
cláusula 1) del programa se reintenta con la 2).
Ejercicios propuestos
1. Demuestre cada una de las equivalencias de la tabla de equivalencias de la sección
4.4.2.1 utilizando tablas de verdad.
2. Para las siguientes fórmulas encuentre una fórmula en forma normal conjuntiva
equivalente a ella; Y luego una forma normal disyuntiva.
(¬PQ)R
P((QR)S)
(¬P(QP)P)(¬P(P¬Q))
Capítulo 6: Resolución y demostración automática de teoremas
162
¬(PQ)(P(PQR))¬S
5. Transformar las siguientes fbfs a forma normal Prenex, con matriz en forma normal
conjuntiva:
(x)(P(x)(y)Q(x,y,z))
(x)(¬((y)P(x,y))((z)Q(z)R(f(x),b)))
(x)(x)((x)P(x,y,z)((u)Q(x,u)(v)Q(y,v)))
3. Encontrar todos los posibles resolventes (si existe alguno) de las siguientes parejas de
cláusulas:
C1 = {¬P(x),Q(x,b)} y C2 = {P(a),Q(a,b)}
C1 = {¬P(x),Q(x,x)} y C2 = {¬Q(a,f(a))}
C1 = {¬P(x,y,u),¬P(y,z,v),¬P(x,v,w),P(u,z,w)} y C2 = {P(g(x,y),x,y)}
5. Para los siguientes conjuntos de cláusulas, encontrar los resolventes necesarios para
evidenciar lo contradictorio de las fórmulas.
{{P,Q,R},{¬Q,R},{P,¬R},{¬P,¬R},{¬P,R}}
{{P,¬R,S},{R,¬S},{P,¬Q,¬R},{P,S},{Q},{¬P,Q,R,¬S}}
b(1,b1).
b(2,B).
b(N,b3).
98 En opinión del autor, desde la apración del FORTRAN, los lenguajes de programación incorporan elementos de la
lógica, en particular todos utilizan términos (o “formulas”) para referirse a los valores manipulados. El centro del texto es
entonces la incorporación de los axiomas de igualdad y las cláusulas como patrón de codificación.
Capítulo 7: Elementos básicos de los Lenguajes de Programación
167
lenguaje en la comunidad de desarrolladores como por la posición del lenguaje en el
contexto histórico del desarrollo de los conceptos.
Vale la pena advertir en este punto que el texto no es un tratado del enfoque lingüístico
“funcional” o “lógico” como tal. En particular, no pretende cubrir las ideas fundamentales
que soportan dichos enfoques, ni las que determinan los avances en este contexto. Es así
que para los lenguajes usados en el texto, se puede afirmar que lo referido no cubre todas
las propiedades del lenguaje ni enfatiza su pureza frente a los conceptos teóricos de la
aproximación a la que se asocie.
Multiplicidad de Lenguajes
¿Cuál es el mejor lenguaje?: Es “la pregunta del siglo”.
Para responderla, el primer problema es la gran cantidad de lenguajes existentes. Como
una muestra, Bill Kinnersley relaciona en su página web 2500 lenguajes, que categoriza
alrededor de 18+ “paradigmas” [Kinnersley 1991-now]; allí Kinnersley destaca como los más
“influyentes” a un conjunto de lenguajes vistos en orden histórico [Kinnersley 1991-now /A
Chronology of Influential Languages]. Wikipedia, por su parte, presenta en su “List of
programming languages” una cantidad igualmente extensa de lenguajes bajo un intento de
clasificación múltiple compuesto de mas de 50+ categorías.
Por otro lado, el grado de interés sobre los distintos lenguajes (su “popularidad”), es
estimado en el “indice TIOBE” a partir de factores tales como las consultas en la WEB, el
número de cursos asociados con cada lenguaje y otros adicionales [TIOBE ¿-now]. Según
este índice, la popularidad de los lenguajes no parece fluctuar mucho con los años; así, en el
índice se refiere entre otras cosas que “Si comparamos los 10 primeros del índice actual
con los 10 primeros del índice de hace 10 años, veremos que contienen exactamente los
mismos lenguajes de programación”99.
Los lenguajes, además, no son un fenómeno aislado; de hecho, las ideas útiles que aparecen
en un lenguaje rápidamante se reproducen en sus “sucesores” creándose relaciones de
influencia entre los lenguajes, o “familias”, que son significativas al momento de
caracterizarlos. Una propuesta de la forma en que los lenguajes históricamente se
relacionan entre sí, aparece en en el grafo de Levenez [Levenez ¿-now].
A partir de estas tres fuentes, y del conocimiento previo del autor sobre la naturaleza de
varios de los lenguaje, hemos señalado como lenguajes de interés para este trabajo, a un
subconjunto de lenguajes que además de ser considerados “populares” ó “relevantes”,
pueden verse como representativos de tres familias lingúisticas, así:
De la gran famila de los lenguajes denominados “procedurales”, que derivan del
FORTRAN y el ALGOL, señalamos al lenguaje clásico C, su versión objetuale
C++, y sus descendientes, los lenguajes de “script”, C#, JAVA, PHP y PYTON.
De la familia de los lenguajes denominados “funcionales” que derivan del LISP y el
ML, señalamos el SCHEME, influenciado por el ALGOL y por el LISP, el
HASKEL influenciado por el ML y por el MIRANDA, el OCAML derivado directo
del ML, el SCALA y el F# como la contraparte funcional del JAVA y del C#, y
100 Los predicados serán entendidos como operadores que tienen como codominio a BOOL.
101 Sin que el intérprete utilizado en el trabajo tenga realmente esta propiedad.
Capítulo 7: Elementos básicos de los Lenguajes de Programación
169
diversos aspectos entre las familias de lenguajes. Entre los diversos aspectos
consideramos diferenciadores los siguientes:
o Definición del proceso computacional: Como es de esperarse la la manera de
definir el proceso que da lugar al cálculo del valor de una evocación, está
determinada por la forma del proceso, así:
Intrucciones de asignacion, control, declaración y E/S: En la familia de
los lenguajes procedurales la especificación del proceso se apoya en
secuencias de “instrucciones de asignación”, “instrucciones de control”, e
instrucciones de “entrada/salida”, que determinan procesos de cambio o
“reescritura” del medio ambiente que define el significado de los rótulos
del programa.
SRT: En la familia de los lenguajes funcionales, la especificación del
proceso se fundamenta en un Sistemas de Reescritura de Terminos ó SRT,
que determina la manera como evolucionan los términos sometidos al
programa para su cálculo.
FNC: En la familia de los lenguajes lógicos, la especificación del proceso
se fundamenta en una fbf en forma normal conjuntiva (FNC) compuesta
por cláusulas de Horn (ver 6.3.1), que determinan la manera como
evoluciona los objetivos sometidos al programa para su verificación.
o Unidades de especificación: El número de unidades de especificación con las
que se define el valor de una evocación, es también un criterio diferenciador.
En la gran mayoría de los lenguajes procedurales se usa una única unidad de
especificación que define el proceso para todas las posibles evocaciones del
operador. En ellos, para distinguir entre los diferentes casos de cálculo, se usan
las instrucciones de control del proceso. En la mayoría de los lenguajes lógicos
y funcionales se permiten múltiples unidades de especificación, cada una
definiendo el proceso para un grupo limitado de evocaciones. En ellos, para
distinguir entre los diferentes casos de cálculo, se tiende a usar unidades de
especificación diferentes
o Argumentos y variables: Una unidad de especificación define el valor de las
evocaciones para todas las posibles particularizaciones de las variables de una
evocación ficticia o “formal” del operador. En los lenguajes que utilizan una
sola unidad de especificación le corresponde, a cada argumento de la evocación
formal, una variable diferente del medio ambiente propio. La variable que le
corresponde a un argumento, toma el valor de dicho argumento al inicio del
proceso en las evocaciones reales del operador. En los lenguajes que utilizan
varias unidades de especificación, los argumentos de la evocación formal
pueden ser términos complejos cuyos subtérminos pueden ser constantes o
variables del medio ambiente propio (no necesariamente diferentes). Estas
variables toman al inicio del proceso el valor que tienen los subtérmino de igual
posición en el árbol sintáctico del argumento que les corresponde en las
evocación reales.
Evaluación de los Operadores: El cálculo del valor para una evocación de un
operador, es un proceso cuyas características y efectos son particulares a cada
familia de lenguajes. En este contexto son criterios diferenciadores los siguientes:
o Estados del Proceso: El cálculo del valor de una evocación se lleva a cabo por
medio de un proceso “computacional”, que no es otra cosa que una secuencia
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
170
de cambios del contenido (o “estado”) de la memoria del computador asociada
con el proceso (ver 1.2). Sin embargo, lo que constituye el estado del proceso es
diferente para las diferentes familias de lenguajes, así:
Medio ambiente: Para la familia de los lenguajes procedurales, un estado
es un “medio ambiente” o “espacio de nombres” formado por asociaciones
de rótulos (comúnmente denominados “variables”), valores, operadores y
tipos manipulados en el programa (ver XX).
Terminos: Para la familia de los lenguajes funcionales un estado es un
conjunto de términos formados por operadores, variables y valores del
programa (ver 5.3.3.2).
Cláusula: Para la familia de los lenguajes lógicos un estado es uno o varios
objetivos o “cláusulas de Horn”, formados por predicados, operadores,
variables y valores del programa (ver 6.5 ).
o Estrategia de cálculo de argumentos: Para calcular el valor de una evocación,
es necesario calcular o “evaluar” el valor de los operandos (u argumentos
reales) de la evocación. Cada lenguaje determina la estrategia de evaluación
que debe usarse al calcular el valor de los operandos, así: la evaluación “en
orden aplicativo” (o “avariciosa”) determina calcular primero los operandos
antes de aplicarlos en la definición del operador; mientras que, las evaluación
en “orden normal” (o “prerezosa”) determina aplicar los operandos en la
definición del operador sin calcularlos, llevando a cabo dichos cálculos sólo
para y cuando los valores de los operandos sean requeridos para obtener el
valor de la evocación. En el lenguaje MAUDE es incluso posible defnir cuales
operandos se calculan antes y cuales operandos se calculan después de ser
aplicados en la definición de un operador.
o Medio ambiente: Para calcular el valor de una evocación, es necesario conocer
el valor de las variables que ocurren al interior de la definición del operador. El
valor de estas variables constituye el “medio ambiente de evaluación propio”
del operador. En la mayoría de los lenguajes funcionales y lógicos, este medio
ambiente está totalmente determinado por el valor de los argumentos de la
evocación. En este caso, diremos que el lenguaje define operadores
“autonomos”. En la mayoría de los lenguajes procedurales existe, sin
embargo, la posibilidad de que los valores de algunas variables del medio
ambiente propio estén determinados por el medio ambiente en el que se evocó
el operador, por el medio ambiente en el que se definío, e incluso por el medio
ambiente de las evocaciones anteriores del mismo operador. En este caso
diremos que el lenguaje define operadores “dependientes”.
o Efectos: La evaluación de una evocación puede cambiar el estado del proceso,
de varias formas posibles, así: (1) substituyendo la evocación por el valor
calculado; (2) asignando valores a variables no asignadas en el medio ambiente
propio; (3) asignando valores a variables no asignadas en el medio ambiente de
la evocación; (4) reasignando valores a variables previamente asignadas en los
medios ambientes, propio, de la evocación, o de la definición. En todos los
lenguajes ocurre como efecto principal el (1). En un lenguaje funcional
usualmente ocurre máximo hasta el (2). En un lenguaje lógico ocurre máximo
hasta el (3). Y, en un lenguaje procedural pueden ocurrir todos los efectos
referidos. Si el efecto de una evocación no incluye el (4), diremos que el
Capítulo 7: Elementos básicos de los Lenguajes de Programación
171
lenguaje tiene “transparencia referencial”, en caso contrario diremos que
permite que las evaluaciones tengan “efectos colaterales”.
Control de tipo en los términos: En los programas de todos los lenguajes señalados
se escriben términos complejos que evocan tanto los operadores nativos del
lenguaje como los definidos en el programa. Sin embargo, los diferentes lenguajes
ejercen diferente grado de control a la correspondencia entre el tipo de los
operandos pre-supuestos para un operador, y el tipo de los operandos realmente
usados en las evocaciones de dicho operador. En efecto, un lenguaje puede señalar
o no señalar”, -- y hacerlo con diferentes grado de severidad--, como incorrectas o
“mal formadas” las evocaciones en las que no se garantiza dicha correspondencia.
Así, los diferentes lenguajes van desde los “fuértemente tipados” en los que
cualquiér evocación riesgoza es rechazada durante la escritura del programa, para
garantizar que los cálculos se lleven a cabo correctamente en ejecución; hasta los
“débilmente tipados” en los que no se rechaza evocacion alguna, dejando al
interprete el problema de corregir las inconsistencias en ejecución (o de asumir
terminaciones anormales del proceso en caso no ser posible la corrección).
Parametrización: La definción de un operador define el valor resultante para todas
las evocaciones que particularicen los argumentos a un valor de su tipo. Para
lograrlo remplaza los operandos a ser usados en los términos de la definción por
variables que “paramerizan” el cálculo que dichos términos representan. La
mayoría de los lenguajes permiten parametrizar los cálculos, remplazando por
variables no sólo los operandos a los que se aplican los operadores sino también los
operadores que se aplican a los operandos. Así, al evocarse un operador deben
particularizarse a un operador las variables que representen operadores dentro de la
definción. Hay lenguajes en que es posible parametrizar los cálculos no sólo en
cuanto a los operandos y operadores que participan en el proceso, sino también en
cuanto a los tipos de los operandos involucrados. En el lenguaje MAUDE la
parametrización se extiende hasta los módulos incluidos dentro de otros módulos en
la estructura del programa. El grado y tipo de parametrización que permite un
lenguaje es, en consecuencia, un importante criterio diferenciador.
Metaprogramación: Si bién los códigos que definen los operadores (y el programa)
son escencialmente estáticos, es posible que al ejecutarse ellos den como resultado
otros códigos ejecutables. Si en un lenguaje es posible escribir programas que
escriban y ejecuten parte del mismo (u otro) programa, diremos que el lenguaje
permite la “metaprogramación”. La capacidad, forma y alcance de la de
metaprogramción es un importante criterio diferenciador de los lenguajes.
En el curso las presentaciones se harán bajo los lenguajes SCHEME, MAUDE, PROLOG,
PHP, el objeto de ilustrar el uso de los enfoques funcional y lógico, así como sus
diferencias con respecto al enfoque procedural.
Se solicitará como ejercicios adicionales, la codificación de los ejemplos en otros lenguajes
(Haskel, Erlang, Scala, ML, etc..)
Usaremos compiladores en línea hasta donde sea posible http://www.compileonline.com,
https://ideone.com/
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
172
Implementaciones e interfaces.
Tanto para poder escribir los programas como para ejecutarlos, es necesario contar con un
programa que reciba las instrucciones del programa, verifique su correctitud y lo convierta
a un programa ejecutable compilándolo o interpretándolo. Estos programas, que
denominamos indistintamente como las “implementaciones” o “interpretes” del lenguaje
(ver 1.3), deben poseer mecanismos efectivos de comunicación con el programador, y ser
capaces de dotar al programa ejecutable con mecanismos efectivos de comunicación con el
usuario. A estos mecanismos los denominaremos las “interfaces” del interprete, y del
programa.
En lo que sigue caracterizaremos brevemente dichas interfaces para presentar luego las que
ofrecen las implementaciones utilizadas en este trabajo
7.4.1 Linea de comandos REPL.
Los lenguajes funcionales están usualmente orientados a definir programas que llevan a
cabo el cálculo de términos (o “expresiones”, o “fórmulas”). En efecto, dichos programas
son básicamente artefactos que le ofrecen al usuario la posibilidad de someter al
computador términos a ser calculados.
A efectos de poder someter los términos a ser calculados y de poder recibir la respuesta del
cálculo, el usuario debe contar con una interfaz de entrada y salida de datos. Esta interfaz
puede consistir en una línea de comandos o en una forma de diálogo, en la que el usuario
escribe uno o varios términos base y recibe como respuesta el o los valores calculados.
La línea de comandos está usualmente señalada por un símbolo específico del sistema (o
“prompt”), luego del cual el usuario escribe una orden a la que el sistema responde con un
ciclo de: “lectura del comando - evaluación del comando - escritura del resultado de la
evaluación – y vuelta al símbolo del sistema”. A este ciclo se le denominó en el lenguaje
LISP como ciclo REPL, por el acrónimo de “Read-Eval-Print Loop”.
La mayoría de los lenguajes funcionales seleccionados en el trabajo ofrecen implementaciones con una
línea de comandos. En particular, un término base escrito en la línea de comandos es inmediatamente
calculado así:
MIT SCHEME103
102
Que no es otra cosa que una notación simplificada para el término (1*100+9*10+6*1).
Capítulo 7: Elementos básicos de los Lenguajes de Programación
173
Para iniciar la línea de comandos o retornar a ella desde un estado de error, se debe usar la combinación
CTRL+G .
1 ]=> (+ (* 3 32) (* 5 20))
;Value: 196
HASKEL104
Prelude> 3*32+5*20
196
SCALA105
scala> 3*32+5*20
res0: Int = 196
MAUDE106
En la implementación de MAUDE, el comando rew (o “rewrite”) ordena la reescritura del témino que
le sigue, con base en los axiomas del módulo “actual”.
Maude> rew 3 * 32 + 5 * 20 .
rewrite in CONVERSION : 3 * 32 + 5 * 20 .
rewrites: 3 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result NzNat: 196
Maude>
En lugar de una línea de comandos, el REPL puede consistir en una forma de diálogo
compleja, con la que el usuario interactúa para plantear uno varios términos a ser
calculados (permitiendo que unos términos se calculen con base los valores de los otros107)
para obtener diversos resultados.
:
En la “hoja de cálculo” siguiente se han dispuesto un conjunto de campos rotulados para que el usuario
ingrese los términos a ser calculados y obtenga los resultados de los cálculos.
A B C
1
2 B1-A1 B1-A2 B2*A2
Dos elementos son destacables en la manera como se disponen los campos en la hoja, así:
Cada término se coloca en el campo en el que se desea obtener el resultado de su cálculo.
En los términos se pueden sustituir tanto valores base como subtérminos por rótulos asociados a
otros campos que contendrán estos valores base y subtérminos. Esta estrategia facilita no sólo
la escritura de términos complejos, sino también la rápida modificación de los términos a ser
calculados modificando sus valores base y/o sus subtérminos.
103
Para el lenguaje SCHEME se usó la implementación MIT SCHEME, ofrecida por el Instituto Tecnológico de
Massachussets (MIT) bajo licencia Publica General GNU [SCHEME].
104 Medio ambiente interactivo GHCi de la implementación GHC (“Glasgow Haskell Compiler”) [HASKELL].
105 Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_40) [SCALA].
106 Maude 2.6 built: Mar 31 2011 23:36:02 [MAUDE].
107 Evitando dependencias circulares entre los téminos.
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
174
En el ejemplo, cuando que el usuario ingresa los datos mostrados en la primera fila, se remplazan los
términos de la segunda con los resultados de los cálculos respectivos.
A B C
1 1 2 3
2 1 1 1
En este contexto los elementos del programa pueden introducirse como un elemento más
del ciclo REPL entremezclándolos con los términos a ser calculados.
Un operador puede ser definido “en línea” introduciendo las ecuaciones del SRT correspondientes:
MIT SCHEME
1 ]=> (define (cua X) (* X X))
;Value: cua
1 ]=> (cua 4)
;Value: 16
HASKEL
Prelude> let cua x = x * x
Prelude> cua 4
16
SCALA
scala> def cua(X:Int):Int = X*X
cua: (X: Int)Int
scala> cua(4)
res0: Int = 16
MAUDE
Maude> fmod CUA is
> protecting INT .
> op cua : Int -> Int .
> var I : Int .
> eq cua(I) = I * I .
> endfm
Advisory: redefining module CUA.
Maude> rew cua(4) .
rewrite in CUA : cua(4)
rewrites: 2 in 16280360
result NzNat: 16
Es importante recordar aquí que bajo el modelo de ejecución por reescritura de términos
propio de los SRT (ver 5.4), propio de los lenguajes funcionales, el orden en que se
introducen las ecuaciones del programa no determina el orden en que se llevan a cabo los
pasos del proceso.
La definición de un operador puede tomarse de un archivo de texto, siempre y cuando esté almacenado
en el directorio de trabajo.
MIT SCHEME
1 ]=> (load “cua.scm”)
;Unable to find file “cua.scm” because: File does not exist.
;To continue …
;…
2 error>;;;use <ctrl+G>
;Quit!
1 ]=> (pwd)
;Value: 13 #[pathname “c:\\...\\mit scheme”]
1 ]=> (cd “c:\\ejscheme”)
;Value: 14 #[pathname “c:\\ejscheme”]
1 ]=> (load “cua.scm”)
;Loading “cua.scm” -- done
;Value: cua
1 ]=> (cua 4)
;Value: 16
HASKEL
Prelude> :cd C:\\ejhaskell
Prelude> :! dir
…
Directory of C:\ejhaskell
…
Prelude> :load cua.hs
[1 of 1] Compiling Main ( cua.hs, interpreted )
Ok, modules loaded: Main.
*Main> cua 4
16
SCALA
scala> :load C:\\ejscala\\cua.txt
Loading C:\ejscala\cua.txt...
cua: (X: Int)Int
scala> cua(4)
res0: Int = 16
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
176
MAUDE
Maude> pwd
/cygdrive/c/Program Files (x86)/MaudeFW
Maude> cd /cygdrive/c/ejmaude
Maude> load cua.maude
Maude> rew cua(4) .
rewrite in CUA : cua(4) .
rewrites: 2 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result NzNat: 16
En la implementación en línea referida en [ideone], es simple escribir un programa que indica primero
leer datos del archivo de texto stdin, para luego escribir los resultados en el archivo de texto stdout, así:
Archivo stdin
Juan
Archivo stdout
Hola Juan
A continuación se muestra dicho programa en algunos lenguajes procedurales:
C
#include <stdio.h>
int main(void) {
char nm[20];
scanf("%s", nm);
printf("%s %s", "Hola", nm);
return 0;
JAVA
import java.io.*;
class Hola
{
public static void main (String[] args) throws
java.lang.Exception
{
BufferedReader r = new BufferedReader (new
InputStreamReader (System.in));
String s = new String("Hola ");
s =s.concat(r.readLine());
System.out.println(s);
}
}
PYTHON
nm = raw_input()
print "Hola " + nm
PHP
<?php
$input = trim(fgets(STDIN));
fwrite(STDOUT, "Hola $input\n");
?>
Es importante notar que en los lenguajes procedurales, el orden de las instrucciones del
programa, junto con las instruccions de conrol, determinan el orden en que se llevan a cabo
los pasos del proceso. De esta manera, para leer en el orden correcto los datos de un
archivo de entrada basta con colocar, en el mismo orden, las instrucciones de lectura en el
programa.
Ls lenguajes funcionales, sin embargo, deben proveer mecanismos de ordenamiento de los
pasos del proceso para lleva a cabo, por ejemplo, la lectura de los datos en el orden
correcto.
La lectura de una serie de datos desde stdin para llevar a cabo un calculo y escribir el resultado en
stdout:
Archivo stdin
Juan
24
Archivo stdout
La edad de Juan es 24
Requiere de instruccines que lleven a cabo las lecturas en el orden correcto, así:
MIT SCHEME
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
178
(begin (define Nm (read))(display "la edad de ")
(write Nm)(display " es ")
(define Ed (read))
(write Ed)
)
HASKEL
import System.IO
main = do
nm <- readLn :: IO [Char] – requiere strings en “xx” o getLine.
ed <- readLn :: IO Int
putStr ("la edad de " ++ nm ++ " es " ++ (show ed))
SCALA
import scala.io.StdIn.{readLine,readInt}
object Main extends App {
val nm = readLine()
val ed = readLine().toInt
printf("la edad de " + nm + " es " + ed)
}
Algunas características importantes de los operadores aritméticos que implementa un lenguaje, pueden
descubrirse fácilmente con ejemplos sencillos como el que se muestra a continuación109.
C
Codigo
#include <stdio.h>
int main(void) {
printf("%s%i \n", "1+1(%i)=", 1+1);
109 Para las ejecuciones se usaron las implementaciones en línea de la página web referida en [ideone].
Capítulo 7: Elementos básicos de los Lenguajes de Programación
179
printf("%s%f \n", "1+1(%f)=", 1+1);
return 0;
}
Salida comentada
La escritura de un valor entero con formato equivocado puede generar resultados inesperados en lugar de
un mensaje de error110.
1+1(%i)=2
1+1(%f)=-
80589945585618742857856832637749543328912847380906773773586705717352014841
76638265689135570516912376420923811154711064155433560393093297457331668876
57675915854327924207637688957348577500845761677301122642136188331523314394
401447520268135324599670866761407739228848128.000000
Un operando flotante induce transformar el operando entero a flotante (por los que se debe ser
especialmente cuidadoso con el formato de escritura).
1+1.0(%i)=0
1+1.0(%f)=2.000000
La división entera da como resultado un entero truncando los decimales, esto ocurre en cualquier rama
del árbol sintáctico en la que los operandos son enteros. El resultado (truncado) puede ser
posteriormente transformado a flotante al ser operando de otro operador. Note que el formato
corresponde al tipo del valor resultante.
4/2(%i)=2
4/2.0(%f)=2.000000
4/3(%i)=1
4/3.0(%f)=1.333333
4/3+2(%i)=3
4/3+2.0(%f)=3.000000
4/3+3/5(%i)=1
4/3.0+3/5.0(%f)=1.933333
La precisión del valor flotante da lugar a resultados errados cuando se combinan valores de magnitudes
muy diferentes.
1.0e1+0.001-1.0e1(%f)=0.001000
1.0e13+0.001-1.0e13(%f)=0.001000
1.0e15+0.001-1.0e15(%f)=0.000977
1.0e17+0.001-1.0e17(%f)=0.000000
Salida comentada
Un operando flotante forza transformar el otro operando a flotante (en este caso el intérprete se ocupa de
escribir el valor corréctamente según el tipo112).
(+ 1 1)=2
(+ 1 1.0)=2.0
(/ 4 2)=2
(/ 4 2.0)=2.0
La aparición del tipo fraccionario evita el truncamiento en la división entera. Sólo la ocurrencia de un
operando flotante obliga la conversión del fraccionario a flotante.
(/ 4 3)=4/3
(
/ 4 3.0)=1.3333333333333333
(+ 2 (/ 4 3))=10/3
(+ 2.0 (/ 4 3))=3.3333333333333335
(+ (/ 3 5) (/ 4 3))=29/15
(+ (/ 3 5) (/ 4 3) 0.0)=2.933333333333333
La precisión del valor flotante da lugar a resultados errados cuando se combinan valores de magnitudes
muy diferentes.
(+ 10.0 0.001 (- 10.0))=9.999999999994458e-4
(+ 100000.0 0.001 (- 100000.0))=0.0010000000038417056
(+ 1.0e10 0.001 (- 1.0e10))=9.9945068359375e-4
(+ 1.0e13 0.001 (- 1.0e13))=0.001953125
(+ 1.0e14 0.001 (- 1.0e14))=0.0
:- main.
Salida comentada
Un operando flotante forza transformar el otro operando a flotante.
1+1= 2
1+1.0= 2.0
A pesar de contar con el tipo fracionario114, al efectuar la división se transforman los operandos enteros a
flotante cuando hay operandos en flotante o cuando podría haber truncamiento.
4/2= 2
4/2.0= 2.0
4/3= 1.3333333333333333
4/3.0= 1.3333333333333333
4/3+1.0= 2.333333333333333
4/3.0+1.0= 2.333333333333333
4/3+3/5= 1.9333333333333331
La operación aritmética con un operando tipo “string” genera un resultado inesperado sin que se reporte
error algunao.
1+"1"= 50
La precisión del valor flotante continúa dando resultados errados cuando se combinan valores de
magnitudes muy diferentes.
1.0e15+0.001-1.0e15= 0.0
PHP
Codigo
<?php
echo " 1+1= " . (1+1) . " | printf" . sprintf(" 1+1(d)= %d 1+1(f)= %f\n", 1+1, 1+1);
echo " 1+1.0= " . (1+1.1) . " | printf" . sprintf(" 1+1.0(d)= %d 1+1.0(f)= %f\n",
1+1.0 , 1+1.0);
printf(" 1+1.0(f)= %f \n", 1+1.0);
printf(" 1+'1'(f)= %f \n", 1+'1');
echo "4/2=" . (4/2) . " 4/2.0=" . (4/2.0) . " 4/'2.0'=" . (4/'2.0') . "\n";
echo "4/3+3/5=" . (4/3+3/5) . " 4/3.0+3/5.0=" . (4/3.0+3/5.0) . " 4/'3.0'+'3/5'="
. (4/'3.0'+'3/5') . "\n";
echo "1e1+0.001-1e1=" . (1e1+0.001-1e1) .
" 1e13+0.001-1e13=" . (1e13+0.001-1e13) .
" 1e14+0.001-1e14=" . (1e14+0.001-1e14) . "\n";
Salida comentada
La operación mantiene la precisión de un cálculo en flotante, independientemente de los tipos de los
operandos. Es incluso posible operar con un entero representado en un tipo “string”. El formato, por su
parte, no genera una salida con valores errrados, ya que sólo tiene efecto sobre el formato de la salida y el
número de decimales.
1+1= 2
printf 1+1(d)= 2 1+1(f)= 2.000000
1+1.0= 2.1
printf 1+1.0(d)= 2 1+1.0(f)= 2.000000
1+1.0(f)= 2.000000
1+'1'(f)= 2.000000
4/2=2
4/2.0=2
4/'2.0'=2
4/3+3/5=1.9333333333333
4/3.0+3/5.0=1.9333333333333
Aunque es posible operar con números representados por “string”, no se opera corréctamente con una
expresión representada de tal forma.
4/'3.0'+'3/5'=4.3333333333333
La precisión del valor flotante continúa dando resultados errados cuando se combinan valores de
magnitudes muy diferentes.
1e1+0.001-1e1=0.00099999999999945
1e13+0.001-1e13=0.001953125
1e14+0.001-1e14=0
Ejercicios Propuestos.
1- Someta a los intérpretes de HASKELL, SCALA, PYTHON y MAUDE los
términos usados en los ejemplos del numeral 7.5 .
Capítulo 8
Operadores Definidos
Capítulo 8: Operadores Definidos
186
Introducción
En el capítulo anterior se presentaron los lenguajes funcionales y procedurales que serán
analizados en este trabajo. Se indicó, además, que sus respectivas implementaciones
ofrecían una serie de operadores nativos que pueden ser usados por el usuario para llevar a
cabo cálculos.
Si dichas implementaciones ofrecieran sólo estos operadores, no tendrían más poder que la
de una calculadora de bolsillo. La potencia real de los lenguajes surge, en efecto, de las
facilidades que tienen para proponer nuevos operadores y nuevos tipos de dato con sus
operadores asociados.
En éste capítulo se presentan los elementos básicos disponibles por los diferentes lenguajes
para definir nuevos operadores. En principio, nos ocuparemos del planteamiento de
operadores definidos sencillos en el marco de los tipos de datos nativos al lenguaje.
Dejaremos para capítulos posteriores el problema del planteamiento de nuevos tipos de dato
y de sus operaciones asociadas.
Con base en el planteamiento de estos operadores sencillos, sin embargo, se discutirán
asuntos importantes como la definición del operador, el operador de selección, el manejo de
los tipos, el medio ambiente de evaluación, las estrategias de evaluación y la estructuración
de los programas.
Aunque la discusión se extiende a las características particulares de cada lenguaje sólo en
los ejemplos, se presenta como factor unificador una caracterización abstracta de los
diferentes enfoques como conceptos diferenciadores entre los lenguajes (ver 7.3).
Justificación.
La capacidad de definir nuevos operadores o “funciones”115 es un problema central en
todos los lenguajes de programación. Vale, entonces, la pena recordar las razones básicas
que justifican su inclusión en prácticamente todos los lenguajes.
8.2.1 Reuso de código.
En casi todo proceso de cómputo es normal que un mismo cálculo se lleve a cabo varias
veces y sobre diferentes datos. Esto obligaría, en principio, a repetir la fórmula o pieza de
código que define el cálculo cada vez que se requiera, posiblemente cambiando los datos
sobre los que se aplica.
La solución obvia a este problema es la de definir una sola vez el cálculo y asociarlo a un
nuevo operador. Así, cuando se requiera indicar que el cálculo se debe llevar a cabo sobre
datos específicos, simplemente se escribe (o se “evoca”) el operador colocándole las
referencias a los datos específicos como sus operandos.
115 La distinción entre operadores y funciones se asocia en los lenguajes procedurales a elementos meramente sintácticos.
En particular suelen ser denominados como “operadores” sólo los operadores de notación infija que son prefijados por el
lenguaje; y, suelen ser denominados como “funciones”, los operadores con notación prefija que pueden ser introducidos
libremente por el programador. En este documento el término “operador” se asocia al rasgo sintáctico usado en los
programas (sea este prefijo o infijo), y el término “función” se asocia a la función matemática que representa el operador,
o sea a su semántica.
Capítulo 8: Operadores Definidos
187
8.2.2 Arquitectura de funciones y de objetos.
Con el crecimiento del uso del computador, en situaciones cada vez mas variadas, los
programas se han hecho cada vez más grandes y complejos. El aumento del tamaño y
complejidad del software va asociado con un aumento en los costos de producción y en las
dificultades para mantener su calidad.
Estos problemas asociados con la complejidad del software, impulsaron desde los años 60,
una serie de esfuerzos para regular el entonces “arte” de escribir software, y acercarlo al
nivel de una disciplina de ingeniería.
Los primeros esfuerzos se orientaron a examinar el software mismo, y a moldearlo a formas
más inteligibles para darle solución al problema de la complejidad. Se planteó entonces,
que la complejidad del software se asociaba principalmente al hecho de estar compuesto
por múltiples pequeños elementos con múltiples relaciones entre sí, haciéndolo difícil de
comprender por los seres humanos [Miller 1956].
Se propuso entonces como solución, la aplicación de los principios de la modularidad y de
la descomposición progresiva, así:
Bajo el paradigma de funciones (ver 2.3), una pieza de software debe ser considerada
como un agregado de un pequeño conjunto de funciones encargadas de transformar los
datos, que recibían del usuario o de otras funciones, en resultados a ser enviados hacia el
usuario o hacia otras funciones del programa. Una función debe, además, ser
considerada también como como un agregado de funciones más pequeñas, y asi
sucesivamante, hasta llegar a las instrucciones básicas del lenguaje. El programador
debe entonces reflejar el diseño en el programa escribiendo las funciones (u operadores)
correspondientes a las componentes de la descomposición progresiva.
Bajo el paradigma de objetos (ver 2.5), una pieza de software debe ser considerada como
el agregado de objetos, que interactúan entre sí por medio de mensajes. Como respuesta
a un mensaje, un objeto lleva a cabo un proceso definido como un operador definido en
el medio ambiente de los atributos del objeto (ver 8.6). El programador debe entonces
reflejar el diseño en el programa consignando en las “clases” las características comunes
a grupos de objetos, codificando los operadores con los que estos responden a los
diferentes mensajes que reciben.
En consecuencia, si en un lenguaje no es posible proponer nuevos operadores no es posible,
tampoco, escribir con él programas que reflejen la arquitectura de sus funciones u objetos.
t1 = t2
Donde:
El símbolo = es substituido por algún símbolo del lenguaje.
El término t1 tiene como operador principal el operador definido y sus
operandos son patrones de emparejamiento que serán denominados en lo que
sigue “argumentos formales de la especificación”.
El término t2 es término que remplaza la evocación y utiliza las variables
que aparecen en los argumentos formales.
MIT SCHEME
En el lenguaje SCHEME la capacidad de definir operadores se asocia a la capacidad de “definir” el
significado de los rótulos116 a ser usados en el programa [Abelson 1985 1.1.2]. Así la “definición”
siguiente le da valor al rótulo pi:
(define pi 3.141542)
La definición de rótulos se extiende de una manera natural a rótulos con argumentos llamados
“procedures” (procedimientos), que se constituyen en los operadores definidos en el programa. Para ello
el SCHEME ofrece una construcción de la forma siguiente:
(define (t1) (t2))
Donde: Los argumentos formales son todos nombres de variable. Los paréntesis son obligatorios. Los
términos son listas de elementos separados por espacio, donde el primer elemento es el operador y los
siguientes los operandos.
Son definiciones de operador las siguientes:
(define (cua X) (* X X))
116Estos rótulos son denominados “variables”. En este trabajo, sin embargo, preferimos usar el término “variable” en el
sentido de la lógica.
Capítulo 8: Operadores Definidos
189
(define (sum_cua X Y) (+ (cua X) (cua Y))
HASKELL
El lenguaje HASKELL ofrece las mismas facilidades del SCHEME con base en construcciones de la
forma siguiente:
t1 = t2
Donde: Los argumentos formales pueden ser nombres de variable, constantes o términos compuestos.
Las variables deben estar en minúscula. Cada definción debe iniciarse en una línea de texto diferente.
Los términos son listas de elementos separados por espacio donde el primer elemento es el operador y
los siguientes son los operandos. Un término compuesto que aparece como operando debe encerrarse
entre paréntesis.
Son definiciones de operador las siguientes:
cua x = x*x
sum_cua x y = (cua x)+(cua y)
SCALA
Para definir funciones, el lenguaje SCALA ofrece construcciones de la forma siguiente:
def t1 : [tr] = t2
def t1 : [tr] = {tn … t3 t2}
Donde : Los argumentos formales tienen la forma <v>:<ty> siendo v un nombre de variable y ty su tipo;
tr es opcional y representa el tipo del resultado de la evocación. Cada definción debe iniciarse en una
línea de texto diferente. tn … t3 t2 es una lista de términos ocupando cada un o una línes diferente,
siendo el último el que determina el valor de la evocación. Los términos usan notación estándar.
Son definiciones de operador las siguientes:
def cua(X:Int):Int = X*X
def sum_cua(X:Int,Y:Int):Int = cua(X)+cua(Y)
MAUDE
El lenguaje MAUDE ofrece una construcción de la forma siguiente, para definir los operadores:
eq t1 = t2 .
Donde: Los argumentos formales pueden ser nombres de variable, constantes o términos compuestos. El
prefijo eq señala la ecuación que define el operador. Lós términos usan notación estándar por defecto.
Es además necesario declarar el perfil del operador inidicando el tipo de los argumentos y el tipo del
resultado. Todas las variables deben, además, estár declaradas con su tipo, así:
op cua : Float -> Float .
op sumcua : Float Float -> Float .
vars X Y : Float .
eq cua(X) = X * X .
eq sumcua(X, Y) = cua(X) + cua(Y) .
Donde: Los prefijos op, vars, identifican las declaraciones del perfil del operador y de las variables.
Nótese que todas las construcciones se deben terminar con un punto.
h t1, t2, , tn
Donde:
El símbolo es substituido por algún símbolo del lenguaje.
El literal h es un predicado que representa al operador y tiene como
argumentos los operandos del operador junto con una variable que
representa el reultado del cálculo.
Los operadores en PROLOG deben definirse como predicados en los que tanto los operandos como el
resultado del cálculo se asocian con sus argumentos. La definición siguiente implementa un predicado
para elevar al cuadrado un número dado:
cua(X,Y) :- Y is X*X .
sum_cua(X,Y,Z) :- cua(X,X2), cua(Y,Y2), Z is X+Y .
Que al ser usado en una consulta, entrega como resultado el valor de la variable que se coloque en el
lugar de la variable calculada en el predicado is, así:
2 ?- cua(10,W).
W = 100.
Sintaxis: Los predicado se escriben en notación estándar. Las variables deben comenzar con letras en
mayúscula. El es substituido por “:-“ . Cada cláusula del programa debe terminar en punto.
Nótese que puesto que en la resolución SLD las metas de la consulta (los predicados t1, t2, …) se
verifican de izquierda a derecha, el orden de las metas es significativa al momento de obtener el
resultado correcto. Así, La definición siguiente del predicado anterior, genera un mensaje de error, al
tratar de llevar a cabo la suma sin haber calculado antes los sumandos:
sumcua(X,Y,S) :- S is X2+Y2, cua(X,X2), cua(Y,Y2) .
1 ?- sumcua(2,3,Z).
ERROR: is/2: Arguments are not sufficiently instantiated
C
PYTHON
SCALA
Capítulo 8: Operadores Definidos
191
PHP
Selección.
Es, usual que el término a ser calculado para obtener el valor de una evocación de un
operador, dependa del valor de los operandos de la evocación. Este es, por ejemplo, el caso
de operadores que representan funciones discontinuas que componen otras funciones
indexándolas por regiones del dominio.
Considere la función valor absoluto de un número real |x| . Esta función compone las funciones f(x) = x
y f(x) = -x , indexándolas según si el valor del argumento es mayor o menor que cero.
Para hacer posible la definición de este tipo de operadores, los lenguajes de programación
ofrecen mecanismos de selección. Los mecanismos de selección se orientan a escoger entre
las varias disponibles, una definición particular para el operador. Así, para el caso de los
lenguajes procedurales, la selección escoge entre varios grupos de instrucciones candidatos
el que va a ser ejecutado para calcular una evocación del operador; para el caso de los
lenguajes funcionales la selección escoge entre varios términos candidatos el que
remplazará una evocación del operador; y, en los lenguajes clausales la selección escoge
entre varios objetivos candidatos el que remplazará una evocación del operador.
Además, en todos los casos la escogencia ha de fundamentarse en los valores de los
operandos o argumentos de la evocación, y ocasionalemente en los valores asociados a los
rótulos del medio ambiente en que fúe definido el operador; tótulos que, en adelante,
consideraremos como argumentos adicionales (aunque implícitos) de la evocación. En lo
que sigue nos referiremos a los valores de los operandos o argumentos de la evocación
como “argumentos reales”; y nos referiremos, de forma sumaria, al grupo de instrucciones,
o al término, o al objetivo que será usados para calcular el valor de una evocación, como el
“caso de reescritura”.
Planteados en estos términos, los mecanismos de selección planten un conjunto (finito) de
posibles casos de reescritura, y luego asocian un conjunto específico de evocaciones a cada
uno de estos casos. Puesto que cada una de las posibles evocaciones corresponde a una
combinación específica de valores para los argumentos reales, los mecanismos de selección
reálmente asocian los casos de reescritura con un subconjunto del conjunto de todas las
posibles combinación de valores para los argumentos reales.
En términos más precisos, podemos decir que los mecanismos de selección definen una
partición sobre el producto cartesiano de los tipos de los argumentos reales, y rotulan cada
miembro de dicha partición con un elemento (usualmente distinto) del conjunto de posibles
casos de reescritura.
Una forma de definir una partición sobre el producto cartesiano de los tipos de los
argumentos, es la de imponer a cada uno de sus miembros (es decir a una combinación
específica de valores para los argumentos reales), una condición de pertenencia a cada
miembro de la partición.
Capítulo 8: Operadores Definidos
192
Para definir esta condición de pertenencia, los diferentes lenguajes analizados usan en
sucesión dos posibles mecanismos, el primero es el emparejamiento o “pattern matching”, y
el segundo es una proposición explícita o condición de “guardia”.
El emparejamiento se apoya en plantear para cada caso de reescritura, una evocación
“formal” del operador cuyos operandos pueden ser términos complejos con constantes y
variables, y en verificar, frente a una evocación “real” del operador, si existe una
substitución de las variables de la evocación formal que haga dicha evocación idéntica a la
real (en términos sintácticos). La condición de guardia, por su parte, se apoya en plantear
para cada caso de reescritura, una proposición formada con las variables de la evocación
formal, y en verificar que las evocaciones que satisfacen el emparejamiento satisfagan
también la proposición.
En términos abstráctos, al definir un operador O de aridad n, para el que existen m casos
posibles de reescritura (para m≥1), todos los lenguajes le permiten al programador plantean
un conjunto de parejas {casos de reescritura - condiciones de pertenencia para los
argumentos reales}, que pueden asimilarse con las m expresiones siguientes que en lo que
sigue denominaremos como “expresiones de selección de un caso” o simplemente
“selección de un caso”:
V11 V12 ….O1( af11(V11, V12, ...), af12(V11, V12, ...), ...) {C1} G1(V11, V12, ...)
V21 V22 ….O2( af21(V21, V22, ...), af22(V21, V22, ...), ...) {C2} G2(V21, V22, ...)
…
Vm1 Vm2 ….Om( afm1(Vm1, Vm2, ...), afm2(Vm1, Vm2, ...), ...) {Cm} Gm(Vm1, Vm2, ...)
Donde:
Ok(…..) es la evocación del operador para el caso de orden k{1..m}.
afkj(….) para j{1..n} son los argumentos “formales” de la evocación para el
caso de orden k. Los argumentos formales pueden ser una varible, una
constante o un término complejo con varios operadores constantes y
variables.
Vki spara i{1..nvk}, son las variables que ocurren en los argumentos formales
de la evocación para el caso de orden k.
Ck es el caso de reescritura de orden k que, en general, depende de las
variables que ocurren en los argumentos formales asociados al mismo caso.
Gk(...) es la condición de guardia del caso de reescritura de orden k que, en
general, depende de las variables que ocurren en los argumentos formales
asociados al mismo caso.
Para hallar el caso de reescritura correspondiente a una evocación real, basta con hallar la
expresión en la que la evocación real empareja con la evocación formal de la expresión y en
la que, además, se satisface la condición de guarda, para luego, usar como caso de
reescritura el planteado en dicha expresión.
XXXXXcuando hay varias que cumplen???XXX y cuando hay evocaciones no
contempladas???
Si bien la selección puede entenderse en todos los lenguajes con base en las expresiones de
selección arriba presentadas, ella toma diversas formas entre los diferentes lenguajes e
Capítulo 8: Operadores Definidos
193
incluso diversas formas dentro de un mismo lenguaje. Así, en las secciones que siguen
presentamos las formas más usadas para definir las expresiones de selección que a pesar de
consistir básicamente en formas sintácticas simplificadas117, son de gran importancia al
momento de sospesar la simplicidad y potencia de un lenguaje.
8.4.1 Selección por emparejamiento de la evocación formal con la evocación
real.
En varios de los lenguajes analizados, se usa el emparejamiento como un medio para
seleccionar el caso de reescritura. Para ello el lenguaje permite plantear varias unidades de
especificación asociando cada una de ellas a un conjunto (diferente) de casos de reescritura.
Esta estrategia corresponde a condensar en una sóla unidad de especificación, las
expresiones de selección del caso que comparten los mismos argumentos formales.
Aun cuando la mayor utilidad del emparejamiento ocurre frente a valores compuestos,
donde permite minimizar el uso de “selectores” (ver ..), es particulamente útil cuando un
argumento real debe corresponder con un valor entre un conjunto finito de opciones.
Considere un operador que dado un valor numérico calcula calcula ya sea el área de un circulo cuyo
radio es dicho valor, o ya sea el área de un triángulo equilátero inscrito en dicho círculo, y que distingue
el cálculo requerido con un string con la palabra “circulo” o con la palabra “triangulo”.
HASKELL
area x "circulo" = pi*x*x
area x "triangulo" =
PROLOG
area(X,"circulo",R) :- R is pi*X*X
area(X,"triangulo",R) :- R is pi*X*X
MAUDE
eq area(X,"circulo") = pi*X*X
eq area(X,"triangulo") =
Si se evoca un operador con operandos reales que no emparejan con alguna de las opciones
de emparejamiento propuestas, el cálculo no puede llevarse a cabo y el proceso falla
generando alguna forma de mensaje de error. En por tanto deseable incluir casos de
emparejamiento que cubran todas las evocaciones posibles, para darle un tratamiento
adecuado a los casos excepcionales.
Es importante notar que de ser una variable el argumento formal, cualquier argumento real
cuyo tipo sea compatible con el tipo de la variable empareja transfiriendo su valor a la
variable. En algunos lenguajes existe un símbolo especial (v.g. “_”), que representa una
variable muda, y que puede usarse cuando sólo es necesario garantizar el emparejamiento.
PROLOG
area(X,"circulo",R) :- R is pi*X*X
area(X,"triangulo",R) :- R is pi*X*X
% para capturar los casos no considerados en area use una de las dos
area(X,OOP,R) :- string_concat("argumento”, OOP, S1),
string_concat(S1," equivocado al evocar area”, S2,
write(S2), fail .
area(X,_,R) :- write("argumento equivocado al evocar area”),fail .
Nótese que en los lenguajes del ejemplo anterior la ambigüedad que introduce la nueva
unidad de especificación se elimina debido a que las unidades existentes son consideradas
en el orden en que aparecen en el código, así, al colocar de última la unidad que empareja
con todas la evocaciones posibles, se garantiza que no entra en conflicto con las anteriores.
De no estár garantizado el orden en que se consideran dichas unidades, no es posible
garantizar que se cubren todas las evocaciones posibles.
En los lenguajes siguientes el intérprete detecta las ambigüedades que puedan presentarse entre las
unidades de especificación de un operador.
MAUDE
op area(X,"circulo") = pi*X*X .
op area(X,"triangulo") =
op area(X,OOP) = error ….
En las especificaciones que siguen el cálculo del valor absoluto de un número, se apoya en seleccionar
entre varias unidades de especificación, usando emparejamiento y condiciones de guardia.
PROLOG
abs(0.0,0.0) .
abs(X,R) :- X>0.0, R is X .
Capítulo 8: Operadores Definidos
195
abs(X,R) :- R is -X .
MAUDE
eq abs(0.0) = 0.0 .
ceq abs(X) = X if(X>0.0) .
ceq abs(X) = -X if(X<0.0) .
Elector debe notar que en el caso del PROLOG no es necesaria la condición en la última unidad de
especificación debido a que las reglas son examinadas de arriba abajo por el intérprete. Por no ser este
el caso en el lenguaje MAUDE, requiere que se incluya la condición de guardia en todas las unidades.
Xxxxx casos de no cumplir xxxx
En las especificaciones que siguen el cálculo del valor absoluto de un número, se apoya en seleccionar
entre varios términos con base en condiciones de guardia, utilizando el “if funcional” (o el denominado
“operador ternario” en lenguajes procedurales).
Siendo obvio el rol de las componenetes de la especificación, se omiten explicaciones adicionales.
MIT SCHEME
(define (abs x)
(if (< x 0) (- x) x )
)
)
HASKELL
abs x = if (x>=0) then x else 0-x
SCALA
def abs(x: Int) = if (x >= 0) x else -x
MAUDE
var X : Float .
op op abs : Float -> Float .
eq abs(X) = if (X > 0.0) then X else (- X) fi .
Capítulo 8: Operadores Definidos
196
C
float abs(float X) { return X>0.0 ? X : -X }
JAVA
// colocar como un metodo de una clase
public float abs(float X) { return X>0.0 ? X : -X ;}
PHP
function abs($X) { return $X>0.0 ? $X : -$X }
PYTHON
def abs(X) :
return X if X>0.0 else -X
En las especificaciones que siguen el cálculo, se apoya en seleccionar entre varios términos con base en
varias condiciones de guardia. Para evaluar una evocación de un operador de selección el intérprete de
evalúa las condiciones de guardia p1 … pm, asociadas a los términos e1 … em, de forma sucesiva hasta
hallar la primera que evalue a verdadero. De hallarse que la condición de orden k es la primera que
evalúa a verdadero, se usa como caso de reescritura el término ek asociado a dicho predicado. Nótese
que la última condición de guardia es la guardia de defecto.
MIT SCHEME
El lenguaje SCHEME ofrece las “formas especiales” if y cond como mecanismos de selección118. La
forma general del y del cond es las siguientes:
(if <p1> <e1> <a>)
(cond
(<p1> <e1>)
(<p2> <e2>)
(<pm-1> <em-1>)
[(else < em>)])
)
Donde: La palabra reservada else es la guardia de defecto siendo opcional usarla. De no satisfacerse
ninguna de las condiciones el resultado se considera “indeterminado”.
Con ellas se puede definir una función que calcule el valor absoluto, así:
(define (abs x)
(cond ((> x 0) x)
((= x 0) 0)
(else (- x))
)
)
HASKELL
118
No se considran operadores debido a la rigida estrategia de evaluación de los operandos de los operadors en
SCHEME.
Capítulo 8: Operadores Definidos
197
Haskell ofrece la construcción if como un operador de selección y una sintaxis para definir operadores
que llevan a cabo selección por medio de guardias, que tiene la forma siguiente:
<id> af1 … afm
| <p1> = e1
| <p2> = e2
...
| <pm-1> = em-1
[| otherwise = em]
Donde: id es el nombre del nuevo operador, afj para j{1..m} son los argumentos formales del operador,
pi es una condición de guradia y ei es su término asociado. La palabra reservada otherwise es la guardia
de defecto siendo opcional usarla. De no satisfacerse ninguna de las condiciones la ejecución fracasa.
Con ellas se pueden definir funciones que calculen el valor absoluto, así:
abs x
| x>0.0 = x
| x<0 = 0.0-x
| otherwise = 0.0
SCALA
En Scala la construccion match implementa un mecanismos de selección que utiliza tanto condiciones
de guardia como emparejamiento. La forma general de las construcciones match es la siguiente:
<v> match {
case <af01> [(| <afj1>)*] [if <p1>] => <e1>
case <af02> [(| <afj2>)*] [if <p2>] => <e2>
….
case <af0n> [(| <afjn>)*] [if <pn>] => <en>
}
Donde: v es un valor, afij para j{1..n} y para i{0..mj} , son los argumentos formales del operador
match, que pueden ser, entre otras cosas, un nombre de variable (con o sin tipo), el símbolo “_” que
representa un nombre de variable no especificado, una constante, o un término complejo. pi es una
proposición y ei es un termino, para i{1..n}..
Para evaluar una evocación de un operador de selección match SCALA procede de forma sucesiva a
considerar las construcciones case hasta hallar la primera en la que uno de sus af empareja con el valor v
y el predicado p evalúe a verdadero. Nótese que el emparejamiento le da valor a las variables de af que
son locales al case (son diferentes de otras con el mismo nombre en la función), y pueden ser usadas en
las p y e del case. De hallarse que la construcción case de orden k cumple con las dos condiciones
referidas, el término a ser calculado para obtener el valor de la evocación es el término asociado a dicha
construcción, es decir ek. De no hallarse una construcción que satisfaga las dos condiciones se genera un
mensaje de error.
Con ellas se puede definir una función que calcule el valor absoluto, así:
def abs1(x: Int) = if (x >= 0) x else -x
La instrucción de selección más sencilla, denominada “if procedural”, selecciona entre dos
grupos de instrucciones con base en un solo predicado, donde el predicado mismo es la
condición de guarda del primer grupo y su negación es la condición de guardia del segundo
grupo. Además, cualquiera de los dos casos de reescritura (que en este caso son grupos de
Capítulo 8: Operadores Definidos
198
instrucciones), puede contiene otras instrucciones de selección posibilitando que una
cantidad indeterminada de caminos de ejecución en una solo instrucción de selección.
En las especificaciones que siguen el cálculo del valor absoluto de un número, se apoya en seleccionar
entre instrucciones con base en condiciones de guardia, utilizando el “if procedural”.
Siendo obvio el rol de las componenetes de la especificación, se omiten explicaciones adicionales.
C
float abs(float X) { float Y; if(X>0.0) Y=X; else Y=–X; return Y;}
JAVA
// colocar como un metodo de una clase
public float abs(float X) { float Y; if(X>0.0) Y=X; else Y=–X; return Y;}}
PHP
function abs($X) { if(X>0.0) $Y=$X; else $Y=–$X; return $Y;}
PYTHON
def abs(X):
if X>0.0:
Y=X
else:
Y=-X
return Y
La construcción switch aparece en varios de los lenguajes procedurales considerados, y permite que el
proceso se apoye en una selección entre varios posibles caminos de ejecución , así:
switch (e) {
case g1: [s1 [break;]]
case g1: [s2 [break;]]
…
case gm: [sm [break;]]
[default: sm+1 [break;]]
}
Donde: e es un término; gj para j{1..m} son términos que definen las condiciones de guardia; sj para
j{1..m} son grupos opcionales de instrucciones; break es una instrucción opcional de control de
secuencia; y default es la condición de guardia por defecto.
Al ejecutarse la instrucción switch, el valor del término e se evalúa (una sola vez) y se compara, de
forma sucesiva, con los valores de los términos gk hasta hallar uno cuyo valor sea igual. De hallarse que
el término de orden k es el primero cuyo valor es igual al de e, el proceso se reanuda con el primer
grupo de instrucciones s situado adelante de ck, y prosigue hasta que se terminen los grupos de
instrucciones s o hasta hallar un instrucción break, para luego continuar el proceso con las instrucciones
que siguen al switch. El comportamiento del switch, sin embargo, se adapta en cada caso a las
restricciones del lenguaje incluyendo el concepto de igualdad que éste aplique.
C
Capítulo 8: Operadores Definidos
199
En el lenguaje C el término e debe calcular a un valor de tipo integral (los varios tipos de enteros y el
tipo char) o “enumerated”. Las condiciones deben ser constant-expressions cuyo valor debe poderse
calcular durante la compilación del programa (por lo que no deben tener variables ni, en general,
evocaciones a funciones).
JAVA
En el lenguaje JAVA la expresión e debe tener como resultado un valor de tipo byte, short, char, int,
enumerated o string. Las condiciones g deben ser constantes o literales del mismo tipo al que evalúe e.
PHP
En el lenguaje PHP la expresiones e es comparada con los valores de los casos ck usando el operador de
comparación “==” (que tiene un comportamiento diferente al del operador “===”) para todas las
expresiones a los que se aplique (incluyendo expresiones con variables y funciones). El caso de defecto,
puede estar, además, colocado en cualquier posición y aún sólo se tiene en cuenta si los demás fallan.
Que posibilita escribir términos que al calcularse podrían dar lugar a errores de ejecución:
1 ]=> (+ 1 (op1 1) )
;Value: 2
1 ]=> (+ 1 (op1 2) )
;The object “abb” passed as the second argument to integer-add, is not the correct type.
; To continue.......
La redefinición del operador suma para ser aplicado a caracteres se sobrepone a la definición previa
aplicada a valores numéricos, quedando redefinido el operador:
boolean? _
number? _
complex? _
real? _
rational? _
integer? _
char? _
string? _
pair? _
list? _
vector? _
bit-string? _
symbol? _
cell? _
record? _
Establecen si su operando es del tipo correspondiente.
119 En el sentido de que todos los elementos de un sort están contenidos en otro sort diferente.
Capítulo 8: Operadores Definidos
203
Donde:
<identificador_de_variable> Es un nombre que en adelante será asociado a
una variable.
<lista_de_identificadores_de_variable> Es una lista de
<identificador_de_variable> separados por espacio.
8.5.2.3 Declaración de operadores en MAUDE
Por ser importantes tanto el sort del valor resultante como el sort de sus operandos, al
momento de definirse un operador, MAUDE provee construcciones específicas para
declarar los operadores y definir su perfil.
Tal como se refiere en [Clavel 2007, Sec 3.4], la declaración de operadores se apoya en
construcciones de una de las dos formas siguientes:
op <plantilla> : <sorts_dominio> -> <sort_rango> [<atributos_del _operador>] .
ops <plantillas> : <sorts_dominio> -> <sort_rango> [<atributos_del _operador>] .
Donde:
<plantilla> Es el nombre del operador constituido por una secuencia de uno o
más identificadores del operador (separados entre si por caracteres
especiales120 o espacio en blanco).
<plantillas> Es una secuencia de dos o mas <plantilla>, separadas entre si por
caracteres especiales o por espacios en blanco121.
<sorts_dominio> es una secuencia de cero o más identificadores de sort,
separados entre si por espacios en blanco, que identifican el dominio de la
función asociada al operador.
<sort_rango> Es un nombre de sort que identifica el rango de la función
asociada al operador.
<atributos_del_operador> es una secuencia de cero o más identificadores de
propiedad, separados entre si por coma. En caso de ser cero los
identificadores de propiedad se omiten los paréntesis cuadrados. Cada una
de los posibles atributos del operador será discutida en el resto del trabajo
cuando el significado de la propiedad corresponda al tópico en discusión.
Para una discusión completa de dichos atributos el lector puede consultar a
[Clavel 2007, sec 4.4].
Las construcciones que comienzan con op se usan para definir un operador, mientras que
las construcciones que comienzan con ops permiten definir varios operadores (de igual
dominio y rango).
Los operadores definidos antes requieren de una declaración previa para el perfil del operador y de una
declaración de tipo para las variables usadas en la definición:
La declaración del perfil impide definir operadores que tengan como resultado un valor de tipo
indeterminado. Así, la definición siguiente es detectada como un error:
var y : string .
ceq op1(y) = 1 .
Ni, por supuesto, evocarlo con argumentos errados:
La notación infija permite usar símbolos más adecuados para identificar el operador:
8.5.2.3.2 Constantes
Cuando hay cero identificadores de sort en <sorts_dominio> se dice que el operador es una
constante del sort, y se usa para representar de forma explícita un elemento determinado del
mismo.
Una constante no es otra cosa que un operador de aridad cero. Así, la declaración siguiente define la
constante 0 de los números enteros:
op 0 : -> Int .
eq 0 + N3 = N3 .
En la declaración siguiente [Clavel 2007, sec. 3.9], se le da a los operadores suma y multiplicación un
valor de precedencia determinado:
En la declaración siguiente [Clavel 2007, sec. 3.9], se le da a los operadores suma y multiplicación un
Capítulo 8: Operadores Definidos
207
valor de precedencia y un patrón de gathering determinado:
pred1(1,1) . % -- 1 es un entero
pred1(2,"ab") . % -- "ab" es un arreglo de enteros
pred1(3,X) :- string_to_list(X, "ab") . % -- X es un string
pred1(4,"a") . % -- "a" es el entero 97
Que posibilita escribir predicados que al calcularse podrían dar lugar a errores de ejecución o a
resultados inesperados:
?- pred2(1,2,Y).
ERROR: '.'/2: Type error: `[]' expected, found `[98]' ("x" must hold one character)
2 ?- pred2(1,3,Y).
ERROR: is/2: Type error: `number' expected, found `ab'
3 ?- pred2(1,4,Y).
Y = 98.
Por ser un lenguaje débilmente tipado, el SWI PROLOG ofrece “predicados de tipo”.
Al igual que el SCHEME, el SWI PROLOG ofrece una serie de predicados de tipo nativos (ver
[swi_ref_man secc 4.6 ]), que permiten saber si un nombre de variable <var> se asocia o no con un valor
(si está o no “ligada”), y si un término <t> es o no de un tipo determinado:
var(<var>)
nonvar(<var>)
integer(<t>)
float(<t>)
rational(<t>)
rational<t>,<numerador>, <denominador>)
number<t>)
atom(<t>)
blob(<t>, <tipo>)
string(<t>)
Capítulo 8: Operadores Definidos
208
Cabe notar que como muestra el ejemplo anterior “aab” no es un string sino un arreglo de enteros (ver
[swi_ref_man secc 4.2.3]) .
?- string("aab").
false.
No implica esto que le PROLOG en si mismo deba ser débilmente tipado, por lo que es
posible hallar implementaciones fuertemente tipadas del mismo.
122 En los lenguajes procedurales las “variables” son rótulos que están asociadas a un único valor del dominio cuando se
inicia la ejecución del cuerpo de una función, pero son variables debido a que pueden reasociarse a otro valor diferente,
durante la ejecución del cuerpo de dicha función, al ejecutarse una instrucción de asignación. Sin embargo los
argumentos “formales” que aparecen en la definición de las funciones, representan a todos los valores de su tipo de la
misma manera que lo hacen las variables cuantificadas de los axiomas.
123Nótese que en un lenguaje procedural, un símbolo de variable representa un lugar de memoria que siempre contiene un
valor (dejado allí por quien antes hizo uso del lugar, y que puede ser un proceso no relacionado con el actual) Así, cuando
se usa una variable que no ha sido explícitamente asignada en el programa, éste tiene un comportamiento impredecible.
Capítulo 8: Operadores Definidos
209
8.6.1 Medio ambiente en SCHEME.
En la versión de SCHEME analizada, se provee un medio ambiente inicial con las variables
y símbolos predefinidos en el lenguaje. A medida que el usuario interactúa con el medio
ambiente, evocando los operadores nativos del lenguaje denominados “formas especiales”,
extiende124 el medio ambiente, hasta definir el medio ambiente en el que se ejecutan las
reescrituras [Hanson 2002, sec. 1.2.3].
Debido a que para el MIT SCHEME, tanto la definición de los operadores como su
evocación se llevan a cabo a través de un mismo interfaz125, dichas definiciones y
evocaciones forman una secuencia continua de interacciones donde se mezclan definiciones
y evocaciones de operadores en un orden arbitrario. Al someter un término para su cálculo,
es además posible usar nombres de “variable” en lugar de constantes para referirse a los
valores involucrados en el cálculo. La única condición de orden entre las definiciones y las
evocaciones de los operadores, es que tanto los operadores como las variables usadas en los
términos sometidos a cálculo, hayan sido definidos antes de su utilización. Así, las
condiciones bajo las cuales se somete un término pueden cambiar ya sea redefiniendo sus
operadores, por medio de axiomas de igualdad, o redefiniendo el valor de sus variables, por
medio de un operador de asignación nativo al lenguaje. En consecuencia, el resultado del
cálculo de un término dependerá de la secuencia de interacciones del usuario con el
intérprete, antes de someter el término a ser calculado.
Esta secuencia de interacciones es, entonces, la que define, o “extiende” el medio ambiente
de los cálculos. En los subnumerales que siguen se presenta la manera como se extiende el
medio ambiente por la evocación de las diferentes formas especiales ofrecidas en el
SCHEME
8.6.1.1 Introducción de Símbolos y de su Significado.
Cada vez que el usuario propone un nuevo operador por medio de una evocación del
operador define (ver, “Definición de Operadores en SCHEME”), introduce un nuevo
símbolo en el medio ambiente que había inmediatamente antes de la evocación,
asociándolo con la operación definida.
El operador de definición puede usarse también, para introducir símbolos de variable y
asociarlos al valor resultante de evaluar una expresión en el medio ambiente de la
evocación.
124No se hará aquí una definición precisa del concepto “extender” el medio ambiente, esperando que quede clara al leer el
texto que sigue.
125Esto es cierto aún si el programa se incluye ya que esto no es más que una secuencia de evocaciones que se lleva a
cabo de forma automática.
Capítulo 8: Operadores Definidos
210
1 ]=> pi2
;Value: 6.283082
Es posible, sin embargo, usar el operador define para introducir un nuevo símbolo al medio
ambiente sin “asignarlo” a valor u operador alguno. Sin embargo, sólo símbolos definidos
y asignados a un significado126 pueden usarse en las definiciones y cálculos siguientes.
1 ]=> (* 2 pi21)
;Unbound variable: pi21.
; To continue.......
Un símbolo puede definirse sin significado alguno:
1 ]=> (* 2 pi21)
;Unassigned variable: pi21.
; To continue.......
A la variable definida pero no asignada del ejemplo anterior se le puede asociar un (nuevo) valor con la
forma especial set!.
Que no puede, sin embargo, ser usada para aumentar el medio ambiente
126En esto difiere el SCHEME de los lenguajes procedurales ya que una variable debe ser necesariamente asignada antes
de usarse, sin que tome el valor del espacio de memoria donde reside su valor.
Capítulo 8: Operadores Definidos
211
8.6.1.3 Medio ambiente de una evocación.
Al llevarse a cabo el cálculo de la evocación de un operador, los operandos se evalúan en el
medio ambiente en que se llevó a cabo la evocación. Luego de evaluados los operandos, se
reescribe la evocación usando el (único) axioma asociado al operador. El término
resultante de la reescritura se calcula, a su vez, en el medio ambiente en que se definió el
operador, extendido con la asociación de las variables que constituyen los argumentos
formales a los valores de los operandos ya evaluados (ver [Hanson 2002, sec. 1.2.4]). En
otras palabras, las variables usadas en el cuerpo de la definición de un operador no
necesariamente tienen que ser argumentos formales en dicha definición.
La definición de un operador en SCHEME se lleva a cabo en un medio ambiente determinado por los
operadores ejecutados antes que el define correspondiente. Así, la definición del operador f que se
muestra a continuación:
1 ]=> (define x 4)
...
1 ]=> (define y 2)
...
1 ]=> (define (f y z) (+ x y z))
1 ]=> (set! y 1)
...
1 ]=> (define z 3)
...
1 ]=> (f (+ 2 y ) (+ z y)))
Los argumentos de f se evalúan en el medio ambiente E2 = {y/1, z/3}, resultando que el término a ser
calculado es el siguiente:
(f 3 4)
Que al reescribirse con base en su definición da lugar al término:
(+ x y z)
Que se evalúa en un medio ambiente definido al aumentar el medio ambiente de la definición con la
asignación de argumentos formales a los valores calculados para los operandos E3 = E1+ {y/3,z/4} =
{x/4, y/3, z/4}, dando como resultado el siguiente:
;Value: 11
1 ]=> (define x 4)
...
1 ]=> (define y 2)
...
1 ]=> (define (f y z) (let ( (x (+ x y)) (y z) ) (+ x y)))
Y las evocaciones siguientes:
1 ]=> (set! y 1)
...
1 ]=> (define z 3)
...
1 ]=> (f (+ 2 y ) (+ z y)))
127 La diferencia entre let, let* y letrec estriba en que el medio ambiente de evaluación de <valor> para una <variable>
determinada se aumenta con las variables definidas antes que ella en el caso del let* y con todas las variables definidas en
el caso de letrec.
Capítulo 8: Operadores Definidos
213
A ser evaluado en el medio ambiente E3 = {x/4, y/3, z/4}.
La extensión del medio ambiente inducida por el let da finalmente lugar a la evaluación del término
siguiente:
(+ x y)
Que ahora debe ser evaluado en el medio ambiente E4 = E3 + {x/7, y/4} = {x/7, y/4, z/4}
;Value: 11
Una vez finalizado el cálculo de la evocación del operador, el medio ambiente se restituye
al existente antes de la evocación. En efecto, el medio ambiente resultante al extender el
medio ambiente de la definición con la asociación de los argumentos formales a los valores
de los operandos es local al término al que se reescribe el operador. Es posible, sin
embargo, usar la operación de asignación dentro del cuerpo del operador para redefinir el
medio ambiente de la definición.
1 ]=> (define x 4)
...
1 ]=> (define y 2)
...
1 ]=> (define (f y z) (+ x y z))
1 ]=> (set! y 1)
...
1 ]=> (define z 3)
...
1 ]=> (set_x 8)
1 ]=> (f (+ 2 y ) (+ z y)))
;Value: 15
Más de una instrucción define para un operador da lugar a una redefinición del operador:
128Si el lector se pregunta si existe una forma especial para lleva a cabo procesos repetitivos, la repuesta es SI. La forma
especial do puede verse en [Hanson 2002, sec. 2.9].
129La única restricción sobre los define es que deben ocurrir al principio del cuerpo donde aparecen [Hanson 2002, sec.
2.4]
Capítulo 8: Operadores Definidos
215
;Value: 144
Estrategia de Evaluación.
Dado un término complejo a ser calculado por el intérprete de un lenguaje funcional, cabe
siempre la pregunta siguiente: ¿en que orden se van a aplicar los axiomas asociados con los
(varios) operadores del término durante el proceso de reescritura?. Nótese que este orden
130MAUDE permite usar un tipo de condición de igualdad (que se denomina “matching equations” [Clavel 2007, Sec
4.3]) en las condiciones de una ecuación condicional, que permite usar términos con variables que no aparecen al lado
izquierdo de la ecuación condicional. En este caso la valoración de estas variables proviene del proceso de selección del
axioma y del emparejamiento, sin que influyan de forma alguna de los cálculos efectuados con anterioridad.
131 Es posible, sin embargo, usar operadores de nivel meta que manipulen los elementos de un programa como valores
(strings) que luego son ejecutados. Con esto se pueden escribir programas que evoluciona con su uso.
Capítulo 8: Operadores Definidos
216
es esencialmente arbitrario ya que el proceso de reescritura puede, en principio, aplicarse a
cualquiera de los subtérminos del término a ser calculado132.
Desde el punto de vista del árbol sintáctico, esto significa que el mecanismo de derivación
debe reescribir los operadores del término complejo en una secuencia específica,
decidiendo, por ejemplo, si reescribe primero los operadores de la parte inferior del árbol, si
reescribe primero los operadores de la parte superior del árbol, si los reescribe en algún
orden diferente predeterminado, o si reescribe varios operadores en forma paralela (o
simultánea).
Para definir este orden, cada implementación usa una “estrategia de evaluación”. Son
estrategias típicas, la de calcular primero los operandos de los operadores antes de abordar
el cálculo de los mismos, que ha sido denominada “evaluación en orden aplicativo”133, y la
de abordar primero el cálculo de los operadores antes de abordar el cálculo de los
operandos, que ha sido denominada “evaluación en orden normal”134 [Abelson 1985 sec.
1.1.3].
Dada la definición de los operadores sumcua y cua de los ejemplos anteriores, en el cálculo del
término siguiente:
sumcua(2+1, 8/2)
Se puede proceder en orden normal, en cuyo caso la secuencia de reescrituras dará lugar a la derivación
siguiente:
cua(2+1)+cua(8/2)
(2+1) * (2+1) + (8/2) * (8/2)
3*3+4*4
9 + 16
25
O se puede proceder en orden aplicativo, en cuyo caso la secuencia de reescrituras dará lugar a la
derivación siguiente:
sumcua(3, 4)
cua(3) + cua(4)
3*3+4*4
9 + 16
25
Formas de evaluación que para el ejemplo difieren sólo en el número de veces que deben efectuarse las
operaciones involucradas en los operandos de la aplicación del operador sumcua.
132 Si el SRT es convergente, para el resultado final del cálculo el orden de aplicación de los axiomas es, además,
irrelevante.
133 O “estricto”
134 O “perezosa”
Capítulo 8: Operadores Definidos
217
Si bien del ejemplo anterior parece desprenderse que el orden de evaluación aplicativo es
más eficiente que el orden de evaluación normal, este no es siempre el caso. En particular
frente a un operador de selección ocurre precisamente lo contrario.
> fun(4,2)
Al proceder en orden normal, la secuencia de reescrituras dará lugar a la derivación siguiente:
fun(4,2)
if (4 > 2) then (4 / 2 + 4) else (2 / 4 + 2) fi
if (true) then (4 / 2 + 4) else (2 / 4 + 2) fi
(4 / 2 + 4)
6
Al proceder en orden aplicativo, la secuencia de reescrituras dará lugar a la derivación siguiente:
fun(4,2)
if (4 > 2) then (4 / 2 + 4) else (2 / 4 + 2) fi
if (true) then (6) else (2.5) fi
6
Donde el orden aplicativo indujo a que se llevara a cabo el cálculo del término que de todas formas sería
desechado al evaluarse el operador de selección.
El uso del orden normal, puede ser además necesario para garantizar la ejecución correcta
del programa y su capacidad para terminar la ejecución.
eq fun(4,0) .
Al proceder en orden aplicativo, la ejecución termina de forma anormal, al llevar a cabo la división por
cero, mientras que si se procede en orden normal la ejecución terminaría normalmente.
Considere la ejecución del programa definido en el Ejemplo 32 que usa una definición recursiva para el
operador sum:
¿Que resultado se obtendría de llevarse a cabo la derivación en orden aplicativo?.
Capítulo 8: Operadores Definidos
218
La manera como se determina el orden de aplicación de los axiomas en el proceso de
reescritura de términos complejos basados en operadores nativos o definidos es, en
consecuencia, un asunto importante en el lenguaje. En lo que sigue examinaremos la
estrategia de evaluación utilizada en cada uno de los lenguajes estudiados e indicaremos el
grado en que el usuario puede influir en ella.
8.7.1 Estrategia de Evaluación en SCHEME.
En general, las expresiones complejas en SCHEME se evalúan en dos pasos, para cada
aplicación de un operador, primero se evalúan todos los operandos (o subexpresiones), y
luego se evalúa el operador con los valores resultantes de la evaluación previa de los
operandos [Abelson 85 sec 1.1.3]. La evaluación de los operandos de un operador se lleva
a cabo en un orden no predeterminado por el lenguaje.
En otras palabras en SCHEME se usa un orden de evaluación aplicativo, para todos los
operadores nativos y definidos.
Para evitar que esto ocurra en el caso de los operadores de selección, se opta por
considerarlos como “formas especiales”, en lugar de ser considerados como
procedimientos, negándoles, así, el carácter operador en propiedad. Las formas especiales
son nativas al lenguaje y no pueden ser propuestas por el programador. Para cada forma
especial el lenguaje determina, entonces, el orden en que se procede al llevar a cabo la
derivación.
8.7.2 Estrategia de evaluación en MAUDE
Tal como se refiere en [Clavel 2007, sec 4.4.7], MAUDE le permite al programador
determinar la estrategia de evaluación, de forma particular, para cada uno de los operadores
que propone.
La estrategia de evaluación se indica utilizando el siguiente identificador de propiedad
dentro de la sección de <atributos_del_operador> en la especificación del perfil del operador
(ver 8.5.2):
strat( <orden_de_evaluación > 0 )
Donde:
<orden_de_evaluación > es una lista de enteros, separados por espacio,
cada uno señalando por su valor a uno de los operandos del operador (el que
tiene como posición dicho valor). Los operandos del operador se reescriben
en el orden en que son señalados en la lista. La lista no requiere señalar a
todos los operandos y puede incluso ser vacía (en cuyo caso no se escribe).
0 señala al operador propiamente dicho e indica el orden en que se debe
reescribir en relación con la reescritura de sus operandos. En efecto, los
operandos no señalados en <orden_de_evaluación > se evalúan luego de la
evaluación del operador.
Un ejemplo sencillo tomado de [Clavel 2007, sec 4.4.7] ilustra el uso de la estrategia de evaluación en
un operador de selección para evitar el cálculo de la parte de la evocación no seleccionada.
Capítulo 8: Operadores Definidos
219
fmod EXT-BOOL is
protecting BOOL .
op _and-then_ : Bool Bool -> Bool [strat (1 0)] .
op _or-else_ : Bool Bool -> Bool [strat (1 0)] .
var B : [Bool] .
eq true and-then B = B .
eq false and-then B = false .
eq true or-else B = true .
eq false or-else B = B .
endfm
En ese código se define una versión de los operadores lógicos que tienen la ventaja de evaluar el
segundo operando sólo si se necesita. Esto se deber a que la estrategia de evaluación indica evaluar
primero el primer operando, y luego el operador. Al evaluar el operador el axioma seleccionado puede
definir el resultado final sin necesidad de evaluar el segundo operando, o depender del valor del segundo
operando, en cuyo caso obliga a su evaluación.
El operador t1=t2 verifica si t1 y t2 unifican. Tiene por tanto un sentido sintáctico y produce como
respuesta los valores que deben sustituir a las variables no ligadas de t1 y t2 que permiten la unificación.
3 ?- 4=2+2 .
false.
4 ?- X=2+2 .
X = 2+2.
2 ?- 2+2 = X .
X = 2+2.
3 ?- 2+X = 2+2 .
X=2
1 ?- 2+4/X = Y+4/7 .
X = 7,
Y = 2.
2 ?- 2+4/X = Z+4/Y .
X = Y,
Z = 2.
En SWI PROLOG, las variables de igual nombre entre t1 y t2 se consideran ligadas al mismo
cuantificador, así135:
135Puede entenderse que el = es un predicado que posibilita la unificación y sus variables están en el alcance de los
mismos cuantificadores.
Capítulo 8: Operadores Definidos
220
1 ?- 2+4/X = X+4/Z .
X = 2,
Z = 2.
En consecuencia para hallar el mgu entre t1 y t2 el usuario debe asegurarse de que no tengan variables
en común.
El operador == tiene también un sentido meramente sintáctico, verificando si sus operandos se asocian
con el mismo término, pero no puede ser usado para instanciar una variable, así:
4 ?- 2+2 == 4.
false.
1 ?- X==2+2.
false.
6 ?- X=2+2, X=4.
false.
El operador =\= es la negación de == .
Frente a la consulta:
1 ?- is(5, +(3,2)).
El intérprete responde: true.
También es posible escribir la misma consulta con notación infija así:
2 ?- 5 is 3+2 .
true.
Mejor aun, se puede colocar una variable como primer argumento del predicado is, para que por
unificación el SWI PROLOG determine su valor136, por ejemplo con la consulta siguiente:
3 ?- X is 3+2 .
El intérprete responde X = 5.
Pero no puede colocarse una expresión como primer argumento
2 ?- 3+1 is 1+3 .
false.
136En lo que sigue consideraremos que la unificación para cálculos aritméticos se llevan a cabo con una serie de reglas
nativas que denominaremos de forma genérica *.
Capítulo 8: Operadores Definidos
221
7 ?- (3+2) is 5 .
false
El segundo argumento del predicado is debe estar completamente instanciado en el momento de
ejecutarse. Así, para las consultas siguientes:
4 ?- 3+2 is X .
ERROR: is/2: Arguments are not sufficiently instantiated
6 ?- 5 is X .
ERROR: is/2: Arguments are not sufficiently instantiated
El intérprete responde con un error de instanciación.
Cabe anotar, que los operadores lógicos de comparación aritmética, =:= fuerzan el cálculo de sus
operandos pero requieren que sus argumentos estén instanciados (ver [Blackburn 01: secc 5.4] ), así:
5 ?- 4 =:= 2+2 .
true.
8 ?- X =:= 2+2 .
ERROR: =:=/2: Arguments are not sufficiently instantiated
Un operador puede definir a otro operador y usarlo en la expresión que define su valor, así:
El operador definido en el ejemplo anterior puede usarse como un operador cualquiera. Así, en la
definición siguiente:
138 Los nombres en castellano corresponden a la traducción libre del autor de los nombres de los módulos en MAUDE
Capítulo 8: Operadores Definidos
223
archivos de texto139. Un archivo de texto puede contener uno o varios módulos completos.
Una vez iniciada una sesión del intérprete de MAUDE, se deben incluir los módulos desde
lo archivos usando los comandos in o load [Clavel 2007, cap. 18]. Se pueden incluir los
módulos de varios archivos usando de forma repetida dichos comandos. Es posible,
también, colocar comandos en archivos de texto que sean ejecutados al incluir el archivo
donde aparecen; con ello que es posible incluir múltiples archivos con un sólo comando de
inclusión. Además, si en el comando usado para ejecutar el intérprete se coloca como
argumento el nombre de un archivo de texto, su contenido es automáticamente incluido
luego de iniciado el programa.
Los módulos que definen los tipos nativos se incluyen automáticamente de primeros en el
intérprete140, por lo que están siempre disponibles para relacionarlos con los módulos del
programa que serán incluidos posteriormente.
En las subsecciones que siguen describiremos el contenido de los módulos funcionales y
los operadores que permiten establecer relaciones entre módulos.
8.8.2.1 Definición y contenido de módulos en MAUDE
Un módulo funcional se define así:
fmod <nombre> is <cuerpo_del_modulo> endfm
Donde:
<nombre> Es el identificador del módulo, usualmente colocado en letra
capital.
<cuerpo_del_modulo> Es un conjunto de operadores de relación entre
módulos (ver sección 5.3.1), seguido de una especificación en lógica
ecuacional multisort {(S,),X,E} (ver sección 5.3.1).
Los símbolos de sort, S son las declaraciones de los sort propuestos por el programador
(ver “Declaraciones de sort en MAUDE” (sec. 7.5.2.1)).
Los símbolos de operación son las declaraciones de los operadores propuestos por el
programador (ver “Declaraciones de operadores en MAUDE” (sec. 7.5.2.3)).
El conjunto de variables X son las declaraciones de variables del módulo (ver
“Declaraciones de variables en MAUDE” (sec. 7.5.2.2)).
El conjunto de aserciones E comprende en MAUDE tres tipos de aserción, así:
Las ecuaciones con las que se definen los operadores (ver “Declaraciones
de operadores en MAUDE” (sec. 7.3.4)).
Las relaciones de subconjunto entre los sort, que son tratadas en 8.5.2.1
Las ecuaciones de membresía condicional, que son tratadas en 10.2.5 y
11.4.4.
8.8.2.2 Relaciones entre módulos en MAUDE
139Es posible incluirlos desde la línea de comandos, aunque por las pocas facilidades del editor de la línea de comandos,
esto carece de sentido.
140 De un archivo denominado prelude.maude, que debe estar a disposición del interprete a través de la variable path del
sistema operativo...
Capítulo 8: Operadores Definidos
224
Las relaciones entre los módulos son establecidas por medio de construcciones que
soportan la definición de jerarquías de inclusión entre módulos, una algebra de módulos y
técnicas de programación parametrizada (ver [Clavel 2007, sec. 6]).
En las secciones siguientes se presentan los dos primeros tipos de construcciones. Una
descripción detallada de la programación parametrizada se encuentra en el Capítulo 11.
8.8.2.3 Inclusión de Módulos en MAUDE
En MAUDE es posible incluir un módulo en otro, mediante el uso de las construcciones
siguientes:
protecting <expresion_de_modulo> .
extending < expresion_de_modulo> .
including < expresion_de_modulo> .
Donde:
<expresion_de_modulo> es el identificador de un módulo o la evocación
de un operador entre módulos que de lugar a un módulo.
Cuando en un módulo se incluye otro modulo, los operadores y sorts, del módulo incluido
se ponen a disposición del módulo que lo incluye. Las variables por su parte siguen siendo
locales al módulo y no se ponen a disposición del módulo que incluye.
Las diferencias entre los tres modos de inclusión, están por fuera del alcance de este texto.
El lector interesado puede consultarlas en [Clavel 2007, sec. 6.1]. En términos resumidos
protecting significa que las declaraciones del módulo incluido, no pueden ser modificadas;
including, significa que se puede modificar el módulo incluido, y extending, está en el
medio de las dos anteriores.
8.8.2.4 Algebra de Módulos en MAUDE
La construcción de la forma siguiente permite construir un módulo a partir de dos ya
existentes:
<expresion_de_modulo> + <expresion_de_modulo>
El módulo construido incluye los elementos de los módulos representados por los
operandos.
La construcción de la forma siguiente permite cambiar los identificadores de un módulo:
<expresion_de_modulo> * ( <lista_de_cambios_de_nombre> )
Donde:
<lista_de_cambios_de_nombre> es una lista de <cambio_de_nombre>
separados por coma.
<cambio_de_nombre> es una construcción de una de las formas
siguientes:
sort <nombre> to <nombre>
op <nombre> to <nombre>
op <nombre> to <nombre> [ <atributos_de_sintaxis> ]
op <perfil_operador> to <plantilla>
op <perfil_operador> to <plantilla> [<atributos_de_sintaxi>]
Capítulo 8: Operadores Definidos
225
label <nombre> to <nombre>
Donde:
<nombre> son el nuevo y viejo identificador.
<perfil_operador> es el perfil del operador (ver sección “Declaración de
Operadores en MAUDE” sección 7.5.2.3) cuya <plantilla> se desea
modificar.
<atributos_de_sintaxis> pueden ser los atributos prec, gather y format.
Para una explicación más precisa del efecto de los cambios de nombre y en particular de la
necesidad de cambiar los atributos del operador el lector debe referirse a [Clavel 2007, sec.
6.2.2].
Ejercicios Propuestos.
1- Realizar los siguientes ejercicios de la sección 1.1. de [Abelson 85], que está disponible
en línea en la WWW (ver referencia)
1.1, 1,2, 1.3, 1.4,1.5
2- Escriba un procedimiento en SCHEME que lleve a cabo los cálculos siguientes:
- El área de un trapecio, dado el tamaño de las bases y la altura.
Capítulo 8: Operadores Definidos
228
- La longitud de una recta dada las coordenadas cartesianas de sus dos extremos.
- El área de un triangulo dadas las coordenadas cartesianas de sus tres lados.
3- Escriba un procedimiento en SCHEME que determine lo siguiente:
- Si la raíz de una ecuación cuadrática de coeficientes dados tiene, o no, componente
imaginaria.
- Si con tres lados de longitudes L1, L2 y L3 se puede o no formar un triangulo.
- Si un triangulo cuyos lados tienen longitudes L1, L2 y L3 es o no rectángulo.
- Si tres números dados A, B, C se hallan en orden ascendente.
Capítulo 9
Definición Recursiva de
Operadores
Capítulo 9: Definición Recursiva de Operadores.
230
Introducción.
Definir con base en lo definido es una práctica común tanto en el lenguaje común como en
la matemática. Este tipo de definición es denominada “recursión”141.
En este capítulo se discute la definición de operadores que de forma directa o indirecta usan
el operador definido en su definición. A este tipo de definición la denominaremos en lo
que sigue “definición recursiva de operadores” y a los operadores así definidos los
denominaremos “operadores recursivos”. La importancia de la definición recursiva de
operadores reside en que existe una familia muy grande de operadores que deben definirse
de forma recursiva, y que prácticamente todos los programas en un lenguaje funcional
requieren de este tipo de operadores.
Un asunto de gran importancia frente a la definición recursiva de operadores, es la manera
como se desenvuelve el proceso de cálculo en cuanto al tamaño del término que evoluciona
con la derivación. En efecto, dependiendo de la manera como se lleva a cabo la definición
del operador, el término puede mantener un tamaño fijo y reducido, o crecer de forma
descontrolada hasta agotar los recursos disponibles. La forma como se desenvuelve el
proceso de reescritura durante el cálculo es, en consecuencia, un factor importante al definir
operadores de forma recursiva y el programador debe conocerla para evitar programas que
desborden los recursos disponibles.
Por otro lado, los programas que usan operadores recursivos se aplican a problemas que
deben resolverse llevando a cabo cálculos que se repiten una y otra vez por un número de
veces que es, en general, determinado por los datos involucrados en el problema. En
consecuencia el tiempo que se demora la ejecución del programa, dependerá de los datos
involucrados en el problema. La forma que toma dicha dependencia es, en consecuencia,
un factor clave al momento de definir operadores recursivos y el programador debe
conocerla para evitar ejecuciones que se demoren más de lo previsto.
En los apartados siguientes se introduce la definición recursiva de operadores en el marco
de los lenguajes estudiados, para luego analizar detalladamente las consecuencias de las
posibles formas de definir la recursión. La presentación se lleva a cabo con base en
problemas específicos, para los que, primero, se construyen diferentes soluciones, y luego,
se estudia el comportamiento espacio-temporal del proceso de cálculo que ellas determinan.
La presentación va, principalmente, orientada a que el lector desarrolle las habilidades
necesarias para llevar a cabo definiciones recursivas de operadores que sean útiles, sin
entrar en estudios teóricos que están por fuera del alcance del trabajo.
n
n i
2
{1}
Lo primero que debemos hacer notar es que la fórmula de la sumatoria puede reescribirse
de forma recursiva, sugiriendo la ecuación siguiente:
j j
n2 i2
n i
n
n i 1
2
{2}
(define (sum_n2 i j)
(+ (* i i) (sum_n2 (+ i 1) j) ) )
Una posible especificación en MAUDE de la ecuación sería como sigue:
El cálculo de una evocación del operador así definido, sin embargo, conduciría a una
reescritura interminable.
El proceso de reescritura de una evocación del operador definido en el ejemplo anterior seria como la
que se ilustra a continuación:
;;( sum_n2 1 3)
;;(+ (* 1 1) (sum_n2 2 3))
;;(+ 1 (+ (* 2 2) (sum_n2 3 3)))
;;(+ 1 (+ 4 (+ (* 3 3) (sum_n2 4 3))))
;;…
;;…
Capítulo 9: Definición Recursiva de Operadores.
232
Para evitar esta circunstancia, basta con notar que:
i
n
n i
2
i 2 {3}
Con lo que se hace evidente que la substitución recursiva definida por la ecuación {2} debe
interrumpirse cuando el índice inferior de la sumatoria sea igual al superior, caso en el que
la reescritura debe usar la substitución sugerida en la ecuación {3}.
Este cambio en la ecuación a ser utilizada puede expresarse fácilmente en los lenguajes
funcionales, usando alguna forma de selección.
La selección en SCHEME debe efectuarse en el marco del único axioma de definición usando un
selector.
(define (Sm-n2 I J)
(if (= I J) (* I I)
(+ (* I I) (Sm-n2 (+ I 1) J) )
)
)
(sum_n2 1 6)
;;(+ (* 1 1) (sum_n2 2 6)
;;(+ 1 (+ (* 2 2) (sum_n2 3 6)))
;;(+ 1 (+ 4 (+ (* 3 3) (sum_n2 4 6))))
;;(+ 1 (+ 4 (+ 9 (+ (* 4 4) (sum_n2 5 6)))))
;;(+ 1 (+ 4 (+ 9 (+ 16 (+ (* 5 5) (sum_n2 6 6))))))
;;(+ 1 (+ 4 (+ 9 (+ 16 (+ 25 (* 6 6))))))
;;(+ 1 (+ 4 (+ 9 (+ 16 (+ 25 36)))))
;;(+ 1 (+ 4 (+ 9 (+ 16 61))))
;;(+ 1 (+ 4 (+ 9 77)))
;;(+ 1 (+ 4 86))
;;(+ 1 90)
;; 91
En MAUDE, por otro lado, basta con incluir el nuevo axioma, limitando el uso del anterior al caso que
corresponde, así:
El proceso de reescritura de una evocación del operador en MAUDE, es muy similar al del operador en
SCHEME por lo que dejamos al lector la tarea de analizarlo.
Capítulo 9: Definición Recursiva de Operadores.
233
La versión recursiva en SWI PROLOG del predicado que suma los cuadrados de los números situados
entre dos enteros I y J (I>=J), es la siguiente:
sm_n2_r(I,I,R):- R is I*I .
sm_n2_r(I,J,R):- J>I, I1 is I+1, sm_n2_r(I1,J,R1), R is R1+I*I .
El comportamiento en memoria del proceso de resolución SLD determinado por la versión recursiva del
predicado del ejemplo anterior, puede ilustrarse por medio de una tabla que muestra la evolución del
objetivo para una evocación específica del predicado, así:
Donde las filas de la tabla ilustran el resultado del proceso para uno o varios pasos de la resolución, en la
primera columna de la tabla se muestran los pasos que conducen al resultado de la fila, en la segunda
columna se indica el número de orden en el programa PROLOG de la regla a ser usada en la(s)
resolución(es), en la tercera columna se muestra la substitución de variables necesaria para la(s)
unificación(es) --de la primera meta y la cabeza de la regla--, y en la cuarta columna el objetivo
resultante.
El lector debe notar, además, que antes de llevar a cabo la resolución se efectuaron cambios de nombre a
las variables (agregándoles ’), para evitar las colisiones.
Nótese que la ejecución de este procedimiento daría lugar a una substitución infinita de la
evocación de la función por otra evocación en la que el primero de los argumentos tendría
un valor cada vez más cercano al valor buscado, así:
El proceso de reescritura de una evocación del operador hallar-raíz-cuadrada sería como la que se
ilustra a continuación:
(hallar-raíz-cuadrada 1.0 3)
;;(hallar-raíz-cuadrada 2.0 3)
;;(hallar-raíz-cuadrada 1.75 3)
Capítulo 9: Definición Recursiva de Operadores.
235
;;(hallar-raíz-cuadrada 1.7321 3)
….
….
Para que el proceso finalice y de como resultado una buena aproximación de la raíz, es
suficiente con colocar una pregunta de parada que evite la substitución recursiva en el
momento en que se haya alcanzado la aproximación, y entregue como resultado el valor del
argumento que la contiene.
Así, si consideramos que una buena aproximación de x es un valor que elevado al
cuadrado difiere en menos de dos cifras decimales del valor de x, podemos completar el
procedimiento anterior para obtener el procedimiento buscado, de la manera siguiente:
.....
El resultado de una evocación del operador raíz-cuadrada definido en el ejemplo anterior sería como el
que se ilustra a continuación:
(hallar-raíz-cuadrada 1.0 3)
;;(hallar-raíz-cuadrada 2.0 3)
;;(hallar-raíz-cuadrada 1.75 3)
;;(hallar-raíz-cuadrada 1.7321 3)
;;1.7321….
Capítulo 9: Definición Recursiva de Operadores.
236
La forma del Proceso de Cálculo (uso de memoria).
La importancia de la manera como se desenvuelve el proceso de cálculo para un operador
definido, puede intuirse comparando los procesos de reescritura correspondientes al cálculo
de los operadores definidos en la sección anterior (Ejemplo 32 y Ejemplo 35).
En el caso del operador que calcula la suma de los cuadrados comprendidos entre dos
enteros dados (Ejemplo 32), es fácil observar que, durante la substitución, el término de la
derivación crece hasta alcanzar un tamaño máximo y luego comienza a reducirse para
alcanzar la respuesta deseada. Este crecimiento se debe a que la operación suma que se
introduce en cada paso de reescritura, no puede llevarse a cabo inmediatamente debido a
que no se dispone del valor del segundo sumando. Este sumando, en efecto, se calcula
primero para la última operación suma introducida, que, al ser efectuada, obtiene el
segundo sumando de la penúltima suma introducida, y así sucesivamente. Nótese también
que el tamaño máximo alcanzado por el término durante el proceso depende del valor de
los argumentos, ya que el proceso de substitución debe introducir j-i veces la operación
suma antes de que se pueda calcular el último sumando.
En el caso del operador que obtiene la raíz cuadrada de un número dado, (Ejemplo 35), es
fácil observar que, durante la substitución el término de la derivación no crece como el del
caso anterior, sino que se mantiene estable manteniendo el mismo número de operadores y
operandos, hasta que se obtiene la respuesta deseada. Esta estabilidad se debe a que el
efecto neto de cada paso de substitución es el de cambiar el valor del primer argumento de
la evocación de la función, haciéndolo evolucionar hasta el valor deseado.
A los procesos que presentan inestabilidad en la memoria se le ha denominado “proceso
recursivo” mientras que A los procesos que presenta estabilidad se les ha denominado
“proceso iterativo” [Abelson 85 1.2.1]. El lector debe notar que esta definición refiere a la
forma como se comporta el proceso en la memoria y no a la forma como se define el
operador (que arriba denominamos definición recursiva)142.
Un proceso iterativo es, sin duda, más deseable que un proceso recursivo ya que no se corre
el riesgo de, que para ciertos valores de los argumentos, el cálculo se haga imposible por
agotamiento de la memoria. Cabe entonces preguntarnos si: ¿es posible modificar la
definición recursiva de un operador que genera procesos recursivo para que genere
procesos iterativos?; y si esto es posible, si: ¿es posible en todos los casos?.
La respuesta a la primera de las preguntas anteriores es SI, y lo probaremos modificando la
definición del operador encargado de obtener la suma de los cuadrados, para que los
procesos que determina sean iterativos.
La respuesta a la segunda pregunta es NO, y, aunque no lo probaremos en este trabajo, en
los ejemplos, mostraremos problemas cuya solución estará siempre basada en operadores
que determinan procesos recursivos. En este caso culparemos al problema, en si mismo, y
142 En un lenguaje procedural toda especificación recursiva determina un proceso recursivo debido al apilamiento del
“stack” que ocurre en cada evocación. Para poder definir procesos iterativos, el programador debe usar las construcciones
de repetición de grupos de comandos (for, do, while, etc..). En un capítulo posterior interpretaremos estas construcciones
en el marco de un lenguaje funcional, probando que no adicionan semántica alguna al lenguaje de programación.
Capítulo 9: Definición Recursiva de Operadores.
237
lo consideraremos de un grado de complejidad más alto que el que tiene soluciones a través
de procesos iterativos.
Para concebir la solución iterativa podemos apoyarnos, inicialmente, en la naturaleza del
problema. En efecto si desarrollamos un poco la fórmula {2}, que fue base para concebir el
procedimiento:
j j
sum n i ((i 1) ((i 2) ((i 3) n 2 ))) {5}
2 2 2 2 2
n i n i 4
Es fácil ver que tomando ventaja de la propiedad asociativa del operador suma, podríamos
plantear otra forma distinta de desarrollar la fórmula durante el cálculo., así:
j j
sum n 2 (((i 2 (i 1) 2 ) (i 2) 2 ) (i 3) 2 ) n 2 {6}
n i n i 4
La ventaja de esta forma de desarrollar la fórmula es que nos permite intuir que las sumas,
introducidas de izquierda a derecha en el proceso deben reducirse a un valor sin esperar a
que se introduzca el último sumando, dejando, en cada iteración, un valor ya totalizado y
una sumatoria mas pequeña por calcular.
Para separar este valor totalizado de la parte por totalizar, debemos usar un “lugar de
memoria” que lo almacene y que en cada iteración lo aumente adicionándole un término
más de la sumatoria143.
Este lugar de memoria no es otra cosa que un operando del operador.
El procedimiento SCHEME que se muestra a continuación reserva un operando para “acumular” los
términos de la sumatoria.
(suma_it 0 1 6)
;;(suma_it (+ 0 (* 1 1)) (+ 1 1 ) 6) → ;;(suma_it 1 2 6)
;;(suma_it (+ 1 (* 2 2)) (+ 2 1 ) 6) → ;;(suma_it 5 3 6)
;;(suma_it (+ 5 (* 3 3)) (+ 3 1 ) 6) → ;;(suma_it 14 4 6)
;;(suma_it (+ 14 (* 4 4)) (+ 4 1 ) 6) → ;;(suma_it 30 5 6)
;;(suma_it (+ 30 (* 5 5)) (+ 5 1 ) 6) → ;;(suma_it 55 6 6)
;;(suma_it (+ 55 (* 6 6)) (+ 6 1 ) 6) → ;;(suma_it 91 7 6)
;;(suma_it (+ 91 (* 7 7)) (+ 7 1 ) 6) → ;;(suma_it 140 8 6)
..
..
143Esto es precisamente lo que se hace al programar este operador en un lenguaje procedural. La diferencia es que para
contar con el “lugar de memoria” se debe declarar una variable, para usarlo se debe contar con instrucciones “tome de” el
lugar de la memoria el valor, “coloque en” dicho lugar el resultado; y para repetir el proceso una y otra vez se deben tener
construcciones que de forma específica indiquen la repetición. Así, en suma, el lenguaje procedural requiere de un mayor
número de construcciones sintácticas que el funcional.
Capítulo 9: Definición Recursiva de Operadores.
238
Donde, por claridad, se resaltó el hecho de que antes de cada paso de reescritura la estrategia de
evaluación propia del SCHEME obliga a totalizar el acumulador.
En esta última definición el término de la derivación es, en efecto, de tamaño estable como
deseábamos, y, al igual que en el caso de la raíz cuadrada, el valor del resultado se va
obteniendo de forma paulatina en el primer argumento.
Al igual que en el caso de la raíz cuadrada, para que el proceso entregue el resultado desde
el argumento que lo contiene, es necesario completar la definición con una condición que
suspenda la evocación recursiva luego del paso en que se alcanza el resultado. Además,
para garantizar que la evocación al procedimiento se lleva a cabo con el valor inicial
correcto para el acumulador, la evocación debe efectuarse desde otro procedimiento cuya
única función es encargarse de “inicializar” el acumulador.
La versión iterativa del operador de sumatoria en SCHEME es, en consecuencia, como sigue:
fmod C8-SUMN2I is
protecting INT .
op Sn2I_ _ : Int Int -> Int .
op sum-i : Int Int Int -> Int .
vars I J T : Int .
eq Sn2I I J = sum-i(0,I,J) .
ceq sum-i(T,I,J) = sum-i((T + I * I),(I + 1),J) if(I <= J) .
eq sum-i(T,I,I) = T .
endfm
La versión iterativa del predicado PROLOG para el cálculo de la sumatoria es la siguiente:
sm_n2_i(I,J,R):- sm_n2_i(I,J,0,R) .
sm_n2_i(I,I,S,R):- R is S+I*I .
sm_n2_i(I,J,S,R):- J>I, I1 is I+1, S1 is S+I*I, sm_n2_i(I1,J,S1,R) .
n i n 1
Capítulo 9: Definición Recursiva de Operadores.
239
Con lo que es fácil concebir la definición del ejemplo siguiente:
La versión iterativa del operador de sumatoria en MAUDE, que acumula los términos de la sumatoria de
atrás hacia adelante es como sigue:
fmod C8-SUMN2I-BK is
protecting INT .
op Sn2IB_ _ : Int Int -> Int .
op sum-ib : Int Int Int -> Int .
vars I J T : Int .
eq Sn2IB I J = sum-ib(0,I,J) .
ceq sum-ib(T,I,J) = sum-ib((T + J * J),I,(J - 1)) if(I <= J) .
eq sum-ib(T,I,I) = T .
endfm
Que converge a π/8 y cuyas sumas parciales aproximan tal resultado. Y el cálculo de lo que
se conoce como una fracción continua infinita.
1
{9}
2
1
2
3
22
3
2
Estos dos ejemplos difieren básicamente en el operador de más alto nivel que en el primer
caso es la suma y en el segundo la división. Es importante notar desde ahora que el
primero es un operador asociativo mientras que el segundo no lo es.
Nos concentraremos en obtener un operador que calcule una aproximación del valor de la
serie para un número dado de términos, truncando la fracción infinita. Así, queremos
definir dos operadores que llamaremos sum_pi(n) y cont_frac(n), que reciben un entero n
como argumento y devuelven el valor dado respectivamente por las expresiones:
n
1 1 1 1 1
Sn {10}
k 1 ( 4k 3)(4k 1) 1 3 5 7 9 11 (4n 3) (4n 1)
Y
1
Fn {11}
2
1
2
3
22
n
n2
9.3.1.1 Acumulador recursivo
Ya en este punto es evidente que existe una similitud entre los dos casos ( Sn y Fn). En
efecto, para cualquier valor de n>1, el valor de la función se construye llevando a cabo
algún tipo de “acumulación” (con el operador “+” o con el operador “/”) de los términos
genéricos de una serie, diferentes en cada caso, cuyo valor depende de su posición.
Así, un posible acercamiento para construir un operador que genere un proceso recursivo,
es identificar en la fórmula, una serie de subfórmulas auxiliares S*(k, n) y F*(k, n) que se
relacionan entre sí por ocurrencias del término genérico.
Estas subfórmulas, que se entienden como “acumular los términos desde k hasta n”, se
muestran gráficamente en las figuras 2.2 y 2.3.
Capítulo 9: Definición Recursiva de Operadores.
241
1 1 1 1 1
Sn 0
1 3 5 7 9 11 ( 4( n 1) 3) ( 4( n 1) 1) ( 4n 3) ( 4n 1)
S*(n+1, n)
S*(n, n)
S*(3, n)
S*(n-1, n)
1
Fn
2
12
3
22
n2
...
n 1
(n 2) 2
n
(n 1) 2
n 0
2
F*(n+1,n)
F*(n,n)
F*(2,n)
F*(n-1,n)
F*(n-2,n)
S n S * (1, n) Fn F * (1, n)
1 k
S * ( k , n) S * (k 1, n) F * ( k , n)
(4k 3)(4k 1) k F * (k 1, n)
2
S * (n 1, n) 0 F * (n 1, n) 0
Descubrir las funciones auxiliares y la manera como se relacionan equivale a obtener la
versión recursiva del operador, ya que para definirlo basta expresar dichas funciones y
relaciones en el lenguaje de programación.
Capítulo 9: Definición Recursiva de Operadores.
242
La versión recursiva de los operadores sum_pi(n) y cont_frac(n), en SCHEME expresan directamente
las relaciones mostradas arriba, así:
(define (sum-pi-rec K N)
(if (> K N) 0.
(+ (/ 1. (* (- (* 4. k) 3.) (- (* 4. k) 1.)))
(sum-pi-rec (+ K 1) N)
)
)
)
(define (cont-frac-rec K N)
(if (> K N) 0.
(/ K (+ (* K K ) (cont-frac-rec (+ K 1) N) ))
)
)
Donde sum_pi_rec y cont_frac_rec expresan las funciones S*(k, n) y F*(k, n), respectivamente.
Y Finalmente:
Que se desprenden de la primera afirmación, y dejan definida Sn y Fn que eran lo que realmente
queríamos definir.
La versión recursiva de los operadores sum_pi(n) y cont_frac(n), en MAUDE expresa las mismas
relaciones en una sintaxis similar a las de las fórmulas matemáticas, así:
fmod C8-PI-FRAC is
protecting FLOAT .
vars I J T : Float .
sum_pi8_rec(K,N,0.0):- K>N .
sum_pi8_rec(K,N,R):- K1 is K+1, sum_pi8_rec(K1,N,R1), R is R1+1.0/((4.0*K-3.0)*(4.0*K-1.0)) .
sum_pi_rec(N,R):- sum_pi8_rec(1,N,R8), R is R8*8.0 .
La ejecución para 300 términos, da como resultado los valores siguientes como aproximación a π:
3 ?- sum_pi_rec(300,X).
X = 3.1399259880805297 .
Para el cálculo de la fracción:
frc_r(N,R):- frc_r(1,N,R) .
frc_r(I,N,0.0):- I>N .
frc_r(I,N,R):- I1 is I+1, frc_r(I1,N,R1), R is I/((I*I)+R1) .
La ejecución para 5 niveles, da como resultado los valores siguientes:
1 ?- frc_r(5,X).
X = 0.683766096685666 .
La versión iterativa del operador sum_pi(n) en MAUDE tiene la misma estructura que la de la suma de
los cuadrados de Ejemplo 238 , sólo que el término a ser acumulado es diferente, así:
fmod C8-PI-FRAC is
protecting FLOAT .
vars I J T : Float .
Capítulo 9: Definición Recursiva de Operadores.
244
op Spi2 _ : Float -> Float .
op Spi-i _ _ _ : Float Float Float -> Float .
eq Spi2 J = 8. * (Spi-i 1. J 0.) .
ceq Spi-i I J T = (Spi-i (I + 1.) J
(T + (1. / ((4. * I - 1.)*(4. * I - 3.))))) if(I < J) .
eq Spi-i I I T = T + 1. / ((4. * I - 1.)*(4. * I - 3.)) .
endfm
La versión iterativa del predicado en SWI PROLOG, correspondiente al cálculo de la sumatoria es la
que se muestra a continuación:
sum_pi8_ite(K,N,R):- sum_pi8_ite(K,N,0.0,R) .
sum_pi8_ite(K,N,S,S):- K>N .
sum_pi8_ite(K,N,S,R):- K1 is K+1, S1 is S+1.0/((4.0*K-3.0)*(4.0*K-1.0)), sum_pi8_ite(K1,N,S1,R) .
sum_pi_ite(N,R):- sum_pi8_ite(1,N,R8), R is R8*8.0 .
Donde el lector debe notar que la variable que se asocia con la respuesta, R, sólo se toma del valor
acumulado al momento llevarse a cabo la unificación con la segunda regla del programa.
4 ?- sum_pi_ite(300,X).
X = 3.1399259880805284
2
1
, pues haría falta la estructura completa para colocar el nuevo valor en el lugar
2
1
2
3
22
32
apropiado de la expresión.
Esta posibilidad, sin embargo, aparece claramente si llevamos a cabo el proceso de
acumulación de abajo hacia arriba (en lugar de atrás hacia delante como en el caso de la
sumatoria de los cuadrados).
Así, como se muestra en la figura 2.1, empezaremos por encontrar el último cociente,
correspondiente a n y luego con ese cociente hallaremos el penúltimo cociente,
correspondiente a n-1, haciendo la suma y división necesaria. El proceso continuará hasta
que encontremos el valor de la primera fracción.
Capítulo 9: Definición Recursiva de Operadores.
245
1
Fn
2
12
3
22
n2
...
n 1
( n 2) 2
n
(n 1) 2
n2
Figura 2.1. El proceso de construcción de cont-frac iterativo.
La versión iterativa del operador cont_frac(n) en MAUDE tiene la misma estructura que el de sum-
pi(n), con la diferencia de que ahora es necesario acumular de abajo hacia arriba (o de atrás para
adelante), así:
fmod C8-PI-FRAC is
protecting FLOAT .
vars I J T : Float .
endfm
Donde la dirección del cálculo simplificó el número de argumentos, ya que para detener el cálculo
bastaba que el índice del proceso llegara a cero.
La versión iterativa en SWI PROLOG, del predicado correspondientes al calculo de la fracción continua
se muestran a continuación:
frc_i(I,R):- frc_i(I,0.0,R) .
frc_i(0,A,A) .
frc_i(I,A,R):- I1 is I-1, A1 is I/((I*I)+A), frc_i(I1,A1,R) .
Donde el lector debe notar que la acumulación se lleva a cabo de abajo hacia arriba en la fórmula, por lo
que sólo se requiere un contador; y que la variable que se asocia con la respuesta, R, sólo se toma del
valor acumulado A al momento llevarse a cabo la unificación con la segunda regla del programa.
2 ?- frc_i(5,X).
X = 0.683766096685666 .
La versión iterativa del operador coseno(x,n) en MAUDE con la misma estructura que la de la suma de
los cuadrados del ejemplo 238, es el siguiente:
fmod C8-COSENO is
protecting FLOAT .
protecting INT .
protecting CONVERSION .
vars I J K N : Int .
vars T X : Float .
…..
op coseno1 : Float Int -> Float .
op Cos-i : Float Int Int Float -> Float .
Capítulo 9: Definición Recursiva de Operadores.
247
eq coseno1(X,N) = Cos-i(X,N,1,1.) .
ceq Cos-i(X,N,K,T) = Cos-i(X,N,(K + 1),
(T + ((- 1.) ^ float(K)) *
((X ^ float(2 * K)) / ((2 * K) !)))
) if(K < N) .
eq Cos-i(X,N,N,T) = T .
endfm
.....
Suponiendo que existen procedimientos predefinidos _! (que calcula el factorial) y _^_ (que eleva un
número a una potencia).
Comparando el cálculo del coseno usando nuestra función, con el cálculo usando la función nativa del
lenguaje, así:
144 Posiblemente debido que el cálculo en el procesador aritmético no se lleva a cabo por series de taylor.
Capítulo 9: Definición Recursiva de Operadores.
248
demasiado agudo, para notar que el resultado de estos cálculos tiende a producir valores
demasiado grandes para la capacidad de almacenamiento de los valores en la mayoría de las
implementaciones. Las limitaciones en la capacidad de almacenamiento de valores se
traducen en algunos casos en el almacenamiento de valores equivocados y en otros en una
pérdida de precisión145.
Así a, pesar de que el valor de X^(2 * K) / (2 * k)!) es pequeño, en general, no se calcula
correctamente debido a que el cálculo de sus dos componentes ya presenta errores de
precisión. Las expresiones matemáticas, pese a ser claras y concisas, no son
necesariamente la mejor manera, ni siquiera la más exacta, de calcular.
Para eliminar los problemas de precisión en el cálculo del término optaremos por mantener
los valores intermedios del proceso de cálculo lo mas pequeños posibles. Para ello nos
apoyaremos en el hecho de que, por un lado, los términos de la secuencia {16}, van
disminuyendo su valor hasta un punto en que se hace despreciable, y que, por otro, es fácil
ver que cada término puede encontrarse a partir del anterior, mediante un producto
adecuado.
Por ahora, tratemos de expresar, en términos matemáticos, este hecho. Primero separemos
el primer término de la suma que llamaremos a0:
n
(1) k x 2 k n
(1) k x 2 k n
cos(x) S n 1 1 ak {17}
k 0 (2k )! k 1 (2k )! k 1
145 En las implementaciones de nuestros lenguajes, los valores se almacenan usando un espacio fijo y predeterminado en
la memoria. Existen, sin embargo, lenguajes como Mathematica
(http://www.wolfram.com/products/mathematica/index.html) y Maxima (http://maxima.sourceforge.net/), que proveen
implementaciones que almacenan los valores en espacios de memoria variables, manteniendo un grado de precisión
predeterminado.
Capítulo 9: Definición Recursiva de Operadores.
249
El cálculo del término de orden n en la serie {19}, lo podemos llevara a cabo de forma precisa por
medio del operador definido en MAUDE siguiente:
La versión iterativa del operador coseno(x,n) en MAUDE, debe ahora usar el operador definido para
calcular el término de la sumatoria, así:
Con lo que el cálculo de coseno(x,n), es ahora siempre posible para valores altos de n, así146:
.....
eq cua(x) = x * x .
eq sumcua(x, y) = cua(x) + cua(y) .
.....
146El cálculo con la serie de taylor da diferencias significativas, con respecto al cálculo con el operador nativo del
lenguaje, para valores grandes de X (v.g. 30 radianes). El lector debe examinar si puede reducir estas diferencias
reduciendo el valor en radianes a uno menor que 2π (usando el residuo de dividirlo por 2π).
Capítulo 9: Definición Recursiva de Operadores.
250
La evocación del operador sumcua, con argumentos escalares dará lugar a un proceso de cálculo que
lleva a cabo 2 reescrituras, 2 multiplicaciones y 1 suma, independientemente de los valores de los
argumentos:
Esto se debe a que las definiciones de los operadores, se apoyaron siempre en términos
construidos con operadores diferentes, que, a su vez, fueron definidos con base en otros
operadores diferentes, sin que en la cadena de definiciones sucesivas apareciera de nuevo
alguno de los operadores definidos147.
Por otro lado, la evocación de los operadores recursivos genera procesos en los que el
número de reescrituras y cálculos elementales, depende no sólo del número operaciones
especificadas en su definición y en la definición de los operadores que participan en
cálculo, sino también del número de veces que el operador se evoca a si mismo durante el
proceso. Tal como puede observarse en los ejemplos presentados antes, este último número
depende, en general, del valor de los operandos involucrados en la evocación. En otras
palabras, el tiempo de ejecución de un operador recursivo puede depender del valor de sus
operandos.
Con unos pocos ejemplos sencillos (ver 9.6), es fácil probar que, para un mismo problema,
el tiempo de ejecución de diferentes implementaciones puede ser tan dramáticamente
diferentes, que algunas de ellas carezcan completamente de valor práctico.
En términos absolutos el tiempo de ejecución de un operador depende de múltiples factores,
entre ellos los siguientes:
La velocidad del computador donde se ejecuta.
El lenguaje de programación.
La implementación del lenguaje de programación.
Los valores de los argumentos (o sea el “caso” al que se aplica).
El “algoritmo” resultante de la definición del operador.
La forma en que el programador define el “algoritmo”.
Para reducir la complejidad de esta dependencia, y capturar por medio del concepto de
“algoritmo” el factor de mayor relevancia frente a la eficiencia, se ha planteado el
“principio de invarianza” [Brassard 88, sec. 1.3]. Según este principio, dos códigos
diferentes cualesquiera (es decir que difieran en el lenguaje, el computador, la
implementación del lenguaje y forma de definir el algoritmo), que implementen un mismo
operador, implementan también el mismo “algoritmo” si las funciones que relaciona el
tiempo de ejecución con los valores de los argumentos (que en adelante denominaremos
“funciones de rendimiento”), están mutuamente acotadas por una constante
multiplicativa148. Así, para las implementaciones A y B de un mismo algoritmo α se
cumple que existen constantes c1 y c2 que satisfacen los predicados siguientes:
147En general será importante establecer si la función que relaciona el tiempo de ejecución con el valor de los operandos
esta acotada superiormente por una la función de dicho valor de la forma C*1 donde C es un valor constante.
148El concepto de algoritmo es de hecho, bastante intangible, y con frecuencia interpretado erróneamente como la
implementación de una solución en un lenguaje procedural. A este respecto, debemos destacar que toda implementación
implementa un algoritmo particular, pero que diferentes implementaciones pueden implementar un mismo algoritmo a
pesar de sus diferencias (en lenguaje y en la disposición de sus elementos).
Capítulo 9: Definición Recursiva de Operadores.
251
TαA(M).<= c1*TαB(M)
TαB(M).<= c2*TαA(M)
Donde:
Tαk(M) es la función de rendimiento de la implementación k del algoritmo α.
M es el conjunto de elementos que describen el caso de ejecución.
En términos simples el principio de invarianza plantea que, el efecto de mejorar todos los
factores que afectan el rendimiento, con excepción del algoritmo y del caso, puede
resumirse en que, a lo sumo, las mejoras en el tiempo de ejecución estarán acotadas por una
constante multiplicativa
Otra simplificación útil es la de caracterizar el caso por unas pocas medidas de magnitud
(v.g. m y n siendo ambos enteros), y considerar, dentro de los casos de un tamaño dado,
sólo al de la peor disposición de sus elementos (o al de la disposición mas frecuente o al de
la disposición “promedia”). En consecuencia el esfuerzo de desarrollo de programas debe
enfocarse a obtener algoritmos en los que la función de rendimiento, para un cierto tipo de
disposición de los elementos, sea la “mejor posible”.
Una manera de definir cual es la “mejor” entre dos funciones de rendimiento, para el caso
de funciones con una sola variable independiente, es usar el concepto matemático del
“orden de una función”149.
El “orden” de una función f(n) denominado O(f(n)), es el conjunto de funciones t(n)
definido de la manera siguiente [Brassard 88, sec. 2.1]:
eq cua(x) = x * x .
eq sum_cua(x, y) = cua(x) + cua(y) .
Tienen una función de rendimiento en O(1), ya que su tiempo de ejecución es independiente del valor
de los datos (asumiendo que el producto lo sea).
El operador definido en el Ejemplo 39:
eq Σn2 I J = sum_ite(0, I, J) .
ceq sum_ite(Total, I, J) = sum_ite((Total + I * I), (I + 1), J) if(i<=j) .
ceq sum_ite(Total, I, J) = Total if(i>j) .
.....
Tienen una función de rendimiento en O(n), siendo n el número de sumandos, ya que la evocación
recursiva de sum_ite se lleva a cabo n+1 veces y el O(n+1) es el mismo O(n).
El operador del Ejemplo 36:
(define (raíz-cuadrada Y Y)
( If (< (abs (- (* Y Y) Y )) 0.001) Y
(raíz-cuadrada (/ (+ Y (/ x Y)) 2) x ) )
)
)
Tienen una función de rendimiento en O(n1/2), siendo n el número de cifras decimales de la precisión
deseada, ya que la taza de convergencia del método de newton-rapson es cuadrática ( ver
http://en.citizendium.org/wiki/Newton%27s_method )
En lo que sigue nos preocuparemos por valorar la eficiencia de los operadores que se
definan por medio del orden de sus funciones de rendimiento. En cada caso procuraremos
situar dichas funciones en el marco del orden de las funciones presentadas más arriba. Para
ello nos apoyaremos en planteamientos intuitivos sin entrar en mayores disquisiciones
teóricas. El objetivo de nuestra valoración es tanto el de alertar al lector sobre la
importancia del asunto, como el de dotarlo de una intuición básica que le permita evitar la
definición de algoritmos groseramente ineficientes. Para una mejor discusión del tema
remitimos al lector a [Brassard 88].
9.5.1.1 Segundo cambio al cálculo del coseno: mejora en la eficiencia.
Si bien resolvimos el grave problema de cálculo que tenía la solución inicial para el
operador que calculaba el coseno, todavía queda un problema de eficiencia.
En efecto, si analizamos el número de veces que se lleva a cabo la reescritura, para el caso
de la última versión iterativa, podemos ver que durante el proceso de acumulación de los
términos de la serie, esta se lleva a cabo N+2 veces, al igual que en el caso del cálculo de la
suma de los cuadrados.
Capítulo 9: Definición Recursiva de Operadores.
253
Sin embargo el cálculo propio de cada término de la serie requiere de K+2 reescrituras
siendo K el índice del término en la serie. Así, una aproximación al número total de
reescrituras, es el siguiente:
Nro_Reescrituras = 2+3+4+......N+(N+1)+(N+2) = (N+4)*(N+2)/2 {20}
Este número contrasta con el requerido para el cálculo de la suma de los cuadrados que era
básicamente N+2. En términos de la teoría del orden, se puede demostrar que ahora
nuestro algoritmo es del orden de N2, cuando los que teníamos antes eran del orden de N.
Para corregir este problema de eficiencia, modificaremos la última definición del operador
coseno(x,n), acoplando el cálculo de los términos de la serie, con el cálculo de la serie
misma. A esta estrategia la denominaremos “tejer” las definiciones de los operadores
termino(n), y coseno(x,n) del Ejemplo 48. La especificación separada de los operadores y
su posterior tejido, facilita una estrategia de programación en la que las diferentes partes del
programa se conciben de forma independiente, para juntarlas luego de que se han definido
y probado150.
La posibilidad de llevar a cabo este tejido se debe a que en el proceso definido por
termino(n) van apareciendo de forma sucesiva los términos de la serie, y en el proceso de
coseno(x,n) se van utilizando de forma sucesiva dichos términos. Así, si se acoplan el
cálculo de los términos con su uso, se pueden fundir las dos iteraciones en una sola.
Para tejer los dos operadores del ejemplo anterior, basta con acoplar el cálculo de los términos parciales
con su uso. Para ello basta con recibir como argumento de Cos-i3 el término requerido en cada
iteración, usarlo, y luego actualizarlo en la evocación recursiva para que en la iteración siguiente esté
disponible el término siguiente, así:
150Naturalmente que si el tejido se llevara a cabo de forma automática, el problema de la programación se acercaría
significativamente al de concebir el modelo matemático del problema.
151La persistencia de los valores de tiempo de cpu para las cuatro funciones programadas (...in 1628036047000ms),
parece indicar que éste se calcula de forma incorrecta.
Capítulo 9: Definición Recursiva de Operadores.
254
En las definiciones del operador coseno anteriores usamos un número de iteraciones fijo
suministrado por el usuario como argumento al operador. Sin embargo, un mejor criterio
para detener el proceso de aproximación, sería poder decir si la aproximación es
suficientemente buena, según algún criterio.
Se puede demostrar que los términos de la sucesión tienden a cero cuando n tiende a
infinito. Entonces, lo que haremos será detener el proceso cuando la última pareja (término
positivo + término negativo), sumen un valor cercano a cero. Así, podemos estar seguros
que la aproximación es buena, pues, el resto de término no sumados, no aportan mucho.
Para garantizar la precisión del valor calculado, se debe incluir en la sumatoria un número suficiente de
términos. En esta versión se acumulan términos hasta llegar a que la última pareja, término positivo +
término negativo, adicionen a la sumatoria un valor despreciable, así:
1 ?- coseno4(2.0,R).
R = -0.41614683654714246
Serie de Fibonacci.
En esta sección nos apoyaremos en el problema de calcular los elementos de la serie de
Fibonacci, para resaltar el efecto que tiene el enfoque usado al definir un operador, en la
eficiencia del proceso de reescritura.
En efecto, para este problema, la eficiencia del proceso pude ir desde un número de
reescrituras del orden de n!, hasta un número de reescrituras del orden de log2(n). Y, si
Capítulo 9: Definición Recursiva de Operadores.
255
bien este cambio tan dramático de orden no siempre es posible, el ejemplo ilustra la
necesidad de analizar con seriedad el problema de la eficiencia del operador.
En la serie de Fibonacci se parte de los número 0 y 1 y, a partir de ellos, el término
siguiente se obtiene sumando los dos anteriores, así:
Fib(n) = 0 si n = 0
Fib(n) = 1 si n = 1
Fib(n) = Fib(n-1) + Fib(n-2) si n > 1 {20}
Donde:
Fib(n) Es un operador que recibe como argumento n y calcula el valor del
número de Fibonacci de posición n en la serie.
En esta sección partimos del problema de definir el operador Fib(n).
9.6.1 Proceso recursivo.
Como es usual, es posible obtener un operador que determina un proceso recursivo
plasmando en el lenguaje funcional el modelo matemático del problema.
Una primera aproximación al operador Fib(n) se obtiene al escribir el modelo matemático en {20} en un
lenguaje funcional.
En SCHEME tendríamos la siguiente definición para Fib(n):
(define (fib1 N)
(if (= N 0) 0
(if (= N 1) 1
(+ (fib1 (- N 1)) (fib1 (- N 2)))
))
)
fmod C8-FIBONACCI is
protecting INT .
eq fib1(0) = 0 .
eq fib1(1) = 1 .
ceq fib1(N) = fib1(N - 1) + fib1(N - 2) if(N > 1) .
endfm
La forma del proceso que determina esta definición es sin duda la de un proceso recursivo.
Una evocación del operador definido en el ejemplo anterior procede como se muestra a continuación.
(fib 5)
;;(+ (fib 4) (fib 3) )
;;(+ (+ (fib 3) (fib 2)) (+ (fib 2) (fib 1)) )
;;(+ (+ (+ (fib 2) (fib 1)) (+ (fib 1) (fib 0))) (+ (+ (fib 1) (fib 0)) 1) )
;;(+ (+ (+ (+ (fib 1) (fib 0)) 1) (+ 1 0) (+ (+ 1 0) 1) )
;;(+ (+ (+ (+ 1 0) 1) 1 ) (+ 1 1) )
;;(+ (+ (+ 1 1) 1 ) 2 )
;;(+ (+ 2 1 ) 2 )
;;(+ 3 2 )
;;5
Que sin duda corresponde a un proceso recursivo. Nótese que para calcular (fib 5), es necesario
calcular 3 veces a (fib 0) y a (fib 1):
Para evaluar el número de veces que se lleva a cabo la reescritura bajo la definición
recursiva de fib(n), debemos notar que este depende de n, y que la función número de
reescrituras vs. N, que denominaremos Nr(n), satisface las ecuaciones siguientes:
Nr(1) = 1 si n = 0
Nr(0) = 1 si n = 1
Nr(n) = 1 + Nr(n-1) + Nr(n-2) si n > 1 {21}
Estas ecuaciones pueden resolverse de la forma que se muestra en [Brassard 88], dando
como respuesta una función que tiene el orden de n!.
El efecto de este tipo de orden puede fácilmente intuirse si se miran los valores que toma el
número de repeticiones para n=10,100,1000, etc.
En efecto, este operador es inútil en términos prácticos, por el tiempo de ejecución asociado
a valores moderados de n.
9.6.2 Proceso iterativo.
Como es bien conocido por todo programador, es fácil definir un operador que calcule los
números de Fibonacci por medio de un proceso iterativo. Este operador se apoya
Capítulo 9: Definición Recursiva de Operadores.
257
simplemente en mantener almacenado dos números consecutivos para calcular con ellos el
siguiente.
Una primera versión de un operador fib(n) que determine un proceso iterativo se obtiene manteniendo
en la memoria los dos últimos números calculados, que son utilizados para calcular el siguiente.
En SCHEME tendríamos la siguiente definición para un fib(n) iterativo:
fmod C8-FIBONACCI is
protecting INT .
eq fib2(0) = 0 .
eq fib2(1) = 1 .
ceq fib2(N) = fib-it(0,1,N) if(N > 1) .
ceq fib-it(Faa,Fa,N) = fib-it(Fa,(Fa + Faa),(N - 1)) if(N > 1) .
eq fib-it(Faa,Fa,1) = Fa .
endfm
Cuyos tiempos de ejecución para los mismos casos del ejemplo anterior son los siguientes:
Del ejemplo anterior puede verse que el proceso iterativo generado es significativamente
más eficiente, así:
Una evocación del operador definido en el ejemplo anterior procede como se muestra a continuación.
(fib 5)
;;(fib-iter 1 0 4 )
;;(fib-iter 1 1 3 )
Capítulo 9: Definición Recursiva de Operadores.
258
;;(fib-iter 2 1 2 )
;;(fib-iter 3 2 1 )
;;5
Para evaluar el número de veces que se lleva a cabo la reescritura bajo la definición
iterativa de fib(n), basta con notar que en cada paso de reescritura de fib-iter, el valor del
argumento K se disminuye en 1, que K se inicia en N y que la reescritura que produce el
resultado se lleva a cabo cuando K llega a cero. Así el número de reescrituras es N+2, que
es una función del orden de N.
Un interrogante que surge naturalmente frente a esta nueva forma de hacer el cálculo, es si
es posible buscar otra diferente (otro algoritmo) que lo efectúe con un número de
operaciones aún menor. La respuesta es de nuevo SI, y para probarlo presentamos la
definición del operador.
Una versión más avanzada del operador fib(n) es basado en la “Matriz de Fibonacci” que puede
calcularse por medio de un algoritmo del orden O(ln n). El algoritmo se basa en el cálculo de la Matriz
de Fibonacci (http://en.wikipedia.org/wiki/Fibonacci_number )
fib(n 1)
n
1 1 fib(n)
1 0 fib(n) fib(n 1
El cálculo de An, para A un real o una matriz, puede llevarse a cabo por medio de un algoritmo del O(ln
n) [Abelson 85, sec. 1.2.4].
Para evaluar el número de veces que se lleva a cabo la reescritura bajo la definición
iterativa de fib(n), basta con notar que en cada paso de reescritura de fib-iter, el valor del
argumento K se divide por 2, que K se inicia en N y que la reescritura que produce el
resultado se lleva a cabo cuando K llega a 1. Así el número de reescrituras es el número al
que hay que elevar 2 para que de N, o sea log2(N).
El efecto de este tipo de orden puede fácilmente intuirse si se miran los valores que toma el
log2(n) para n=10,100,1000, etc.
Para este momento debe ser obvio que el precio a pagar por el incremento en la eficiencia
del operador definido, es el de abandonar la simplicidad de la especificación matemática
del problema original, buscando en métodos, posiblemente más complejos, ventajas
computacionales inexistentes en el modelo original.
Utilidad de la recursión
Aunque los ejemplos anteriores muestran una clara superioridad, en lo que a eficiencia se
refiere, de los procesos iterativos sobre los procesos recursivos, no debemos concluir que la
definición de procesos recursivos es inútil.
Capítulo 9: Definición Recursiva de Operadores.
259
En primer lugar, de los ejemplos anteriores, debe ser claro que es significativamente más
fácil concebir una solución recursiva que concebir una iterativa. Prueba de esto es la
naturalidad con que surge el primer procedimiento para calcular los números de Fibonacci,
en contraste con el segundo que genera el proceso iterativo más eficiente, pero un poco más
difícil de idear y entender, y ni que hablar de la dificultad asociada al último algoritmo
presentado.
La recursión es, en efecto, un instrumento útil para entender y diseñar la solución a
problemas específicos. Una vez concebida una solución recursiva, se puede intentar
transformarla a una versión iterativa152.
El ejemplo del cambio de moneda presentado por Abelson en [Abelson 1985 sección 1.2.2],
que resumimos a continuación, es una muestra patente del poder de la recursión como
medio para hallar una solución a un problema.
El problema es el siguiente: ¿De cuantas maneras diferentes se puede cambiar una cantidad
de dinero dada, si contamos con monedas de 20, 50, 100, 200 y 500 pesos?.
Este problema tiene una solución relativamente sencilla, si se plantea de forma recursiva.
Este planteamiento se basa en los hechos siguientes:
El conjunto £(V,20 a 500) formado por todas las maneras de cambiar V con las
cinco denominaciones, puede ser dividido en dos conjuntos separados: las maneras
que emplean al menos una moneda de 500 y las maneras que no utilizan ninguna
moneda de 500.
El conjunto de todas las maneras que no usan monedas de 500 no es otra cosa que el
conjunto £(V,20 a 200) formado por todas las maneras de cambiar V con las
monedas de 20 a200.
El conjunto de todas las maneras de hacer el cambio con al menos una moneda de
500 es equipotente con el conjunto £(V-500,20 a 500) de todas las manera de hacer
el cambio con monedas de 20 a 500, de la cantidad inicial menos 500.
Así, para el número de formas de cambiar el dinero original se puede plantear una ecuación
que la relaciona de forma recursiva, con el número de formas de cambiar menos dinero y de
usar menos denominaciones:
|£(V,20 a 500)| = |£(V,20 a 200)| + |£(V-500,20 a 500)|
Para escribir un procedimiento que efectúe este cálculo, basta con definir un mecanismo
para manejar las denominaciones e incluir los casos particulares. Remitimos al lector
interesado a [Abelson 1985 sección 1.2.2], para una versión en SCHEME de este
procedimiento.
Ejercicios propuestos.
1- Siga paso a paso el proceso de cálculo en MAUDE, bajo la definición de los operadores
Σn2 y √ propuestos en Ejemplo 29, Ejemplo 30, Ejemplo 31 (use los comandos referidos en
[Clavel 2007, sec. 16.5]
2- Defina en MAUDE el operador Σn2 y √ del Ejemplo 31 y del Ejemplo 36, usando el
operador de selección,
if <predicado> then <consecuencia> else <alternativa> fi
- Realizar los ejercicios 1.9, 1.10, 1.11, 1.12, 1.37 (sin usar funciones genéricas), de la
sección 1.2. de [Abelson 85].
5- Lleve a cabo los ejercicios del punto anterior pero con las siguientes variaciones:
- Alterne los signos de los sumandos.
- Cambie los signos de los sumandos cada N sumandos siendo N dado.
- Cambie los signos aumentando en 1 el número de términos con un signo cada cambio de
signo.
6- Elabore su propia versión de un operador de selección en MAUDE, investigue el efecto
de las posibles Estrategias de evaluación definidas por medio del atributo
strat( <orden_de_evaluación > 0 )
7 - ¿por que no es necesario que en la solución MAUDE del Ejemplo 31 todas las
ecuaciones sean condicionales?.
8. Elabore la versión en SCHEME de los ejemplos Ejemplo 40, Ejemplo 44, y Ejemplo 45.
9- Elabore la versión que procede de atrás para adelante para el caso del Ejemplo 29 y del
Ejemplo 31.
10- Elabore un código en MAUDE que defina los operadores _! (que calcula el factorial) y
_^_ (que eleva un número a una potencia).
11- Lleve a cabo los ejercicios 1.16 a 1.19 referido en [Abelson 85 sec. 1.2.4]
12- Verifique la validez del operador coseno(n) definido, para una gama de valores de x
que alcance valores de x > 1000.
13- . Verifique la capacidad de su intérprete para almacenar los valores calculados, por
medio de un programa que calcule las diferencias de dos valores consecutivos. Analice en
que momento estas generan un valor obviamente errado.
14- Reelabore la definición del operador coseno del Ejemplo 46, primero cambiando la
ecuación eq coseno1(X, N) = coseno-ite(X,N,1,1) por la ecuación eq coseno(X, N)
= coseno-ite(X,N,1,0) y luego por la ecuación eq coseno(X, N) = coseno-
ite(X,N,2,1).
15- En el operador que calcula los términos de la serie del coseno definido en el Ejemplo
51, se usó la expresión Tr * (-1) * (X * 2 / ((2 * k) * (2 * k – 1))) para obtener cada término
a partir del anterior. En esta expresión se lleva a cabo dos veces la subexpresión (2*k) .
Una manera de evitar esta doble multiplicación es usar como variable iteradora a J=2*K
(que corresponde en cada término al exponente de X), y que debe ahora avanzar de 2 en 2
en cada iteración, para llegar hasta JMX=2*N. Reelabore el operador termino(N) teniendo
en cuenta este detalle.
16- Defina operadores que obtengan Xn/n!, X(2n-1)/(n+1)!, y X2n/(2n-2)!
Capítulo 9: Definición Recursiva de Operadores.
263
17- En la definición del operador coseno(X) del Ejemplo 52:
a- como debe cambiarse la definición en cada uno de los casos siguientes:
En lugar de (0., 1., (- X * X / 2.), 2, X) se tiene (1.., 1., (- X * X / 2.), 2, X).
En lugar de Ta * (X^2 / ((2 * K) * (2 * K – 1)) se tiene Ta * (-1) * (X^2 / ((2 * K) * (2 * K – 1))
b- ¿puede cambiarse?:
siguiente(Ta, K), siguiente(siguiente(Ta, K), (K + 1)) por siguiente(Ta, K), siguiente(Ta, (K + 2)).
d- Reelabore la definición del operador, para que el contador K, avance en uno cada
iteración.
18- Cuente el número de veces que se debe calcular a (fib 0) y a (fib 1) en una evocación
del operador definido en el Ejemplo 53, para valores de n=6,7,8,9. Grafique la función
número de veces vs. n.
19- Realizar los siguientes ejercicios de la sección 1.1. de [Abelson 85], que está disponible
en línea en la WWW (ver referencia)
1.6, 1.8
20- Escriba un procedimiento SCHEME para llevar a cabo el cálculo siguiente:
- La suma de los n primeros números naturales pares.
- El valor de x n , dados x y n
n
1
- La sumatoria de los inversos de los n primeros números naturales: i
i 1
n
- El factorial de un número entero positivo: n! i
i 1
25. Suponga que cuenta con un procedimiento (mcd a b), que toma dos enteros positivos
a y b y devuelve el máximo común divisor entre ellos. Escriba un procedimiento (primo?
n) que verifique si un entero positivo n es primo o no. Compárelo con el primer
procedimiento para este mismo fin desarrollado en la sección 1.2.6 de [Abelson 85].
26. Elabore procedimientos SCHEME para obtener el valor de la expresión siguiente dados
x y n. Escriba tanto un procedimiento que determine un proceso recursivo como un
proceso iterativo.
0
n+xn e
....................
.....................
(k+1)+...
k+xk e
.....e
...
7+ ....
6+x6 e
5
5+x e
4+x4 e
3+x3 e
2
2+x e
1+x1 e
27. Para cada uno de los cuatro grupos de ecuaciones que siguen, vistas como la definición
MAUDE del operador del punto anterior, seleccione las opciones a y b que les
corresponden en las listas de términos que siguen a las ecuaciones.
eq exp(X, N) = exp-ite(0, X, N) .
ceq exp-ite(T, X, K) = exp-ite(_________a_________, X, (K - 1)) if(K>0) .
eq exp-ite(T, X, 0) = _____b_______ .
eq exp(X, N) = exp-ite(1, X, N) .
ceq exp-ite(T, X, K) = exp-ite(_________a_________, X, (K - 1)) if(K>0) .
eq exp-ite(T, X, 0) = _____b_______ .
eq exp(X, N) = exp-ite(0, X, N) .
ceq exp-ite(T, X, K) = exp-ite(________a__________, X, (K - 1)) if(K>1) .
eq exp-ite(T, X,1) = _____b_______ .
Capítulo 9: Definición Recursiva de Operadores.
265
eq exp(X, N) = exp-ite(1, X, N) .
ceq exp-ite(T, X, K) = exp-ite(________a__________, X, (K - 1)) if(K>1) .
eq exp-ite(T, X,1) = _____b_______ .
a: ( N + X ^ N * T)
a: ( N + X ^ N * e ^ T)
a: e ^ ( N + X ^ N * T)
a: X ^ N * e ^ (N + T)
a: e ^ ( N + X ^ N * e ^ T)
b: T
b: ln(T)
b: e^T
b: 1+X*e^T
b: 1+X*T
Capítulo 10
Valores y Tipos Compuestos
Capítulo 10: Valores y Tipos Compuestos.
268
Introducción
Todo lenguaje de programación ofrece tipos nativos de datos que el programador utiliza
para definir datos concretos asociados a los elementos de sus problemas. Para estos tipos,
el lenguaje ofrece una serie de operaciones que el programador usa para manipular y
transformar los datos. Al utilizar estos datos y sus operaciones asociadas, el programador
no necesita conocer ni la manera como los datos se representan en el computador, ni la
manera en que las operaciones se llevan a cabo. En este sentido diremos que el
programador se “abstrae” de conocer los detalles asociados a la implementación de los
tipos de datos que utiliza [Arango 97].
Por otro lado, en los capítulos anteriores, solo hemos tenido contacto con datos numéricos
escalares y en particular, con números enteros y números con decimales. Una característica
de estos tipos de dato es que son vistos como una unidad por las operaciones que los
manipulan sin permitir el acceso directo a sus partes componentes (si es que las tienen).
Estos tipos de dato son, sin embargo, insuficientes para representar la información asociada
a muchos problemas de interés. Considere, por ejemplo, la representación de un punto en
un espacio cartesiano, la representación de un número complejo, o la representación de la
cola de compradores en un supermercado.
Para representar estos elementos de información, es necesario contar con datos compuestos
que le ofrezcan al programador, tanto mecanismos que le permitan acceder y manipular las
partes que lo componen (que denominaremos en lo que sigue como “vías de acceso”),
como operaciones que le permitan operar con el compuesto como un todo.
Es conveniente, además, que estos datos compuestos le sean ofrecidos al programador
como “tipos abstractos” de datos en el sentido citado arriba153, permitiéndole su utilización
sin tener que preocuparse por la manera como se almacenan en memoria, la forma de unir
los componentes, o por la manera como se llevan a cabo las operaciones.
En este capitulo se analizan las maneras de definir y representar datos compuestos en los
lenguajes funcionales analizados, junto con la manera para definir las operaciones que se
les aplican.
Enfatizaremos el grado en que cada lenguaje permite considerar estos datos como
verdaderos tipos de datos propuestos por el programador, permitiendo su uso posterior en
condiciones similares a las de los tipos nativos.
153En este trabajo usaremos la denominación “tipo abstracto” en el sentido citado, sin desmedro de asociarlo luego a una
teoría en una lógica ecuacional que define las propiedades de operaciones definidas sin asociarlas a un tipo específico de
datos (y que son, por tanto, aplicables a cualquier álgebra que sea modelo de la teoría).
Capítulo 10: Valores y Tipos Compuestos.
269
año, mes y día; un estudiante es descrito por su nombre, un numero de identificación, una
dirección, un teléfono, una fecha de nacimiento, etc...
Para tratar con estas entidades, es de gran utilidad poder considerar el conjunto de valores
que las describen como unidades de valor o datos compuestos. Al considerarlas de esta
manera, podemos definir variables que hagan referencia a un conjunto de dichos valores,
usarlas como argumentos al evocar operadores, y sustituir variables con entidades
particulares para el emparejamiento de axiomas.
La mayoría de los lenguajes de programación ofrecen construcciones para manipular este
tipo de compuesto, denominándolos “registros”154 (“records”), o “estructuras”
(“structures”). En lo que sigue nos referiremos a ellos como “estructuras” o “datos
estructurados”.
Caracterizaremos una estructura por las siguientes propiedades:
Ser un compuesto que tiene un número determinado de componentes155.
Los componentes pueden ser de diferentes tipos (incluyendo tipos
compuestos).
Cada componente se asocia al todo en un “lugar” determinado.
Las vías de acceso a las componentes se apoyan en el “lugar” de la
componente dentro de la estructura, que es identificado por medio de un
número o un rótulo.
Para facilitar los cálculos con datos estructurados el lenguaje debe ofrecer, además,
mecanismos para darle la categoría de tipo de dato al conjunto de valores estructurados que
tengan la misma estructura de composición. A estos tipos los denominaremos en lo qiue
sigue como “tipos estructurados”. Para definir tipos estructurados el lenguaje debe proveer
lo siguiente:
Un mecanismo para “construir” valores estructurados a partir de los valores
de sus componentes.
Un mecanismo para imponer condiciones adicionales de pertenencia al tipo
a las tuplas que estructuren un número igual de componentes con los
mismos tipos.
Un mecanismo para obtener o “seleccionar” los valores de las componentes
a partir de un valor estructurado.
La posibilidad de definir operadores que actúen sobre datos del tipo
estructurado.
Un modo para representar de forma explícita datos estructurados
particulares, o valores base, dentro del tipo.
En esta sección presentaremos las facilidades ofrecidas por cada uno de los lenguajes
analizados para crear y manipular tipos estructurados. Para ello nos apoyaremos en un
ejemplo específico. Para este ejemplo presentaremos, en cada lenguaje, las construcciones
que definen los elementos referidos en el parágrafo anterior, señalando las ventajas y
desventajas relativas de las mismas.
154 Por referencia al hecho de que tradicionalmente constituyen la unidad básica de lectura/escritura de datos en archivos.
155 Aunque no necesariamente fijo.
Capítulo 10: Valores y Tipos Compuestos.
270
Supongamos que nos es necesario lleva a cabo numerosos cálculos que involucran magnitudes
vectoriales en dos dimensiones. Si contáramos con un tipo abstracto correspondiente a los vectores, la
especificación de estos cálculos se simplificaría enormemente.
El tipo abstracto asociado a los vectores debe proveernos de los tres elementos siguientes:
Un nuevo tipo de datos, que denominaremos “Vector”.
Una manera de construir una instancia de vector a partir de dos números reales.
Una manera de acceder a los reales que forman un vector.
Una manera para representar vectores, por ejemplo los vectores V1 = 2i+5j y V2 = 3i+4j,
Un conjunto de operadores aplicable a los vectores que representemos, por ejemplo los operadores suma
de vectores (+), producto escalar (.) y el operador de igualdad (=) aplicado a vectores.
Donde asociamos el valor de tipo record-type, resultante de evocar a make-record-type con el rótulo
Vector, para poder usar dicho valor en las operaciones siguientes.
El predicado de tipo para el nuevo tipo se define de la manera siguiente:
La declaración de nuestro tipo Vector en MAUDE se lleva a cabo declarando a un identificador como el
medio para hacer referencia al tipo, así:
sort Vector .
Capítulo 10: Valores y Tipos Compuestos.
272
10.2.2 Construcción de instancias del tipo compuesto estructurado.
Para tener valores de tipo Vector, es necesario construirlos partiendo de sus componentes
reales. Para que ello sea posible, el lenguaje debe ofrecer un operador “constructor”.
10.2.2.1 Construcción de instancias del tipo compuesto estructurado en
SCHEME.
El MIT_SCHEME le da soporte a la construcción de los valores de un tipo compuesto
estructurado, por medio del operador nativo siguiente:
(record-constructor <record_type> [<lista_de_nombres_de_campos>] )
Donde:
<record_type> es el valor obtenido como resultado de declarar el tipo con
el operador make-record-type.
<lista_de_nombres_de_campo> es una lista opcional con elementos de la
<lista_de_nombres_de_campo> usada al declarar el tipo con el operador
make-record-type. Si esta lista se omite, se asume que es igual a la dada al
declara el tipo.
La evocación del operador record-constructor da como resultado un operador constructor,
que acepta como argumentos valores para las componentes incluidas en la
<lista_de_nombres_de_campo> y da como resultado un valor del tipo registro descrito
por el argumento <record_type>.
Para definir un operador constructor de valores de tipo Vector se usa el operador nativo record-
constructor que toma como argumento el valor de tipo record-type creado al declarar el tipo Vector,
así:
(define make-Vector (record-constructor Vector))
Donde asociamos el símbolo make_Vector al operador resultante de evocar record-constructor, para
poder usar dicho operador al crear instancias de Vector, así:
(define V1 (make-Vector 2 5 ))
(define V2 (make-Vector 3 4 ))
La declaración del constructor para el sort Vector en MAUDE, puede tomar ventaja de la notación infija
para que los términos base mantengan la forma tradicional, así:
fmod C9-VECTOR is
protecting FLOAT .
sort Vector .
op _i+_j : Float Float -> Vector [ctor] .
endfm
Permitiendo que los vectores V1 y V2 se representen de forma tradicional por medio de los términos
siguientes:
2. i+ 5. j y 3. i+ 4. j
Para definir los operadores selectores de las componentes de valores de tipo Vector se usa el operador
nativo record-accessor que toma como argumento el valor de tipo record-type creado al declarar el
tipo Vector y el nombre del componente a ser seleccionado, así:
Los operadores selectores para las componentes del vector se pueden declarar y definir fácilmente en
MAUDE con los mismos elementos que se usan para definir y declarar cualquier otro operador:
Dados estos operadores se pueden lleva a cabo operaciones sobre valores previamente
creados.
Dado el constructor y los operadores definidos se puede operar sobre valores del tipo sin especificar
cada vez como llevar a cabo las operaciones, así:
Vale la pena notar que, al igual que con los demás operadores definidos, de ser evocados
los operadores del ejemplo con valores errados, el error sólo será detectado al tratar de
aplicar los operadores de selección a valores de tipo incorrecto.
...
1 ]=> (suma-vector 6 9)
;The object 1 is not a record of type vector
……..
fmod C9-VECTOR is
protecting FLOAT .
sort Vector .
op _i+_j : Float Float -> Vector [ctor] .
op _+_ : Vector Vector -> Vector .
op _*_ : Vector Vector -> Float .
vars Xi Xj Yi Yj : Float .
endfm
Los operadores pueden ser usados para llevar a cabo cálculos, así:
Considere el tipo compuesto estructurado “Fecha”, usado para representar las fechas del calendario
Juliano.
Las fechas pueden representarse con tres valores, uno para el año, uno para el mes y uno para el día.
Así, si se opta por representar las fechas usando números enteros (v.g. año 2013, mes 5, día 31), es
posible definir el tipo de las fechas usando una estructura que contenga tres valores enteros como
componentes.
En SCHEME:
Capítulo 10: Valores y Tipos Compuestos.
277
;;; tipo compuesto estructurado "Fecha"
;;; ------------------------------------------
fmod C9-FECHA is
protecting INT .
protecting STRING .
sort Fecha .
endfm
El problema de definir las fechas de esta manera, es que cualquier terceta de enteros puede ser
considerada como una fecha. Así:
Se hace entonces necesario contar con una manera para señalar como erradas, ciertas
instancias de los tipos compuestos estructurados, que hayan sido definidos usando los
mecanismos presentados en los numerales anteriores.
Para ello debemos asegurarnos que las componentes de los valores de un tipo compuesto
estructurado, cumplan con una serie de condiciones. A estas condiciones nos referiremos en
lo que sigue como las “invariantes del tipo”.
10.2.5.1 Invariantes de tipo en SCHEME
Por ser el SCHEME un lenguaje débilmente tipado, es necesario evocar explícitamente el
predicado del tipo (ver 8.5.1.1), para verificar si un valor pertenece o no al tipo esperado.
En consecuencia, para verificar las invariantes del tipo en un tipo definido, es necesario
incorporarlas como parte de la definición del predicado del tipo.
A continuación se muestra la definición del predicado de tipo para el tipo Fecha en SCHEME:
Capítulo 10: Valores y Tipos Compuestos.
278
(define (Fecha? X)
(boolean/and
((record-predicate Fecha) X)
(>= (get-Anio X) 0)
(and (> (get-Mes X) 0) (<= (get-Mes X) 12))
(and (> (get-Dia X) 0) (<= (get-Dia X) 31))
(and (or (<= (get-Dia X) 28)
(and (<= (get-Dia X) 29) (or (not (= (get-Mes X) 2))
(bisiesto? (get-Anio X))))
(and (<= (get-Dia X) 30) (not (= (get-Mes X) 2)))
(and (<= (get-Dia X) 31) (not (or (= (get-Mes X) 2)
(= (get-Mes X) 4)
(= (get-Mes X) 6)
(= (get-Mes X) 9)
(= (get-Mes X) 11)
)
)
)
)
)
)
)
Que permite discriminar entre las fechas correctas y las incorrectas, Así:
El siguiente ejemplo muestra el uso de la ecuación de membresía condicional para distinguir entre las
instancias correctas y las incorrectas del constructor para las fechas presentado en el ejemplo anterior.
fmod C9-FECHA is
protecting INT .
protecting STRING .
protecting BOOL .
vars M D A : Int .
….
….
endfm
Nótese que el sort Fecha? (fechas dudosas), sirve como paso intermedio para construir el sort Fecha
(fechas correctas). El operador _de_de_, sirve para definir el constructor de las fechas correctas e
incorrectas, y la ecuación de membresía condicional permite distinguir entre las dos.
De esta manera, es responsabilidad del intérprete, garantizar que sólo los valores que
cumplen con las invariantes del tipo, se usen como argumento en las operaciones relativas
al mismo.
Para garantizar que sólo se usen fechas correctas con los operadores relativos a las fechas, es suficiente
con garantizar que tanto el perfil de dichos operadores como las variables usadas en su definición, sólo
haga referencia al tipo de las fechas correctas.
fmod C9-FECHA is
…..
…..
var F : Fecha .
eq dia-nombre(F) = dia-s(zeller-gr(dy(F),mt(F),yr(F))) .
endfm
Bajo esta circunstancia el cálculo del día de la semana sólo se puede llevar a cabo con fechas correctas,
así:
eq dia-nombre(D de M de A) =
dia-s(zeller-gr(dy(D de M de A),mt(D de M de A),yr(D de M de A))) .
156 Implementación para windows sobre ECLIPSE® del proyecto MOMENT, versión: MaudeFW_2.6.exe,
Capítulo 10: Valores y Tipos Compuestos.
282
Se puede acceder a los componentes individuales por medio de un
mecanismo que denominaremos “vía de acceso”.
Entre las componentes de un compuesto y, posiblemente, entre las
componentes de varios compuestos pueden ocurrir conexiones o
“relaciones”.
Para facilitar los cálculos con datos iterados el lenguaje debe ofrecer, también, mecanismos
para darle la categoría de tipo de dato a todos los conjuntos de valores iterados que tengan
la misma estructura de composición. Para ello debe proveer lo siguiente:
Un mecanismo para “construir” de forma progresiva valores iterados a partir
de los valores de sus componentes.
Un mecanismo para obtener o “seleccionar” los valores de las componentes
a partir de un valor iterado.
La posibilidad de definir operadores que actúen sobre datos del tipo iterado.
En esta sección presentaremos las facilidades ofrecidas por cada uno de los lenguajes
analizados, para crear y manipular datos compuestos iterados. Para ello nos apoyaremos en
iterados con relaciones específicas entres sus componentes. Para estos tipos de iterados
presentaremos, en cada lenguaje, las construcciones que permiten crearlos y manipularlos,
haciendo énfasis en la manera de definir los elementos referidos en el párrafo anterior.
10.3.1 Conexión entre componentes: Pares.
Para componer datos se debe contar con mecanismos de agregación y desagregación. El
mecanismo de agregación más simple es el que junta o “pega” dos valores para formar una
pareja. El mecanismo de desagregación mas simple es el que toma una pareja y obtiene o
“selecciona” sus componentes.
La importancia del mecanismo de manipulación de parejas, es que a partir de él, algunos
lenguajes implementan los mecanismos para manipular estructuras más complejas, y en
particular las iteradas.
En esta sección se presenta, entonces, la manera de agregar y desagregar parejas en los
lenguajes analizados.
10.3.1.1 Parejas en SCHEME.
En concordancia con la estrategia general del lenguaje, el SCHEME ofrece los operadores
nativos especializados a la gestión de parejas que se describen a continuación
Un operador encargado de formar una pareja.
(cons <objeto_1> <objeto_2> )
Donde:
<objeto_1> y <objeto_2> son dos valores de tipos arbitrarios, que pueden
incluir valores compuestos, que constituyen las componentes de la pareja.
Dos operadores encargados de obtener de una pareja su primer o segundo componente,
respectivamente.
(car <pareja> )
(cdr <pareja> )
Capítulo 10: Valores y Tipos Compuestos.
283
Donde:
<pareja> es el valor compuesto de tipo pareja del que se desea obtener las
componentes.
Dos operadores encargados de reasignar el primero o el segundo componente de una pareja
ya creada.
(set_car! <pareja> <objeto>)
(set_cdr! <pareja> <objeto>)
Donde:
<pareja> es el valor compuesto de tipo pareja al que se le desea cambiar un
componente.
<objeto> es el nuevo valor que tomará el componente a ser modificado.
Un operador que permite establecer si un valor es o no es una pareja.
(pair? <objeto> )
Donde:
<objeto> es el valor que se desea verificar como pareja.
Dado el constructor y los operadores definidos se puede operar sobre valores del tipo sin especificar
cada vez como llevar a cabo las operaciones, así:
Como a continuación usaremos ampliamente las parejas, comenzaremos por introducir una
manera de visualizarlas. Dentro de las muchas maneras que existen, escogimos una en la
que la pareja se representa como una caja con una línea punteada que separa los dos objetos
componentes. Por ejemplo, (cons 1 2) se representa por:
1 2
La ventaja de esta visualización frente a otras, es que cuando aparecen parejas cuyos
elementos son a su vez parejas, esta representación nos previene de confundir u olvidar la
Capítulo 10: Valores y Tipos Compuestos.
284
verdadera posición de cada objeto. Además hace evidente la imposibilidad para acceder a
ciertos elementos del compuesto directamente. En la figura 4.1, podemos observar, por
ejemplo, dos maneras de combinar los números 1, 2, 3, y 4 utilizando parejas.
1 2 3 4 1 2 3 4
Para definir mecanismos que permitan la gestión de parejas, basta con usar operadores constructores y
selectores de igual forma que para el caso de los compuestos estructurados (solo que ahora tienen dos
componentes).
El siguiente código podría ser usado para manipular parejas de enteros:
fmod C9-PAR is
protecting INT .
sort Par-Int .
op _|_ : Int Int -> Par-Int [ctor] .
op =>_ : Par-Int -> Par-Int .
vars I J : Int .
endfm
Nótese que no es necesario definir operadores de selección, ya que para obtener las partes de una pareja
tomaremos ventaja del emparejamiento usando más de un operador al lado derecho de la ecuación.
El operador definido toma como argumento a una pareja y de como resultado a otra pareja que tenga los
componentes en orden ascendente.
157Justificado por el hecho de que una conexión entre más de dos componentes pueden representarse por varias
conexiones entre dichos componentes.
Capítulo 10: Valores y Tipos Compuestos.
286
La importancia de estos tipos de compuestos iterados, es que, por un lado, permiten
modelar diversos tipos de elementos en el dominio del problema, y por otro, se pueden
manipular con códigos estandarizados que se abstraen del significado específico de la
relación.
Así, en lo que sigue, nos limitaremos a ejemplificar la manera de gestionar compuestos tipo
LISTA y tipo ARBOL en cada uno de los lenguajes seleccionados. Con ello sentaremos las
bases suficientes para la gestión de los demás tipos, y mostraremos las capacidades
relativas de los diferentes lenguajes frente a este tipo de compuesto.
Gestión de Listas.
Para la gestión de listas, como es usual para un compuesto, debemos contar con los
mecanismos necesarios para construirlas y seleccionar sus componentes.
Estos mecanismos, sin embargo, no son tan simples como los que se presentaron más
arriba. En particular:
La selección de un componente no puede basarse ahora, en un nombre
asociado al componente, sino que debe fundamentarse ya sea en su posición
en la lista, o en una característica asociada al valor del componente mismo.
La selección de un sólo componente no es, tampoco, suficiente. Es
necesario contar con mecanismos que permitan obtener sublistas que
satisfagan condiciones específicas.
La construcción de la lista debe poder llevarse a cabo de forma paulatina,
con base en operadores que introduzcan, supriman y modifiquen
componentes.
Debe ser posible reposicionar los elementos en la lista de modo que esta
satisfaga condiciones específicas (v.g. que los elementos estén en orden).
Debe ser posible, tanto combinar los elementos de varias listas, como
separar los elementos de una lista para obtener nuevas listas con propiedades
determinadas.
En las secciones que siguen, presentaremos los mecanismos que ofrece cada lenguaje para
construir listas y definir operadores que lleven a cabo las tareas de los tipos referidos.
10.4.1 Declaración de Listas.
Para darle categoría de tipo a una lista el lenguaje debe poder ofrecernos construcciones
que lo permitan.
10.4.1.1 Tipo nativo Lista en SCHEME.
El MIT_SCHEME le da soporte a las listas como un tipo nativo por lo que no es necesario
usar construcción alguna para declararlo. Además, por ser el SCHEME un lenguaje
débilmente tipado, no es necesario distinguir las listas según el tipo de componente. En
efecto las listas son listas del tipo genérico object.
Para establecer si un valor es o no es una lista el SCHEME ofrece el operador siguiente.
(list? <objeto> )
Donde:
Capítulo 10: Valores y Tipos Compuestos.
287
<objeto> es el valor que se desea verificar como lista.
10.4.1.2 Declaración de un tipo Lista en MAUDE
El lenguaje MAUDE ofrece en el preludio (ver [Clavel 2007, sec. 2.2]), la especificación
de un tipo genérico denominado List{X}. Este tipo es paramétrico en el tipo del
componente, pudiendo instanciarse a una lista de valores de cualquier tipo. La
parametrización de tipos será, sin embargo, un motivo de estudio en el Capítulo 10, por lo
que prescindiremos de este aspecto en el presente.
La especificación de la lista nativa es, en todo caso, llevada a cabo en MAUDE como la
especificación de un tipo cualquiera propuesto por el programador. Así, en lo que sigue
usaremos una especificación similar para ilustrar las capacidades del lenguaje en la
definición de este tipo de compuesto. Nuestra especificación difiere de la de la lista nativa,
en que considera, sólo listas con un tipo definido de componente (de tipo Int).
La declaración de un nuevo tipo en MAUDE se lleva a cabo declarando un identificador
como el nombre a ser usado para referirse a un sort propuesto por el programador. Para
ello se usa la construcción descrita en la sección 8.5.2.1.
La declaración de un tipo asociado con todas las listas de enteros, que llamaremos ListInt en MAUDE
se lleva a cabo declarando a un identificador como el medio para hacer referencia al tipo, así:
sort ListInt .
’()
La construcción para una lista que contiene 1, 2, 3 y 4 sería entonces como sigue:
158Tala como lo refiere [Hanson 2002, sec. 10.1], en la versión de SCHEME usada en este trabajo se distingue la lista
vacía del símbolo nil, usado en otras versiones de SCHEME , tal como la referida en [Abelson 85 , sección 2.2.1]
Capítulo 10: Valores y Tipos Compuestos.
288
(cons 1
(cons 2
(cons 3
(cons 4 ’()))))
Que visualizada por medio de cajas luciría de la manera siguiente:
1 2 3 4
La lista del ejemplo anterior puede construirse de forma más simple como sigue:
(list 1 2 3 4)
Dada una lista, el procedimiento car, devuelve el primer elemento de la lista (cabeza), mientras que cdr,
retorna la sublista que contiene todos los elementos excepto el primero (cola), así:
Es importante entender, también, que sólo el uso adecuado del cons da lugar a listas.
El constructor cons puede ser usado para insertar un elemento al principio de la lista así:
1 2 3 2 3 1 2 3 4
1 2 3 4
(list (list 1 2 3) 4)
ó
(cons (list 1 2 3) (cons 4 ’()))
Donde debe notarse que se usó el valor ’(), para señalar el final de la cadena.
La declaración del constructor para el sort ListInt en MAUDE, usará el espacio vacío como símbolo de
operación. Se definirá el operador nilLint para referirse a la lista vacía, así:
fmod LISTA-INT is
protecting INT .
sort ListInt .
subsort Int < ListInt .
endfm
De esta manera una serie de enteros “pegados” con el espacio en blanco, constituyen un elemento de la
lista de enteros:
El efecto del atributo ecuacional assoc, es que dos listas con los mismos componentes son
consideradas iguales sin importar cuales son sus sublistas159.
159 Los atributos ecuacionales assoc e idem no pueden ser usados juntos.
Capítulo 10: Valores y Tipos Compuestos.
291
Dadas las dos sublistas siguientes:
(3 5 7)
(2 1)
Y las sublistas siguientes:
(3 5)
(7 2 1)
La dos listas resultantes de pegar cada pareja:
(3 5 7) (2 1)
(3 5) (7 2 1)
Son consideradas iguales (mismos elementos en el mismo orden) y pueden representarse sin necesidad
de encapsulamiento alguno:
3 5 7 2 1
El efecto del atributo ecuacional id, es el de señalar una constante como el elemento
identidad para el operador (ver [Clavel 2007, SEC 4.4.1]). Así, toda aplicación del
operador que tenga como uno de sus operandos al elemento identidad es rescrita, antes de
que tenga lugar cualquier otra reescritura, al otro operando de la operación.
(3 nilInt 7)
(2 1 nilInt)
Ellas son rescritas a las listas siguientes antes de efectuarse el cálculo del operador:
(3 7)
(2 1)
La lista nula es, en consecuencia representada por el elemento identidad, así:
(nilInt nilInt)
Es rescrita a:
nilInt
En los ejemplos que siguen no se considerará la posibilidad de tener listas que siempre terminan en
nilInt como ocurre con las listas en SCHEME. En los ejemplos se solicita al lector, como ejercicio, que
indique el efecto de eliminar el atributo de identidad y considere la forma de construir una lista que
mimifique las listas nativas en SCHEME
Se induce que sólo variables instanciadas pueden ser listas, y que lo son sólo si están
instanciadas al átomo [], o a un término complejo bajo el operador (o “functor”) de dos
argumentos [_|_], en el que el segundo argumento es una lista.
Nótese que bajo esta definición, [] representa la lista vacía, y los dos argumentos del
functor [_|_], representan respectivamente la cabeza y la cola de la lista. De lo anterior se
infiere que las listas PROLOG son estructruralmente idénticas a las listas en SCHEME, por
lo que los códigos que gestionan las listas pueden realizarse de forma similar a la
presentada para dicho lenguaje.
Así, la lista compuesta por los enteros 1, 2 y 3 se escribiría así:
[1 | [2 | [3 | [] ] ] ]
Y tendría el árbol sintáctico siguiente:
.
.
1
.
2
3 []
El lenguaje permite, además, representar las listas con la notación simplificada siguiente:
[<elemento 1>, <elemento 2>, <elemento 3>,...]
Con lo que la lista anterior se puede representar de la forma:
[1, 2, 3,...]
Para una mayor ilustración se recomienda el capítulo 4 de [Blackburn 01].
10.4.3 Recorridos básicos sobre listas.
Un proceso básico sobre compuestos iterados es el de recorrer la estructura, visitando en
algún orden específico los componentes elementales que la forman. En esta sección
ilustraremos la manera de llevar a cabo recorridos en el marco de los lenguajes analizados.
Para ello declararemos y definiremos tres operadores elementales, a saber:
Un operador que obtenga el tamaño de la lista, contando los componentes
que esta posee.
Un operador que obtenga la suma de los cuadrados de los elementos de la
lista.
Un operador que determine si un valor específico se encuentra en la lista.
Con el objeto de ilustrar la capacidad de los lenguajes frente a la definición de recorridos,
mostraremos, además, la manera de especificar recorridos que visten las componentes de la
Capítulo 10: Valores y Tipos Compuestos.
293
lista en orden directo, es decir moviéndose del primer elemento hacia el último, y en orden
inverso, es decir moviéndose desde el último hacia el primero.
10.4.3.1 Recorridos básicos sobre Listas en SCHEME.
Los recorridos en orden directo en SCHEME son planteados de una forma muy simple, por
medio de procesos que en cada paso de reescritura procesan el elemento de la cabeza de la
lista, que puede ser obtenido usando el operador car, y le dan al paso siguiente la cola de la
lista, que puede ser obtenida usando el operador cdr, para que repita la acción.
Los dos primeros operadores deben recorrer toda la lista.
En la definición siguiente el operador largo obtiene el tamaño de la lista, recorriéndola en orden directo.
Nótese que no es necesario obtener la cabeza de las lista ya que su valor no afecta el resultado., así:
En la definición siguiente el operador sum_cua obtiene la suma de los cuadrados de los componentes
de la lista, recorriéndola en orden directo. Nótese que se usa el operador car para obtener cada elemento
y usar su valor, así:
(define (sum-cua L)
(if (null? L) 0
(+ (* (car L) (car L))(sum-cua (cdr L)))
)
)
(define (pertenece? E L)
(if (null? L) #f
(if (= E (car L))#t
(pertenece? E (cdr L))
Capítulo 10: Valores y Tipos Compuestos.
294
)
)
)
A pesar de que el proceso asociado al operador del ejemplo anterior, es muy semejante a
los asociados con los dos operadores que le anteceden, en este caso, debe tenerse en cuenta
al determinar el orden del proceso que el recorrido puede terminar luego de visitar
cualquiera de las componentes de la lista. En efecto, el número de veces que se lleva a
cabo la reescritura es, en este caso, aleatorio, pudiendo ser un número cualquiera entre 1 y
N. En estos casos es conveniente valorar el orden del proceso con el peor de los casos del
fenómeno aleatorio. Así, diremos que el orden del proceso en el peor de los casos es O(N).
Nótese que en la definición de los dos primeros operadores se utilizó un proceso recursivo.
Igualmente podría haberse usado un proceso iterativo.
Los operadores siguientes obtienen el tamaño de una lista de enteros y la suma de los cuadrados de sus
elementos, por medio de un proceso ITERATIVO, así:
(define (largo-i L)
(if (null? L) 0 (lrg-it L 0))
)
(define (lrg-it L N)
(if (null? L) N
(lrg-it (cdr L) (+ N 1))
)
)
(define (sum-cua-i L)
(if (null? L) 0 (sucua-it L 0))
)
(define (sucua-it L T)
(if (null? L) T
(sucua-it (cdr L) (+ (* (car L) (car L)) T))
)
)
Los recorridos en orden inverso en SCHEME no son tan simples como el recorrido en
orden directo. Esto se debe a que no existe un operador nativo, equivalente al car, que de
como resultado inmediato el último componente, ni un operador nativo, equivalente al cdr,
que de como resultado la sublista que queda quitando el último componente.
Una posible estrategia para lleva a cabo este recorrido es usar una función auxiliar que
provea en cada iteración el elemento adecuado y la sublista adecuada.
En la definición siguiente el operador largo obtiene el tamaño de la lista, recorriéndola en orden inverso
y usando un proceso iterativo, así:
Capítulo 10: Valores y Tipos Compuestos.
295
(define (largo L) (largo_ite 0 L))
(define (largo_ite T L)
( if (null? L) T (largo_ite (+ 1 T) (cdr_inverso L)) )
)
En esta definición se usó el operador cdr_inverso que tiene como objeto reducir la lista en un
componente pero suprimiendo el último, en lugar del primero. Este operador tendría, sin embargo, un
nivel de complejidad algorítmica O(N), mientras que el operador cdr tiene una complejidad algorítmica
O(1).
En la especificación siguiente el operador > |..| obtiene el tamaño de la lista, recorriéndola en orden
directo, mientras que el operador < |..| obtiene el tamaño de la lista, recorriéndola en orden inverso.
fmod LISTA-INT is
protecting INT .
sort ListInt .
subsort Int < ListInt .
op nilLint : -> ListInt [ctor] .
op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op >|_| : ListInt -> Int .
op <|_| : ListInt -> Int .
var L : ListInt . var I : Int .
eq >| nilLint | = 0 .
eq >| I | = 1 .
eq >| I L | = 1 + >| L | .
eq <| nilLint | = 0 .
eq <| I | = 1 .
eq <| L I | = 1 + >| L | .
endfm
Donde es importante notar que el emparejamiento determina que se asocie el primero o último elemento
de la lista a la variable I (que solo se puede asociar a un entero), y la sublista restante a la variable L (que
Capítulo 10: Valores y Tipos Compuestos.
296
debe asociarse al resto para emparejar).
Nótese también que es necesaria una ecuación particular para el caso de una lista con un sólo elemento,
ya que, como se explicó en el Ejemplo 82, en las listas cuyo elemento identidad es el nilLint se asume
que solo aparece nilLint en la lista vacía (a diferencia del SCHEME en el que el nilLint es siempre el
último elemento de la lista). Se deja al lector la tarea de definir el operador en orden inverso para el caso
en que la lista tiene siempre el nilLint al final.
fmod LISTA-INT-SUMCUA is
protecting INT .
sort ListInt .
subsort Int < ListInt .
op nilLint : -> ListInt [ctor] .
op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op SCua_ : ListInt -> Int .
var I : Int . var L : ListInt .
eq SCua I = I * I .
eq SCua nilLint = 0 .
eq SCua (I L) = I * I + SCua L .
endfm
Que produce el resultado que se muestra a continuación:
fmod LISTA-INT-PERTENECE is
protecting INT .
protecting BOOL .
sort ListInt .
subsort Int < ListInt .
op nilLint : -> ListInt [ctor] .
op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op _in_ : Int ListInt -> Bool .
vars I J : Int . var L : ListInt .
eq I in nilLint = false .
eq I in I = true .
eq I in (I L) = true .
ceq I in J = false if(I =/= J) .
ceq I in J L = I in L if(I =/= J) .
endfm
Que produce los resultados que se muestran a continuación:
El número de reescrituras requeridas para llevar a cabo los procesos definidos en el ejemplo
anterior es el tamaño de la lista. Si asumimos que las operaciones en cada reescritura del
proceso, incluida el emparejamiento, es O(1)160, el orden de los procesos definidos para el
pero de los casos, es O(N) siendo N el tamaño de la lista. Se deja al lector como ejercicio
validar la conjetura de que el emparejamiento es siempre O(1).
Nótese que a diferencia del SCHEME, en este caso, el orden del proceso no cambia con el
sentido del recorrido. Esto gracias a que la selección de las componentes se apoyó en el
emparejamiento en ambos casos. La posibilidad de emparejar una variable entera con el
último elemento de la lista es, por otro lado, debida a que por la propiedad asociativa todos
los elementos de la lista están al mismo nivel de encapsulamiento. Así la posible ganancia
en el orden sería una consecuencia directa del uso del atributo ecuacional assoc en la
declaración del constructor.
Se deja al lector como ejercicio plantear en MAUDE la definición de los operadores, de tal
manera que determinen un proceso iterativo.
10.4.3.3 Recorridos básicos sobre Listas en PROLOG.
Para los dos primeros operadores, al igual que en el SCHEME se plantea un proceso que en
cada paso procesa el elemento de la cabeza de la lista. Para acceder a este elemento, sin
embargo, ahora se usa la unificación.
En la definición siguiente el operador largo obtiene el tamaño de la lista, recorriéndola en orden directo.
Nótese que no es necesario obtener la cabeza de las lista ya que su valor no afecta el resultado., así:
largo([],0) .
largo([_|T],R) :- largo(T,R1), R is 1+R1 .
En la definición siguiente el operador sum_cua obtiene la suma de los cuadrados de los componentes
de la lista, recorriéndola en orden directo. Nótese que se usa un término con variables del operador [_|_]
para que pro medio de la unificación las variables X y T tomen el valor de la cabeza y de la cola de la
lista respectivamente, así:
sum_cua([],0) .
sum_cua([X|T],R) :- sum_cua(T,R1), R is X*X+R1 .
En ejecución.
1 ?- largo([1,4,6,7],X).
160 Esta suposición no es validada en este trabajo, y depende de la manera como el intérprete represente la lista. Así, si los
elementos de la lista se representan en unidades de memoria de igual tamaño, el emparejamiento que asocia una variable
al primer elemento y al último será O(1); en contraste, si los elementos de la lista se representan en unidades de diferente
tamaño, el emparejamiento para el primer elemento será de O(1) y el emparejamiento para el último elemento
probablemente será O(N)
Capítulo 10: Valores y Tipos Compuestos.
298
X = 4.
2 ?- sum_cua([1,2,3],X).
X = 14.
pertenece(X,[X|_]) .
pertenece(X,[_|T]) :- pertenece(X,T) .
En ejecución.
1 ?- pertenece(2,[1,3,7]).
false.
2 ?- pertenece(2,[1,2,3]).
true .
Nótese que en la definición de los dos primeros operadores se utilizó un proceso recursivo.
Igualmente podría haberse usado un proceso iterativo.
Los operadores siguientes obtienen el tamaño de una lista de enteros y la suma de los cuadrados de sus
elementos, por medio de un proceso ITERATIVO, así:
largo_i(L,R) :- largo_i(L,R,0) .
largo_i([],R,R) .
largo_i([_|T],R,A) :- A1 is A+1, largo_i(T,R,A1) .
sum_cua_i(L,R) :- sum_cua_i(L,R,0.0) .
sum_cua_i([],R,R) .
sum_cua_i([X|T],R,A) :- A1 is A+X*X, sum_cua_i(T,R,A1) .
En ejecución.
1 ?- largo_i([1,3,7],R).
R = 3.
2 ?- sum_cua_i([1,2,3],R).
R = 14.0.
(define (iesimo I L)
(if (null? L) -1
(if (= I 1) (car L)
(iesimo (- I 1) (cdr L))
)
)
)
Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I de una lista
es elemento de orden I-1 de la cola de la lista cuando I es diferente de 1.
En la definición siguiente el operador primero-menor-que obtiene el primer componente (de izquierda
a derecha), cuyo valor satisface la condición de ser menor que un valor dado, retornando -1 en caso de
que éste no exista.
(define (primero-menor-que I L)
(if (null? L) -1
(if (< (car L) I) (car L)
(primero-menor-que I (cdr L))
)
)
)
La similitud de las dos definiciones presentadas nos hace pensar en la posibilidad de crear
un operador genérico que busque un elemento recibiendo como argumento la condición de
acierto. Este problema, será tratado en el capítulo 10.
La diferencia del tercer operador con los dos anteriores es que ahora se deben tener en
cuenta el valor de dos componentes consecutivas de la lista.
Capítulo 10: Valores y Tipos Compuestos.
300
(define (primero-menor-que-anterior L)
(if (or (null? L) (null? (cdr L))) -1
(if (< (car (cdr L)) (car L)) (car (cdr L))
(primero-menor-que-anterior (cdr L))
)
)
)
Donde la condición de inexistencia debe contemplar tanto el caso de que la lista sea vacía como el caso
de que no haya sino un elemento haciendo imposible la comparación.
El cuarto operador, por su parte, no sólo debe recorrer la lista, sino que debe construir otra
usando el operador cons.
En la definición siguiente, el operador select-menor-que obtiene una lista que contiene los elementos
de la lista dada cuyo valor es menor que un umbral dado.
(define (select-menor-que I L)
(if (null? L) '()
(if (< (car L) I) (cons (car L) (select-menor-que I (cdr L)))
(select-menor-que I (cdr L))
)
)
)
En la definición siguiente, el operador select-menor-que-i obtiene una lista que contiene los elementos
de la lista dada cuyo valor es menor que un umbral dado, por medio de un proceso iterativo.
Donde la lista resultante tiene los elementos debidos pero en el orden inverso al que tenían en la lista
original.
Capítulo 10: Valores y Tipos Compuestos.
301
Por estar basados en recorridos simples sobre la lista, es fácil ver que el orden de los
procesos asociados a las definiciones de esta sección, es en el peor de los casos O(N),
siendo N el tamaño de la lista.
10.4.4.2 Selección sobre Listas en MAUDE.
Al igual que antes los dos primeros operadores se apoyan en recorridos que se interrumpen
al encontrar la componente buscada. En este caso, sin embargo, proveemos un valor
especial del tipo definido en la teoría como señal de que la componente buscada no se
encuentra en la lista.
fmod LISTA-INT-IESIMO is
protecting INT .
sort ListInt .
subsort Int < ListInt .
eq E[1] = E .
eq (E L)[1] = E .
ceq (E L)[I] = L[I + (- 1)] if(I =/= 1) .
ceq E[I] = error-indice if(I =/= 1) .
eq nilLint[I] = error-indice .
endfm
Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I de una lista
es elemento de orden I-1 de la cola de la lista cuando I =/= 1.
fmod LISTA-INT-MENORQUE is
protecting INT .
sort ListInt .
subsort Int < ListInt .
Para acceder a dos valores consecutivos de la lista se usará, como siempre, la unificación.
En la definición siguiente, el operador {X<X1en _} obtiene el primer componente cuyo valor es menor
que el valor del componente anterior, retornando un valor especial de error en caso de que éste no exista.
fmod LISTA-INT-MENORQUE-ANTERIOR is
protecting INT .
sort ListInt .
subsort Int < ListInt .
op nilLint : -> ListInt [ctor] .
op _ _ : ListInt ListInt -> ListInt [ctor assoc ] .
op {X2<X1en_} : ListInt -> Int .
op no-existe-X2-menor-que-X1 : -> Int [ctor] .
endfm
Donde la condición de inexistencia debe contemplar tanto el caso de que la lista sea vacía como el caso
de que no haya sino un elemento haciendo imposible la comparación.
El cuarto operador, por su parte, debe construir otra usando el operador constructor.
En la definición siguiente, el operador {*Xen_/X<_} obtiene una lista que contiene los elementos de la
lista dada cuyo valor es menor que un umbral dado.
fmod LISTA-INT-MENORQUE is
Capítulo 10: Valores y Tipos Compuestos.
303
protecting INT .
sort ListInt .
subsort Int < ListInt .
endfm
Que produce los resultados que se muestran a continuación:
La especificación del operador del ejemplo anterior, que define un proceso iterativo no
ofrece dificultad alguna en MAUDE.
En la definición siguiente, el operador {*Xen_/X<_} del ejemplo anterior es definido de manera que
genere un proceso iterativo.
fmod LISTA-INT-MENORQUE-IT is
protecting INT .
sort ListInt .
subsort Int < ListInt .
Por estar basados en recorridos simples sobre la lista, es fácil ver que el orden de los
procesos asociados a las definiciones de esta sección, es para el peor de los casos O(N),
siendo N el tamaño de la lista.
10.4.4.3 Selección sobre Listas en PROLOG.
Los dos primeros dos operadores, recorren la lista, elemento por elemento, hasta hallar el
buscado o agotar la lista.
iesimo(1,[X|_],X) .
iesimo(_,[],-1) .
iesimo(I,[_|T],X) :- I1 is I-1, iesimo(I1,T,X) .
Donde el lector puede notar, de nuevo, que la idea clave de la definición es que el elemento de orden I
de una lista es elemento de orden I-1 de la cola de la lista cuando I es diferente de 1.
En la definición siguiente el operador primero_menor_que obtiene el primer componente (de
izquierda a derecha), cuyo valor satisface la condición de ser menor que un valor dado, retornando -1 en
caso de que éste no exista.
primero_menor_que(I,[X|_],X) :- X<I .
primero_menor_que(_,[],-1) .
primero_menor_que(I,[_|T],X) :- primero_menor_que(I,T,X) .
En ejecución.
1 ?- iesimo(3,[1,2,3,4,5],X).
X=3.
2 ?- primero_menor_que(5,[6,7,4,6,1],X).
X=4.
3 ?- iesimo(7,[1,2,3],X).
X = -1 .
4 ?- primero_menor_que(4,[7,8,9],X).
X = -1
Es fácil observar que el comportamiento de los procesos de la búsqueda planteados es esencialmente el
de un proceso iterativo, estable en memoria. Para ilustrar este hecho presentamos a continuación la tabla
que muestra la evolución del objetivo para la primera de las evocaciones mostradas del predicado
iesimo, así:
La diferencia del tercer operador con los dos anteriores es que ahora se deben tener en
cuenta el valor de dos componentes consecutivas de la lista.
primero_menor_que_anterior([],-1) .
primero_menor_que_anterior([_|[]],-1) .
primero_menor_que_anterior([X|[Y|_]],Y) :- Y<X .
primero_menor_que_anterior([_|T],X) :- primero_menor_que_anterior(T,X) .
Donde la condición de inexistencia debe contemplar tanto el caso de que la lista sea vacía como el caso
de que no haya sino un elemento haciendo imposible la comparación.
En ejecución.
1 ?- primero_menor_que_anterior([],X).
X = -1.
2 ?- primero_menor_que_anterior([1],X).
X = -1 .
3 ?- primero_menor_que_anterior([1,2,3],X).
X = -1 .
4 ?- primero_menor_que_anterior([7,9,5,8,4],X).
X=5
El cuarto operador, por su parte, no sólo debe recorrer la lista, sino que debe construir otra
usando el operador _ | _ .
En la definición siguiente, el operador select_menor_que obtiene una lista que contiene los elementos
de la lista dada cuyo valor es menor que un umbral dado.
select_menor_que(_,[],[]) .
select_menor_que(E,[X|T],[X|W]) :- X<E, select_menor_que(E,T,W) .
select_menor_que(E,[_|T],W) :- select_menor_que(E,T,W) .
En ejecución.
1 ?- select_menor_que(4,[],X).
X = [] .
2 ?- select_menor_que(4,[7,1,6,3],R).
R = [1, 3]
Para ilustrar la manera como ocurre el retroceso y se construye la respuesta, presentamos a continuación
la tabla que muestra la evolución del objetivo para la segunda evocación mostrada del predicado
Capítulo 10: Valores y Tipos Compuestos.
306
select_menor_que, así:
Donde un paso i con * corresponden al nuevo paso i luego de un retroceso. Nótese que la respuesta se
construye con base en las substituciones de las variables en las diferentes unificaciones, así:
En la definición siguiente, el operador select_menor_que_i obtiene una lista que contiene los
elementos de la lista dada cuyo valor es menor que un umbral dado, por medio de un proceso iterativo.
select_menor_que_i(E,L,R) :- select_menor_que_i(E,L,R,[]) .
select_menor_que_i(_,[],R,R) .
select_menor_que_i(E,[X|T],R,A) :- X<E, select_menor_que_i(E,T,R,[X|A]) .
select_menor_que_i(E,[_|T],R,A) :- select_menor_que_i(E,T,R,A) .
Donde la lista resultante tiene los elementos debidos pero en el orden inverso al que tenían en la lista
original.
En ejecución.
1 ?- select_menor_que_i(4,[],X).
X = [] .
2 ?- select_menor_que_i(4,[7,1,6,8,3,6,2],X).
X = [2, 3, 1]
En la definición siguiente, el operador insert coloca un componente dado en una posición dada de la
lista, incluyéndolo al principio si la posición dada es menor o igual a cero, e incluyéndolo de último si la
posición dada es mayor que el tamaño de la lista.
(define (insert E I L)
(if (null? L) (list E)
(if (<= I 1) (cons E L)
(cons (car L) (insert E (- I 1) (cdr L)))
)
)
)
Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I en la lista, es
el elemento de orden I-1 en la cola de la lista.
En la definición siguiente el operador borre suprime el componente localizado en una posición dada de
la lista, o no suprime ninguno si la posición dada es inválida.
(define (borre I L)
(if (null? L) L
(if (= I 1) (cdr L)
(cons (car L) (borre (- I 1) (cdr L)))
)
)
)
Capítulo 10: Valores y Tipos Compuestos.
308
En la definición siguiente el operador borre-e suprime los componentes que tienen un valor dado.
(define (borre-e E L)
(if (null? L) L
(if (= (car L) E) (borre-e E (cdr L))
(cons (car L) (borre-e E (cdr L)))
)
)
)
Donde el lector puede notar que el operador borra, no sólo la primera ocurrencia del elemento sino todas
ellas.
Si el lector sigue la línea de pensamiento que aplicamos antes para derivar el orden de los
procesos asociados con los operadores definidos con base en recorridos, podrá fácilmente
intuir que el orden de los procesos para los operadores definidos en el ejemplo anterior es
O(N).
En la definición siguiente, el operador pode elimina los elementos repetidos de la lista dejando la última
ocurrencia de cada elemento. Para ello usa el operador pertenece? definido previamente en el Ejemplo
84.
(define (pode L)
(if (null? L) L
(if (pertenece? (car L) (cdr L)) (pode (cdr L))
(cons (car L) (pode (cdr L)))
)
)
)
Donde el lector puede notar que la definición del operador difiere de la del anterior sólo en que la
condición de eliminación del componente es diferente.
La posibilidad de crear un operador que reciba como argumento, de forma genérica, la condición de
eliminación, será tratada en el capítulo 10
Para derivar el orden del proceso asociado al operador definido en el ejemplo anterior, se
debe tener en cuenta que en cada paso de la reescritura especificada, se evoca a otro
operador que induce, a su vez, tantas reescrituras como el tamaño de la lista que recibe.
Así, el número de reescrituras total para el peor de los casos es (N-1)+(N-2)+(N-3)+.....+2,
por lo que el orden del proceso es ahora O(N2).
Para definir el operador que adiciona al final de una lista otra lista dada, se debe tener en
cuenta que los elementos de la lista adicionada deben quedar en el nivel de profundidad
adecuado. En efecto, la simple aplicación del operador cons a las dos listas dadas, sólo
resultaría en una pareja cuyas componentes serían las dos listas, sin que dicha pareja se
constituya en una lista.
Capítulo 10: Valores y Tipos Compuestos.
309
En la definición siguiente, el operador append obtiene una lista que contiene los elementos de dos listas
dadas.
Donde la evocación recursiva del operador definido tiene como único objeto profundizar en los niveles
de encapsulamiento de L1, para llegar al nivel donde aparece la lista vacía. Esta lista es, entonces,
substituida por la lista L2.
Para intuir el orden del proceso asociado al operador definido en el ejemplo anterior, es
suficiente con notar que la reescritura especificada en la definición se debe llevar a cabo
tantas veces como los niveles de encapsulamiento que existen en la primera lista. Así si se
acepta que las operaciones en cada paso de dicha reescritura se asocian con un proceso de
orden O(1), el orden total del proceso es O(N), siendo N el tamaño de la primera lista.
10.4.5.2 Modificadores de listas en MAUDE.
Tal como en el caso del SCHEME los cuatro primeros operadores se pueden definir
fácilmente, por medio de un recorrido que en cada paso indica si el elemento visitado hace
parte o no de la lista resultante o si debe ser substituido por otro elemento diferente.
En la definición siguiente, el operador _[_]<=_ coloca un componente dado en una posición dada de la
lista, incluyéndolo al principio si la posición dada es menor o igual a cero, e incluyéndolo de último si la
posición dada es mayor que el tamaño de la lista.
fmod LISTA-INT-INSERT-IESIMO is
protecting INT .
sort ListInt .
subsort Int < ListInt .
Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I en la lista, es
el elemento de orden I-1 en la cola de la lista.
fmod LISTA-INT-SUPRIMA-IESIMO is
protecting INT .
sort ListInt .
subsort Int < ListInt .
En la definición siguiente el operador _not-in_ suprime los componentes que tienen un valor dado.
fmod LISTA-INT-NO-PERTENECE is
protecting INT .
sort ListInt .
subsort Int < ListInt .
Donde el lector puede notar que el operador borra, no sólo la primera ocurrencia del elemento sino todas
ellas.
fmod LISTA-INT-PODE is
protecting INT .
protecting BOOL .
sort ListInt .
subsort Int < ListInt .
eq I in nilLint = false .
eq I in I = true .
eq I in (I L) = true .
ceq I in J = false if(I =/= J) .
ceq I in J L = I in L if(I =/= J) .
eq set(nilLint) = nilLint .
eq set(I) = I .
ceq set(I L) = I set(L) if(not(I in L)) .
ceq set(I L) = set(L) if(I in L) .
endfm
Donde el lector puede notar que la definición del operador difiere de la del anterior sólo en que la
condición de eliminación del componente es diferente.
Dado que en la definición de los operadores del ejemplo anterior, se especifican reescrituras
similares a las de su correspondiente definición en SCHEME, los procesos asociados a
dichos operadores tiene el mismo orden que sus correspondientes en SCHEME. Nótese
que aquí, de nuevo, se parte de asumir que el emparejamiento es siempre O(1).
El operador que adiciona al final de una lista otra lista dada en MAUDE, simplemente
aplica el constructor de listas definido antes. La simplicidad de esta definición es una
consecuencia directa de la eliminación de los niveles de profundidad para los componentes,
asociada a la forma como se definió en MAUDE el constructor de la lista.
En la definición siguiente, el operador _|_ adiciona a los elementos de una lista otra lista dada dando
como resultado una lista.
El orden del proceso asociado con dicho operador es la misma que la del asociado al
constructor de listas (que por simplicidad asumimos O(1)).
10.4.5.3 Modificadores de listas en PROLOG.
Los cuatro primeros operadores se pueden definir fácilmente, por medio de un recorrido
que en cada paso determina si el elemento visitado hace parte o no de la lista resultante o si
debe ser substituido por otro elemento diferente.
Nótese, además, que el resultado de la operación debe ser siempre una lista.
Capítulo 10: Valores y Tipos Compuestos.
313
En la definición siguiente, el operador insert coloca un componente dado en una posición dada de la
lista, incluyéndolo al principio si la posición dada es menor o igual a cero, e incluyéndolo de último si la
posición dada es mayor que el tamaño de la lista.
insert(_,E,[],[E|[]]) .
insert(I,E,L,[E|L]) :- I=<1 .
insert(I,E,[X|T],[X|W]) :- I1 is I-1, insert(I1,E,T,W) .
Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I en la lista, es
el elemento de orden I-1 en la cola de la lista. Al ejecutarse:
1 ?- insert(3,6,[],X).
X = [6] .
2 ?- insert(3,6,[1,3],X).
X = [1, 3, 6] .
3 ?- insert(3,6,[1,4,7,4,2],X).
X = [1, 4, 6, 7, 4, 2]
En la definición siguiente el operador borre suprime el componente localizado en una posición dada de
la lista, o no suprime ninguno si la posición dada es inválida.
borre(_,[],[]) .
borre(I,L,L) :- I<1 .
borre(1,[_|T],T) .
borre(I,[X|T],[X|W]) :- I1 is I-1, borre(I1,T,W) .
Al ejecutarse:
1 ?- borre(5,[],X).
X = [] .
2 ?- borre(-4,[1,2,3],X).
X = [1, 2, 3] .
3 ?- borre(2,[1,2,3],X).
X = [1, 3]
En la definición siguiente el operador borre_e suprime todas las componentes que tienen un valor dado.
borre_e(_,[],[]) .
borre_e(E,[E|T],W) :- borre_e(E,T,W) .
borre_e(E,[X|T],[X|W]) :- borre_e(E,T,W) .
Al ejecutarse:
1 ?- borre_e(4,[],X).
X = [] .
2 ?- borre_e(4,[1,4,3,5,4,6,4],X).
X = [1, 3, 5, 6]
En la definición siguiente, el operador pode elimina los elementos repetidos de la lista dejando la última
ocurrencia de cada elemento.
pode([],[]) .
pode([E|T],W) :- pertenece(E,T), pode(T,W) .
pode([E|T],[E|W]) :- pode(T,W) .
Al ejecutarse.
Capítulo 10: Valores y Tipos Compuestos.
314
1 ?- pode([],X).
X = [].
2 ?- pode([1,2,3],X).
X = [1, 2, 3].
3 ?- pode([1,2,1,3,1,1,5],X).
X = [2, 3, 1, 5]
Al igual que en el SCHEME, para definir el operador que adiciona al final de una lista otra
lista dada, se debe tener en cuenta que los elementos de la lista adicionada deben quedar en
el nivel de profundidad adecuado.
En la definición siguiente, el operador append obtiene una lista que contiene los elementos de dos listas
dadas.
append([],L2,L2) .
append(L1,[],L1) .
append([X|T],L,[X|W]) :- append(T,L,W) .
Donde la evocación recursiva del operador definido tiene como único objeto profundizar en los niveles
de encapsulamiento de L1, para llegar al nivel donde aparece la lista vacía. Esta lista es, entonces,
substituida por la lista L2. Al ejecutarse:
1 ?- append([],[1,2,3],X).
X = [1, 2, 3] .
2 ?- append([1,2,3],[],X).
X = [1, 2, 3] .
3 ?- append([1,2,3],[4,5,6],X).
X = [1, 2, 3, 4, 5, 6]
Para ilustrar la manera como se construye la respuesta, presentamos a continuación la tabla que muestra
la evolución del objetivo para la tercera evocación mostrada del predicado append, así:
Nótese de nuevo que la respuesta se construye con base en las substituciones de las variables en las
diferentes unificaciones, así:
En la definición siguiente, el operador intercambie recorre la lista en orden directo intercambiando los
elementos que estén en desorden.
(define (intercambie L)
(if (or (null? L) (null? (cdr L))) L
(if (< (car L) (car (cdr L)))
(cons (car L) (intercambie (cdr L)))
(cons (car (cdr L)) (intercambie (cons (car L) (cdr (cdr L)))))
)
)
)
Donde el lector puede notar que el intercambio se suspende cuando sólo queda un elemento, y que luego
de una comparación, y un posible intercambio, el elemento mayor de cada pareja vuelve a participar en
la comparación, y posible intercambio, con el elemento siguiente a la pareja.
Por llevar a cabo un recorrido simple, y asumiendo que el proceso en cada paso del
recorrido es del orden O(1), el orden del proceso asociado con el operador del ejemplo es
O(N).
Un primer algoritmo de ordenamiento puede concebirse fácilmente con base en el operador
anterior. En efecto, el lector puede verificar experimentalmente que la aplicación reiterada
del operador anterior conduce eventualmente a una lista ordenada.
(define (buble-sort L)
(if (or (null? L) (null? (cdr L))) L
(buble-sort-ite L (largo L))
)
)
(define (buble-sort-ite L N)
Capítulo 10: Valores y Tipos Compuestos.
316
(if (= N 0) L
(buble-sort-ite (intercambie L) (- N 1))
)
)
Donde el lector puede notar que el intercambio se lleva a cabo tantas veces como el tamaño de la lista.
Dado que la reescritura que evoca el intercambio se lleva a cabo N veces, y que el
algoritmo de intercambio en si mismo tiene el orden O(N), el sort tendrá el orden O(N2).
El algoritmo de ordenamiento más usado actualmente, es el conocido con el nombre de
“Quick Sort” [Hoare 61]. El Quick-Sort es usado además como un ejemplo clásico de la
expresividad de los lenguajes funcionales [Sylvan 2007]. En lugar de describir el algoritmo
en palabras, presentamos la definición del operador correspondiente, con la expectativa de
que sea lo suficientemente descriptivo.
En la definición siguiente, el operador quick-sort lleva a cabo un ordenamiento de la lista por el método
del “Quick Sort”.
(define (quick-sort L)
(if (or (null? L) (null? (cdr L))) L
(append (quick-sort (select-menor-que (car L) (cdr L)))
(cons (car L)
(quick-sort
(select-mayor-igual-que (car L) (cdr L))))
)
)
)
Aunque el orden del proceso derivado de la definición del Quick Sort es, en el peor de los
casos O(N2), en el caso promedio el orden del algoritmo es O(N logN), constituyéndose en
el algoritmo de escogencia para el ordenamiento de listas con un número muy alto de
componentes. Para una descripción completa del Quick Sort y de la derivación del orden
asociado al proceso se refiere al lector a otras fuentes161.
10.4.6.2 Transformación de listas en MAUDE.
El primer operador es un recorrido simple que, en cada iteración, debe acceder a dos
valores consecutivos de la lista y declarar la posición de uno de ellos en la lista resultante.
En la definición siguiente, el operador <-> recorre la lista en orden inverso intercambiando los
elementos que estén en desorden.
sort ListInt .
subsort Int < ListInt .
endfm.....
Donde el lector debe notar el uso de paréntesis para indicar la lista a la que se aplica el operador en cada
caso (la expresión (<-> L E)aplica el operador <-> sólo al L).
Por llevar a cabo un recorrido simple, y asumiendo que el proceso en cada paso del
recorrido es del orden O(1), el orden del proceso asociado con el operador del ejemplo es
O(N).
Al igual que en SCHEME el primer algoritmo de ordenamiento se concibe fácilmente con
base en la aplicación reiterada del operador anterior. El lector puede verificar
experimentalmente que la aplicación reiterada del operador anterior conduce eventualmente
a una lista ordenada.
En la definición siguiente, el operador B>> lleva a cabo un ordenamiento de la lista evocando de forma
repetida el operador intercambie definido arriba.
fmod LISTA-INT-BUBLE is
protecting INT .
sort ListInt .
subsort Int < ListInt .
…..
endfm
Donde el lector puede notar que el algoritmo toma ventaja de que el proceso de intercambio, efectuado
en el orden inverso, siempre deja el elemento menor de la lista de primero.
Dado que la reescritura que evoca el intercambio se lleva a cabo N-1 veces, y que el
algoritmo de intercambio en si mismo tiene el orden O(M) siendo M el tamaño de la lista
que recibe (M va disminuyendo desde N hasta 1), el sort tendrá el orden O(N2).
El operador que implementa el “Quick Sort” es, en este caso, un ejemplo de simpleza y
expresividad (el lector puede comparar la especificación del ejemplo que sigue con la
correspondiente en C++ presentada en [Sylvan 2007]).
En la definición siguiente, el operador Q>> lleva a cabo un ordenamiento de la lista por el método del
“Quick Sort”.
fmod LISTA-INT-QUICKSORT is
protecting INT .
sort ListInt .
subsort Int < ListInt .
Donde se usaron los operadores de selección {*Xen_/X<_} y {*X en_/X>=_}, definidos, el primero en
el Ejemplo 98, y el segundo como ejercicio para el lector.
Tal como fue referido arriba el orden del Quick Sort es, en el peor de los casos O(N2) y en
el caso promedio es O(N logN).
10.4.6.3 Transformación de listas en PROLOG.
Para invertir la lista usaremos un proceso simple iterativo.
Capítulo 10: Valores y Tipos Compuestos.
319
En la definición siguiente, el operador ivertir parte de una lista vacía sobre la que coloca los elementos
de la lista original.
invertir([],[]) .
invertir([E|[]],[E|[]]) .
invertir(L,R) :- invertir(L,R,[]) .
invertir([X|T],R,LI) :- invertir(T,R,[X|LI]) .
invertir([],R,R) .
En ejecución:
1 ?- invertir([1,2,3],X).
X = [3, 2, 1]
El segundo operador es un recorrido simple que, en cada iteración, debe acceder a dos
valores consecutivos de la lista y declarar la posición de uno de ellos en la lista resultante.
En la definición siguiente, el operador intercambie recorre la lista en orden directo intercambiando los
elementos que estén en desorden.
intercambie([],[]) .
intercambie([E|[]],[E|[]]) .
intercambie([X|[Y|T]],[Y|W]) :- X>Y, intercambie([X|T],W) .
intercambie([X|[Y|T]],[X|W]) :- intercambie([Y|T],W)
Donde el lector puede notar que el intercambio se suspende cuando sólo queda un elemento, y que luego
de una comparación, y un posible intercambio, el elemento mayor de cada pareja vuelve a participar en
la comparación, y posible intercambio, con el elemento siguiente a la pareja.
En ejecución:
1 ?- intercambie([],X).
X = [].
2 ?- intercambie([1,2,3],X).
X = [1, 2, 3] .
3 ?- intercambie([3,2,1],X).
X = [2, 1, 3]
Para ilustrar la manera como ocurre el retroceso y se construye la respuesta, presentamos a continuación
la tabla que muestra la evolución del objetivo para la tercera evocación mostrada del predicado
intercambie, así:
Construcción de la respuesta:
bubble_sort([],[]) .
bubble_sort([E|[]],[E|[]]) .
bubble_sort(L,R) :- largo(L,T), bubble_sort(L,R,T) .
bubble_sort(L,L,0) .
bubble_sort(L,R,I) :- intercambie(L,W), I1 is I-1, bubble_sort(W,R,I1) .
Donde el lector puede notar que el intercambio se lleva a cabo tantas veces como el tamaño de la lista.
En ejecución:
1 ?- bubble_sort([],X).
X = [] .
2 ?- bubble_sort([2],X).
X = [2] .
3 ?- bubble_sort([3,2,1],X).
X = [1, 2, 3]
En la definición siguiente, el operador quick-sort lleva a cabo un ordenamiento de la lista por el método
del “Quick Sort”.
quick_sort([],[]) .
quick_sort([E|[]],[E|[]]) .
quick_sort([E|T],R) :- select_menor_que(E,T,Mn), quick_sort(Mn,MnS),
select_mayoroig_que(E,T,My), quick_sort(My,MyS),
append(MnS,[E|MyS],R) .
En ejecución:
1 ?- quick_sort([],X).
X = [].
2 ?- quick_sort([3],X).
X = [3] .
3 ?- quick_sort([3,2,1],X).
X = [1, 2, 3] .
5 ?- quick_sort([3,6,2,5,5,4,1,2],X).
X = [1, 2, 2, 3, 4, 5, 5, 6]
Capítulo 10: Valores y Tipos Compuestos.
321
Gestión de Árboles.
Al igual que para las listas, en el caso de los árboles se debe contar con operadores que
permitan construirlos, modificarlos, transformarlos y seleccionar sus componentes. En esta
sección nos limitaremos, sin embargo, a ilustrar de forma superficial la manera de construir
y manipular árboles usando los elementos de los lenguajes analizados. Esperamos, con
ello, que el lector adquiera las bases necesarias para llevar a cabo una especificación más
completa por su propia cuenta.
Para ilustrar la gestión de los árboles nos apoyaremos en el denominado “árbol binario de
búsqueda”. Para este árbol presentaremos un constructor, un modificador y un selector.
10.5.1 Ejemplo: Árbol Binario de Búsqueda.
El árbol binario de búsqueda es uno de los compuestos más usados en computación. Este
árbol fue concebido como un mecanismo para agilizar las operaciones de inserción y
localización de las componentes de un compuesto iterado, y, en alguna de sus variantes, es
piedra fundamental en todos los gestores de datos.
El árbol binario de búsqueda se puede caracterizar por una serie de condiciones que debe
cumplir, así:
Es un iterado donde en cada componente hay un valor de identificación que
es único, diferenciándolo de los demás componentes. Los valores de
identificación tienen la característica de ser ordenables.
Con excepción de las hojas, todos los componentes pueden ser padres de
máximo dos hijos diferentes, que llamaremos “izquierdo” y “derecho”.
Cuando un nodo tiene sólo un hijo, este puede ser tanto el izquierdo como el
derecho, dependiendo de su valor de identificación.
Para todo componente se cumple que el hijo de la derecha, si existe, posee
un valor de identificación menor que el suyo propio, y el hijo de la
izquierda, si existe, posee un valor de identificación mayor que el suyo
propio.
Cada hijo es la raíz de un árbol (o sub-árbol), siendo disjuntos los árboles
que parten del hijo izquierdo y del derecho.
10.5.2 Construcción del Árbol Binario de Búsqueda.
Para tener árboles binarios de búsqueda específicos, se debe contar con un constructor.
Para definir el constructor tomaremos ventaja de la naturaleza recursiva del árbol binario de
búsqueda, Así:
Un árbol binario de búsqueda puede siempre verse como un componente (la
raíz) unido a dos árboles binarios de búsqueda disjuntos.
Cuando alguno de los hijos de un componente falta, podemos considerar que
el árbol que se le asocia es un árbol vacío.
10.5.2.1 Construcción del Árbol en SCHEME.
Al igual que con la lista, los elementos del árbol deben ser unidos formando pares por
medio del operador cons. A la manera de unir los elementos del árbol (usando pares), la
denominaremos su “representación”.
Capítulo 10: Valores y Tipos Compuestos.
322
Para formar el árbol con base en pares, usaremos la representación que se describe a
continuación:
Un árbol binario de búsqueda es un par que junta su raíz con sus sub-
árboles.
Los sub-árboles se juntan en un par que tiene como su primer elemento el
sub-árbol de la derecha, y como su segundo elemento el sub-árbol de la
izquierda.
En caso de que alguno de los sub-árboles sea el árbol vacío este será
representado por medio de un nil (ó un '(), en el MIT-SCHEME).
La construcción para un árbol binario de búsqueda que lista que tiene como sus componentes los
números enteros 1, 2, 5, 9 y 11, con el número 5 en la raíz, sería entonces como sigue:
(define A1
(cons 5 (cons (cons 2 (cons
(cons 1 (cons '() '()))
(cons 3 (cons '() '()))
)
)
(cons 9 (cons
(cons 7 (cons '() '()))
'()
)
)
)
)
)
El operador make-Btree, recibe un componente y dos árboles dando como resultado un árbol binario
de búsqueda elemental, que tiene al componente como su raíz, y a los dos sub-árboles como los sub-
árboles que parten de la raíz, así:
El operador get_ArI, obtiene el sub-árbol Izquierdo de un árbol suministrado como operando, así:
El operador get_ArD, obtiene el sub-árbol derecho de un árbol suministrado como operando, así:
En la especificación MAUDE siguiente se propone un sort asociado con todos los árboles binarios de
búsqueda con componentes enteras, que llamaremos BTreeInt, se define un constructor par el árbol y
una constante para representar el árbol vacío, así:
fmod ARBOL-BINARIO-INT is
protecting INT .
sort BTreeInt .
endfm
Donde el árbol representado por la constante A1 es el que se muestra en la figura siguiente:
3 8
2 4 15
1 6 13
En la definición siguiente, el operador esta-en? recorre el árbol de la manera referida arriba para
verificar si un elemento se encuentra o no, en el árbol.
(define (esta-en? E A)
(if (null? A)#f
(if (= E (get-root A))#t
(if (< E (get-root A)) (esta-en? E (get-ArI A))
(esta-en? E (get-ArD A))
)
)
)
)
Una característica importante del proceso asociado al operador del ejemplo anterior, es que
no debe visitar todos los elementos del árbol para hallar el que busca. Esto se debe a que en
cada nudo visitado desecha una de las ramas y prosigue el recorrido visitando sólo
elementos de la otra. El efecto de este comportamiento es una sensible reducción en le
tiempo de proceso, con respecto al operador equivalente de la lista.
Para tener una intuición del orden del algoritmo, el lector debe notar que el número de
visitas, correspondiente con el número de reescrituras determinadas por la definición,
depende de la “forma” del árbol.
En efecto, es posible que de forma sistemática, ningún componente del árbol tenga hijos a
su izquierda (o a su derecha), constituyéndose en un árbol “degenerado”. En este caso la
relación entre los elementos es equivalente a la que tendrían en una lista. Para este caso, el
orden del proceso de localización de un elemento es, en el peor de los casos, O(N), siendo
N el número de componentes del árbol.
Es posible también, que los sub-árboles que parten de cada nodo del árbol tengan el mismo
número de elementos, constituyéndose en un árbol perfectamente “balanceado”162. En este
caso el número de visitas que deben efectuarse para llega a una hoja del árbol es siempre
log2N, siendo N el número de componentes en el árbol163. En este caso el orden del
proceso de localización de un elemento es, en el peor de los casos O(log2N).
10.5.3.2 Localización de un Componente del Árbol en MAUDE.
162 Caso en el que el número de componentes del árbol debe ser una potencia de N.
163 Esta relación es fácil de intuir y de demostrar, si se organizan los componentes formando capas o “niveles”
correspondientes con el número de ancestros que tienen, y se tiene en cuenta que, en un árbol perfectamente balanceado,
en cada nivel el número de elementos es el doble de los que hay en el nivel anterior.
Capítulo 10: Valores y Tipos Compuestos.
326
En el ejemplo siguiente se define un operador que indica si un componente con un valor de
identificación dado se encuentra o no se encuentra en el árbol.
Por implementar el mismo algoritmo, el orden del proceso asociado al operador del ejemplo
anterior es el mismo que el de su correspondiente operador en SCHEME.
10.5.4 Inserción de un Componente en el Árbol Binario de Búsqueda.
Cada vez que se inserta un nuevo componente, en el árbol, este debe situarse en el lugar
adecuado para mantener las relaciones entre los nodos y sus hijos. Este lugar, por otra
parte, no es otro que el lugar donde el componente debería estar de ser buscado.
Capítulo 10: Valores y Tipos Compuestos.
327
10.5.4.1 Inserción de un Componente en el Árbol en SCHEME.
La inserción de un nuevo componente en SCHEME es, esencialmente, un recorrido de
búsqueda del elemento a ser insertado, que va declarando en cada visita la composición del
árbol resultante, y lo inserta al momento de no encontrarlo en el árbol.
En la definición siguiente, el operador inserte recorre el árbol buscando el elemento a ser insertado y lo
coloca en lugar que le corresponde al momento de no hallarlo, así:
(define (inserte E A)
(if (null? A) (make-e-BTree E)
(if (= E (get-root A)) A
(if (< E (get-root A))
(make-BTree (get-root A) (inserte E (get-ArI A))
(get-ArD A)
)
(make-BTree (get-root A) (get-ArI A)
(inserte E (get-ArD A))
)
)
)
)
Por estar el algoritmo de inserción, fundamentado en una localización del elemento a ser
insertado, si se supone que el orden del proceso efectuado en cada visita es O(1), entonces
el orden del proceso de inserción sería el mismo que el de la localización de un elemento
con base en su valor de identificación.
La forma del árbol, por otro lado, depende del orden en que se insertan sus componentes.
En efecto, si éstas se insertan en orden por su valor de identificación el árbol resultante es
degenerado; en cambio, si se insertan de forma aleatoria el árbol tiende a tener una forma
cercana a la de uno balanceado. Una manera de garantizar que el árbol mantiene una forma
cercana a la balanceado es la de mejorar el algoritmo de inserción. El lector interesado
puede consultar en [Arango 2006, SEC, 9.3] para hallar mejores algoritmos de inserción.
10.5.4.2 Inserción de un Componente en el Árbol en MAUDE.
En el ejemplo siguiente se define un operador que inserta un componente en el lugar que le
corresponde en el árbol, siguiendo la misma estrategia que el correspondiente en SCHEME.
Por implementar el mismo algoritmo, el orden del proceso asociado al operador del ejemplo
anterior es el mismo que el de su correspondiente operador en SCHEME.
Ejercicios propuestos
Datos compuestos estructurados.
1- En [Abelson 85, sec. 2.1.1 ay 2.1.2] se definen directamente los constructores y
selectores de un tipo estructurado, con base en pares, sin utilizar el tipo registro ofrecido
pro el SCHEME. Lea las secciones correspondientes y lleve a cabo los ejercicios
siguientes: 2.1, 2.2, 2.3.
2- Aritmética de Intervalos: Realizar basándose en los lenguajes analizados, los siguientes
ejercicios de la sección 2.1.4 en [Abelson 85, sec. 2.1.4]: 2.7, 2.8, 2.9, 2.10, 2.11.
3- Punto en un plano: Elabore en MAUDE y en SCHEME un tipo compuesto estructurado
que represente un punto en un plano. Defina operadores para:
Hallar las coordenadas cartesianas del punto.
Hallar las coordenadas polares del punto.
Hallar la distancia entre dos puntos.
Hallar el área de un rectángulo cuyos vértices son dos puntos dados.
4- Rectángulo en un plano: Elabore en MAUDE y en SCHEME un tipo compuesto
estructurado que represente un rectángulo en un plano con base en dos puntos que
representen dos esquinas diagonalmente opuestas del rectángulo cualquiera que estas
sean. Elabore luego operadores para:
Hallar el área de un rectángulo.
Hallar el rectángulo más pequeño que incluye dos rectángulos dados.
Desplazar el rectángulo una distancia dada representada por un punto.
Deformar el rectángulo con base en un desplazamiento de la esquina superior izquierda
representado por medio de un punto.
Determinar si un punto dado es o no es interior al rectángulo.
4- Rectángulo normalizado: Asuma que los puntos que representan el rectángulo en el
ejemplo anterior, son siempre su esquina superior izquierda y su esquina inferior
derecha. Reelabore para este caso los operadores del ejercicio anterior.
4- Elabore en SCHEME y MAUDE un tipo compuesto estructurado que registre la
información relativa a un estudiante del curso “Lenguajes Declarativos”.
Pares
1- SCHEME: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2.2]: 2.25, 2.26.
Capítulo 10: Valores y Tipos Compuestos.
331
2- Dibuje la representación de cajas de las listas referidas y resultantes en los ejercicios
2.26, 2.27 de [Abelson 85, sec. 2.2.2]
Listas
Definición SCHEME
1-: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2]: 2.17, 2.18, 2.21, 2.22, 2.34.
Definición en MAUDE:
1- Cambie el constructor de listas en MAUDE por el siguiente:
op _ _ : Int ListInt -> ListInt [ctor ] .
Explique el efecto que tendría éste cambio en los ejemplos sobre listas en MAUDE.
Reelabore los ejemplos en MAUDE para que operen con base en el nuevo constructor.
2- Para un operador con el siguiente perfil:
op sel : ListInt -> Int .
Elabore ecuaciones que den como resultado los elementos siguientes:
Primer elemento de la lista.
Suma del primero y tercer elemento de la lista.
Diferencia entre el segundo elemento y el penúltimo.
Suma de las diferencias entre el primero y el último y el segundo y el penúltimo.
El valor del elemento del medio.
Listas de enteros:
Elabore en los lenguajes analizados operadores que lleven a cabo las tareas que se muestran
a continuación. Elabore operadores que lleven a cabo la tarea por medio de un proceso
tanto recursivo como iterativo. Indique el orden de cada una de las soluciones definidas.
1- Dados dos vectores X y Y de N componentes:
X1, X2, X3,....., Xi,..... Xn
Y1, Y2, Y3,....., Yi,..... Yn
Correspondientes a las coordenadas cartesianas de los vértices de un polígono cerrado:
Hallar el perímetro aplicando la fórmula siguiente:
Perímetro = i (Xi+1 – Xi)2 + (Yi+1 – Yi)2
Hallar el área aplicando la fórmula siguiente:
Area = i (Yi * Xi+1 - Xi * Yi+1)
2- Suprima un elemento dado de un vector, contemplando la posibilidad de que dicho
elemento ocurra más de una vez en el vector.
3- Suprima las ocurrencias múltiples de los elementos en una lista, dejando como resultado
una lista con los mismos elementos pero sin ocurrencias múltiples.
Capítulo 10: Valores y Tipos Compuestos.
332
15- Declare un operador podarDerecha, que elimine ocurrencias múltiples en una lista
dejando únicamente la primera ocurrencia, y defina su funcionalidad mediante
ecuaciones.
4- Intercale los elementos de dos listas ordenadas para obtener una nueva lista ordenada con
los elementos de las dos listas originales.
5- Separe los elementos de una lista en los que son menores que un valor dado (el umbral),
y los que son mayores o iguales que ese valor dado, dejando como respuesta dos listas,
una con los mayores que el umbral y otra con los menores que el umbral.
6- Suprimir un elemento de un vector, dada su posición en el vector.
7- Incluir un elemento en un vector delante del elemento que tiene una posición dada.
8- Sumarle a todos los elementos de un vector hasta una posición dada, un valor dado.
9- Sumarle un valor dado al primer componente de un vector que tenga un valor mayor que
otro valor dado.
14- Obtenga el valor de las fórmulas del ejercicio 5 del Capítulo 8, pero considerando que
los valores de x son obtenidos de forma sucesiva de una lista de valores dados.
11- Reconozca la existencia en una lista de un segmento dado, dejando como respuesta la
posición del carácter donde aparece el segmento.
13- Coloque la primera mitad de la lista luego de la segunda sin cambiar el orden de los
elementos de cada mitad.
Listas de Registros:
Para una lista de componentes, cada uno de ellos representando a un estudiante del curso
“Lenguajes Declarativos”, lleve a cabo las tareas siguientes en los diferentes lenguajes
analizados:
1- Defina el tipo de dato con su correspondiente constructor.
2- Elabore operadores para lleva a cabo las tareas siguientes.
Hallar el promedio de notas de los estudiantes del curso.
Hallar la diferencia entre el promedio de notas de los estudiantes que están viendo el
curso por primera vez, y los estudiantes que están repitiendo el curso.
Seleccionar los M estudiantes con mejores notas, para un valor de M suministrado.
Para una lista de componentes, cada uno de ellos representando un punto en el plano, lleve
a cabo las tareas siguientes en los diferentes lenguajes analizados:
1- Defina el tipo de dato con su correspondiente constructor.
2- Elabore operadores para lleva a cabo las tareas siguientes.
Hallar el área y el perímetro de la poligonal cerrada formada por los puntos, asumiendo
que el último tramo de la poligonal une el último punto con el primero.
Determine si un punto dado se halla al interior de la poligonal cerrada formada por los
puntos.
Capítulo 10: Valores y Tipos Compuestos.
333
Listas de listas
1-: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2]: 2.27, 2.28, 2.32.
2- Cree un constructor en MAUDE que le permita representar listas de listas, y lleve a cabo
los ejercicios del punto anterior con base en dicha representación.
Árbol Binario de Búsqueda.
1-.Desarrolle un operador, que cuente el número de nodos de un árbol binario de búsqueda.
2-.Desarrollar un operador que tome como argumentos un árbol y un identificador. En caso
de que el identificador se encuentre en el árbol, lo elimine del árbol. Hay que tener
cuidado con las posibles ramas que se desprenden del nodo a eliminar, estas deben ser
reubicada. La función debe retornar el árbol reorganizado.
3- Desarrolle un operador de inserción que mantenga el árbol balanceado.
1. Considere la definición de los predicado prefijo, sufijo y sublista que se muestran
a continuación:
prefijo(P, L) :- append(P, _, L) .
sufijo(S, L) :- append(_, S, L) .
sublista(SubL, Lista) :- prefijo(P, Lista), sufijo(SubL, P) .
Construir el árbol de búsqueda para la consulta :- sublista([2,3,4],[0,1,2,3,4,5]).
2. Definir un predicado extraer que tome tres argumentos (X, L1, L2) que se satisfaga
cuando L2 sea el resultado de extraer todas las ocurrencias de X en L1.
extraer1(X, [X|L], L) .
extraer1(X, [H|T1], [H|T2]) :- extraer1(X, T1, T2)
El predicado agregar, toma tres argumentos (X, L1 y L2), el primero es un elemento, y los
otros dos son listas. Se satisface cuando L2 es igual a L1 tras agregar del elemento X. Se
define así:
agregar(X, L1, L2) :- extraer1(X, L2, L1) .
Construir el árbol de búsqueda para la consulta agregar(3,[1,2,3,4],[1,3,2,3,4]).
4. Elabore dos programas PROLOG que obtengan el factorial de un número entero dado, el
primero debe definir un proceso recursivo y el segundo un proceso iterativo. Construir dos
árboles de búsqueda para la consulta factorial(4,F), basándose en cada una de las
implementaciones elaboradas.
Capítulo 10: Valores y Tipos Compuestos.
334
5. Resolver el taller de listas que se encuentra en la página asociada al texto, utilizando
predicados definidos en PROLOG.
quicksort([], []) .
quicksort([A], [A]) .
quicksort([A,B|T], R) :-
split([A,B|T], L1, L2),
quicksort(L1, R1),
quicksort(L2, R2),
merge(L1, L2, R) .
El operador largo definido en la sección 9.4.3.1 puede aplicarse tanto a listas de enteros como a listas de
vectores (ver sec. 9.2.1.1), así:
Y pueden ser usados incluso en listas cuyos componentes son de diferentes tipos.
El operador largo definido en la sección 9.4.3.1 puede aplicarse tanto a listas de enteros y vectores (ver
sec. 9.2.1.1), así:
Vale la pena destacar en este punto, que a pesar de las ventajas frente a la abstracción de
tipos del enfoque débilmente tipado del SCHEME, se debe pagar el precio de no contar con
sobrecarga de operadores (ver 8.5.2). Así, cuando a los elementos de una lista se les deba
aplicar una operación que dependa del tipo, se debe, ya sea escribir un operador específico
para las listas de cada tipo, o averiguar dentro de una función genérica aplicable a varias
listas, por el tipo del componente para aplicar la operación asociada con el tipo164.
164 Fraccionar una aplicación que manipula diversos tipos de datos, en un conjunto de funciones genéricas que aplican un
“mismo” proceso a todos los datos de la aplicación (v.g. lectura de datos -> validación-de datos -> cálculos ->
presentación de resultados), constituye una “descomposición funcional del programa”, que separa en diversos módulos los
tratamientos que se realizan a un tipo específico de datos. Modernamente se prefiere descomponer las aplicaciones por
módulos que reúnen las funciones que llevan a cabo todos los tratamientos de un tipo de datos. A esta descomposición se
le denomina “descomposición por tipos de dato” y conduce a la “descomposición por objetos”.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
338
Un operador que le suma un valor a las componentes de una lista de Vectores, debe usar un operador
específico para las listas de Vectores., así:
(define (sume-valor-lista-vector LV V)
(if (null? LV) nil
(cons (sume-valor-vector (car LV ) V ) (sume-valor-lista-vector (cdr LV ) V ) )
)
)
Donde la función sume-valor-vector, es la adecuada al tipo Vector (que el lector deberá elaborar por
su propia cuenta).
Un operador genérico aplicable a varias listas deberá aplicar la operación adecuada al tipo de
componente. Así, un operador que le suma un valor a las componentes de una lista mixta de enteros y
Vectores, debe usar el operador adecuado en cada caso, preguntando, de forma explícita, por el tipo del
componente.
(define (sume-valor-lista LV V)
(if (null? LV) nil
(cond
( (Vector? (car LV ))
(cons (sume-valor-vector (car LV ) V ) (sume-valor-lista (cdr LV ) V ) )
)
( (integer? (car LV ))
(cons (+ (car LV ) V ) (sume-valor-lista (cdr LV ) V) )
)
)
)
)
Siguen un patrón típico de los procesos que denominamos de “acumulación”, tratados en la sección
9.3.1.
Este patrón puede observarse fácilmente en las definiciones, si se tiene en cuenta que éstas sólo difieren
en la parte que se muestra en negrita a continuación.
(define (sum_pi_rec k n)
(if (> k n) 0
(+ (/ 1 (* (- (* 4 k) 3) (- (* 4 k) 1))) (sum_pi_rec (+ k 1) n)
)
)
)
(define (cont_frac_rec k n)
(if (> k n) 0
(/ k (+ (* k k) (cont_frac_rec (+ k 1) n)))
)
)
Es posible, entonces, pensar que un operador genérico de acumulación, podría capturar lo que es común
a dichos procesos, y que para especializarlo a cada proceso de acumulación particular, bastaría
suministrarle en una evocación, como argumento, la parte que los hace diferentes.
Para capturar la parte común a todos los operadores de “acumulación”, podemos construir
un operador genérico de acumulación, parametrizado en la parte que hace cada
acumulación diferente de las demás.
Para obtener un operador genérico de acumulación, aplicable a los dos casos del ejemplo anterior, basta
con abstraer la parte común a las acumulaciones presentadas, parametrizándola por medio de un
operador, así:
(define (acumule-rec k n f )
(if (> k n) 0
(f k (acumule-rec (+ k 1) n)
)
)
)
Donde la variable f es usada como operador.
Para obtener las acumulaciones deseadas se deben definir los operadores adecuados a cada caso y darlos
como argumento al evocar el operador genérico, así:
A esta altura debe ser claro para el lector que el operador genérico del ejemplo anterior
determina procesos de acumulación recursivos. La definición de un operador genérico de
acumulación que determine un proceso iterativo se deja como ejercicio. Además, se
recomienda enfáticamente que el lector consulte a [Abelson 85 sección 1.1.3], donde
hallará otros ejemplos y aplicaciones de abstracción de operadores por parametrización en
SCHEME.
En este punto es importante anotar que, si bien los operadores que se pasan como
argumento a un segundo operador, deben conformar en número y tipo de argumentos con la
evocación que se indica en la definición de este segundo operador, esta circunstancia no se
verifica al momento de la evocación. Así, si la evocación del segundo operador se lleva a
cabo con un operador que no conforma, el error será detectado sólo en el momento en que
se lleva a cabo la evocación del operador pasado dentro del cuerpo del segundo operador.
Los operadores que calculan las acumulaciones, definidos en el ejemplo anterior, podrían haberse
definido usando una función equivocada, así:
El operador siguiente es definido por medio de una expresión lambda e inmediatamente evocado, para
llevar a cabo el cálculo que describe.
El uso más obvio de una expresión lambda es evitar tener que definir un operador de forma
independiente de la evocación que lo ha de recibir como argumento.
Al definir los operadores de acumulación presentados en los ejemplos la sección anterior, con base en el
operador genérico definido, puede evitarse la definición de las funciones auxiliares usando la forma
especial lambda, así:
(define (sum_pi n)
(acumule-rec 1 n (lambda (k S) (+ (/ 1 (* (- (* 4 k) 3) (- (* 4 k) 1))) S) ))
)
(define (cont_frac n)
(acumule-rec 1 n (lambda (k S) (/ k (+ (* k k) S) ))
)
En términos prácticos, la diferencia entre definir una función que da como resultado una
función que luego debe ser evocada, y definir una función que de cómo resultado, de una
vez, el valor de la evocación de la función generada en la opción anterior, estriba en que es
mejor construir la función una vez y luego evocarla varias veces que tenerla que construir
cada vez que se va a evocar.
165La abstracción de módulos incluidos permite también la abstracción de rótulos asociados con axiomas (ver [Clavel
2007, secs 4.5.2]).
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
343
“teoría”. Así, cada parámetro de un módulo parametrizado se asocia con una teoría que
debe satisfacer el modulo real incluido, permitiendo que varios parámetros se asocien con
la misma teoría pero (por ser únicos) cada parámetro se asocia con una sola teoría. La
teoría define, además, los rótulos que serán usados en el módulo inclusor para hacer
referencia a los elementos del módulo incluido.
Para indicar la manera como los elementos de un módulo real satisfacen las condiciones de
una teoría, se debe crear una “vista” de la teoría sobre el módulo real. Entonces al
instanciar un módulo parametrizado se substituye cada parámetro con el nombre de una
vista de la teoría del parámetro sobre el modulo real. La vista define, además, cuales son
los elementos específicos del módulo real a los que se hace referencia con los rótulos
definidos en la teoría.
Como ejemplo de la parametrización en MAUDE, presentamos a continuación los
elementos básicos de la especificación de un módulo paramétrico, en el que se define una
lista genérica con sus operaciones respectivas usando la lista que se definió en el capítulo
anterior de forma específica para los enteros. Este módulo puede ser, en consecuencia,
utilizado para crear listas de elementos de cualquier tipo. Para una especificación completa
de la lista genérica nativa en MAUDE el lector debe consultar a [Clavel 2007, sec 7.12.4].
La declaración de la lista genérica sigue los alineamientos presentados en el capítulo anterior, pero
teniendo en cuenta los elementos asociados con la parametrización para hacer genérico el elemento de la
lista, así:
vars I : Int .
vars E H : X$Elt .
vars L L1 L2 : Lista{X} .
Donde: X es el parámetro que representa el módulo a ser incluido, y es usado para cualificar, dentro del
módulo parametrizado, a los elementos del módulo incluido. Por ejemplo X$Elt hace referencia a un sort
del módulo incluido a través del parámetro X. TRIV es la teoría que determina las condiciones que deben
satisfacer los elementos del módulo incluido a través de X, siendo Elt un rótulo definido en la teoría para
ser asociado con un sort del módulo incluido. Nótese que la vista indicará cual es el sort del módulo real
incluido al que se hará referencia con el rótulo Elt, y si existiera más de un parámetro asociado con la
teoría TRIV, Elt podría hacer referencia a dos sort diferentes, para parámetros diferentes.
Lista{X} es un sort definido en el módulo parametrizado y es cualificado con X debido a que
representará diferentes sorts para instancias diferentes del parámetro X.
Para obtener una lista de enteros, basta crear un módulo que instancie el módulo paramétrico usando una
vista de TRIV, sobre los enteros así:
fmod LISTA-INT is
protecting LISTA{Int} .
endfm
Donde Int es el nombre de una vista de TRIV sobre INT.
Tanto la vista Int como la teoría TRIV están incluidas en el archivo preludio.maude (ver ¡Error! No se
encuentra el origen de la referencia.) de MAUDE [Clavel 2007, sec 6.3], y se reproducen a continuación:
fth TRIV is
sort Elt .
endfth
En las secciones que siguen se describe en más detalle, los elementos que involucra la
parametrización de los módulos, resumiendo el contenido y usando los ejemplos del
manual de MAUDE [Clavel 2007, sec 6.3]. Para una descripción más detallada se remite al
lector a dicho documento.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
345
11.3.1 Parametrización de módulos.
Un módulo funcional parametrizado se define de la manera siguiente:
fmod <nombre>{<lista_de_parametros>} is <cuerpo_del_modulo> endfm
Donde:
<nombre> Es el identificador del módulo, usualmente colocado en letra
capital.
<lista_de_parametros> Es una lista de <Parametro> separados por coma “,”.
<cuerpo_del_modulo> Es el cuerpo de un módulo funcional tal como fue
descrito en la sección 8.8.2.1.
Cada <Parametro> es una construcción de la forma:
<Xi> :: <Ti>
Donde:
<Xi> Es el identificador del parámetro en la posición i de la lista de
parámetros.
<Ti> Es el identificador de la teoría asociada al parámetro en la posición i en
la lista de parámetros.
Los identificadores Xi de los parámetros deben ser únicos en la lista de parámetros. Los
identificadores Ti de las teorías asociadas a los parámetros pueden, por su parte, repetirse.
De esta manera, cada parámetro es usado para incluir un módulo diferente, pero pueden
incluirse varios módulos diferentes bajo las condiciones definidas en una misma teoría.
En el <cuerpo del módulo> pueden ser usados los operadores y sorts definidos en las
teorías asociadas a los parámetros. Sin embargo, cuando se usan los nombres de los sort
definidos en una teoría, ellos deben cualificarse con el nombre del parámetro asociado con
dicha teoría. Así, si dentro del módulo parametrizado se desea usar el sort S definido en la
teoría T, asociada con el parámetro de posición i, se debe hacer referencia a éste sort con el
nombre Xi$S, siendo Xi el nombre del parámetro de posición i.
Nótese que cuando la teoría T aparece asociada, tanto al parámetro de posición i como al
parámetro de posición j, a un sort S definido en T, se le hace referencia tanto por medio del
nombre Xi$S como por medio del nombre Xj$S dentro del módulo parametrizado. Esto no
implica que se esté haciendo referencia al mismo sort de dos formas distintas, ya que las
teorías son sólo un medio para definir las condiciones que deben satisfacer los módulos
incluidos a través de los parámetros. Es, por tanto, posible que se incluyan dos módulos
diferentes a través de parámetros diferentes, bajo la misma teoría. Y si éste es el caso de la
teoría T y de los parámetros de posición i y j, los nombres Xi$S y Xj$S estarán haciendo
referencia a sorts definidos en dos módulos importados completamente diferentes (el uno
importado con el parámetro Xi y el otro importado con el parámetro Xj, ambos satisfaciendo
la teoría T)
Por otro lado, un módulo parametrizado puede ser incluido en otro módulo más de una vez,
instanciándolo de diferentes formas (es decir incluyéndole a través de los parámetros
diferentes módulos). Entonces, si se tiene en cuenta que un sort declarado dentro de un
módulo parametrizado no es, en general, el mismo para diferentes instancias del módulo
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
346
(debido a que puede depender de los parámetros), se hace necesario poder distinguir este
sort para diferentes instancias del módulo parametrizado que se incluyan en otro módulo.
Para distinguir entre estos sorts basta con cualificar el nombre del sort, primero dentro del
módulo parametrizado con el nombre de los parámetros de los que depende, en todas las
ocurrencias de dicho nombre, y segundo con cualificar el nombre del sort dentro de los
módulos que incluyen instancias del módulo parametrizado, con el nombre de las vistas con
que se instancian los parámetros de los que depende el sort. Un sort depende de un
parámetro si se relaciona con un sort de la teoría asociada al parámetro (v.g. existe una
relación de subsort con dicho sort, o en el dominio de los constructores del sort ocurre el
sort de la teoría).
Los módulos funcionales que se muestran a continuación, basados en ejemplos de [Clavel 2007, sec
6.3.3], ilustran la manera de especificar módulos funcionales parametrizados.
En el primer módulo se ilustra el uso de parámetros para definir el tipo de los elementos de un conjunto.
var E : X$Elt .
vars S S’ : Set{X} .
eq E, E = E .
endfm
Donde es importante notar que el nombre del sort definido en el módulo parametrizado (Set) depende
del parámetro X debido a que es superconjunto del sort X$Elt de X . En consecuencia el sort Set es
cualificado con el único parámetro del módulo (Set{X}). Así, de incluirse conjuntos de dos tipos de
elementos diferentes, instanciando el sort parametrizado anterior (con dos vistas diferentes), los sort Set
de los dos tipos de conjunto se distinguirán por estar cualificados con la vista. En el módulo siguiente se
incluye dos veces el módulo parametrizado anterior, instanciado con la vista int y con la vista float, y las
dos versiones del sort Set definido en dicho módulo, se distinguirán por estar cualificados con la vista:
fmod X is
protecting SET{int} .
protecting SET{float} .
…
op opx : Set{int} Set{float} -> Set{float} .
endfm
En el segundo módulo se ilustra el uso de más de un parámetro bajo la misma teoría, así:
fmod X is
protecting PAIR{int,float} .
…
endfm
El nombre de sort X$Elt representará el sort de los enteros, mientras que el nombre de sort Y$Elt,
representará al sort de los reales dentro de la instancia del módulo parametrizado.
166 Pueden ser usados al nivel del metalenguaje para llevar a cabo demostraciones de teoremas de forma controlada.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
348
Con el objeto de ilustrar el uso de las teorías y sus relaciones, reproducimos aquí las teorías
presentadas en [Clavel 2007, sec 6.3.1], que ilustran la definición de los operadores de
orden _<_ y _<=_ para conjuntos total y parcialmente ordenados.
La teoría SPOSET define el operador _<_ como un operador irreflexivo, antisimétrico y transitivo, por
medio de axiomas no ejecutables.
fth SPOSET i s
protecting BOOL .
sort Elt .
op _<_ : Elt Elt -> Bool .
vars X Y Z : Elt .
ceq X < Z = true if (X < Y /\ Y < Z) [nonexec label transitive] .
ceq X = Y if (X < Y /\ Y < X) [nonexec label antisymmetric] .
eq X < X = false [nonexec label irreflexive] .
endfth
La teoría POSET adiciona a SPOSET el operador _<=_ como un operador irreflexivo, definido en
términos de los operadores definidos antes.
fth POSET is
including SPOSET .
op _<=_ : Elt Elt -> Bool .
vars X Y : Elt .
eq X <= X = true [nonexec] .
ceq X <= Y = true if(X < Y) [nonexec] .
ceq X = Y if( X <= Y /\ X < Y = false) [nonexec]
endfth
La teoría TOSET adiciona un axioma que garantiza que el orden sea total.
fth TOSET is
including POSET .
vars X Y : Elt .
ceq X <= Y = true if(Y <= X = false) [nonexec label total] .
endfth
Nótese que los axiomas incluidos en las teorías anteriores no participan en procesos de reescritura. Ellos
sólo afirman las propiedades que deben cumplir los operadores de orden a ser incluidos en un módulo
parametrizado, por medio de un parámetro asociado con la teoría.
La teoría elemental TRIV (tomadas de [Clavel 2007, sec 7.11.1]) no impone condición alguna a los
miembros de su único sort, el sort Elt:
fth TRIV
sort Elt .
endfth
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
349
La teoría DEFAULT (tomadas de [Clavel 2007, sec 7.11.2]) adiciona a TRIV un elemento distinguido
para el sort Elt:
fth DEFAULT is
including TRIV .
op 0 : -> Elt .
endfth
Las teorías STRICT-WEAK-ORDER y STRICT-TOTAL-ORDER (tomadas de [Clavel 2007, sec
7.11.3]), define las propiedades del operador _<_ para conjuntos débilmente y totalmente ordenados:
fth STRICT-WEAK-ORDER is
protecting BOOL .
including TRIV .
op _<_ : Elt Elt -> Bool .
vars X Y Z : Elt .
ceq X < Z = true if( X < Y /\ Y < Z) [nonexec label transitive] .
eq X < X = false [nonexec label irreflexive] .
ceq X < Y or Y < X or Y < Z or Z < Y = true if( X < Z or Z < X) [nonexec label
incomparability-transitive] .
endfth
fth STRICT-TOTAL-ORDER is
including STRICT-WEAK-ORDER .
vars X Y : Elt .
ceq X = Y if(X < Y = false /\ Y < X = false) [nonexec label total] .
endfth
fth TOTAL-PREORDER is
protecting BOOL .
including TRIV .
op _<=_ : Elt Elt -> Bool .
vars X Y Z : Elt .
eq X <= X = true [nonexec label reflexive] .
ceq X <= Z = true if(X <= Y /\ Y <= Z) [nonexec label transitive] .
eq X <= Y or Y <= X = true [nonexec label total] .
endfth
fth TOTAL-ORDER is
inc TOTAL-PREORDER .
vars X Y : Elt .
ceq X = Y if( X <= Y /\ Y <= X) [nonexec label antisymmetric] .
endfth
Donde:
<nombre> Es el identificador de la vista, donde es costumbre usar el mismo
nombre asociado con <destino>167.
<fuente> Es el nombre asociado a una teoría, o una expresión que evalúe a
una teoría. En lo que sigue nos referiremos a ella como la fuente.
<destino> Es el nombre asociado a un módulo o a una teoría, o una
expresión que evalué a uno de ellos. En lo que sigue nos referiremos a este
módulo o teoría como el destino.
<mapeo> Es una serie de <expresiones-de-mapeo> separadas por
espacios,
Las <expresiones-de-mapeo> proyectan los sorts y operadores de la teoría referida en
<fuente> a los sorts y operadores del módulo o teoría referida en <destino>.
La proyección de un sort se define por medio de una expresión de la forma siguiente:
sort <nombre-fuente> to <nombre-destino> .
Donde:
<nombre-fuente> Es el nombre de un sort de la fuente.
<nombre-destino> Es el nombre de un sort del destino.
Dada la proyección de un sort de la fuente a un sort del destino, el sort del destino será
representado por el sort de la fuente, tanto dentro de la fuente como dentro de los módulos
parametrizados que incluyen el destino a través del parámetro asociado a la fuente. Los
sorts que no aparezcan en el mapeo se asumen proyectados a los sorts de igual nombre.
Para cada sorts de la fuente debe existir un sort correspondiente en el destino. Además si el
sort S es subsort del sort T en fuente, los sorts correspondientes en el destino S´ y T´ deben
mantener la misma relación de subsort. Así, si dos sort S y T de la fuente pertenecen a un
mismo “kind” (ver [Clavel 2007, sec 3.5]) entonces los sort correspondientes del destino
deben asimismo pertenecer a un mismo kind.
La proyección de un operador se define por medio de una construcción de la forma
siguiente:
op <plantilla-fuente> to <plantilla-destino> .
167Es decir pueden existir vistas con el mismo nombre de módulos sin que se produzca ambigüedad. No deben existir, sin
embargo, nombres de parámetro con el mismo nombre de vistas ya que ambos pueden usarse para instanciar módulos
parametrizados creando ambigüedad.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
351
Donde:
<plantilla-fuente> Es la plantilla de un operador declarado en la fuente.
<plantilla-destino> Es la plantilla de un operador declarado en el destino.
<sorts-fuente-dominio> Son los sorts que constituyen en la fuente el
dominio del operador.
<sorts-fuente-rango> Es el sorts que constituyen en la fuente el rango del
operador.
<termino-fuente> Es un término con un sólo operador definido con base en
un operador y variables de la fuente.
<termino-destino> Es un término definido con base en operadores y
variables del destino. Las variables de <termino-destino> deben, sin
embargo, aparecen en <termino-fuente>.
Dada la proyección de un operador de la fuente a un operador del destino, el operador del
destino será representado por el operador correspondiente en la fuente, tanto dentro de la
fuente como dentro del módulo parametrizado que incluye el destino a través del parámetro
asociado a la fuente, así:
La primera de las construcciones afecta a todos los operadores
sobrecargados con las plantillas referidas. Así, un operador con la plantilla
<plantilla-fuente> de la fuente se proyecta al operador con <plantilla-
destino> en destino, cuyos sorts de dominio y rango correspondan bajo el
mapeo de los sort.
La segunda construcción permite el mapeo del operador con el dominio y
rango especificados168.
La tercera construcción permite proyectar un operador de la fuente a un
término en el destino. El sort de una variable en <termino-destino> debe
ser el sort al que se proyecta el sort de la misma variable en <termino-
fuente>. Además, el sort (o kind) del término <termino-fuente> en la
fuente debe estar proyectado al sort (o kind) del término <termino-destino>
en el destino. Las variables usadas en los términos <termino-fuente> y
<termino-destino>, pueden ser declaradas en la vista con el sort de la
fuente, siendo implícita, por la proyección de los sorts, la declaración de la
variable correspondiente al sort del destino.
Los operadores que no aparezcan en el mapeo se asumen proyectados a los operadores de
igual nombre, dada su correspondencia de dominio y rango bajo la proyección de los sorts.
Cada operador de la fuente o de una subteoría de la fuente (una teoría importada en la
fuente) debe tener un operador correspondiente en el destino. Los operadores definidos en
submódulos de la fuente (un módulo importado en la fuente) no pueden, sin embargo, ser
mapeados por medio de la vista al destino169. Entre los operadores correspondientes de la
168En [Clavel 2007, sec 6.3.2] se refiere además, que esta forma “afecta no sólo al operador con dicha aridad y coaridad,
sino a toda la familia de operadores sobrecargados con subsorts” (ver 8.5.2.1). No presenta, sin embargo, ejemplo alguno
de esta circunstancia.
169Nótese que los módulos importados en la fuente pueden tener operadores declarados y definidos (por reescritura) que,
a diferencia de los operadores declarados en la teoría (o las teorías incluidas), no pueden ser redefinidos en los módulos
destino de la vista (por otras formas de reescritura). De hacerlo se genera un mensaje de advertencia.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
352
fuente y el destino, se debe conservar tanto la aridad del operador, como los sorts de su
dominio y rango, dada la proyección de los mismos. Otras restricciones al mapeo de
operadores pueden verse en [Clavel 2007, sec 6.3.2].
Por cada vista se debe cumplir que los axiomas de la fuente, interpretados en el destino bajo
las proyecciones definidas en la vista, se cumplen en el destino. La satisfacción de estos
axiomas (denominados “prof. obligations”), no es, sin embargo, verificada por el
intérprete.
Así como MAUDE ofrece de forma nativa en el archivo prelude.maude, una serie de
teorías, ofrece también una serie de vistas a dichas teorías.
La teoría TRIV se proyecta a una serie de módulos nativos [Clavel 2007, sec 7.11.1]. Así, la vista
siguiente:
Las vistas que proyectan una teoría a otra teoría permiten componer instancias de vistas de
la forma que se discutirá en la sección siguiente.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
353
11.3.4 Creación de instancias de módulos paramétricos.
Dado un módulo parametrizado, basta sustituir sus parámetros por vistas de otros módulos
a las teorías respectivas, para obtener un módulo que puede ser usado en procesos de
reescritura o incluido en otro módulo.
Para obtener un modulo que defina un conjunto de enteros, basta con substituid en el módulo SET{X ::
TRIV}, definido arriba, el parámetro por la vista que proyecta el modulo de los enteros a la teoría TRIV.
Así, la especificación siguiente:
..
protecting SET{Int} .
..
Incluye un módulo que define conjuntos de enteros, mientras que la especificación siguiente:
..
protecting SET{String} .
..
Incluye un módulo que define conjuntos de strings.
Nótese que en ambos casos, el parámetro real con que se instancia al módulo parametrizado, es una vista
de un módulo a la teoría asociada al parámetro formal. Esta vista fue, de forma arbitraria, nombrada
igual que el módulo por lo que, en apariencia, el parámetro real es el módulo mismo. El lector no debe,
sin embargo, olvidar que la vista es el medio para indicar la manera como el módulo satisface las
condiciones del parámetro por lo que el parámetro real debe ser la vista del módulo a la teoría y no el
módulo mismo.
Para incluir en un módulo parametrizado otro módulo parametrizado, instanciado con parámetros del
módulo que lo incluye, es suficiente que los parámetros correspondientes se asocien con la misma
teoría.
Así, en el módulo siguiente:
endfm
Se pueden incluir los módulo SET{X} y SET{Y}, gracias que los parámetros X y Y se asocian con la
misma teoría (TRIV) que se asocia con el parámetro del módulo incluido.
Al instanciarse con vistas el módulo que incluye, los módulos incluidos serán también instanciados con
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
354
las vistas asociadas a los parámetros correspondientes.
Puede ser útil, sin embargo, que las teorías asociadas con los parámetros del módulo
inclusor sean diferentes a las teorías que se asocian con los parámetros del módulo
incluido. Este es el caso de un módulo incluido cuyos parámetros se asocian con subteorías
de las asociadas a los parámetros del módulo inclusor.
Para resolver el conflicto de teorías, se debe usar una vista de las teorías del módulo
incluido a las correspondientes teorías del módulo inclusor. Estas vistas garantizan que las
teorías del modulo incluido son, en efecto, subteorías de las correspondientes en el módulo
inclusor. El modulo incluido se instancia, entonces, con las vista entre teorías, dando como
resultado un módulo incluido que, ahora, es parametrizado con las teorías del módulo
inclusor, y puede, en consecuencia instanciarse usando sus parámetros.
En el módulo siguiente tomado de [Clavel 2007, sec 6.3.4]170 se extiende el módulo SET{X::TRIV}
referido en los ejemplos anteriores al módulo SET-MAX{X::TOSET}, para implementar un operador
que obtenga el máximo de los valores del conjunto.
En el módulo siguiente se desea extender el módulo PAIR{X :: TRIV, Y :: TRIV} para introducir un
operador que permita ordenarlos por el primer elemento del par. Puesto que, para ello, se requiere que
el primer elemento del par sea ordenable, se usa a TOSET como teoría asociada con el parámetro
correspondiente.
171 Esta restricción parece ligarse al intérprete, más bien que al leguaje mismo.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
356
Relaciones entre Tipos.
La relación de contención entre tipos es declarada en MAUDE por medio de la instrucción
subsort (ver 8.5.2.1 ).
Si bien la relación de subsort fue utilizada en 10.4.2.2 para declarar las componentes
elementales de la estructura iterada lista, ella puede también ser utilizada para definir
operadores asociados a funciones parcialmente definidas sobre un tipo, darle apoyo a la
recuperación de errores, indicar de forma más precisa el efecto de aplicar operadores (ya
definidos) a subconjuntos del tipo, y definir tipos con los elementos de un tipo ya existente
que satisfagan alguna condición.
En esta sección se presentan estos usos para la relación de subsort.
11.4.1 “Kinds” y Gestión de Errores.
La instrucción subsort permite definir jerarquías de contención entre tipos.
La declaración siguiente:
La relación de subsort no puede formar ciclos, que permitan que un sort sea subsort de sí
mismo. Así, una jerarquía de contención entre sorts determina un orden parcial entre los
sorts. En esta jerarquía existen sorts que no son subsorts de ningún otro, que
denominaremos “maximales”, y sort que no son supersorts de ningún otro, que
denominaremos “minimales”.
La relación de subsort particiona, además, el conjunto de sorts de una especificación, en
conjuntos de sorts conectados. A un conjunto de sorts conectados se le denomina un “kind”
[Clavel 2007, sec 3.5].
Un kind puede ser interpretado semánticamente como un supersort que contiene el conjunto
de todos los términos que puedan formarse con los operadores que tiene como coaridad
alguno de los sorts del kind.
Si bien al evocarse un operador en MAUDE, la lista de los argumentos reales debe estar de
acuerdo en tamaño y sort con la lista de argumentos formales, en MAUDE se acepta que el
sort de cada argumento real pertenezca al mismo kind que sort del correspondiente
argumento formal. Esto implica que en un kind puede haber términos errados172 siendo
indefinido el sort al que pertenecen. MAUDE permite estos términos concediéndoles el
“beneficio de la duda” en el sentido de que si al ser simplificados pertenecen a un sort
definido, se consideran correctos.
172Por ejemplo un término que tiene un operando de un supersort del sort prescrito para el operador. Vg. 4/0 tiene un
divisor del sort Nat (naturales) debiendo ser NzNat (naturales diferentes de cero) que es un subsort de Nat.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
357
Es posible, además, efectuar simplificaciones al nivel del kind, de tal manera que términos
errados pueden convertirse a términos de error definidos para facilitar las operaciones de
depuración del programa. Para hacer referencia a un kind basta colocar entre parénesis
cuadrados ([ ..]) el nombre de un sort del kind o una lista de nombres de sorts del kind.
MAUDE al hacer referencia a un kind usa la lista de los sorts maximales del kind.
11.4.2 Sobrecarga de operadores en subtipos.
Dada la posibilidad de sobrecargar los operadores (ver 8.5.2.3.3 ), es posible distinguir dos
tipos de sobrecarga en el contexto de las relaciones de subtipo.
El primer tipo de sobrecarga ocurre cuando los sorts del dominio no están relacionados (no
pertenecen al mismo kind) con los correspondientes de la declaración original (ver Ejemplo
8 ). Este tipo de sobrecarga es denominado “sobrecarga ad-hoc” (“ad-hoc overloading”
[Clavel 2007, sec 3.6]).
El segundo tipo de sobrecarga ocurre cuando los sorts del dominio son subsorts de los
correspondientes sorts en la declaración original. Este tipo de sobrecarga es denominado
“sobrecarga de subsorts” (“subsort overloading” [Clavel 2007, sec 3.6]).
La razón de usar sobrecarga de subsorts, es poder adicionar restricciones al
comportamiento del operador original para el caso de argumentos más específicos (los del
subsort).
Para evitar expresiones ambiguas, MAUDE exige que al sobrecargar un operador, si los
correspondientes argumentos del dominio pertenecen al mismo kind, entonces los
argumentos del rango deben, también, pertenecer al mismo kind173.
Cuando un operador es una sobrecarga de subsorts de otro operador declarado, los atributos
de la declaración, con excepción del atributo ctor y atributos con metadatos (ver [Clavel
2007, sec 4.5.2]), deben ser los mismos que los de la declaración original. Para evitar
reescribir estos atributos se puede usar el atributo ditto. El atributo ditto indica que el
operador tiene los mismos atributos (excepto com y atributos con metadatos) que la
declaración original.
En MAUDE es posible sobrecargar las constantes. Sin embargo, para evitar ambigüedades
al momento de usarlas como argumentos reales, ellas (y en general todos los términos)
pueden cualificarse con el sort al que pertenecen.
Para cualificar un término se debe usar una expresión de la forma siguiente:
(<termino>).<sort> .
Donde:
<termino> Es el término a ser cualificado con el sort.
<sort> Es el sort que cualifica al término.
11.4.3 Preregularidad
Bajo la relación de subsort, un término puede pertenecer a varios sorts. Así:
El siguiente ejemplo, tomado de [Clavel 2007, sec 3.8] muestra esta situación, así:
sorts A B C D .
subsorts A < B C < D .
op a : -> A .
op f : B -> B .
Donde el término f(a) tiene sorts B, C, D siendo minimales tanto B como C.
La propiedad de una signatura de que todo término bien formado tenga un solo sort
minimal se denomina “preregularidad”. La preregularidad es una condición deseable en
una especificación y es verificada por MAUDE, generando advertencias cuando no es
satisfecha174.
11.4.4 Ecuaciones de Membresía y de Membresía Condicional
En los capítulos anteriores la relación de subsort fue utilizada para declarar que los
elementos de un sort MAUDE ya creado, hacían parte de los elementos de uno de sus
supersorts. El objeto de esta declaración fue el de definir los elementos del nuevo supersort
con base en los elementos del sort ya creado. En particular, esta fue la manera como se
construyó la lista en 10.4.2.2.
Es posible, sin embargo, declarar también que un subconjunto de los elementos de un sort
ya creado, constituye los elementos de un subsort a ser definido. Para llevar a cabo esto
MAUDE ofrece dos tipos de construcciones: la ecuación de membresía y la ecuación de
membresía condicional.
La ecuación de membresía y la ecuación de membresía condicional, permiten declarar que
algunos términos específicos de un sort determinado, por ellos mismos o cuando satisfacen
una condición definida, constituyen (o dan como resultado) elementos de un subsort del
sort que les correspondía originalmente.
174Tal como se indica en la preregularidad es verificada teniendo en cuenta la ocurrencia de los atributos ecuacionales
assoc, comm y id:, que obliga su verificación en la clase de equivalencia de todos los términos que son iguales bajo los
axiomas implícitos en dichos atributos.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
359
Las ecuaciones de membresía y de membresía condicional tienen la forma siguiente:
mb <termino> : <sort> .
cmb <termino> : <sort> if <temino_boleano> .
Donde:
<termino> Es el término cuyo sort esta siendo redefinido.
<sort> Es un subsort del sort asociado con <termino>.
<temino_boleano> Es un término que al ser evaluado determina si
<termino> pertenece o no al sort <sort>.
Dadas estas ecuaciones un término cualquiera que empareje con <termino> es
reclasificado, de forma dinámica, en un subsort de su sort original.
El siguiente ejemplo, tomado de [Clavel 2007, sec 4.2] muestra el uso de la ecuación de membresía
simple. Así, la especificación siguiente:
...
sort Nat3 .
subsort Nat3 < Nat .
var M3 : Nat3 .
mb 0 : Nat3.
mb (s s s M3) : Nat3 .
…
Define que algunos de los elementos del sort de los naturales (Nat) pertenecen al sort de los naturales
múltiplos de 3 (3*Nat).
El siguiente ejemplo, basado en el presentado en [Clavel 2007, sec 3.5 y 4.3] muestra el uso de la
ecuación de membresía condicional.
Así, la especificación siguiente implementa un álgebra para nodos arcos y caminos en un grafo. Un
grafo está constituido por un conjunto de nodos, y un conjunto de conexiones entre nodos llamados
arcos. Una serie de arcos conectados entre si determina un camino en el grafo. Finalmente un camino
cerrado es aquel que comienza y termina en el mismo grafo.
fmod GRAFO is
sorts Nodo Arco .
ops origen fin : Arco -> Nodo .
var A : Arco .
vars C : Camino .
protecting INT .
sort Camino-Cerrado .
subsort Camino-Cerrado < Camino .
cmb C : Camino-Cerrado if origen(C) == fin(C) .
endfm
Nótese que el sort camino? (caminos dudosos), sirve como paso intermedio para construir el sort
caminos (verdaderos). El operador _;_ que une dos caminos dudosos, sirve de pegadura para crear
secuencias arbitrarias de arcos. Sin embargo, es el axioma de membresía el que permite determinar si
realmente una de estas secuencias es un camino verdadero.
Una construcción análoga se hace para los caminos cerrados, pero nótese como la relación de subsort
Camino-Cerrado < Camino, aleja de entrada la duda acerca de si la secuencia de arcos, candidata a
ser camino cerrado es un camino dudoso. De entrada debe ser camino, antes de considerar siquiera ser
Camino-Cerrado.
Una posible declaración del módulo paramétrico para una lista ordenada genérica que denominaremos
LISTA-ORD, requeriría que los componentes de la lista sean ordenables. Por ello debemos exigir a su
único parámetro, satisfacer la teoría TOSET que define un orden total bajo el operador _>_.
El módulo se apoyaría en el módulo que define la lista genérica presentado en el Ejemplo 138, el
módulo LISTA (ver Nota al final del ejemplo sobre un posible problema al usar este módulo). Este
módulo es importado e instanciado con una vista a la teoría TOSET para luego ligar su parámetro con
el del módulo LIST-ORD.
fmod LISTA-ORD{X::TOSET}
protecting LISTA{TOSET}{X} .
sort StdList{X} .
subsort StdList{X} < Lista{TOSET}{X} .
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
361
vars N : X$Elt .
vars SL : StdList{X} .
mb nil : StdList{X} .
cmb (N SL) : StdList{X} if (SL==nil) or (N <= head(SL)) .
En [Clavel 2007, sec 4.4.4] se presenta el siguiente ejemplo, donde se define una lista que puede ser
utilizada con elementos de diversos tipos, así:
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
362
fmod HET-LIST is
sort List .
op nil : -> List .
op _ _ : Universal List -> List [ctor poly (1)] .
endfm
Ejercicios propuestos
Ejercicios: sección 1.3.* de (Abelson 1985)
1. La regla de Simpson, es otra manera para aproximar el valor de una integral definida
entre a y b. En términos matemáticos, dado un número natural par, n:
h
f (a) f (a h) f (a 2h) f (a nh)
b
a
f ( x)dx
3
Donde h=(b-a)/n. Defina un procedimiento que tome como argumentos, f, a, b, n y que
retorne el valor de la integral definida, aproximado mediante la regla de Simpson. Si lo hizo
sin utilizar el procedimiento sumatoria definido en la sección 3.2, reescríbalo para que
haga uso de tal abstracción.
2. El procedimiento sumatoria (sección 3.2) genera un proceso recursivo. El
procedimiento puede ser reescrito para que lleve a cabo un proceso iterativo. Muestre como
hacer esto llenando los campos faltantes en la siguiente definición:
(define (sumatoria termino a sig b)
(define (iter a resultado)
(if <??>
<??>
(iter <??> <??>)))
(iter <??> <??>))
3. Escriba un procedimiento productoria, análogo a sumatoria, que retorne el producto
de los valores de una función en puntos sobre un rango dado.
a. Muestre como definir factorial en términos del nuevo procedimiento productoria.
b. Use el procedimiento productoria, para encontrar una aproximación a , usando
la fórmula
2 4 4 6 6 8
4 3 3 5 5 7 7
c. Si su procedimiento productoria genera un proceso iterativo, escriba uno que
genere un proceso recursivo. Y si su procedimiento genera uno recursivo, escriba
uno que genere un proceso iterativo.
4. Muestre que sumatoria y productoria son casos especiales de un concepto más
general llamado acumular, que combina una colección de términos, usando alguna función
general de acumulación:
(acumular combinador nulo termino a sig b)
Acumular toma como argumentos la misma especificación de rango y término que
sumatoria, junto con un procedimiento combinador (de dos argumentos) que especifica
como el término actual se combina con la acumulación de los términos precedentes; y nulo
es un valor que especifica la base a usar cuando terminen los valores.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
365
a. Escriba el procedimiento acumular.
b. Reescriba los procedimientos sumatoria y productoria, en términos de
acumular.
5. En el ejercicio 3 del módulo 2, se planteo una fórmula general para una fracción continua
infinita.
a. Escriba un procedimiento frac-inf, con el siguiente perfil (frac-inf N D a b),
donde N y D son procedimiento, con argumento n, que calculan el n-ésimo valor para
Ni y Di, y [a, b] determina el rango de enteros.
ama(juan, maría) .
ama(pedro, claudia) .
ama(pedro, luisa) .
ama(maría, julio) .
ama(julio, luisa) .
ama(verónica, hector) .
ama(hector, verónica) .
ama(claudia, hector) .
ama(pedro, verónica) .
ama(julio, verónica) .
Constituyen un registro de las relaciones existentes en un dominio de aplicación, que bien podrían estar
almacenados en una tabla de una base de datos relacional, por ejemplo la tabla “ama” con columnas
“ama” y “amado”.
ama
ama amado
juan María
pedro Claudia
....
Cualquier objetivo PROLOG que se demuestre cierto con base en los hechos, no es otra
cosa que una consulta sobre la Base de Datos correspondiente.
1 ?- ama(juan,verónica).
Capítulo 12 : PROLOG como lenguaje de búsqueda
369
false.
2 ?- ama(juan,maría).
true.
No son otra cosa que una consulta de existencia para un registro específico de la base de datos.
El objetivo siguiente:
3 ?- ama(X,maría).
X = juan.
Obtiene la primera línea de la tabla que tiene a maría en la columna “amado”
El objetivo siguiente:
1 ?- ama(X,verónica) .
X = hector .
Puede demostrarse cierto con tres hechos diferentes del programa, a saber:
ama(hector, verónica) .
ama(pedro, verónica) .
ama(julio, verónica) .
Sin embargo, sólo el primero de los tres fue tenido en cuenta en el proceso, dando como respuesta un
solo valor para la variable de la consulta.
Para hacer que la resolución SLD, demuestre el objetivo con otra combinación de reglas y
consultas, y, en consecuencia, produzca otro valor para las variables, basta con introducir
“;” luego de obtener el resultado inicial.
Si luego de obtener un resultado positivo, se introduce “;” se obtiene un nuevo resultado, así:
1 ?- ama(X,verónica) .
X = hector ;
X = pedro ;
X = julio.
2 ?-
Donde vale anotar que el intérprete sólo da la posibilidad de introducir el “;” si existen otras
combinaciones de éxito.
La introducción del “;” equivale a forzar un retroceso luego del éxito, para que la
resolución SLD busque otro camino de demostración alternativo. Así, el “;” equivale a
indicarle al proceso que el éxito es considerado un fracaso por el usuario de la consulta.
Capítulo 12: PROLOG como lenguaje de búsqueda
370
1 ?- ama(X,verónica),ama(X,Y),Y\==verónica .
X = pedro,
Y = claudia ;
X = pedro,
Y = luisa ;
X = julio,
Y = luisa ;
false.
Puede comprobarse que corresponden al orden en que se efectúa el retroceso, luego de que cada éxito es
rechazado por el usuario. Así, luego de rechazar la primera pareja X = pedro, Y = claudia, la resolución
SLD se retracta de la unificación de ama(pedro,Y), obteniendo un nuevo valor para Y. Rechazado
también este último resultado, y no existiendo otra opción para ama(pedro,Y), se retracta la unificación
ama(pedro,verónica) obteniendo un nuevo valor para X.
El PROLOG ofrece, también, el predicado findall(_,_,_) para recolectar en una meta de una regla las
diferentes soluciones a una consulta poniéndolas a disposición de las siguientes metas de la regla. Para
más información al respecto remitimos al lector a [Blackburn 01: secc 11.2].
La adición de reglas asociadas con los datos contenidos en una base de datos, posibilita
deducir información de la base de datos sin que dicha información esté explícitamente
registrada en los hechos. A este tipo de Bases de Datos se le ha denominado Bases de
Datos “deductivas” [Celma 95],[Ceri 90] .
rivales(X,Y) :- ama(X,Z),ama(Y,Z),X\==Y .
Y permite deducir quienes son rivales, a partir de las relaciones amorosas:
2 ?- rivales(X,Y)
X = pedro, Y = julio ;
X = julio, Y = pedro ;
X = verónica, Y = claudia ;
X = hector, Y = pedro ;
X = hector, Y = julio ;
X = claudia, Y = verónica ;
X = pedro, Y = hector ;
X = pedro, Y = julio ;
X = julio, Y = hector ;
X = julio, Y = pedro ;
false.
Cabe notar, que por la simetría de la relación si X y Y son rivales, también lo son Y y X:
Un objetivo formado por una secuencia de metas posibilita efectuar consultas más
complejas.
Capítulo 12 : PROLOG como lenguaje de búsqueda
371
Los registros almacenados en una base de datos pueden ser considerados como átomos de predicados
asociados con cada tipo de registro (o tabla de la BD). Así, los siguientes seis grupos de registros:
8 ?- cursa(maria,CURSO),salon_curso(CURSO,S),lugar_salon(S,minas) .
CURSO = calculo, S = d-100 ;
CURSO = algoritmos, S = b-240 ;
false.
¿En que campus podrían verse juan y maria?
Los comandos assert, (retract,) asserta, y assertz toman como argumento un hecho o regla y lo
incluyen (o retiran) del programa (ver [Blackburn 01: secc 11.1]). Ellos requieren, sin embargo, que el
predicado que redefinen sea declarado en el programa como “dinámico”. Así, si en el programa del
ejemplo anterior se incluye, la cláusula siguiente:
:- dynamic cursa/2 .
Es posible introducir y quitar hechos durante la ejecución:
2 ?- cursa(X,fisica) .
Capítulo 12: PROLOG como lenguaje de búsqueda
372
X = juan.
3 ?- assert(cursa(jose,fisica)) .
true.
5 ?- cursa(X,fisica) .
X = juan ;
X = jose.
6 ?- retract(cursa(juan,fisica)) .
true .
7 ?- cursa(X,fisica) .
X = jose.
8 ?-
Nótese que el cambio en la base de hechos sólo tiene efecto durante la ejecución (el archivo con los
hechos originales queda inmodificado)175.
padre(jose,ana) .
padre(pedro,maria) .
padre(jose,pedro) .
ancestro(X,Y) :- padre(X,Y) .
ancestro(X,Y) :- ancestro(X,Z),padre(Z,Y) .
Es claro que ana y maria tienen a jose como ancestro común ya que es padre de ana y abuelo de maria.
Sin embargo, al llevar a cabo la consulta siguiente:
1 ?- ancestro(A,maria),ancestro(A,ana) .
ERROR: Out of local stack
La resolución SLD es incapaz de hallar la respuesta correcta, cayendo en ciclo infinito que es inducido
por la transitividad inherente al concepto de ancestro(). Para ilustrar la manera como ocurre la falla en el
proceso, presentamos a continuación la tabla que muestra la evolución del objetivo.
175Cabe resaltar que el PROLOG, en si mismo, no es un gestor de Bases de Datos, ya que no incorpora de forma nativa
los elementos necesarios para preservar las restricciones de integridad, crear vistas, optimizar consultas, gestión de
usuarios, etc…
Capítulo 12 : PROLOG como lenguaje de búsqueda
373
5* 5 pedro/X, Z´/Y ancestro(pedro,Z´´),padre(Z´´,Z´), padre(Z´,Z),padre(Z,ana)
6 4 pedro/X, Z´´/Y padre(pedro,Z´´),padre(Z´´,Z´), padre(Z´,Z),padre(Z,ana)
7 2 maria/Z´´ padre(maria,Z´), padre(Z´,Z),padre(Z,ana)
8 ? Falla
....
....
Donde se puede observar que el proceso tratar de resolver el objetivo introduciendo, de forma reiterada,
un miembro más en la cadena de ancestría.
Un simple cambio en el orden de las metas de la consulta, produce el resultado esperado, así:
2 ?- ancestro(A,ana),ancestro(A,maria) .
A = jose
De lo anterior se induce que el proceso de resolución SLD no sólo puede ser ineficiente para ciertas
consultas, sino que, por si mismo, es incapaz de dar cuenta de todas las situaciones
Para controlar los fallos y retrocesos del proceso SLD, el PROLOG ofrece los predicados
de “!” (corte) y “fail” (negación), así:
!, colocado como predicado (o meta) en la cola de una regla obliga al
proceso de resolución a mantener las substituciones de las variables de la
regla que se hayan efectuado antes del corte, en caso de que el proceso falle
luego del corte. En otras palabras un retroceso no puede tratar de unificar de
nuevo ninguno de los predicados que aparecen en la regla antes del corte,
incluyendo la cabeza de la regla; así, los valores de las variables de la regla
ya ligadas, no pueden ser redefinidos.
fail, colocado como predicado (o “meta”) en la cola de una regla, induce un
fracaso generando un retroceso en el proceso.
Para mayor información sobre estos predicados ver [Blackburn 01: secc 10].
El operador pertenece definido antes para establecer si un valor se encuentra en una lista de valores:
pertenece(X,[X|_]) .
pertenece(X,[_|T]) :- pertenece(X,T) .
Permite obtener, uno a uno, los valores de la lista. Para ello basta con evocar el predicado usando una
variable para el valor cuya pertenencia se quiere verificar, y luego, rechazar (con “;”) una y otra vez la
respuesta obtenida, así:
1 ?- pertenece(X,[1,2,3]) .
X=1;
X=2;
X=3;
false.
2 ?-
Para hallar en un conjunto, un elemento que satisfaga una condición dada es suficiente con
obtener los valores, uno a uno, y verificar la condición (fallando en caso contrario).
El operador halle_menor_que obtiene el primer elemento de una lista que cumple la condición de ser
menor que un valor dado:
2 ?- halle_menor_que(4,[5,2,6,7,1],X).
X=2.
Para obtener todos los valores basta con rechazar de forma sucesiva la respuesta obtenida:
2 ?- halle_menor_que(4,[5,2,6,7,1],X).
X=2;
X=1;
false.
Para recolectar todos los valores puede usarse el predicado nativo findall, así:
halle_menores_que(U,L,R) :- findall(X,halle_menor_que(U,L,X),R) .
En ejecución:
1 ?- halle_menores_que(4,[5,2,3,6,7,1],R) .
R = [2, 3, 1].
Capítulo 12 : PROLOG como lenguaje de búsqueda
375
Para el caso en que el conjunto es una lista, y se desea obtener parejas de valores de la lista,
el mecanismo para obtener uno a uno las diferentes parejas de valores, puede construirse
fácilmente a partir del predicado de pertenencia.
El predicado copy_2 toma una pareja de valores de la lista, permitiendo que los dos elementos de la
pareja se tomen de la misma posición de la lista:
1 ?- copy_2([1,2,3],R) .
R = [1, 1] ;
R = [1, 2] ;
R = [1, 3] ;
R = [2, 1] ;
R = [2, 2] ;
R = [2, 3] ;
R = [3, 1] ;
R = [3, 2] ;
R = [3, 3] ;
false.
El predicado pick_2 toma una pareja de valores de la lista, evitando que los dos elementos de la pareja se
tomen de la misma posición de la lista:
pertenece(X,[X|T],T) .
pertenece(X,[_|T],R) :- pertenece(X,T,R) .
En ejecución:
2 ?- pick_2([1,2,3],R) .
R = [1, 2] ;
R = [1, 3] ;
R = [2, 3] ;
false
Para hallar en un conjunto, una pareja de elementos que satisfaga una condición dada es
suficiente con obtener las parejas, una a una, y verificar la condición (fallando en caso
contrario).
El operador halle_parque_sumev obtiene el primer par de una lista que cumple la condición de que sus
componentes suman un valor dado:
1 ?- halle_parque_sumev(3,[0,1,2,3,4],R).
R = [0, 3] .
Capítulo 12: PROLOG como lenguaje de búsqueda
376
Para obtener todos los valores basta con rechazar de forma sucesiva la respuesta obtenida:
1 ?- halle_parque_sumev(3,[0,1,2,3,4],R).
R = [0, 3] ;
R = [1, 2] ;
R = [2, 1] ;
R = [3, 0] ;
false.
Para recolectar todos los valores puede usarse el predicado nativo findall, así:
halle_losparque_sumev(V,L,R) :- findall(X,halle_parque_sumev(V,L,X),R) .
En ejecución:
1 ?- halle_losparque_sumev(3,[0,1,2,3],R) .
R = [[0, 3], [1, 2], [2, 1], [3, 0]].
Para el caso en que el conjunto es una lista, y se desea obtener tuplas de n valores de la
lista, se debe generalizar el predicado propuesto en los ejemplos anteriores.
El predicado copy_n toma una tupla de n valores de la lista, permitiendo que los elementos de una tupla
se tomen de la misma posición de la lista:
copy_n(0,_,[]) .
copy_n(N,L,[X|A]) :- N>0, pertenece(X,L), N1 is N-1, copy_n(N1,L,A) .
En ejecución:
4 ?- copy_n(3,[1,2],R) .
R = [1, 1, 1] ;
R = [1, 1, 2] ;
R = [1, 2, 1] ;
R = [1, 2, 2] ;
R = [2, 1, 1] ;
R = [2, 1, 2] ;
R = [2, 2, 1] ;
R = [2, 2, 2] ;
false.
El predicado pick_2 toma una pareja de valores de la lista, evitando que los dos elementos de la pareja se
tomen de la misma posición de la lista:
pick_n(0,_,[]) .
pick_n(N,L,[X|A]) :- pertenece(X,L,T), N1 is N-1, pick_n(N1,T,A) .
En ejecución:
5 ?- pick_n(3,[1,2,3,4],R) .
R = [1, 2, 3] ;
R = [1, 2, 4] ;
R = [1, 3, 4] ;
R = [2, 3, 4] ;
false.
Capítulo 12 : PROLOG como lenguaje de búsqueda
377
Para hallar en un conjunto, una tupla de elementos que satisfaga una condición dada es
suficiente con obtener las tuplas, una a una, y verificar la condición (fallando en caso
contrario).
El operador halle_tuplaque_orden obtiene la primer tupla de una lista que cumple la condición de que
sus componentes estén ordenados en orden estricto creciente:
1 ?- halle_tuplaque_orden(3,[1,2,3,4],R) .
R = [1, 2, 3] .
Para obtener todos los valores basta con rechazar de forma sucesiva la respuesta obtenida:
1 ?- halle_tuplaque_orden(3,[1,2,3,4],R) .
R = [1, 2, 3] ;
R = [1, 2, 4] ;
R = [1, 3, 4] ;
R = [2, 3, 4] ;
false.
Para recolectar todos los valores puede usarse el predicado nativo findall, así:
halle_tuplasque_orden(N,L,R) :- findall(X,halle_tuplaque_orden(N,L,X),R) .
En ejecución:
1 ?- halle_tuplasque_orden(3,[1,2,3,4],R) .
R = [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]].
Un ejemplo clásico de este tipo de problema es el del coloreo de mapas. El problema consiste en hallar
la forma de colorear las diferentes regiones de un mapa, a partir de un conjunto finito de colores,
evitando que dos regiones contiguas queden con el mismo color.
Para representar los elementos del problema, usaremos los siguientes elementos de datos:
Capítulo 12: PROLOG como lenguaje de búsqueda
378
Un número (N) que representa el número de regiones del mapa.
Una lista de pares de regiones (LPRG) que representa las relaciones de contigüidad entre las
mismas. Así, si en la lista se halla el par [i,j], la región i y la región j son contiguas en el mapa.
Una lista de nombres de colores (CLRS) , asumidos como diferentes.
Para representar una alternativa de solución usaremos los siguientes elementos de datos:
Una lista (CLREO) de N colores que corresponden a la asignación de colores a las N regiones
del mapa.
Para verificar si una alternativa es correcta, se debe verificar que si dos regiones aparecen en una de las
parejas de contigüidad ellas deben tener colores diferentes:
get_color(1,[C|_],C) .
get_color(I,[_|T],C) :- I>1, I1 is I-1, get_color(I1,T,C) .
verifique_coloreo([],_) .
verifique_coloreo([[I,J]|T],CLREO) :- get_color(I,CLREO,CI), get_color(J,CLREO,CJ), CI\=CJ,
verifique_coloreo(T,CLREO) .
Para hallar un coloreo adecuado, basta con generar uno a uno los posibles coloreos verificando que sean
correctos hasta hallar uno que lo sea:
1 ?- halle_parque_sumev(3,[0,1,2,3,4],R).
R = [0, 3] .
Para obtener todos los valores basta con rechazar de forma sucesiva la respuesta obtenida:
halle_coloreo_mapa(N,LPRG,CLRS,CLREO) :-
copy_n(N,CLRS,CL), verifique_coloreo(LPRG,CL), CLREO = CL .
ejemplo_fisher(C) :-
halle_coloreo_mapa(5,[[1,2],[1,3],[1,4],[1,5],[2,3],[2,4],[3,4],[4,5]],[red,green,blue,yellow],C) .
En ejecución:
3 ?- halle_coloreo_mapa(4,[[1,2],[1,3],[2,4],[3,4]],[rojo,verde],C) .
C = [rojo, verde, verde, rojo] ;
C = [verde, rojo, rojo, verde] ;
false.
1 ?- ejemplo_fisher(C).
C = [red, green, blue, yellow, green] ;
C = [red, green, blue, yellow, blue] ;
C = [red, green, yellow, blue, green] ;
C = [red, green, yellow, blue, yellow]
Acertijos
El siguiente ejemplo, el acertijo de la zebra, consiste en la construcción de un predicado
que permita resolver el siguiente acertijo:
“Hay una calle con tres casas contiguas de tres colores diferentes. Una es roja, la otra azul y
la otra verde. Gente de diferente nacionalidad vive en las diferentes casas y todos tienen
diferentes mascotas. Además se sabe también que:
El inglés vive en la casa roja.
El jaguar es la mascota de la familia española.
El japonés vive a la derecha del dueño del caracol.
El dueño del caracol vive a la izquierda de la casa azul.
Capítulo 12 : PROLOG como lenguaje de búsqueda
379
¿Quien es el dueño de la zebra?”
Lo que se busca es un predicado zebra, que tome un argumento que corresponda a la
nacionalidad del dueño de la zebra, de manera que al hacer la consulta
:- zebra(NAC) .
El intérprete responda NAC=..., con la respuesta correcta al acertijo.
Para resolverlo haremos uso de algunos de los predicados construidos en las secciones
anteriores. Realmente no queda más que agregar. En medio del código se incluyeron
comentarios (aquellas líneas encabezadas por %%) para mayor claridad.
zebra(N) :-
%% La calle es representada como una lista de 3 casas.
%% Una casa es representada como una lista de 3 propiedades:
%% color, nacionalidad y mascota.
%% Hay una casa roja en la calle.
miembro([roja,_,_], [Casa1,Casa2,Casa3]),
%% Hay una casa Azul en la calle.
miembro([azul,_,_], [Casa1,Casa2,Casa3]),
%% Hay una casa verde en la calle.
miembro([verde,_,_], [Casa1,Casa2,Casa3]),
%% El inglés vive en la casa roja.
miembro([roja,inglés,_], [Casa1,Casa2,Casa3]),
%% El jaguar es la mascota de la familia española.
miembro([_,español,jaguar], [Casa1,Casa2,Casa3]),
%% El japonés vive a la derecha del dueño del caracol.
sublista([[_,_,caracol],[_,japonés,_]], [Casa1,Casa2,Casa3]),
%% El dueño del caracol vive a la izquierda de la casa azul.
sublista([[_,_,caracol],[azul,_,_]], [Casa1,Casa2,Casa3]),
%% La zebra es la mascota de N.
miembro([_,N,zebra], [Casa1,Casa2,Casa3]) .
Vale la pena construir un árbol de búsqueda, al menos incompleto, para ilustrar la búsqueda
que se genera al plantear la consulta.
:- zebra(N) .
Como conocemos el funcionamiento de el predicado miembro, y también el de sublista,
no hará falta exponer en el árbol toda la búsqueda generada por estos predicados, tan solo
recobraremos el resultado que sabemos arrojarán. En la figura siguiente se muestra el árbol
incompleto generado.
Capítulo 12: PROLOG como lenguaje de búsqueda
380
zebra(N)
N = _01
miembro([roja,_,_],[Casa1,Casa2,Casa3]),...,
miembro([_,_01,zebra],[Casa1,Casa2,Casa3])
Casa1 = [roja,_02,_03]
miembro([azul,_,_],[[roja,_02,_03],Casa2,Casa3]),...
Casa2 = [azul,_04,_05]
miembro([verde,_,_],[[roja,_02,_03],[azul,_04,_05],Casa3]),...
Casa3 = [verde,_06,_07]
miembro([roja,inglés,_],[[roja,_02,_03],[azul,_04,_05],[verde,_06,_07]]),...
_02 = ingles
miembro([_,español,jaguar],[[roja,ingles,_03],[azul,_04,_05],[verde,_06,_07]]),...
_04 = español _06 = español
_05 = jaguar _07 = jaguar
sublista([[_,_,caracol],[_,japonés,_]],
[[roja,ingles,_03],[azul,español,jaguar],[verde,_06,_07]]),...
sublista([[_,_,caracol],[_,japonés,_]],
[[roja,ingles,_03],[azul,_04,_05],[verde,español,jaguar]]),...
_03 = caracol
_04 = japonés
sublista([[_,_,caracol],[azul,_,_]],
[[roja,ingles,caracol],[azul,japonés,_05],[verde,español,jaguar]]),...
miembro([_,_01,zebra],
[[roja,ingles,caracol],[azul,japonés,_05],[verde,español,jaguar]])
_01 = japonés
_05 = zebra