Lógica Ecuacional en Desarrollo de Software
Lógica Ecuacional en Desarrollo de Software
Software.
Volumen I: Lógica ecuacional y
lenguajes funcionales.
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.
ISBN:
MATERIA:
Informática 681.3
Formato: Digital
FERNANDO ARANGO I.
Aplicaciones de la Lógica al Desarrollo de Software.
No está permitida la reproducción total o parcial de esta obra
Ni su tratamiento o transmisión por cualquier medio o método
Sin autorización escrita de la Editorial.
DERECHOS RESERVADOS
2008 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
AGRADECIMIENTOS ................................................................................................................................. IX
CAPÍTULO 1.................................................................................................................................................... 1
LÓGICA ECUACIONAL.............................................................................................................................. 61
4.1 INTRODUCCIÓN. ............................................................................................................................. 62
4.2 TÉRMINOS. ..................................................................................................................................... 62
4.2.1 Operadores. .............................................................................................................................. 63
4.2.2 Criterios formativos de los términos. ....................................................................................... 64
4.2.3 Términos Complejos ................................................................................................................. 65
4.2.4 Interpretación de los Operadores............................................................................................. 69
4.3 LÓGICA ECUACIONAL .................................................................................................................... 70
4.3.1 El predicado de igualdad.......................................................................................................... 70
4.3.2 Criterios de Demostración. ...................................................................................................... 72
4.3.3 Teoría en lógica ecuacional. .................................................................................................... 72
4.4 RESUMEN DEL CAPÍTULO. .............................................................................................................. 75
4.5 EJERCICIOS PROPUESTOS................................................................................................................ 76
CAPÍTULO 5.................................................................................................................................................. 79
Este trabajo se presenta al final del año sabático del autor iniciado en Agosto 7 2007. Con
él el autor pretende cumplir con uno de los objetivos propuestos para éste año, a saber:
“El libro de texto incluirá los elementos siguientes:
• Los conceptos de la lógica que dan soporte a los lenguajes declarativos de
programación.
• Una caracterización y clasificación de los lenguajes declarativos más
representativos.
• Uso de los lenguajes declarativos en diferentes tipos de aplicación, incluyendo
ejemplificación y ejercicios propuestos.
• El uso de la lógica como medio para extender la expresividad de los modelos
del paradigma de orientación por objetos que son usados en el
UN_METODO.”
Tal como se indica en el prefacio del trabajo, éste parte del libro anterior: “Elementos
Básicos de Los Lenguajes Declarativos” (Fernando Arango Isaza y Daniel Cabarcas, ISBN:
958-97945-1-3) del que toma varios elementos. De los capítulos del libro original, sólo se
preservaron sin mayores modificaciones los relativos a la lógica de predicados y los que
tienen que ver con el lenguaje PROLOG. Se reutilizaron también muchas de las figuras y
casi todos los ejercicios propuestos (que fueron revisados y completados). A grosso modo
de las 310 páginas de éste trabajo, aproximadamente 70 pertenecen al libro original. Los
cambios conceptuales son, por otro lado, más profundos tal como se explica en el prefacio
del trabajo propiamente dicho.
El trabajo, sin embargo, no puede considerarse un producto terminado. En efecto, aparte de
las obligadas revisiones a los ejemplos y nuevos ejercicios, aún faltan secciones
importantes. Principalmente en lo relativo a las partes III y IV del trabajo que tienen que
ver con aspectos importantes tales como el estudio de los demostradores generales de
teoremas, la formalización del modelo objetual y los procesos de refinamiento que permiten
convertir los modelos del análisis a programas ejecutables propiamente dichos, y que, por
ser tema de investigación los mecanismos para automatizar estos procesos, constituyen un
objetivo importante a ser satisfecho en el futuro próximo.
Prefacio. viii
PREFACIO
AGRADECIMIENTOS
A los estudiantes que contribuyeron de forma directa e indirecta a la realización del trabajo.
A Daniel Cabarcas que trabajó en la elaboración de los módulos de clase para el curso
“Lenguajes Declarativos”, que dieron lugar al libro anterior, del que aún se conservan
varios elementos. A Willington Vega que se ha encargado de validar los códigos
propuestos en el trabajo, así como de validar y completando algunos apartes del mismo.
Parte I: Introducción
Capítulo 1
Evolución de los Lenguajes de
Programación
Capítulo 1: Evolución de los Lenguajes de Programación
2
1.1 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 complejidad1.
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 adjudica2, ha impulsado el rápido desarrollo de una disciplina asociada con el problema
del desarrollo del software3.
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.
1
En [Link] se presentan algunas consideraciones sobra la
naturaleza del software y las causas de su complejidad.
2
En [Berryman 2007] se refiere que en el año 2007 se invirtieron 12 billones de dólares en procesos para la innovación en
la industria del software.
3
Usualmente denominada “Ingeniería del Software” [Pressman 05]. Ver [Link]
Capítulo 1: Evolución de los Lenguajes de Programación
3
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
programación, han venido incorporado de forma paulatina nuevas categorías de conceptos
para facilitar la elaboración de modelos cada vez más cercanos al universo del problema4.
Esta circunstancia 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 parten de
aserciones que involucren conceptos de estas categorías.
4
O al menos a la manera como entendemos el universo del problema.
5
Esta tarea se lleva a cabo, además, por largos períodos de tiempo y se extiende entre lugares distantes en el espacio. En
este sentido, el computador lleva también a cabo funciones de almacenamiento, transporte y distribución de datos.
Capítulo 1: Evolución de los Lenguajes de Programación
4
ejecución de los programas, y de controlar el funcionamiento de todos los dispositivos del
computador.
Dispositivos internos de Almacenamiento: Los datos, los programas, y demás
información asociada a los procesos, son almacenados en el computador en diversos
dispositivos de almacenamiento interno. Estos dispositivos almacenan la información en
una serie de lugares que contienen una o varias unidades de almacenamiento. Cada
unidad de almacenamiento puede almacenar un número específico de dígitos según el
computador6.
Entre los dispositivos de almacenamiento vale la pena destacar los siguientes:
• Registros del procesador: Son un grupo relativamente pequeño de unidades de
almacenamiento, que contienen los datos con los que el procesador está llevando a cabo
las operaciones más elementales del momento. En ellos el sistema provee la más alta
velocidad de manipulación y el más bajo grado de permanencia para los datos.
• Memorias de acceso directo (RAM ó ROM7): Son un conjunto de unidades de
almacenamiento, a los que el procesador puede acceder directamente para tomar o
colocar los datos y resultados que se manipulan y crean durante el proceso. El
procesador accede a estos lugares, por medio de un número que identifica a cada lugar
(o “dirección”), y lo hace en un tiempo uniforme que no cambia entre lugares
diferentes8. En la memoria de acceso directo, el sistema provee una velocidad
relativamente alta de manipulación para los datos, y, una permanencia determinada y
limitada por el tiempo de ejecución del programa que los usa.
• Dispositivos de almacenamiento a largo plazo: Son un conjunto relativamente grande
de unidades de almacenamiento, con capacidad para almacenar grandes cantidades de
datos por largos períodos de tiempo. En estos dispositivos la información se almacena
en grupos denominados “Archivos”, cada uno de ellos asociado a un nombre o
identificador único. Los datos en los archivos permanecen almacenados aunque el
computador no se encuentre encendido, por lo que se usan para compartir datos entre
diferentes programas diferentes ejecuciones de un programa y diferentes computadoras9.
El procesador no tiene acceso directo a los datos contenidos en los archivos, y ellos
deben ser transferidos primero a la RAM con la ayuda de unidades especializadas de
proceso. La velocidad de acceso a la información almacenada en estos dispositivos, es
sensiblemente menor que en la RAM y, además, no ocurre en tiempo uniforme ya que
depende del lugar donde se encuentre. Entre estos dispositivos se encuentran los discos
magnéticos y ópticos, fijos y removibles, y las cintas de almacenamiento magnético.
6
La 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
7
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.
8
En 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.
9
Ya que algunas de ellas son “removibles”.
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, en efecto, formando uno o varios “hilos
de ejecución” que ocurren ya sea en “paralelo”, o 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ás10.
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 por el programa, 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
10
De 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.
11
No 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
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 el nivel de las
aplicaciones es el más especializado y de más alto nivel posible para comunicarse con la
máquina.
12
En [Link] pueden verse una recopilación de lenguajes de
programación, con enlaces web a descripciones más extensas de cada lenguaje. En
[Link] se presenta gráficamente las fechas de aparición, evolución y relaciones entre
un conjunto de lenguajes relevantes.
13
En [Link] se presentan documentos relacionados
con la clasificación de los lenguajes de programación.
14
Uno de los motores del desarrollo de lenguajes, es la expansión de las áreas de aplicación del computador, que trae
consigo nuevos problemas que deben resolverse y que con frecuencia, promueven la inclusión de características nuevas en
los lenguajes. Es claro por ejemplo, que los primeros lenguajes se orientaban a llevar a cabo cálculos complejos, con lo
que aparecieron lenguajes capaces de utilizar directamente fórmulas matemáticas (Vg. FORTRAN, Pascal, LISP); al
orientarse el uso de la computadora al manejo de las grandes cantidades de datos y reportes que se generan en las
organizaciones, aparecen lenguajes especializados en el manejo de archivos y reportes (COBOL, RPG); al detectarse los
problemas de complejidad que surgen del crecimiento del tamaño de los programas y de la necesidad de compartir
archivos, aparecen lenguajes que estructuran, modularizan y definen claramente las relaciones entre datos y procesos
(Lenguajes estructurados y SQL); al aparecer las computadoras con capacidades gráficas y posibilidad de utilizar sonido,
aparecen lenguajes que obvian la programación en secuencias texto, y se basan en representaciones gráficas de los
elementos del problema (programación visual); al ser utilizada la computadora como el vehículo para, controlar y registrar
las operaciones de las organizaciones, aparecen lenguajes que permiten describir, no solo los elementos que se involucran
en dichas operaciones, sino también las reglas que las controlan y el efecto que tienen sobre los elementos a los que se les
aplican (lenguajes orientados a objetos); al usarse el computador como medio de comunicación a través de la WWW,
Capítulo 1: Evolución de los Lenguajes de Programación
9
Una tendencia que vale la pena destacar, es la de la especialización de los lenguajes de
programación según el área específica de aplicación. En el Capítulo siguiente mostremos
cómo los lenguajes que se usan en el desarrollo del software, tanto a nivel de la su
concepción (análisis y diseño del software) como de su construcción (o programación del
software), incorporan categorías conceptuales que les permiten representar (en el software),
cada vez de forma más precisa, los elementos del domino del problema (o “área de
aplicación”).
aparecen lenguajes que facilitan elaborar elementos ejecutables que se transmiten por la red y se interpretean 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.
15
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.
16
Tiene una memoria que almacena la dirección de la instrucción que está ejecutando.
17
Aumente 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
dichos registros, y, por último, llevara a la RAM los resultados contenidos en los registros.
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ás18.
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écnicos19.
Ejemplo 1
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:
18
Usando la dirección del lugar del programa que llevo a cabo la evocación, que debió haberse almacenado antes del
salto.
19
Ver [Link] 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 definido20. 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 “hardware21” 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.
[Link] 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...
20
Y posiblemente restringido a ciertos tamaños preestablecidos.
21
O máquina física.
Capítulo 1: Evolución de los Lenguajes de Programación
12
El traductor de un lenguaje de tercera generación se encarga de generar un programa en
lenguaje de máquina, correspondiente al escrito en el lenguaje de 3ª generación, sin
ninguna participación del programador en el proceso de traducción. Es, entonces, el
traductor quien aporta el conocimiento de la máquina al programa traducido, y quien, por
tanto es específico a una familia particular de máquinas.
Por otro lado, la estandarización de los lenguajes de 3ª generación22, en cuanto a la forma
de las instrucciones (“sintaxis”) y el significado de las mismas (“semántica”), ha permitido
que los programas escritos en estos lenguajes sean ejecutables en prácticamente cualquier
computador.
[Link] Valores, Tipos, Operadores y Términos.
Una característica básica de los lenguajes de tercera generación es su capacidad para
manipular de forma explícita diferentes tipos de valor; cada uno de ellos orientado a
representar un tipo específico de datos en el área de aplicación.
Así, en los lenguajes de 3ª generación aparecieron primero, los tipos enteros y reales para
manipular cantidades numéricas y los tipos caracteres y cadenas para manipular textos.
Estos tipos se han ido extendiendo a tipos más especializados, como fechas, números
complejos, vectores, tablas, etc...
Para cada tipo de dato el lenguaje ofrece operadores específicos al tipo. Así, para los
números enteros se ofrecen las operaciones aritméticas básicas, para los números reales se
ofrece además el cálculo de las funciones trascendentales, y para las cadenas de caracteres
se ofrecen funciones de concatenación, de extracción de subcadenas, de búsqueda etc...
En los lenguajes de 3ª generación más avanzados el programador puede, también, crear sus
propios tipos de datos y definir las operaciones que se les aplican (ver Capítulo 2).
Los valores de los diversos tipos de datos se representan en las instrucciones del programa
por medio de términos que pueden ser literales, variables o expresiones.
Los literales son referencias directas a un valor específico del tipo (Vg. 3.1415).
Una variable es un rótulo (o “identificador”) que representa un lugar en la memoria donde
se almacena un valor del tipo, y sirve tanto para referirse al valor almacenado cuando se va
a usar en un calculo, como para referirse al lugar de almacenamiento cuando se va a colocar
en él un valor.
Las expresiones son fórmulas matemáticas que evocan la aplicación de una o varias
operaciones a un conjunto de valores (Vg. 3+5/A). Las expresiones por un lado, instruyen
a la máquina para que lleve a cabo las operaciones que evocan, y por el otro, refieren al
valor resultante de llevar a cabo dichas operaciones, en el contexto de la instrucción donde
aparece la expresión.
La manera de representar y almacenar en la memoria de una máquina los valores de cada
tipo, y la manera de llevar a cabo con base en las instrucciones de la máquina el cálculo de
las operaciones y términos complejos para cada tipo, son de conocimiento exclusivo del
traductor del lenguaje para dicha máquina. El traductor tiene, en efecto, “compilados” los
22
Ver por ejemplo la página web de la ANSI (American National Standars Institute) en [Link]
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 fuente23.
[Link] 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.
[Link] 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 “;” o 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.
23
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
anómala en el proceso.
[Link] 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.
El lenguaje GAUSS, un lenguaje de 3ª generación, introduce la posibilidad de minimizar el
uso de instrucciones procedurales ofreciendo poderosos operadores sobre matrices. En el
lenguaje MATHEMATICA, todo se escribe por medio de expresiones que son evaluadas en
una línea de comandos. En estos lenguajes aparece claramente la idea de sustituir la
descripción del proceso computacional por una expresión declarativa.
La aparición de lenguajes que evitan describir de forma explícita el proceso computacional,
por medio de instrucciones de asignación, Entrada/Salida e instrucciones de
Secuenciamiento fue denominado en [Martin XX] como lenguajes de “cuarta generación”.
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.
Dado que en los gestores de “bases de datos” se generalizan operaciones comunes de
organización acceso y presentación de la información que contienen, 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 Lanaguages”) y
Capítulo 1: Evolución de los Lenguajes de Programación
15
lenguajes orientados a describir la forma y contenido de los reportes y pantallas (o “Report
Generation Languages”). Entre ellos 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 aspectos específicos del programa. En particular es usual que la definición
del interfaz del programa en un medio ambiente grafico, como el que ofrecen los sistemas
operativos modernos, se lleve 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.
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 como medio para
especificar el programa, constituye un avance significativo, que, aquí incluiremos en la 5ª
generación.
En los lenguajes de 5ª generación se describen las características de los elementos de una
á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.
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” 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
Capítulo 1: Evolución de los Lenguajes de Programación
16
lógica ecuacional. Un “intérprete” de la teoría así descrita, se usa para llevar a cabo el
cálculo de términos base24 construidos con los operadores definidos.
En los lenguajes “lógicos” 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.
24
Expresiones que no tienen variables.
Capítulo 2
Evolución de los paradigma
arquitectónicos
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
18
2.1 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
área de aplicación del software, en contraste con lenguajes orientados a describir el proceso
computacional asociado con la ejecución del software en el computador. 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 la lógica y la ejecución de los programas son
procesos de demostración en el marco de la 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
y una modularización, o “arquitectura”, al software en concordancia con las necesidades y
arquitectura del área de aplicación. Esto usualmente se traduce en la división del proceso
de desarrollo en las fases25 siguientes: modelamiento del área de aplicación o “análisis”,
especificación del enfoque, características y estructura 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”.
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 XX]. 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.
Si la construcción de los modelos se apoya en una lógica, esto se traduce en que los
modelos del análisis y diseño no necesariamente deben estar restringidos por las
condiciones de ejecutabilidad que se exige a los lenguajes de programación basados en una
lógica.
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 conceptos26 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”27. Los diferentes tipos de construcción que ofrecen
25
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, mientras que en un desarrollo “incremental” las fases se llevan a cabo de
forma repetitiva.
26
Consideraremos, por ejemplo los conceptos casa, habitación, silla etc.., como miembro de un tipo de concepto, el tipo
de los “objetos físicos”.
27
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
[Link] , 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
19
los lenguajes de programación permiten representar entes de las diferentes categorías. Así,
por ejemplo, las variables permiten representar “propiedades”, los subprogramas permiten
representar “relaciones funcionales” 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.
La progresión de los 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 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. En el capitulo
siguiente se termina la parte introductoria presentando los elementos básicos de la lógica
proposicional.
Dominio Rango
Para especificar las diferentes operaciones se utilizan tanto el lenguaje natural como
formalismos lógicos, así:
• Lenguaje natural: Para verbalizar la transformación que lleva a cabo cada
función. La contundencia de dicha verbalización garantiza la calidad de la
descomposición.
sqrt(y,x,dx)
{if(|y-x*x|<=dx)x
else sqrt(y,(y/x+x)/2, dx)
}
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
21
• Pre y Post condiciones: Para describir el efecto de la función especificando las
condiciones que prevalecen antes y después de su ejecución.
Sea:
J={x Є Gasto/[Link] = [Link]}
Antes de la transacción:
|J|=1
[Link] = R
|[Link]|=1
[Link] + R ≤ ([Link]).Disponible
Después de la transacción:
[Link] = AumentoReserva + R
Donde:
“AumentoReserva” es la transacción cuyos datos se entran por pantalla
“J” Identifica al 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.
2.3.2 Arquitectura.
Bajo este paradigma, la arquitectura del software se basa en la descomposición progresiva
de las operaciones en operaciones más elementales (o de más bajo nivel). Esta
descomposición se lleva a cabo hasta llegar a operaciones elementales previamente
definidas (Vg. las básicas del lenguaje).
Las operaciones que componen a otra operación cooperan entre si, activándose de forma
coordinada e intercambiando datos. Las “formas estructuradas” restringen la activación de
las funciones componentes a patrones específicos de coordinación para facilitar la
elaboración de las especificaciones.
Para especificar la forma como se asocian las funciones (de más bajo nivel) para componer
otras funciones (de más alto nivel), se utilizan principalmente gráficos y “pseudo-código”,
así:
• El diagrama de composición jerárquica de funciones (DHF) especifica la
estructura de composición de las funciones, indicando que funciones son
componentes de otras funciones.
F1 F2
F11 F22
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
22
• El diagrama de Flujo de Control (DFL), describe la forma se secuencian,
repiten, y seleccionan para ejecución, las diferentes funciones componentes, al
llevarse a cabo la ejecución de la operación que componen.
P
P1
¿1?
P11 P12
P2
P21
¿2?
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
23
• El diagrama de Flujo de Datos (DFD) describe la manera como se intercambian
los datos las componentes de una función, indicando que funciones reciben
como datos los valores que otras funciones producen como resultados.
Í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
24
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.
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)
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
27
2.4.2 Problemas con los Archivos y Aparición de los Gestores de Bases de
Datos.
Este manejo de la información almacenada mostró rápidamente serios defectos. Algunos
de ellos ligados a los factores que se indican a continuación:
Cuando varios programas requieren los mismos datos, deben duplicarlos en archivos
diferentes o compartir los archivos.
Excesivo acoplamiento Programas-archivos de almacenamiento:
• Compartir los archivos implica que varios programas tienen la misma definición
del archivo, así un cambio en la estructura del archivo obliga a cambiar todos
los programas que lo usan.
• La nomenclatura, estructura y representación usada para los datos en los
registros del archivo domina sobre la usada por los programas amplificando el
efecto de los errores y dificultando la integración de programas desarrollados
independientemente.
El sistema no consigna las dependencias funcionales entre los datos:
• El “mismo” dato puede aparecer en diversos registros sin que el sistema los
actualice simultáneamente. Una actualización genera una anomalía pues el
usuario puede recibir dos valores para el mismo dato.
• Un dato almacenado puede ser calculado con base en otros pero no es
actualizado (o recalculado) cuando los otros lo son.
Funciones repetidas o imposibles de implementar.
• Cada programa debe incluir los mecanismos de control de acceso y recuperación
haciendo difícil implementar una política uniforme (depende de la voluntad de
individuos).
• No es posible tener sistemas que permitan acceso a los datos por múltiples
usuarios.
• La dificultad para llevar a cabo consultas no planeadas que deben ser
programadas.
A la solución de estos problemas se orientaron los programas encargados de los datos o
gestores de bases de datos que aplican diversas estrategias, entre ellas las siguientes:
• Separar de los programas de aplicación la especificación y control de los
archivos, aislándolos de ellos por medio de una interfaz de comunicación (un
lenguaje) con otro programa especializado en su gestión.
• Incorporar en el gestor de los archivos la especificación de los procesos de
consulta, actualización y control de acceso para facilitar su reutilización y
garantizar su aplicación permanente y homogénea.
• Definir mecanismos para representar y garantizar el mantenimiento de las
relaciones de dependencia entre los datos (tales como colocar índices o
referencias al dato en lugar del dato).
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
28
2.4.3 Modelo Relacional.
La creación de programas especializados en el manejo de archivos impulsaron también a un
cambio en la manera como los usuarios de los archivos (ahora programas y usuarios que
consultan), perciben la organización de los datos que usan, o sea la arquitectura de los
datos, que ahora no tiene que estar ligada a la manera como los programas los usan.
La arquitectura de datos más utilizada actualmente, se apoya en el paradigma relacional,
que es el producto de la aplicación a la informática de algunos de los conceptos de la teoría
de conjuntos, y en particular, del concepto matemático de relación.
• En esta arquitectura, los datos, o “atributos”, se agrupan en relaciones.
• Las dependencias entre los datos se representan con base en los atributos clave
de las tuplas que permiten representar relaciones de dependencia funcional.
• La normalización garantiza que un conjunto de relaciones represente
adecuadamente las relaciones de dependencia funcional entre los datos
(minimizando la redundancia y evitando “anomalías” por el mantenimiento de
datos).
• El álgebra relacional permiten 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 calculo relacional que constituye 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 SQL le da soporte tanto al álgebra relacional
como al cálculo relacional.
2.4.4 Arquitectura Relacional.
La definición de las relaciones de una aplicación y de las relaciones de dependencia entre
ellas, determina la arquitectura de los datos (frente a los programas). Para ello el analista
parte del reconocimiento de los datos relevantes del área de aplicación y del análisis de las
relaciones funcionales existentes entre los datos, así:
• Las tuplas se relacionan a las entidades concretas del área de aplicación y las
relaciones a las entidades abstractas (conceptos, o tipos de entidades).
• Los datos que distinguen una entidad concreta de otra se escogen como clave de
la relación y van acompañados en la relación de los datos que describen sus
propiedades.
• Entre las entidades existen diversos tipos de conexiones que son representadas
por las claves foráneas que hacen parte de las relaciones.
Las principales herramientas de diseño para la arquitectura de datos bajo el modelo
relacional son las siguientes:
• El modelo relacional que representa las relaciones y las posibles conexiones
entre los datos.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
29
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
30
El modelo Entidad/Relación, que representa las entidades lógicas del área de aplicación
junto con sus interconexiones. La derivación del modelo relacional desde el modelo E/R es
prácticamente automática.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
31
2.5 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 de las diferentes funciones de la aplicación. Esto genera 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 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 (Vg. el lenguaje ADA).
2.5.1 Concepto Clásico de Objeto.
La capacidad de definir nuevos tipos se le incorpora la de redefinir los tipos existentes por
medio de la relación de herencia, para impulsar el reuso de especificaciones ya existentes.
A las instancias de estos tipos susceptibles de herencia se les denominó, entonces, como
“clases” y a las instancias de ellos se les denominó “objetos”.
Modernamente se considera la aproximación Orientada Por Objetos como una disciplina
para el desarrollo de Software, que se aplica de forma uniforme en todas las etapas y fases
del desarrollo.
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 efecto.
La clase como medio de especificación de las propiedades comunes a grupos de
objetos, y de las propiedades de todos los objetos: La clase debe ser además el ente
básico de modularización del software, así, cualquier otra forma de modularización debe
respetar las fronteras entre las clases. Algunas características de las clases son las siguientes
• Las clases se asocian a tipos de datos (en [Meyer 98 sec 6.5] se considera que
una clase es la implementación de un Tipo Abstracto de Datos) que se
especifican y puede reutilizarse en múltiples piezas de software.
• Todo objeto es instancia de una clase y debe poder ser interrogado sobre la clase
de la que es instancia.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
32
• La clase define el estado y el interfaz de todos los objetos que son instancias de
ella.
El "encapsulamiento" como medio para simplificar estandarizar y minimizar las
interacciones entre los objetos que componen una pieza de software: El
encapsulamiento divide las componentes del objeto en la parte "pública" a la que acceden
los demás objetos con los que interactúa (en una pieza de software), y a la parte privada a le
que sólo puede acceder él mismo. El encapsulamiento supone que se cumplan las
propiedades siguientes:
• La interacción con los demás objetos sólo se lleva a cabo por invocación de las
componentes públicas (el lenguaje de programación debería garantizar esta
propiedad).
• El lenguaje de especificación de las clases debe permitir especificar en las
clases, las componentes de sus instancias que pertenecen a la parte pública y a la
parte privada.
• El usuario de una clase debe poder conocer el comportamiento del objeto sin
acceder a la parte privada. Para ello se debe contar con la especificación de
dicho comportamiento en un lenguaje diferente al de la programación (Vg.
aserciones de una lógica).
Herencia como mecanismo para establecer relaciones entre las clases: Ella facilita la
especificación de unas clases con base en otras previamente especificadas. Algunas
características deseables de la herencia son las siguientes:
• Herencia múltiple: Una clase debe poder heredar propiedades de diversas clases,
y deben existir mecanismos para resolver los conflictos de identificación de
dichas propiedades cuando se presenta colisión de nombres y/o herencia
repetida.
• Herencia Completa: Los objetos de una clase descendiente deben poder jugar el
mismo rol de un objeto de la clase ascendiente, en una sociedad de objetos en la
que aparece un objeto de esta última clase. Es decir ellos deben poder
considerarse como instancias de dichas clases.
El polimorfismo: Es el medio para permitir la definición de código genérico, en
particular:
• El comportamiento de un objeto que juega el rol de un objeto de una clase
ascendiente debe ceñirse a las reglas propias del objeto (aun cuando debe
comportarse de la forma esperada en el medio en que actúa).
• El comportamiento de los objetos de una clase debe poder diferirse a los objetos
de las clases descendientes de dicha clase. Es decir, dicho comportamiento debe
poder especificarse en la clase sin que en ella se implemente (no puede haber
objetos de dicha clase).
2.5.2 Relaciones entre objetos.
Las obvias similitudes del concepto de entidad y de objeto, dan lugar a la aparición de las
Bases de datos “orientadas por Objetos” y al modelo “Objeto-Relacional”.
Así como la herencia constituye una relación entre las clases, Entre los objetos se dan dos
tipos de relaciones, así:
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
33
Relaciones de asociación y agregación, que son relaciones estáticas similares a las
relaciones entre las entidades del modelo relacional. Estas relaciones pueden ser, sin
embargo, consideradas como clases por lo que pueden tener y atributos y métodos.
Relaciones de uso, que son relaciones dinámicas y representan el intercambio de mensajes
entre objetos de la aplicación. Así, un objeto puede crear otros objetos con los que luego
interactúa, o puede “enterarse” de la existencia de un objeto para luego interactuar con él.
2.5.3 Aplicaciones como Sistemas Dinámicos.
El concepto de método como una pieza de código en la clase, puede dividirse en aquellos
que consultan el estado del objeto, constituyéndose en sus atributos, y aquellos que
modifican el estado del objetos, constituyéndose en los ”eventos” potenciales.
Una secuencia de eventos sobre los objetos define una secuencia de estados de los mismos
que se denomina “vida del objeto”.
Las restricciones a la aplicación de eventos, en cuanto a las condiciones en que puede ser
aplicados, los posibles estados resultantes de su aplicación, y las posibles secuencias de
eventos aplicables a un objeto, determinan las posibles vidas de los objetos. La vida de un
objeto se inicia en su creación y se termina cuando es destruido.
Una aplicación es una colección de clases que definen objetos por existir. La ejecución de
la aplicación determina la existencia, o vida, para un conjunto de objetos que se comunican
entre ellos y con los usuarios de la aplicación a través de sus respectivos interfaces.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
34
2.5.4 Arquitectura Objetual.
La definición de las clases de una aplicación y de sus relaciones de herencia determina la
arquitectura de la aplicación.
Para ello el analista parte del reconocimiento de los objetos relevantes del área de
aplicación y los clasifica por sus propiedades y relaciones para definir las clases.
Las principales herramientas de diseño para la arquitectura de datos bajo el modelo
relacional son las siguientes:
• El modelo de clases que representa las clases con sus atributos y métodos, junto
con sus relaciones de herencia, asociación y uso.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
35
• El modelo de transición de estados que representa las posibles secuencias de
eventos aplicables a los objetos de una clase.
Evaluación
del TDG
Nombrar Jurado
Calificador
Ingresar
Disponibilidad Horaria
Director
Escuela
Jurado Estudiante
Calificador
28
Como trabajo futuro.
Capítulo 3: Elementos de Lógica Matemática
39
El criterio de autoridad es, sin embargo, insuficiente e incluso regresivo en el mundo de la
ciencia moderna29. En efecto, el científico moderno debe esperar, no sólo poder comprobar
por si mismo los hechos referidos, sino también, poder convencerse por si mismo de las
aserciones que se derivan de los hechos. Es en este sentido que la ciencia moderna exige
tanto la reproducibilidad de los experimentos30, como la coherencia de los “razonamientos”
que se derivan de los hechos.
Razonar es por otro lado una capacidad básica del ser humano. Por medio del
razonamiento obtenemos nuevas aserciones (o “conclusiones”) que se aceptan como
verdaderas, a partir de otras aserciones (o “premisas”) previamente aceptadas como
verdaderas. La importancia de las conclusiones obtenidas por razonamiento es que ellas
son el soporte de la mayoría de nuestras decisiones.
Garantizar que el proceso de razonamiento es llevado a cabo “correctamente” ha sido, en
consecuencia, una preocupación importante de los matemáticos y filósofos desde la
antigüedad.
La lógica es una disciplina que busca la manera de entender y sistematizar el razonamiento
con el objeto de garantizar la corrección o “veracidad” de las conclusiones que por su
medio se obtengan.
3.2.1 Lógica en la antigua Grecia.
En la Grecia Clásica, los textos matemáticos presentan a partir del siglo V (a.c.) un rigor en
el proceso de razonamiento que es esencialmente el mismo que el de la matemática
moderna. Tal como se señala en [Bourbaki 72, pg 12], para esta época “.. el ideal de un
texto matemático está perfectamente fijado, y encontrará su realización más perfecta en los
grandes clásicos, Euclides, Arquímedes y Apolonio...”.
En su Organon31, Aristóteles pretende sistematizar el proceso de razonamiento matemático,
argumentando que es posible “...reducir todo razonamiento correcto a la aplicación
sistemática de un pequeño número de reglas fijas, independientes de la naturaleza
particular de los objetos de que se trate...” [Bourbaki 72, pg 15]. En el marco de este
propósito lleva a cabo un estudio detallado de un tipo de razonamiento32 que denomina
“silogismo”.
Un silogismo consta de dos premisas y una conclusión, todas consistentes en frases de una
de las formas33: Todo S es P, Todo S no es P, Algún S es P, Algún S no es P, donde S y P son
conceptos o “términos” indeterminados. Las dos premisas deben tener un término común
(el término medio), que permite ligar los dos términos restantes en la conclusión.
29
Al menos de la ciencia “Occidental”.
30
Un caso ilustrativo de la importancia de la reproducibilidad de los experimentos, es el del descubrimiento de los rayos
N (ver [Link] [Klotz 80]), que a pesar del interés que suscitaron y de las publicaciones
asociadas, resultaron ser un fenómeno inexistente.
31
Nombre dado al conjunto de obras sobre Lógica escritas por Aristóteles de Estágira y recopiladas por Andrónico de
Rodas (ver [Link]
32
En [Bourbaki 72, p16] se nos hace notar que el silogismo es insuficiente para dar cuenta de todos los tipos de
razonamiento usados en matemáticas.
33
Ver [Link]
Capítulo 3: Elementos de Lógica Matemática.
40
El silogismo aristotélico no sólo reduce el proceso de razonamiento a construcciones de una
forma predefinida (un lenguaje), sino que lo independiza del significado propio de los
términos involucrados. El razonamiento se asocia entonces a la “forma” de las frases
en lugar de asociarse con su significado.
3.2.2 La lógica en la matemática moderna.
Si bien la lógica aristotélica no tuvo mayor influencia en la matemática, constituyó la base
lógica del pensamiento filosófico hasta la revolución industrial.
En el siglo XVIII Leibnitz, influenciado por el desarrollo del álgebra, se interesó por la
lógica en el marco de la formalización del lenguaje y del pensamiento. Tal como refiere
[Bourbaki 72, pg 18], Leibnitz “...había quedado seducido por la idea (que remontaba a
Raimundo Lulio) de un método que reduciría todos los conceptos humanos a conceptos
primitivos, formando un <<Alfabeto de los pensamientos humanos>>, y volvería a
combinarlos de forma casi mecánica para obtener todas las proposiciones verdaderas...”
Con base en las ideas del álgebra, Leibnitz pretende soportar la lógica en un lenguaje
simbólico34. A este respecto refiere [Bourbaki 72, pg 20] que Leibnitz “ hace notar que
puede remplazarse la proposición <<Todo A es B>>, por la igualdad A=AB y que a partir
de aquí se puede obtener la mayor parte de las reglas de Aristóteles mediante un cálculo
puramente algebraico...” Leibnitz reconoce también, la importancia de la negación como
proposición, proponiendo la equivalencia de la proposición “Todo A es B” con la proposición
“A (no B) no es”.
El padre de la lógica moderna es, sin embargo, Gorge Boole quién en el siglo XIX inventó
el “álgebra Boleana”. El álgebra boleana está constituida por las operaciones que pueden
realizarse sobre el conjunto {0, 1}, que pueden ser vistos como los valores lógicos “falso” y
“verdadero” respectivamente. Las operaciones básicas del álgebra boleana (∨, ∧, ¬),
pueden asociarse directamente con operaciones sobre conjuntos35. Con ello es posible
expresar en un marco algebraico las proposiciones aristotélicas y dar cuenta de los criterios
de razonamiento del silogismo clásico.
El trabajo de Boole es continuado por un grupo de lógicos entre los que se destacan Jevons,
Morgan y Peirce [Bourbaki 72, pg 22], que poco se preocupan por las aplicaciones de la
lógica a la matemática.
Con Frege y Peano se inicia el proceso de fundamentar la matemática sobre la lógica
(comenzando con los números naturales). En este proceso se introducen conceptos
importantes como, los cuantificadores y las variables para referirse a los elementos de un
conjunto36, las variables “proposicionales” que representan proposiciones indeterminadas
dentro de otras proposiciones, y otros elementos de notación que se usan hoy en día (Vg. el
símbolo para pertenece y para subconjunto).
34
Se refiere en que por ejemplo, “ hace notar que puede remplazarse la proposición
35
El algebra boleana puede ser vista como una álgebra de los números enteros “modulo 2” con las operaciones suma y
multiplicación de enteros.
36
que ya se vislumbraban en las proposiciones aristotélicas.
Capítulo 3: Elementos de Lógica Matemática
41
3.3 La Lógica Como una Disciplina de los Lenguajes.
La sistematización del razonamiento no puede llevarse a cabo sin una notación precisa que
permita especificar sin ambigüedades las palabras y frases que caracterizan el área de
aplicación, y definen los problemas.
Si adicionalmente se pretende utilizar la lógica como lenguaje de programación, es
necesario que las especificaciones puedan interpretarse de forma automática por el
intérprete del lenguaje lógico.
La teoría de los lenguajes formales, ofrece las condiciones que permiten lograr dicha
precisión y capacidad de interpretación. Estas condiciones son básicamente los siguientes:
• La existencia de una gramática o “sintaxis” precisa para el lenguaje, que permita
distinguir las frases del lenguaje que son correctas o “bien formadas” de las que
no lo son.
• La existencia de una semántica que permita darle un sentido a las frases bien
formadas del lenguaje para poder, con ellas, inducir a las acciones 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 frases de significado
conocido previamente definidas (premisas).
3.3.1 Sintaxis.
La sintaxis de un lenguaje esta constituida por un conjunto de elementos que regula la
conformación y estructura de las frases. A una frase que satisface la sintaxis del lenguaje
lógico se le ha denominado “formula bien formada” (o 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 atómicas 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 básico constituye o no un símbolo valido
del alfabeto. Estas reglas usualmente se dan en la forma de “expresiones regulares” o
“maquinas de estado finitas”, que son básicamente patrones para definir dichos
ensamblajes37.
En segundo lugar, los “criterios formativos” determinan las formas válidas de ensamblar
los símbolos para formar las frases del lenguaje. Ellos permiten descomponer, de forma
progresiva, una frase del lenguaje en componentes o “subfrases” que pueden a su vez estar
descompuestos por otros componentes y así sucesivamente hasta llegar a los símbolos
37
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 [Ullman 79]
Capítulo 3: Elementos de Lógica Matemática.
42
básicos que forman la frase. Esta descomposición puede representarse en forma de árbol,
formando lo que denominaremos el “árbol sintáctico” de la frase.
Los criterios formativos pueden especificarse por medio de notaciones formales como la de
una gramática en notación de Backus Naur (BNF). Bajo ciertas condiciones las
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”38.
En lo que resta del trabajo presentaremos 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 de BNF.
3.3.2 Semántica.
Para asociar un significado a las frases bien formadas del lenguaje es necesario contar con
un “dominio semántico”, que contenga los significados. El significado de una fbf de la
lógica, es un valor específico dentro del dominio semántico. La precisión de un lenguaje
lógico estriba en que cada fbf tenga un significado único.
La manera obtener el valor semántico 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 del lenguaje lógico, debe estar completamente determinado por el significado de
sus componentes o subfrases más elementales y por la manera que ellas se ensamblan.
Como la descomposición de las frases se puede llevar, de forma progresiva a los símbolos
elementales del lenguaje, la semántica de las frases está ligada al significado de los
símbolos elementales.
El significado de los símbolos elementales es denominado una “interpretación” para
dichos símbolos. La interpretación de los símbolos elementales es, en general, arbitraria y
dependiente del contexto u “área de aplicación” en el que se utilice la lógica.
3.3.3 Inferencia.
Si bien el significado de una fbf debería poder obtenerse del significado de sus símbolos
elementales, en términos prácticos, este significado no siempre se puede obtener (ver
3.5.3). Por otro lado, dado que las posibles fbfs son infinitas en número, no es fácil
conocer con antelación, cuales de ellas tendrán un significado dado.
Para resolver estos problemas se asocian una serie de “criterios demostrativos” con el
lenguaje lógico. Los criterios demostrativos permiten obtener o “inferir” nuevas fbfs, con
una semántica dada, a partir de otras fbfs cuya semántica es previamente conocida. Desde
el punto de vista lingüístico, los criterios formativos no son otra cosa que reglas de
transformación que obtienen nuevas fbfs útiles, a partir de otras fbfs previamente definidas.
38
El lector interesado debe consultar un texto de “teoría de la compilación” tal como [Ullman 79]
Capítulo 3: Elementos de Lógica Matemática
43
3.4 Lógica básica de Proposiciones.
En el marco de la lógica de proposiciones el dominio semántico es el conjunto {0, 1} que
puede ser entendido como los valores de verdad cierto y falso. 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 cualquiera39, en particular del lenguaje natural.
En la lógica básica de proposiciones, las aserciones o “proposiciones” son tratadas de forma
abstracta sin considerar su estructura o significado y son representadas por símbolos
elementales en el lenguaje.
Las proposiciones serán ensambladas para formar proposiciones más complejas, por medio
de los “conectores lógicos”, que reflejan los conectores de las afirmaciones en el lenguaje
natural.
3.4.1 Proposiciones.
Una proposición es una afirmación que es “o bien” verdadera “o bien” falsa.
Nótese que en nuestra definición de proposición, “o bien”, NO es simplemente “o”. En
efecto, a diferencia de “o”, “o bien” excluye la posibilidad de que se den ambas opciones
simultáneamente. Este tipo de problemas de precisión, lo encontraremos con frecuencia, en
nuestro propósito de definir un lenguaje preciso usando el vago lenguaje común.
3.4.2 Sintaxis.
La lógica básica de proposiciones la sintaxis es extremadamente simple ya que las frases
son letras del alfabeto que se ensamblan por medio de unos pocos conectores.
[Link] Alfabeto
Las proposiciones más obvias son precisamente “verdadero” y “falso”, que denotaremos
con las letras V y F respectivamente.
Para referirnos a una proposición cualquiera se usan las “variables proposicionales”. En
lo que sigue utilizaremos letras mayúsculas (P, Q, R,..), con o sin subíndices, para referirnos
a una proposición cualquiera y las letras mayúsculas (A, B, C,..), con o sin subíndices, para
referirnos a las proposición más elementales o “atómicas”.
Los conectores lógicos se utilizan para combinar proposiciones y así formar nuevas
proposiciones. Los conectores lógicos los dividiremos en los conectores “básicos” y los
conectores “derivados”.
Los conectores básicos son los siguientes:
• ¬ : denominado “no” (“not”)
• ∨ : denominado “o” (“or”)
Los conectores derivados son los siguientes:
• ∧, denominado “y” (“and”). F∧G Es una forma de escribir . ¬((¬F)∨(¬G))
• ⇒, denominado “implica” (“implication”). F⇒G Es una forma de escribir .
(¬F)∨G
39
[Link]
Capítulo 3: Elementos de Lógica Matemática.
44
• ⇔ , denominado “doble implicación” (“double implication”). F⇔G Es una
forma de escribir . ((¬F)∨G)∧((¬G)∨F)
[Link] Criterios Formativos
Los conectores permiten ensamblar las variables proposicionales para formar proposiciones
complejas. Por ejemplo si P, Q y R son proposiciones, P∨(((P⇔(¬R))⇒(R∧((¬P)∨Q))) es
una proposición.
No todos los ensamblajes de conectores y variables son, sin embargo, correctos (no
constituyen una proposición). Para que un ensamblaje sea correcto debe cumplir con los
criterios formativos, así:
• Un átomo es una fbf.
• Si F es una fbf entonces también lo es ¬F
• Si F y G son fbf entonces también lo son F ∨ G, F ∧ G, F ⇒ G y F ⇔ G
• Ningún otro ensamblaje es una fbf
Ejemplo 2
La fbf siguiente:
P∨((P⇔¬R)⇒R∧(¬P∨Q))
Puede representarse por medio del árbol sintáctico siguiente:
Capítulo 3: Elementos de Lógica Matemática
45
∨
P ⇒
⇔ ∧
P ¬ R ∨
R ¬ Q
Ejemplo 3
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))))
Ejemplo 4
Nótese que el árbol sintáctico de las fbfs que siguen es el mismo, bajo el orden de precedencia anterior:
P∨(((P⇔(¬R))⇒(R∧((¬P)∨Q))))
P∨((P⇔¬R)⇒R∧(¬P∨Q))
Capítulo 3: Elementos de Lógica Matemática.
46
Ejemplo 5
El lector debe notar que, a pesar de existir un orden de precedencia entre los conectores, no es posible
eliminar completamente los paréntesis de la fbf anterior sin cambiar su árbol sintáctico. Así, el árbol
sintáctico de la fbf siguiente:
P∨P⇔¬R⇒R∧¬P∨Q
Es el que se muestra a continuación:
Ejemplo 6
El árbol sintáctico de la fbf siguiente:
P∧P⇔¬R⇒R∧¬P∧Q
Es el que se muestra a continuación:
3.4.3 Semántica.
El significado de una fbf en lógica de proposiciones es su valor de verdad. Hallar la
semántica de una fbf de la lógica de predicados no es otra cosa que establecer si se trata de
una afirmación cierta o de una afirmación falsa. La verdad o falsedad de una fbf será,
además, considerada en términos absolutos, sin que intervenga un grado o medida alguna
para la verdad.40
40
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
[Link]
Capítulo 3: Elementos de Lógica Matemática
47
Como consecuencia de lo anterior, el dominio de interpretación para las fbfs de la lógica de
predicados es el conjunto {falso, verdadero} (que es isomorfo con el conjunto {0,1}).
De lo referido en 3.3.2, acerca de la semántica, se desprende que el valor de verdad de una
fbf debe debe satisfacer los dos criterios siguientes:
• Debe ser único o de lo contrario se tendría un lenguaje ambiguo.
• Debe poder obtenerse de forma rigurosa a partir del valor de verdad de las fbf
más elementales que la componen.
[Link] Interpretación.
Ya que los átomos son fbfs indivisibles, su valor de verdad de los átomos no puede
calcularse a partir del valor de verdad de sus componentes. En consecuencia su valor de
verdad debe ser definido por las características del contexto en el que se aplique la lógica.
Definición:
Una interpretación Λ, es una asignación de valor de verdad para los átomos que
aparecen en las fbfs de una teoría.
Λ: {A, A1, A2, A3, … , B, B1, B2, B3, …}→ {true, false}
Si la teoría tiene n atomos, para ella existenn 2n posibles interpretaciones.
[Link] Semántica de los conectores.
Como se vió 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 sentido de los
conectores. A este sentido lo denominaremos la “semántica del conector”.
Aunque la semántica de los conectores es definida, ésta definición no se hará de manera
arbitraria. Por ser uno de los propósitos de la lógica proposicional el de aumentar el rigor
de los razonamientos en el lenguaje natural, se tomó el sentido más común de las
conexiones del lenguaje natural para darle el sentido a los conectores de la lógica
proposicional. Para enfatizar este hecho describimos la semántica de los conectores ¬, ∧,
∨, ⇒, ⇔, de la manera siguiente:
Negación de P: Sea P una proposición. Entonces ¬P es una proposición y se cumple que ¬P
es verdadera si P es falsa y ¬P es falsa si P es verdadera. Se lee “No P”.
Conjunción de P y Q: Sean P y Q dos proposiciones. Entonces P∧Q es una proposición y se
cumple que P∧Q es verdadera si y solo si, tanto P como Q son verdaderas. Se lee “P y Q”.
No hace falta especificar los casos en que P∧Q es falsa pues cuando decimos “SI Y SOLO
SI”, estamos asegurando 2 hechos:
• P∧Q es verdadera SI tanto P como Q son verdaderas.
• P∧Q es verdadera SOLO CUANDO tanto P como Q son verdaderas.
Disyunción de P y Q: Sean P y Q dos proposiciones. Entonces P∨Q es una proposición y se
cumple que P∨Q es falsa si y solo si, tanto P como Q son falsas. Se lee “P o Q”.
Implicación de P a Q: Sean P y Q dos proposiciones. Entonces P⇒Q es una proposición y
se cumple que P⇒Q es falsa si y solo si, P es verdadera y Q es falsas. Se lee “si P entonces
Q” o “P implica Q”.
Capítulo 3: Elementos de Lógica Matemática.
48
Doble implicación entre P y Q: Sean P y Q dos proposiciones. Entonces P⇔Q es una
proposición y se cumple que P⇔Q es verdadera si y solo si, P y Q tienen el mismo valor de
verdad (sea verdadero o sea falso). Se lee “P si y solo si Q” o “P equivale a Q”.
3.4.4 Inferencia.
Si bien el significado de una fbf en lógica proposicional debe poder obtenerse del
significado de sus átomos. Este significado 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
predicados permiten, por su parte, inferir el significado de fbfs sin necesidad de recurrir al
significado de todos sus átomos. Igualmente dichos criterios permiten “razonar” para
inferir nuevas fbfs verdaderas a partir de otras previamente dadas como tal.
[Link] Tipos de formulas en relación con las interpretaciones.
Antes de proceder a plantear los criterios demostrativos, definiremos una serie de términos
para caracterizar el comportamiento de las fbfs frente a las posibles interpretaciones de sus
átomos.
Definición:
Una fbf F es satisfacible si existe al menos una interpretación Λ que sea modelo de
F
Ejemplo: A, A ∧ B,
Definición:
Capítulo 3: Elementos de Lógica Matemática
49
Una fbf F es insatisfacible o una contradicción si NO existe una interpretación Λ
que sea modelo de F
Ejemplo: A ∧¬A, (A ∧ ¬B) ∧ (¬A ∧ B)
Definición:
Una fbf F es valida o una tautología si toda posible interpretación Λ es modelo de F
Ejemplos:
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
Definición:
Una fbf F es contingente si para algunas interpretaciones es verdadera, y para otras
es falsa.
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
41
De donde se tomaron los ejemplos de esta sección.
Capítulo 3: Elementos de Lógica Matemática.
50
Definición:
Se dice que dos fbfs P y Q son semánticamente equivalentes (o simplemente
equivalentes) si tienen el mismo valor de verdad para todas las interpretaciones.
Diremos en general que un argumento es válido si la conclusión del argumento es
consecuencia lógica de las premisas. Una deducción, por su parte, no será otra cosa que
hallar una afirmación que es consecuencia lógica de otras afirmaciones previamente
aceptadas como ciertas.
Si se examinan las definiciones de tautología y contradicción, es fácil ver que de las
definiciones anteriores se derivan las siguientes afirmaciones:
Una fbf G es consecuencia lógica de F1,F2, ...,Fn, si y solo si se cumplen las siguientes
propiedades:
• (F1∧F2∧...∧∧Fn)⇒
⇒ G es una tautología.
• (F1∧F2∧...∧∧Fn∧¬ G) es una contradicción.
Si dos fórmulas P y Q son semánticamente equivalentes, entonces la fórmula P⇔Q es una
tautología
En consecuencia basta demostrar que (F1∧F2∧...∧∧Fn)⇒ ⇒G es una tautología, o que
(F1∧F2∧...∧
∧Fn∧¬G)es una contradicción para demostrar que G es consecuiencia lógica de F1,F2,
...,Fn,. Igualmente basta demostrar que P ⇔ Q es una tautología para demostrar que P y Q son
equivalentes.
Definición:
Una teoria 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 ciertas,
denominadas axiomas, y un conjunto de fórmulas que son consecuencia lógica de los
axiomas, que son llamadas teoremas.
[Link] Tablas de verdad
Una forma de demostraar que (F1∧F2∧...∧∧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 la semántica de la fórmula para 2n interpretaciones, siendo n el número de átomos
que involucra.
Cuando n es pequeño, el cálculo de la semántica de la fórmula, para todas las
interpretaciones, puede organizarse fácilmente en una tabla de verdad. Una tabla de verdad
tiene una fila por cada una de las interpretaciones (2n), y una columna por cada una de las
subfórmulas de la fórmula (una columna para cada átomo y una columna para cada
conector). En cada casilla de la tabla se coloca el valor de verdad de la subfórmula e
interpretación correspondiente a la casilla.
Por ejemplo, en la siguiente tabla de verdad se puede verificar que Q es consecuencia lógica
de las proposiciones P y P⇒
⇒Q. Veamos:
Capítulo 3: Elementos de Lógica Matemática
51
P Q P⇒Q P∧(P⇒Q)
V V V V
V F F F
F V V F
F F V F
En efecto, al comparar la columna bajo P∧∧(P⇒ ⇒Q) con la columna bajo Q, se puede
comprobar que Q es consecuencia lógica de las proposiciones P y P⇒ ⇒Q; ya que siempre que
P∧ ⇒Q) es verdadero (solo la primera fila), también lo es Q, que es la consecuencia. Nótese
∧(P⇒
que Q es verdadera para otras interpretaciones, pero esto es permitido por la definición.
Para ilustrar lo anterior, demostremos mediante una tabla de verdad, que R es consecuencia
lógica de las proposiciones P⇒⇒Q, Q⇒
⇒R y P.
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 demostraciones de que R es consecuencia lógica de las
fórmulas {P⇒ ⇒Q,Q⇒⇒R,P}. Como en el ejemplo presentado arriba, se puede comparar la
columna bajo (P⇒ ⇒Q)∧∧(Q⇒ ∧P, con la columna bajo R, notando que siempre que la primera
⇒R)∧
es verdadera también lo es la segunda. Como segunda prueba basta computar la séptima
columna que muestra que ((P⇒ ⇒Q)∧ ∧(Q⇒
⇒R)∧ ⇒R es una tautología, lo que corresponde a la
∧P)⇒
segunda definición. Finalmente la última columna muestra que (P⇒ ⇒Q)∧∧(Q⇒
⇒R)∧
∧P∧∧¬R es una
contradicción.
[Link] Criterios demostrativos
Las tablas de verdad parecen un buen método para llevar a cabo las demostraciones, pero
no son el mejor. Nótese que la cantidad de cómputo necesaria para concluir, crece
exponencialmente con el número de variables proposicionales involucradas. De modo que
cuando los razonamientos son más complejos, la cantidad de cómputo se vuelve
inmanejable.
Es entonces necesario buscar procedimientos que suelen ser más efectivos. Los
matemáticos, por ejemplo,suelen emplear la intuición para escoger un camino de
implicaciones sucesivas que los lleve en pocos pasos a la conclusión deseada. El Principio
de resolución, provee un método para probar la validez de una argumentación, sin
Capítulo 3: Elementos de Lógica Matemática.
52
necesidad de la intuición de los matemáticos, y con un ahorro de procesamiento sustancial
respecto al necesario para computar tablas de verdad grandes.
Ahora, retomemos la fórmula P∨((P⇔¬R)⇒R∧(¬P∨Q)) y supongamos que P, Q y R
son sus átomos y que P es verdadera, Q es falsa y R es verdadera.
Basándonos en la definición de los conectivos, podemos asegurar que la proposición en
cuestión es verdadera. Esto se puede determinar examinando, de adentro hacia afuera, el
valor de verdad de cada sub-expresión.
Las tablas de verdad no son más que una manera sistemática de evaluar el valor de verdad
de una proposición para cada interpretación posible. La tabla de verdad de la proposición
P∨((P⇔¬R)⇒R∧(¬P∨Q)) es:
El proceso, a grandes rasgos, consiste en construir una tabla donde se colocan en cada
columna, una a una, desde la más simple hacia la más compleja, ascendiendo por el árbol
sintáctico, las sub-expresiones que conforman la proposición. Las primeras columnas se
encabezan con las variables y en cada fila una de las posibles asignaciones de las variables.
Luego, se llena la tabla, de izquierda a derecha, basándose en la definición del conectivo
lógico involucrado. Para más detalles y ejemplos ver sección 1.3.5 de [Grassmann 97].
La proposición que hemos estado analizando, posee una propiedad interesante que vale la
pena resaltar. Todas las asignaciones posibles de sus variables, es decir todas las
interpretaciones, hacen a la proposición verdadera. Luego, P∨((P⇔¬R)⇒R∧(¬P∨Q)) es
una tautología. La importancia de las tautologías radica en su uso a la hora de hacer
razonamiento lógico.
(∀x)(H(x)⇒M(x))
H(s)
M(s)
De ésta manera quedan relacionadas las tres frases lo que permitirá decir, por ejemplo, que
la tercera es conclusión lógica de las dos primeras. Pero ésa no es nuestra preocupación
aún. Ahora, ¿cómo se puede expresar la conmutatividad de la suma? veamos:
∀x∀y(I(s(x,y),s(y,x)))
Donde s(x,y) es una función que se entiende, o interpreta como la suma de sus
argumentos; I(t1,t2) es un predicado, que expresa que sus argumentos son iguales. De
manera que la frase se entiende como:
“para todo x, para todo y, x + y = y + x”.
3.5.2 Sintaxis.
Aunque la intuición es importante para conectar los símbolos a las ideas que ya tenemos en
la cabeza, es importante en éste punto formalizar el lenguaje, pues nuestro propósito sigue
siendo un lenguaje que pueda entender una máquina.
Los siguientes son los símbolos de la lógica de primer orden:
Variables: x, y, z, ...
Constantes individuales: a, b, c, ...
Letras para funciones: f, g, h, ...
Letras para predicados: P, Q, R, ...
Conectivos lógicos: ¬, ∨, ∧, ⇒, ⇔.
Cuantificadores: ∀, ∃.
Observaciones:
a. Para lograr un tratamiento general, el lenguaje que definiremos se separa de su
interpretación. La interpretación, que se definirá más adelante con precisión, define
Capítulo 3: Elementos de Lógica Matemática.
54
el significado concreto de los elementos del lenguaje, como el universo donde se
mueven las variables y lo que representan las constantes, las letras de funciones y
las de predicados en ese universo. Por ejemplo, la frase acerca de la conmutatividad,
no tiene significado hasta que no se escoge una interpretación, por ejemplo, que el
dominio de los objetos son los números reales, que s(x,y) significa “x + y” y que
I(x,y) significa “x = y”.
b. El significado de los conectivos y los cuantificadores no varía con la interpretación.
c. Cada letra de función y cada letra de predicado tiene asociado un número que se
denomina aridad y representa el número de argumentos que recibe. Por ejemplo, las
letras de predicados H y M utilizados en la sub sección 3.5.2, tienen aridad uno,
mientras que la letra de función s y la de predicado I tienen aridad dos.
d. No se deben confundir las funciones como s(t1,t2) de los predicados como
I(t1,t2). El resultado de aplicar una función a objetos es otro objeto, mientras que
un predicado es una afirmación acerca de sus argumentos que puede ser verdadera o
bien falsa.
Lo que sigue son una serie de definiciones que determinan la sintaxis del lenguaje.
Término del lenguaje se define recursivamente así:
i) Las variables y las constantes individuales son términos
ii) Si f es una letra de función con aridad n y t1, t2,.., tn, son términos del lenguaje
entonces f(t1,t2,..,tn) es un término.
Los términos van a ser aquellas expresiones del lenguaje formal que se interpretan como
objetos, es decir, las cosas a las que se aplican las funciones, las cosas que tienen
propiedades, y acerca de las cuales se hacen aseveraciones (afirmaciones).
Formula atómica se define así: Si P es una letra de predicado con aridad n y t1, t2,.., tn, son
términos del lenguaje, entonces P(t1,t2,..,tn) es una formula atómica del lenguaje.
Las formulas atómicas son las expresiones más simples del lenguaje que se entienden como
afirmaciones, aseveraciones, o predicados.
Formula bien formada o fbf se define recursivamente así:
i) Toda formula atómica del lenguaje es una fbf.
ii) Si A y B son fórmulas bien formadas (fbfs), también lo son ¬A, A∨B, A∧B,
A⇒B, A⇔B, (∀x)A y (∃x)A, donde x puede ser cualquier variable.
En ésta definición, se reúnen todas las posibles expresiones del lenguaje que son
afirmaciones, aseveraciones o en últimas predicados. En el calculo de predicados, también
se mantienen las reglas de prioridad definidas en la lógica de proposiciones, solo que el
orden de prioridad se amplía con los cuantificadores así: ∀, ∃, ¬, ∧, ∨, ⇒, ⇔
Antes de pasar a definir interpretación, daremos unas ciertas definiciones que aunque no
son indispensables, son útiles para analizar fbfs.
En la fbf (∀x)A, o en la fbf (∃x)A, decimos que A es el radio de acción del cuantificador
(∀x) y (∃x) respectivamente.
Capítulo 3: Elementos de Lógica Matemática
55
Una ocurrencia de una variable x en una fbf se considera ligada si y solo si aparece dentro
del radio de acción de un cuantificador (∀x) o (∃x), en la fbf. Si una ocurrencia de una
variable no es ligada, se dice que es libre.
3.5.3 Semántica.
Como ya mencionamos arriba, el lenguaje que definimos, fue dejado libre, de manera que
sirva para expresar ideas en cualquier contexto, sea de relaciones entre personas, o de la
mortalidad de Sócrates o acerca de los números naturales o las entradas de una tabla en una
base de datos relacional. Lo que sigue de manera natural, es formalizar los elementos que
se deben definir para que el lenguaje cobre sentido en un contexto determinado.
Una interpretación I de una fórmula, en lógica de primer orden, consiste de un conjunto no
vacío D llamado dominio, y una asignación de “valores” a cada constante, letra de función
y de predicado que ocurre en la formula así:
i) A cada constante le asignamos un elemento del dominio.
ii) A cada letra de función n-aria (o de aridad n) le asignamos un mapeo (o
función) de Dn en D. (nótese que Dn={(x1, x2,...,xn)| x1, x2,...,xn∈D}).
iii) A cada letra de predicado n-ario le asignamos un mapeo de Dn en el conjunto
compuesto por verdadero y falso {V,F}.
Observaciones:
a. El segundo numeral de la definición establece simplemente que dentro de la
interpretación es necesario establecer una asignación concreta para todas las letras
de función. Por ejemplo para el símbolo s, de un ejemplo anterior, si queremos
darle el sentido de la suma de enteros, al definir su interpretación, será necesario
asignarle un mapeo del conjunto de todas las parejas de enteros, al conjunto de los
enteros, según el funcionamiento habitual de la suma (ŝ: Z2→Z).
b. De igual manera el tercer numeral establece que a cada predicado se le debe asignar
una interpretación concreta que permita, para una ocurrencia del predicado, con
todas las entradas concretas, determinar su valor de verdad.
Dada una interpretación para una fbf, el valor de verdad no necesariamente se puede
determinar. Cuando la fbf contiene variables libres, no es posible determinar su valor de
verdad. Ahora, si suponemos que todas las variables están ligadas a algún cuantificador, el
valor de verdad se puede determinar siguiendo las siguientes reglas:
i) Las formulas atómicas tienen su valor de verdad determinado por la
interpretación del predicado.
ii) Si A y B son fbfs y su valor de verdad se puede determinar, las fbfs ¬A, A∨B,
A∧B, A⇒B, A⇔B, tienen su valor de verdad determinado por las definiciones
dadas en la sección anterior.
iii) Si A es una fbf, (∀x)A es verdadera si y solo si A es verdadera para todo d
en D (A es verdadera para d en D, quiere decir que la fbf que resulta de sustituir
todas las ocurrencias libres de x en A por d es verdadera).
Capítulo 3: Elementos de Lógica Matemática.
56
iv) Si A es una fbf, (∃x)A es verdadera si y solo si A es verdadera para al
menos un d en D.
Ejemplo:
Si x=1, es fácil observar que existe un valor de y (y=1), tal que P(1,y) es
verdadero.
Si x=1, también hay un valor de y (y=2), tal que P(2,y) es verdadero.
Por tanto, en ésta interpretación, para todo x en D, existe un y en D, tal que P(x,y)
es verdadero. Es decir, (∀x)(∃y)P(x,y) es verdadero para ésta interpretación.
b) (¬P∧Q)∨(P∧¬Q∧¬R)∨(¬P∧¬R)∨R
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.”
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
Asignación para la letra de función f:
F(1) F(2)
2 1
Asignación para el predicado P:
P(1,1) P(1,2) P(2,1) P(2,2)
V V F F
Capítulo 3: Elementos de Lógica Matemática.
58
Determinar el valor de verdad de las siguientes fbfs:
a) P(a,f(a))∧P(b,f(b)) c) (∀x)(∀y)(P(x,y)⇒P(f(x),f(y)))
b) (∀x)(∃y)P(x,y) d) (∃x)(∀y)(P(x,y)∨P(a,x))
7. Explicar porqué 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) Que relación existe entre las fórmulas (F1∧F2∧...∧Fn)⇒Q y (F1∧F2∧...∧Fn∧¬Q).
8. Probar las siguientes deducciones:
4.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 (“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
Capítulo 4: Lógica Ecuacional
63
efecto, si entre los estudiantes del curso de Lenguajes Declarativo ocurren relaciones de
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”.
4.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.
DEF:
operando – Uno cualquiera de los términos agrupados por un operador en un
término.
Cada operador se asocia a un “perfil”. El perfil de un operador es una plantilla que indica
lo siguiente:
• El sort 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,
según su sort.
• 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.
DEF:
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.
Ejemplo 7
El operador “novio|a-de” permitirá referirnos a la relacion de noviazgo en la clase de Lenguajes
Declarativos.
La plantilla de nuestro operador estará basada en la notación prefija “estandar” de función donde el
operador va primero (a la izquierda) seguido de la lista de operandos separados por “,”.y encerrados en
paréntesis.
Así:
novio|a-de(_)
Capítulo 4: Lógica Ecuacional
64
Es la plantilla del operador “novio|a-de” que inidica que agrupa un solo término.
La aridad del operador “novio-de” es 1.
PRECAUCIÓN:
Es usual que el estudiante confunda los operadores con los predicados, ya que ambos tienen una forma sintáctica
similar (lucen parecidos pero son distintos).
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.
Ejemplo 8
El operador “+” permitirá referirnos a la función suma en el dominio de los Naturales.
La plantilla de éste operador usa notación “infija” colocando los operandos a ambos lados del operador.
Así:
_+_
Es la plantilla del operador “+” que indica que el operador separa los operandos.
La aridad del operador “+” es 2.
Ejemplo 9
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:
(+ _ _ _ ...)
Ejemplo 10
Son instancias del operador “novio|a-de” las siguientes.
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))
Ejemplo 11
Son instancias del operador “+” con notación infija las siguientes:
1+1
(2 + 1) + 3
Ejemplo 12
Son instancias del operador “+” del lenguaje LISP las siguientes.
(+ 1 1)
(+ 1 (+ 2 3) 7 (+ 4 5))
Ejemplo 13
Árbol que representa el término 1/(-(4*7))
1 -
4 7
Ejemplo 14
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).
Capítulo 4: Lógica Ecuacional
67
* +
2 + * 7
4 7 2 4
DEF:
Término completamente parentetizado – Un término es completamente
parentetizado si todo operando complejo se halla encerrado entre paréntesis.
En un término completamente parentetizado se cumplen las dos condiciones siguientes:
• los operandos atómicos no aparecen contiguos a más de un operador
• El operador principal de un operando complejo es el único operador que está
encerrado sólo por los paréntesis del operando.
Esta característica permite que la determinación de dicho operador se haga sin ninguna
ambigüedad, facilitando la elaboración del árbol sintáctico.
Ejemplo 15
En el término completamente parentetizado siguiente ningún número es contiguo a más de un operador.
El operador que sigue al primer 1 es el operador principal. Nótese que es el único operador que se halla
únicamente encerrado por los paréntesis que encierran al término.
Ejemplo 16
El uso exclusivo de la notación “estandar” de función (ver ejemplo 63) evita que haya operandos
contiguos a más de un operador impidiendo la aparición de ambiguedades. Es el uso de operadores con
notación infija, lo que posibilita la aparición de ambiguedades.
Ejemplo 17
Dando el siguiente orden de precedencia y sentido de asociatividad a los operadores aritméticos:
El término siguiente
3^4^5 * 4 + 7 / -3 + 5
Capítulo 4: Lógica Ecuacional
69
Sería interpretado en correspondencia con la secuencia de términos equivalentes que se muestra en la
tabla siguiente.
Ejemplo 18
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)))).
Ejemplo 19
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”.42
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.
Ejemplo 20
La aserción siguiente lleva a cabo una afirmación sobre la relación de noviazgo 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) )
42
Es 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 4: Lógica Ecuacional
71
Una razón obvia para que dos términos puedan considerarse iguales es que refieran al
mismo elemento del dominio de interpretación. Esta condición no es, sin embargo,
necesaria para que dos términos sean considerados iguales. 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 (En lo que sigue el significado del concepto de igualdad
estará determinado de forma implícita por el contexto en que se use).
Esto permite plantear diversos criterios de igualdad sobre un mismo dominio de
interpretación con condiciones de igualdad específicas para cada criterio. Cada criterio
puede verse como una categoría (o dimensión) de clasificación para los elementos del
dominio, en el que las condiciones de igualdad determinan los elementos que pertenecen a
cada una de las clases de equivalencia de la categoría.
Ejemplo 21
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.
Ejemplo 22
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.
Ejemplo 23
La aserción siguiente plantea una propiedad fundamental de la relación de noviazgo en la clase de
lenguajes declarativos usando el predicado de igualdad.
∀x ∀y (¬(x=y) ⇒ ¬(novio|de(x)=novio|de(y))
Ejemplo 24
Capítulo 4: Lógica Ecuacional
72
La igualdad es fundamental para especificar las propiedades de las operaciones matemáticas. La
aserción siguiente plantea, por ejemplo, una ley bien conocida del operador suma.
∀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..
4.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 operadores definidos en la teoría, sobre sorts definidos en la teoría.
Capítulo 4: Lógica Ecuacional
73
[Link] 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.
[Link].1 Declaración de sorts.
Nombres de los sorts sobre los que se definen los términos de la teoría.
Ejemplo 25
En una teoría que para razonar sobre la aritmética de los enteros se incluirá la siguiente definición de
sort.
Conjuntos:
int
Ejemplo 26
En la teoría asociada a los enteros se incluirán los operadores siguientes.
Operadores:
-_ : int -> int
_+_ : int int -> int
0 : -> int
s : int -> 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.
[Link].3 Axiomas.
Aserciones básicas sobre las funciones asociadas a los operadores, que son asumidas como
“ciertas” en el marco de la teoría.
Ejemplo 27
En la teoría asociada a los enteros se incluirán los axiomas siguientes.
Axiomas:
(1) ∀x (x + 0 = x)
(2) ∀x (0 + x = x)
(3) ∀x (-x + x = 0)
(4) ∀x ∀y ∀z (x + (y + z) = (x + y) + z)
- -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.
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 [Link], 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.
Ejemplo 29
En la demostración presentada en el Ejemplo 28 el matemático podría haber tomado un camino
divergente como el que se muestra a continuació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 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.
Capítulo 5: Sistemas de Reescritura de Términos.
82
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 (Vg. 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
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 asumpciones de Church-Rosser43, 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 fin44.
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 mostrar, 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 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 términos base más simples (el resultado del cálculo). No serán temas del
texto las derivaciones que parten de términos con variables (como la del Ejemplo 28).
43
Es decir, se asume que el programador da cuenta de ellas.
44
Vg. la palabra reservada “fork” del lenguaje C.
Capítulo 5: Sistemas de Reescritura de Términos
83
Ejemplo 30
La utilidad de estas demostraciones en la vida cuotidiana es obvia, si consideramos que una compra de
un café y dos pancillos, define un término que describe el valor a pagar:
1000 * 1 + 500 * 2
Que debe reducirse a un término “más simple” pero equivalente, antes de efectuar el pago.
Ejemplo 31
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:
45
Si un símbolo de operación aparece en dos familias distintas, se dice que es un operador “sobrecargado”.
Capítulo 5: Sistemas de Reescritura de Términos.
84
S es el conjunto { int }
∑ 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.3.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 | s∈S}
NOTACION:
Cuando sea necesario hacer referencia a los sorts de las variables, ellas se
representarán por expresiones de la forma x:s,
Donde x∈ Xs , s∈S 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 x⊂X , s⊂S 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) | s∈S}
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) , s∈S y s rotula la categoría a la que pertenece t.
Capítulo 5: Sistemas de Reescritura de Términos
85
Un conjunto de términos se representará por expresiones de la forma t:s.
donde t⊂ΓΣ(X) , s⊂S y cada símbolo de s rotula la categoría a la que pertenece el
correspondiente término de t
El conjunto de términos tipados de la signatura ΓΣ(X) = {ΓΣ,s(X) | s∈S} se define por medio
de los criterios formativos siguientes:
• Las variables son términos: Xs ⊆ ΓΣ,s(X) para s∈S
• Las constantes son términos: ∑ε,s ⊆ ΓΣ,s(X) para s∈S
• 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 7), 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 4.2.1.
Ejemplo 32
Es posible definir un conjunto de términos asociado a la signatura presentada en el Ejemplo 31, 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: Sistemas de Reescritura de Términos.
86
5.3.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 (s∈S).
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.
Ejemplo 33
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 27.
5.4 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.
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
razonamiento46 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.4.1 Substitución, Particularización y Emparejamiento.
Una substitución σ es una función que mapea un conjunto de variables
X={x1:sx1,x2:sx2,x3:sx3,...,xn:sxn} a un conjunto de términos T={t1:st1,t2:st2,t3:st3,...,tm:stm}, y
que cumple con la condición siguiente:
46
Sólo pueden usarse las ecuaciones (o axiomas) en un sentido.
Capítulo 5: Sistemas de Reescritura de Términos.
88
σ(xi:sxi) = tj:stj ⇒ sxi = stj .
En otras palabras, una substitución asocia a cada variable de X un término de T que tiene el
mismo sort que la variable.
Si t es un término cualquiera tσ representa el término resultante de remplazar (o reescribir)
en t, cada ocurrencia de xi por su término correspondiente en el mapeoσ. Si una variable z,
no es mapeada por la substituciónσ, se asume σ(z)=z.
Ejemplo 34
Sea t el término M(x,S(0)), y σ, una substitución tal que σ(x) = A(y,0).
Entonces tσ, representa el término M(A(y,0),S(0))
Ejemplo 35
Dada la ecuación siguiente:
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)
Ejemplo 36
Capítulo 5: Sistemas de Reescritura de Términos
89
Sea t el término M(x,A(x,y)).
El término t empareja con los términos siguientes:
M(0,A(0,y))
M(A(S(0),0),A(A(S(0),0),y))
M(A(S(0),0),A(A(S(0),0),A(0,z)))
El término t NO empareja con los términos siguientes:
M(A(y,0),S(0))
M(A(S(0),0),A(S(0),y))
M(A(S(0),y),A(A(S(0),y),A(0,z)))
Ejemplo 37
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: Sistemas de Reescritura de Términos.
90
ε
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[u←r], al término que resulta de reemplazar en t, el subtérmino t|u por r.
Ejemplo 38
Sea t el término M(x,S(A(y,0))), los siguientes términos son reemplazos:
t[ε←0] = 0;
t[1←S(x)] = M(S(x),S(A(y,0)))
t[2←M(x,y)] = M(x,M(x,y))
t[2.1←S(0)] = M(x,S(S(0)))
t[2.1.1←x] = M(x,S(A(x,0)))
t[2.1.2←A(S(0),y)] = M(x,S(A(y,A(S(0),y))))
47
Demostració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: Sistemas de Reescritura de Términos
91
La relación entre t y t’ es denotada como t→E t’ cuando la reescritura se basa en las
ecuaciones de E. La clausura reflexiva y transitiva de la relación de t→E 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.
Ejemplo 39
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 t→E 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
[Link]).
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.4.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.
Si un conjunto de ecuaciones E es Church-Rosser, todo término t podrá reescribirse hasta
un término final t↓E que denominaremos su forma normal. Si para dos términos t y t’ se
cumple que t↓E ≡ 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).
Ejemplo 40
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: Sistemas de Reescritura de Términos.
92
Ejemplo 41
Definamos un nuevo conjunto de reglas de reescritura para el ejemplo anterior, R={M(x,0)→0,
S(M(x,y))→0}. El resultante SRT no es confluente, pues el término S(M(x,0)) puede reescribirse a S(0)
usando la primera regla, o puede reescribirse a 0 usando la segunda y tanto S(0) como 0 son términos
canónicos.
48
Dartmouth College, Hanover, NH, 03755. [Link]
Capítulo 6: Elementos básicos de los Lenguajes Funcionales
97
JOHANNIAC de la RAND-Corporation, orientado a probar y manipular teoremas lógicos
en cálculo de predicados. Es por esto que entre las ideas de partida del lenguaje se destacan
las dos siguientes:
• Extender el uso de las expresiones algebraicas en las asignaciones del
FORTRAN, a escribir programas de forma totalmente algebraica, usando las
expresiones como único medio para describir cálculos complejos sin necesidad
de especificar secuencias de comandos.
• Representar expresiones algebraicas de cualquier grado de complejidad, por
medio de listas cuyos elementos podían, a su vez, ser listas (listas multinivel).
Tal como nos lo refiere Paul Gram en [Gra 2001], el lenguaje desarrollado por McCarthy
no sólo sentó las bases para los lenguajes funcionales, sino que planteó varios elementos
que han venido siendo adoptados de forma paulatina por los lenguajes procedurales. Son
aportes del LISP los siguientes:
• El condicional estructurado (o “IF <then> <else>) de los lenguajes procedurales
modernos, que fue propuesto en LISP como alternativa al salto bajo condición
(o “GO TO”) del FORTRAN.
• La definición de funciones por medio de expresiones recursivas condicionales,
que introduce la posibilidad de que una funcione se evoque a si misma.
• las variables del LISP que, disociadas de lugares específicos en la memoria, se
pueden ligar a expresiones de cualquier tipo, son precursoras de las variables
que almacenan direcciones de memoria en los lenguajes procedurales modernos,
“apuntando” así a valores de diversos tipos almacenados en un lugar cualquiera
de la memoria.
• Las funciones como objetos de primer orden y susceptibles de ser usadas como
argumentos de las funciones, son las precursoras de los mecanismos para el paso
de funciones como argumento en los lenguajes procedurales (Vg. las variables
tipo “puntero a funciones” del lenguaje C).
• Los mecanismos automáticos requeridos para desechar las expresiones
matemáticas intermedias en el proceso de reescritura, son precursores de los
mecanismos de recolección de basura de los lenguajes modernos “orientados por
objetos”.
Por ser el LISP un lenguaje clásico y de uso muy extendido, es posible hallar diferentes
implementaciones y variantes o “dialectos”. En este texto la discusión estará dirigida al
dialecto “SCHEME”, bajo la implementación que referimos más abajo.
6.2.2 MAUDE
El lenguaje MAUDE es uno de los lenguajes funcionales mas recientes y, como tal,
incorpora los avances que se han dado en los últimos años en este tipo de lenguaje. A
diferencia del LISP, MAUDE es un lenguaje de corte experimental de poca difusión en la
comunidad productora de software49. MAUDE fue desarrollado en el SRI (Stanford
49
No aparece en las recolecciones www sobre el desarrollo histórico de los lenguajes de programación (ver
[Link] [Link]
Capítulo 6: Elementos básicos de los Lenguajes Funcionales.
98
Research Institute) como un lenguaje de dominio público bajo licencia GNU y se halla
disponible a través de su propia página web50.
MAUDE es un lenguaje y un medio ambiente de programación que le da soporte a una
lógica denominada “lógica ecuacional con membresía”, (o “membership equational
logic”) y a una lógica denominada “lógica de reescritura”, (o “rewriting logic”)51que son
extensiones a la lógica ecuacional presentada en el capítulo anterior (ver [Clavel 2007, secs
1.2 y 3.2]). MAUDE fue concebido como un lenguaje lógico en el sentido estricto de la
palabra. Un programa MAUDE, en efecto, es una teoría en una lógica, y un cómputo en
MAUDE es una derivación en dicha teoría. En consecuencia, la sintaxis de MAUDE se
ajusta de forma rigurosa, a la sintaxis usada en el capítulo anterior para describir los SRTs.
MAUDE tiene sus raíces en el lenguaje OBJ3 [Goguen 92_a,92_b], que implementa una
lógica ecuacional. OBJ3 es, en efecto, un sublenguaje del lenguaje de MAUDE. La
principal diferencia de MAUDE con respecto a OBJ3 es52 que le da soporte a una lógica
más rica, que extienden la lógica ecuacional de sorts ordenados (u “order_sorted”) de
OBJ3.
La lógica ecuacional con membresía da cuenta del concepto de subsorting (o subtipo)
incorporando una variedad de posibles relaciones entre sorts. La lógica de reescritura
[Mess 92], por su parte, es una lógica relativa al cambio concurrente, que da naturalmente
cuenta del concepto de estado y de cómputos concurrentes y no determinísticos. Ella
constituye un marco general para dotar de semántica a una amplia gama de lenguajes y
modelos de concurrencia. Provee, en particular, muy buen soporte a cómputos en el marco
de Objetos Concurrentes.
50
El interprete y toda la información sobre MAUDE puede obtenerse en [Link]
51
Los nombres en castellano corresponden a la traducción libre del autor de los nombres de los módulos en MAUDE
52
Además de un mejor desempeño.
Capítulo 6: Elementos básicos de los Lenguajes Funcionales
99
En este punto vale la pena recordar que, en el marco de un SRT, “cálculo” de un término no
es otra cosa que su transformación a otro término, semánticamente equivalente que, por
alguna razón, es considerado más útil que el original. Esta transformación es, además, un
proceso de demostración en la teoría lógico-ecuacional que le da soporte al SRT y es
completamente controlada por los axiomas de dicha teoría.
Ejemplo 42
Al someter al programa el término:
3*32+5*20
Este simplemente se reduce a su equivalente semántico:
164
Que no es otra cosa que una notación simplificada para el término:
1*100+6*10+4*1
Este último término tiene el mismo significado que el original (Vg. ambos pueden representar el valor
de una compra), pero es significativamente más útil al momento de manipular la cantidad de objetos que
el representan (Vg. el dinero correspondiente a una compra).
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 en cada
programa.
Esta interfaz puede consistir en un simple campo de texto en un lugar cualquiera de la
pantalla que es usado tanto por el usuario para someter el término como por el programa
para entregar el resultado, o consistir en un complejo conjunto de campos, botones y
gráficos dispuestos en un conjunto de formas de diálogo, con los que el usuario interactúa
para plantear uno varios casos de cálculo y obtener diversos resultados.
Ejemplo 43 :
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.
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.
Capítulo 6: Elementos básicos de los Lenguajes Funcionales.
100
A B C
1 1 2 3
2 1 1 1
Dado que esta obra se orienta principalmente a describir los mecanismos que ofrecen los
lenguajes para definir los operadores involucrados en los términos a ser calculados, en ella
no se discuten de forma extensa las capacidades que existan o puedan existir en
implementaciones particulares para la definición de interfaces y programas complejos. Así,
para probar los ejemplos se opto por implementaciones que permitieran definir interfaces
muy simples, tales como líneas de comandos e ingreso de datos en una pantalla de texto.
6.3.2 Interfaz del intérprete.
La implementación del lenguaje, debe ofrecerle al programador las facilidades necesarias
para ingresar el programa, verificar su consistencia sintáctica y semántica, definir las
interfaces del programa, y traducirlo a código ejecutable. Estas facilidades se soportan en
una interfaz de comunicación programador-implementación.
La implementación puede soportar un conjunto de facilidades incluyendo editores
especializados para el SRT y las interfaces del programa; gestores de proyectos que
manipulen versiones, descomposición y reuso de partes; verificadores de errores sintácticos
y semánticos que muestren los problemas y sugieran las correcciones; e intérpretes y
compiladores que permitan ejecuciones controladas y generación de programas autónomos.
Sin embargo, por las razones antes mencionadas, en esta obra no se discuten de forma
extensa las facilidades que existen o puedan existir en implementaciones particulares. Para
probar los ejemplos se usaron implementaciones que poseen sólo facilidades elementales
fundamentadas en una interfaz de líneas de comandos en una pantalla de texto.
6.3.3 Implementaciones de los lenguajes seleccionados.
A continuación se describen brevemente las implementaciones de los lenguajes que fueron
usadas en la obra para verificar e ilustrar los ejemplos. Con ello se espera que el lector
pueda ejecutar los ejemplos dados y crear los suyos propios sin un estudio más profundo
del medio ambiente de ejecución. Para una información más completa el lector debe
referirse a la literatura indicada en cada caso.
[Link] MIT SCHEME.
Para el lenguaje SCHEME se usó la implementación MIT SCHEME, ofrecida por el
Instituto Tecnológico de Massachussets (MIT) bajo licencia Publica General GNU53. Al
momento de escribir este texto el MIT SCHEME se hallaba disponible en [SCHEME
2003], junto con la documentación correspondiente. Las descripciones que siguen estàn
basadas en el manual de referencia del lenguaje [Hanson 2002] disponible en el sitio
referido. En el marco de la programación en SCHEME, es lectura obligada el excelente
texto de Harold Abelson et Al. [Abelson 85], disponible en la web, al que referiremos con
frecuencia en esta obra.
53
[Link]
Capítulo 6: Elementos básicos de los Lenguajes Funcionales
101
La implementación del SCHEME del MIT, le ofrece al usuario una interfaz textual única
que debe usarse para todo tipo de interacción con el intérprete del lenguaje. Esta interfaz
consiste en una secuencia de líneas de texto en pantalla que termina en una línea de ingreso
de textos a cargo del usuario. El usuario debe usar esta última línea para definir el
programa, someter sus consultas y cambiar las opciones de ejecución (a modo de consultas
especiales).
Una vez que el usuario somete un texto al intérprete, este reacciona colocando a renglón
seguido una serie de líneas de texto como respuesta. Las líneas anteriores a la de ingreso,
no son otra cosa que los textos sometidos con anterioridad por el usuario y las
correspondientes respuestas del intérprete.
Ejemplo 44
Un operador puede ser definido a través del interfaz por medio de la forma especial “define”:
1 ]=> (+ a 3)
;Value: 4
1 ]=> (cua 4)
;Value: 16
1 ]=>
En esta interacción las líneas que comienzan con 1 ]=> corresponden a ingresos de texto por parte del
usuario y las líneas que comienzan por ;Value: corresponden a respuestas del sistema. Como lo
indica la última línea, el sistema se encuentra en espera de un ingreso de textos por parte del usuario.
Una característica de este tipo de interfaz es que permite combinar el ingreso de elementos
de programa (líneas (define ….) ) con la ejecución del mismo (líneas
(<operador> <lista de operandos>) ).
Adicionalmente, para facilitar el ingreso de un conjunto grande de líneas del programa se
ofrece un comando que permite ingresar masivamente las contenidas en un archivo de
texto.
Ejemplo 45
El comando siguiente solicita ingresar el texto contenido en el archivo de nombre aaaa :
[Link] MAUDE
Para el lenguaje MAUDE se usó la implementación Maude 2 ofrecida, bajo licencia pública
GNU, por el Laboratorio de Ciencias de la Computación del Departamento de Ciencias de
la Computación de la Universidad de Illinois en Urbana-Champaign. Al momento de
escribir este texto el Maude 2 se hallaba disponible en [MAUDE 2007], junto con la
documentación correspondiente. Las descripciones que siguen están basadas en el manual
de referencia del lenguaje [Clavel 2007] disponible en el sitio referido.
La implementación del Maude 2, le ofrece al usuario una interfaz textual única que debe
usarse para todo tipo de interacción con el intérprete del lenguaje. Esta interfaz consiste en
una secuencia de líneas de texto en pantalla que termina en una línea de ingreso de textos a
cargo del usuario. El usuario debe usar esta última línea para definir el programa, someter
sus consultas y cambiar las opciones de ejecución (a modo de consultas especiales).
Una vez que el usuario somete un texto al intérprete, este reacciona colocando a renglón
seguido una serie de líneas de texto como respuesta. Las líneas anteriores a la de ingreso,
no son otra cosa que los textos sometidos con anterioridad por el usuario y las
correspondientes respuestas del intérprete.
Ejemplo 46
El módulo de programa “SIMPLE-NAT” ser definido en Maude 2 a través del interfaz [Clavel 2007,
sec. 2.2]:
Ejemplo 47
Son términos válidos cualquiera de los siguientes:
486
(+ 137 349)
Capítulo 6: Elementos básicos de los Lenguajes Funcionales.
104
(/ 10 5)
(+ 21 35 12 7)
(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))
(+ (* 3
(+ (* 2 4)
(+ 3 5)
)
)
(+ (- 10 7)
6
)
)
Que de ser sometidos al intérprete evaluarían a los siguientes valores respectivos:
486
486
75
57
57
Nótese que los dos últimos términos son el mismo, sólo que el último fue distribuido en varias líneas
para hacer más visible su estructura.
Además de los predicados primitivos <, =, y >, las operaciones lógicas también hacen parte
del lenguaje. Las más usadas son:
(and <e1> <e2>...<en>)
Que se evalúa, evaluando una a una las expresiones <e> de izquierda a derecha. Si el valor
de alguna es falso el valor de la expresión es falso y el resto de las expresiones <e> no son
evaluadas. Si todas son evaluadas, el valor de la expresión and es el de la última expresión
<en>.
7.2 Justificación.
La capacidad de proponer nuevos operadores o “funciones”54 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.
7.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 en múltiples lugres del programa, posiblemente cambiando las
referencias a 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.
54
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 7: Operadores Definidos
109
7.2.2 Arquitectura de operadores.
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 mucho mayor 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í:
• Una pieza de software debe ser considerada como un agregado de un pequeño conjunto
de grandes partes que interactúan entre sí. Dichas partes deben a su vez estar
constituidas por un pequeño conjunto de partes más pequeñas, y así sucesivamente...
Las partes del software, se dijo, eran agregados de instrucción es que podían asimilarse a
funciones matemáticas encargadas de transformar los datos que recibían del usuario o de
otras funciones, en resultados a ser usados por el usuario u otras funciones del programa.
• El diseño del software consistiría entonces, en la definición de las funciones del
software, y de sus interrelaciones. Se propuso comenzar con la identificación de las
funciones mas agregadas (las mas grandes), para luego, de forma independiente,
identificar sus funciones componentes, a niveles cada vez mas detallados de agregación.
• El programador debía entonces reflejar el diseño en el programa escribiendo las
funciones (u operadores) correspondientes a las componentes de la descomposición
progresiva.
Tal como lo refiere Abelson en [Abelson 1985 secc. 1.1.8], la técnica de la descomposición
progresiva, permite concebir las partes del programa como “cajas negras” enfatizando lo
que dicha parte lleva a cabo sin tener que preocuparnos en ese momento por definir la
manera como llevará a cabo su tarea. Así, la concepción de un programa puede plantarse
con base en el “si tuviera un operador que haga esta tarea, podría con el llevar a cabo
fácilmente aquella otra...”.
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.
Ejemplo 48
En SCHEME un rótulo puede asociarse a un elemento del programa por medio del operador define.
Así si se ingresa la “definición” siguiente para el rótulo pi:
1 ]=> (* pi 10 10)
;Value: 314.1542
Que se extiende de una manera natural a rótulos con argumentos llamados “procedures”,
que se constituyen en los operadores definidos en el programa.
Ejemplo 49
La definición siguiente implementa un procedimiento para elevar al cuadrado un número dado [Abelson
85 sec 1.1.4]:
55
Estos 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 7: Operadores Definidos
111
1 ]=> (define (cua x) (* x x))
;Value: cua
Define el operador cua como uno que eleva al cuadrado su argumento:
[Link] Sintaxis
La forma general de una definición para operadores (o “procedimientos”) en SCHEME es
la siguiente:
(define (<nombre> <argumentos formales>) <cuerpo>)
Donde:
• <nombre> es un rótulo asociado al procedimiento, es decir el nombre del
operador.
• <argumentos formales> son una secuencia de nombres de variables separadas
por espacios, que serán usados dentro del cuerpo del procedimiento para
referirse a los correspondientes operandos en una evocación.
• <cuerpo> es un término o expresión que decide el valor de la aplicación del
procedimiento.
Ejemplo 50
El operador definido en el ejemplo anterior puede usarse como un operador cualquiera. Así, en la
definición siguiente:
1 ]=> (sum_cua 3 4)
;Value: 25.
[Link] Semántica.
Es fácil ver que una definición de un operador, puede verse como una ecuación de la forma
t1 = t2
donde:
• el símbolo = es substituido por la palabra clave define.
• El término t1 tiene un sólo operador y sus operandos son todos nombres de
variable.
• El término t2 determina completamente al significado del operador de t1.
Una característica importante de la definición de un operador en SCHEME, es que ésta
debe llevarse a cabo completamente en una sola ecuación.
Capítulo 7: Operadores Definidos
112
Ejemplo 51
Más de una instrucción define para un operador da lugar a una redefinición del operador:
Nótese que por ser todos los argumentos de t1 nombres de variable, es siempre posible
emparejar una evocación cualquiera del operador con t1, pues basta con sustituir cada
variable de t1 por el operando correspondiente en la evocación para transformarla en el
término de la evocación.
Sin embargo, al contarse con una sola ecuación de definición, el término t2 de la misma
deben considerar todos los casos de evocación posibles para el operador. Las
consecuencias de esta restricción no son obvias a primera vista, y serán motivo de
posteriores discusiones.
7.3.2 Definición de Operadores en MAUDE.
En el lenguaje MAUDE la capacidad de definir operadores se asocia directamente a la
capacidad escribir axiomas ecuacionales que constituyen verdaderas reglas de reescritura.
[Link] Sintaxis
El significado de los operadores se define en MAUDE por medio de construcciones de la
forma siguiente:
eq < t1 > = < t2 >
donde:
• el símbolo = aparece explícitamente en la definición.
• El término t1 tiene al operador definido como su operador de más alto nivel (el
operador de la ocurrencia t1|ε), pero los operandos de este operador pueden ser
términos cualquiera.
• El término t2 determina el significado del operador de t1|ε para el caso de las
evocaciones del operador que emparejan con t1.
Ejemplo 52
Los operadores definidos en los ejemplos anteriores, se definirían en MAUDE de la manera siguiente:
eq cua(x) = x * x .
eq sum_cua(x, y) = cua(x) + cua(y) .
[Link] Semántica.
Capítulo 7: Operadores Definidos
113
El significado de la definición de operadores en MAUDE es el mismo que el de las
ecuaciones en una teoría lógico-ecuacional de la forma.
t1 = t2
Una característica importante de la definición de operadores en MAUDE, es que ésta
usualmente se lleva a cabo en varias ecuaciones.
Ejemplo 53
En las secciones 1.4 y 2.1 de [Clavel 2007 sec 1.3] se presenta en MAUDE la siguiente especificación
para los números naturales que define las propiedades del operador suma (“+”).
fmod PEANO-NAT-EXTRA is
sort Nat .
op 0 : -> Nat .
op s : Nat -> Nat .
op _+_ : Nat Nat -> Nat .
vars M N : Nat .
eq 0 + N = N .
eq s(M) + N = s(M + N) .
endfm
Nótese que hay dos ecuaciones para el operador definido, especializada cada una de ellas en un tipo
particular de evocación. La segunda usa, además, como el primer operando de un operador a una
evocación del operador s().
Ejemplo 54
El operador suma de reales, por ejemplo, es aplicable a cualquier par de valores del dominio de los
reales del lenguaje. Un término como a+b*a, define una función compuesta que sigue siendo aplicable
a cualquier par de valores del dominio de los reales del lenguaje.
Es, sin embargo, necesario tener la posibilidad de definir operadores asociados a funciones
complejas, en las que la composición de funciones que se aplique a un valor del dominio,
dependa del valor mismo (al que se aplican). Este es, por ejemplo, el caso de operadores
que representan funciones discontinuas que componen otras funciones indexándolas por
regiones del dominio.
Ejemplo 55
Capítulo 7: Operadores Definidos
114
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 funcionales
ofrecen, de forma nativa, mecanismos de selección o de escogencia.
Los mecanismos de selección más usados se dan en la forma de “operadores de selección”.
Un operador de selección recibe entre sus operandos, uno o más términos de tipo boleano
que indexan (o señalan) a otros operandos como el valor de la aplicación. El más sencillo
de estos operadores es el “if funcional”, que usa un sólo término de tipo boleano para
indexar a otros dos operandos, como el valor de la aplicación, siendo uno de ellos el
señalado para el caso de que el boleano evalué a cierto, y el otro el señalado en el caso de
que el boleano evalúe a falso.
Una alternativa al “operador de selección” es un mecanismo de selección entre axiomas,
que permite seleccionar entre los varios axiomas que definen un operador cualquiera, el que
va a ser usado en un proceso de reescritura particular. La selección se basa en el valor de
uno o varios términos boleanos que indexan a los axiomas, y que son definidos con base en
los operandos del operador.
En este numeral se presentan las construcciones disponibles en los diferentes lenguajes para
implementar estos mecanismos de selección.
7.4.1 Selección en SCHEME.
Tal como se describe en [Abelson 1985 sección 1.1.6], el lenguaje SCHEME ofrece las
“formas especiales” if y cond como mecanismos de selección. Estas formas especiales no
son otra cosa que operadores de selección nativos, que tienen asociada una secuencia
particular de evaluación para sus argumentos.
La forma general del if es la siguiente:
(if <predicado> <consecuencia> <alternativa>)
Para evaluar una expresión if, el intérprete evalúa el <predicado>; si su valor es verdadero,
entonces se evalúa la <consecuencia> y se retorna su valor; de lo contrario se evalúa la
<alternativa> y se retorna su valor.
La forma general del cond es la siguiente:
(cond (<p1> <e1>)
(<p2> <e2>)
…
(<pn> <en>)
(else <en+1>))
)
Que consiste del símbolo cond seguido por parejas de expresiones (<p> <e>) llamadas
cláusulas y opcionalmente una construcción (else <e>). La expresión <p> de cada pareja
es de tipo boleano y se denomina predicado.
Las expresiones cond son evaluadas así: El predicado <p1> es evaluado; si su valor es falso
entonces <p2> es evaluado y así sucesivamente. El proceso continúa hasta encontrar un
Capítulo 7: Operadores Definidos
115
predicado cuyo valor sea verdadero, en cuyo caso el intérprete retorna el valor de la
correspondiente expresión <e>, como valor de la expresión condicional. Si no se encuentra
un predicado verdadero el valor de cond es el de la expresión <en+1>.
Ejemplo 56
La función |x| puede definirse en SCHEME de cualquiera de las tres formas siguientes:
(define (valor-absoluto x)
(cond ((> x 0) x)
((= x 0) 0)
((< x 0) (- x))))
(define (valor-absoluto x)
(cond ((< x 0) (- x))
(else x)))
(define (valor-absoluto x)
(if (< x 0)
(- x)
x))
Ejemplo 57
La función |x| puede definirse en MAUDE con base en la selección asociada al emparejamiento:
Ejemplo 58
La función |x| puede definirse en MAUDE con base en ecuaciones condicionales:
Ejemplo 59
La función |x| puede definirse en MAUDE con base en ecuaciones condicionales y emparejamiento:
eq abs( 0. ) = 0. .
eq abs( x ) = x if(x > 0.) .
eq abs( x ) = - x if(x < 0.) .
Ejemplo 60
La función |x| puede definirse en MAUDE con base en el if funcional:
eq abs( x ) = if (x > 0.) then x else (- x) fi .
Ejemplo 61
La definición siguiente define un operador cuyo resultado no tiene un tipo determinado:
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.......
Ejemplo 62
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? _
Capítulo 7: Operadores Definidos
119
char? _
string? _
pair? _
list? _
vector? _
bit-string? _
symbol? _
cell? _
record? _
Establecen si su operando es del tipo correspondiente.
56
En el sentido de que todos los elementos de un sort están contenidos en otro sort diferente.
Capítulo 7: Operadores Definidos
120
[Link] Declaración de variables en MAUDE.
Los nombres de variable usados en la definición de los operadores (ver “ecuaciones en
MAUDE” sección 7.3.4) deben declararse antes de usarse, con el objeto de indicar el sort a
cuyos objetos hacen referencia.
Esta declaración es necesaria para poder asociar los operadores que aparecen en las
ecuaciones y evocaciones con el perfil al que corresponden. En efecto, bajo la posibilidad
de sobrecarga no es suficiente el símbolo asociado al operador para distinguir entre
operadores que tienen el mismo símbolo.
Las variables deben declararse en los módulos por medio de construcciones de la forma
siguiente:
var <identificador_de_variable> .
vars <lista_de_identificadores_de_variable> .
Donde:
• <identificador_de_variable> Es un que en adelante será asociado a una
variable.
• <lista_de_identificadores_de_variable> Es una lista de
<identificador_de_variable> separados por espacio.
[Link] Declaración de operadores en MAUDE
Por ser importantes al momento de definirse un operador, tanto el sort del valor resultante
como el sort de sus operandos, 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 especiales57 o
espacio en blanco).
• <plantillas> Es una secuencia de dos o mas <plantilla>, separadas entre si por
caracteres especiales o por espacios en blanco58.
• <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.
57
{ } [ ] , son caracteres especiales que separan identificadores.
58
Si una plantilla tiene varios identificadores, debe encerrarse entre paréntesis para evitar que los separadores de sus
identificadores se confundan con los separadores de la plantilla.
Capítulo 7: Operadores Definidos
121
• es una secuencia de cero o más identificadores de
<atributos_del_operador>
propiedad, separados entre si por coma. En caso de ser cero los identificadores
de propiedad se omiten los paréntesis cuadrados. Cada una de las posibles
propiedades de un operador será discutida en el resto del trabajo y cuando el
significado de la propiedad corresponda al tópico en discusión. Para una
discusión completa de dichas propiedades 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).
Ejemplo 63
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:
Ejemplo 64
La notación infija permite usar símbolos más adecuados para identificar el operador:
[Link].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.
Ejemplo 65
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 .
Ejemplo 66
Dos operadores distintos pueden tener el mismo símbolo de operación, así:
Ejemplo 67
Para caracterizar el uso del atributo ecuacional comm, tomaremos de [Clavel 2007, sec 4.4.1] el
ejemplo de la definición del operador sume en el sort Nat3 (enteros múltiplos de 3). Así, en la
especificación siguiente:
eq 0 + N3 = N3 .
Ejemplo 68
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:
Ejemplo 70
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
59
Si el SRT es convergente, para el resultado final del cálculo el orden de aplicación de los axiomas es, además,
irrelevante.
60
O “estricto”
61
O “perezosa”
Capítulo 7: Operadores Definidos
125
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.
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.
Ejemplo 71
Dada la siguiente aplicación hipotética de un operador de selección en MAUDE, en un término a ser
calculado:
> 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.
Ejemplo 72
Considere la ejecución del programa siguiente en MAUDE:
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 aplicativo la ejecución terminaría normalmente.
Considere la ejecución del programa definido en el Ejemplo 86 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?.
Ejemplo 73
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.
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.
62
Principalmente en el sentido en que se asocian a un valor único del dominio, como las constantes, en lugar de asociarse
a todos los valores de un tipo, o sort, como ocurre con las variables de los axiomas.
Capítulo 7: Operadores Definidos
128
embargo, esta debe haber sido previamente asociada a un valor específico, ya sea por medio
de la instrucción que la define o por medio de una instrucción de “asignación”63.
Al significado de los símbolos que ocurren en los términos involucrados en un paso de
reescritura, nos referiremos como el “medio ambiente” de la reescritura.
La manera como, en cada lenguaje, se determina este medio ambiente es el motivo de
discusión de las subsecciones siguientes.
7.7.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”,
extiende64 el medio ambiente, hasta definir el medio ambiente en el que se ejecutan las
reescrituras [Hanson 2002, sec. 1.2.3].
Como, en el caso de MIT SCHEME, todas las evocaciones se llevan a cabo a través de un
mismo interfaz, la definición del programa y su utilización constituyen una secuencia
continua de evocaciones que se pueden intermezclar65. La única condición de orden entre
la definición y la evocación de un operador es que el operador haya definido antes de su
utilización. Es sin embargo posible cambiar las condiciones que se establecieron al definir
el operador evocando un operador de asignación, por lo que, en general, el resultado de la
evocación de un operador dependerá de la secuencia de evocaciones que pueda haber entre
su definición inicial y su uso.
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
[Link] 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.
Ejemplo 74
Dada la definición de la variable pi en el ejemplo 48, la definición siguiente:
63
Nó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.
64
No se hará aquí una definición precisa del concepto “extender” el medio ambiente, esperando que quede clara al leer el
texto que sigue.
65
Esto 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 7: Operadores Definidos
129
1 ]=> (define pi2 (* 2 pi))
;Value: pi2
Asociará a la nueva variable pi2 el valor de la expresión calculada:
1 ]=> pi2
;Value: 6.283082
Es posible, sin embargo, usar el operador define para introducir un nuevo símbolo al medio
ambiente sin asociarlo a valor u operador alguno. Sin embargo, sólo símbolos definidos y
asociados con un significado66 pueden usarse en las definiciones y cálculos siguientes.
Ejemplo 75
El uso de un símbolo no incluido en el medio ambiente es inválido:
1 ]=> (* 2 pi21)
;Unbound variable: pi21.
; To continue.......
Un símbolo puede definirse sin significado alguno:
1 ]=> (* 2 pi21)
;Unassigned variable: pi21.
; To continue.......
Ejemplo 76
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
66
En 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 7: Operadores Definidos
130
;Unbound variable: pi212
; To continue.......
Ejemplo 77
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
Ejemplo 78
Considere las definiciones siguientes:
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)))
67
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 7: Operadores Definidos
132
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.
Ejemplo 79
Considere las definiciones siguientes:
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
68
Si 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].
69
La única restricción sobre los define es que deben ocurrir al principio del cuerpo donde aparecen [Hanson 2002, sec.
2.4]
Capítulo 7: Operadores Definidos
134
todas las variables del lado derecho del axioma utilizado, toman su valor del
emparejamiento del término del lado izquierdo del axioma70. Así, dado que las ecuaciones
satisfacen la condición impuesta en la sección 5.3.3: “Ecuaciones” (ver Capítulo 5) para los
SRT, no hay valoración de variables desde el medio ambiente definido en el programa.
Es posible, sin embargo, definir en el programa símbolos de operación sin operandos, y
utilizar un axiomas para reescribirlos a un literal cualquiera. En este sentido, es posible
usar símbolos en lugar de literales en los términos a ser calculado. Lo que no es posible, es
cambiar el valor asociado al símbolo, ya que; de definirlo más de una vez en el programa,
se tendrá una ambigüedad en su significado que será reportada en ejecución; y no existe un
operador de asignación que permita modificar en ejecución las ecuaciones del programa71.
70
MAUDE permite un tipo de ecuación en las condiciones de una ecuación condicional que se denomina “matching
equations” [Clavel 2007, Sec 4.3], y que permite usar variables que no aparecen al lado izquierdo de la ecuación. En este
caso, sin embargo, la valoración de estas variables también proviene del proceso de selección del axioma y del
emparejamiento, en lugar de provenir del medio ambiente definido en el programa.
71
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.
72
Denominada descomposición progresiva de funciones en el marco de la programación “estructurada”.
Capítulo 7: Operadores Definidos
135
característica apoya la mantenibilidad, ya que cambios a operadores definidos dentro de
otro operado sólo pueden afectar a dicho operador.
Ejemplo 80
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:
Ejemplo 81
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:
73
Los nombres en castellano corresponden a la traducción libre del autor de los nombres de los módulos en MAUDE
Capítulo 7: Operadores Definidos
136
de dato nativos al lenguaje junto con sus operaciones son, de hecho, módulos MAUDE que
el usuario debe relacionar con los suyos antes de utilizarlos.
Los módulos MAUDE deben74 estar contenidos en archivos de texto. Un archivo de texto
debe 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. Se pueden incluir los módulos de varios archivos usando de forma repetida dichos
comandos. Es posible, además, 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. De hecho 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érprete75, por lo que están siempre disponibles para 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.
[Link] 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. [Link])).
Los símbolos de operación ∑ son las declaraciones de los operadores propuestos por el
programador (ver “Declaraciones de operadores en MAUDE” (sec. [Link])).
El conjunto de variables X son las declaraciones de variables del módulo (ver
“Declaraciones de variables en MAUDE” (sec. [Link])).
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 el Capítulo 11.
• Las ecuaciones de membresía condicional, que son tratadas en el Capítulo 11.
74
Es 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.
75
De un archivo denominado [Link], que debe estar a disposición del interprete a través de la variable path del
sistema operativo...
Capítulo 7: Operadores Definidos
137
[Link] Relaciones entre módulos en MAUDE
Las relaciones entre los módulos son establecidas por medio de construcciones que
soportan tanto la definición de jerarquías de inclusión entre módulos una algebra de
módulos y técnicas de programación parametrizada y (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 10.
[Link] 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 los incluyen. 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.
[Link] 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 7: Operadores Definidos
138
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 [Link]) 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].
Ejemplo 82
Considere la siguiente definición de “ancestro”:
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 calculo 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 a estudios teórico que están por fuera del alcance del trabajo.
76
Ver definición de “Recursión” en Wikipedia [Link]
Capítulo 8: Definición Recursiva de Operadores.
141
8.2 Ejemplos de Definición Recursiva de Operadores.
En esta sección se introduce la definición recursiva de operadores, con base en dos
ejemplos clave para el Capítulo.
8.2.1 Sumatoria.
Considere un operador encargado de llevar a cabo la suma de los cuadrados de los enteros
comprendidos entre dos números enteros dados:
j
∑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}
Ejemplo 83
La ecuación {2} se puede expresar en SCHEME de la manera siguiente:
(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.
Ejemplo 84
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 8: Definición Recursiva de Operadores.
142
∑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.
Ejemplo 85
La selección en SCHEME debe efectuarse en el marco del único axioma de definición usando un
selector.
(define (sum_n2 i j)
(if (= i j) (* i i)
(+ (* i i) (sum_n2 (+ i 1) j) )
)
)
En MAUDE, por otro lado, basta con incluir el nuevo axioma, limitando el uso del anterior al caso que
corresponde, así:
Ejemplo 86
El proceso de reescritura de una evocación del operador definido en el ejemplo anterior sería como la
que se ilustra a continuación:
(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)
Capítulo 8: Definición Recursiva de Operadores.
143
;; 91
Ejemplo 87
La Tabla 8.1 se puede expresar en SCHEME de la manera siguiente:
Capítulo 8: Definición Recursiva de Operadores.
144
(define (raíz-cuadrada y x) (raíz-cuadrada (/ (+ y (/ x y)) 2) x ) )
La representación correspondiente en MAUDE puede ser como sigue:
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í:
Ejemplo 88
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)
;;(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:
Ejemplo 89
El operador raíz-cuadrada en SCHEME quedaría de la manera siguiente:
(define (raíz-cuadrada Y X)
( If (< (abs (- (* Y Y) X )) 0.001) Y
(raíz-cuadrada (/ (+ Y (/ X Y)) 2) X ) )
)
)
El operador √ en MAUDE quedaría de la manera siguiente:
Ejemplo 90
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….
77
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
Capítulo 8: Definición Recursiva de Operadores.
146
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 ejeplos, mostraremos problemas cuya solución estará siempre basada en operadores que
determinan procesos recursivos. En este caso culparemos al problema, en si mismo, y 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 2 = i 2 + ((i + 1) 2 + ((i + 2) 2 + ((i + 3) 2 + ∑ n 2 ))) {5}
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 sumatoria78.
Este lugar de memoria no es otra cosa que un operando del operador.
Ejemplo 91
El procedimiento SCHEME que se muestra a continuación reserva un operando para “acumular” los
términos de la sumatoria.
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.
78
Esto 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, 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 que el declarativo.
Capítulo 8: Definición Recursiva de Operadores.
147
(define (suma_it total i j ) (suma_it (+ total (* i i )) (+ i 1 ) j ))
Al evocar este operador con un valor inicial de 0 para el acumulador, este va almacenando los valores
parciales de la sumatoria, así:
(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)
..
..
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.
Ejemplo 92
La versión iterativa del operador de sumatoria en SCHEME es, en consecuencia, como sigue:
Ejemplo 93
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:
Ejemplo 94
Dadas las definiciones que se muestran a continuación:
.....
eq cua(x) = x * x .
eq sumcua(x, y) = cua(x) + cua(y) .
.....
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 definidos80.
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 8.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” utilizado al definir el operador.
• La forma en que el programador define el “algoritmo”.
79
Diremos entonces que la función que relaciona el tiempo de ejecución con el valor de los operandos esta acotada
superiormente por la función C*f(1) donde C es el tiempo de ejecución del caso más desfavorable.
80
En 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*f(v) donde C es una valor constante.
Capítulo 8: Definición Recursiva de Operadores.
150
Para reducir la complejidad de esta dependencia, y capturar por medio del concepto de
“algoritmo” al factor de mayor relevancia frente a la eficiencia81, se ha planteado el
“principio de invarianza” [Brassard 87, sec. 1.3]. Según este principio, para dos
implementaciones cualesquiera (de un operador) que usen el “mismo” algoritmo (es decir
que difieran en el lenguaje, el computador, la implementación del lenguaje y forma de
definir el algoritmo), las funciones que relaciona el tiempo de ejecución con los valores de
los argumentos, o “funciones de rendimiento”, están mutuamente acotadas por una
constante multiplicativa. Así, para las implementaciones A y B de un mismo algoritmo α
se cumple que existe una constante c que satisface el siguiente predicado:
TαA(M).<= c*TαB(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
(Vg. 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”82.
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 87, sec. 2.1]:
+
O(f(n))={t(n) : N→R* / (∃c∈R ) (∃n0∈N) (∀n ≥ n0) [t(n) ≤ c * f(n)]}
Donde:
• R* es el conjunto de los números reales positivos.
• R+ es el conjunto de los números reales estrictamente positivos
• N es el conjunto de los números naturales.
En otras palabras O(f(n)) es el conjunto de funciones que puede ser superada por un
múltiplo real positivo de f(n), a partir de un valor de n.
81
Que por otra parte es bastante difuso, 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).
82
O “Big O notation”, [Link]
Capítulo 8: Definición Recursiva de Operadores.
151
Diremos entonces que un algoritmo α es “tan bueno o mejor” que otro algoritmo β si la
función de rendimiento asociada con α pertenece al orden de la función de rendimiento
asociada con β, así:
Tα(M) ∈ O(Tβ(M))
Es también posible darle una medida propia de eficiencia a un algoritmo particular,
planteando una serie de funciones matemáticas cuyos órdenes estén contenidos unos dentro
de los otros. La serie de funciones siguiente sirve a este propósito particular:
1/2 2 3 k n
1, ln(n), n , n, n*ln(n), n , n ,..n ,…2
Ya que sus órdenes respectivos se relacionan de la manera siguiente:
1/2 2 3 k n
O(1) ⊂ O(ln(n)) ⊂ O(n ) ⊂ O(n) ⊂ O(n*ln(n)) ⊂ O(n ) ⊂ O(n ) …⊂
⊂ O(n )… ⊂ 2
Así, para clasificar el rendimiento de un algoritmo, basta con señalar la función mas baja de
la serie, cuyo orden contiene el orden de la función asociada al algoritmo.
Ejemplo 95
Los dos operadores definidos en el Ejemplo 52 :
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 92:
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 89:
(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
[Link] )
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 mas 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
Capítulo 8: Definición Recursiva de Operadores.
152
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 87].
8.5 Acumulando.
En esta sección presentaremos una serie de ejemplos para profundizar en aspectos que
deben tenerse en cuenta al momento de definir operadores aparentemente similares al que
lleva a cabo el cálculo de la suma de los cuadrados.
Un objetivo de la sección es el de mostrar que existe una distancia entre la especificación
puramente matemática del problema, y su especificación en un lenguaje de programación
por bien fundamentado que éste sea. Así, mientras no existan intérpretes de las
especificaciones matemáticas con la inteligencia suficiente para evitar los problemas que se
discuten a continuación, será necesario que los programadores desarrollen las habilidades
necesarias para evitarlos83.
8.5.1 Forma del proceso: acumulador asociativo y no asociativo.
Los dos primeros ejemplos se tratan del cálculo de las sumas parciales de la serie
∞
1
∑ (4n − 3)(4n − 1)
n =1
{8}
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 2
1 +
3
22 + 2
3 +O
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 = ∑ = + + +L+ {10}
k =1 ( 4k − 3)( 4k − 1) 1 ⋅ 3 5 ⋅ 7 9 ⋅ 11 (4n − 3) ⋅ (4n − 1)
Y
83
Así, el problema de la programación procede desde especificar las propiedades del sistema, de la forma más simple
posible, en un formalísmo matemático lógico, a transformar dicha especificación en otra menos simple pero con las
propiedades computacionales adecuadas. Es naturalmente ventajoso usar el mismo formalismo para elaborar ambas
especificaciones.
Capítulo 8: Definición Recursiva de Operadores.
153
1
Fn = {11}
2 2
1 +
3
22 +
n
O+
n2
[Link] 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.
1 1 1 1 1
Sn = + + +L+ + +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 +
n−2
O+
n −1
(n − 2) 2 +
n
(n − 1) 2 + 2
n +0
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) = 2
(4k − 3)(4k − 1) k + F * (k + 1, n)
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.
Ejemplo 96
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:
Ejemplo 97
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í:
vars K N : int .
eq sum-pi(N) = sum-pi-rec(1, N) .
ceq sum-pi-rec(K, N) = 1 / ((4 * K - 3) * (4 * K – 1)) + sum-pi-rec((K + 1), N) if(K<=N) .
ceq sum-pi-rec(K, N) = 0 if(K>N) .
Capítulo 8: Definición Recursiva de Operadores.
155
.....
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) .
.....
Donde sum-pi-rec y cont-frac-rec expresan las funciones S*(k, n) y F*(k, n), respectivamente.
Ejemplo 98
La versión iterativa del operador sum_pi(n) en MAUDE tiene la misma estructura que la de la suma de
los cuadrados del ejemplo 80, sólo que el término a ser acumulado es diferente, así:
vars K N : int .
var Total : float .
eq sum-pi(N) = sum-pi-ite(0., 1, N) .
ceq sum-pi-ite(Total, K, N) = sum-pi-ite( Total + 1 / ((4 * K - 3) * (4 * K – 1)),
(K + 1), N) if(K<=N) .
ceq sum-pi-ite(Total, K, N) = Total if(K>N) .
.....
Ejemplo 99
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í:
vars K N : int .
vars Total : float .
eq cont-frac(N) = cont-frac-ite(0, N) .
ceq cont-frac-ite(Total, K) = cont-frac-ite(K / ((K * K) + Total), (K - 1)) if(K>0) .
eq cont-frac-ite(Total, 0) = Total .
.....
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.
Ejemplo 100
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 80, es el siguiente:
84
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
([Link] y Maxima ([Link] que proveen
implementaciones que almacenan los valores en espacios de memoria variables, manteniendo un grado de precisión
predeterminado.
Capítulo 8: Definición Recursiva de Operadores.
159
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
Ejemplo 101
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:
vars K N : int .
vars Tanterior X : float .
eq termino(X, N) = ter-ite(1., 1, N, X) .
ceq ter-ite(Tanterior, K, N, X) =
ter-ite(Tanterior * (-1) * (X^2 / ((2 * K) * (2 * K – 1))), (K + 1), N, X) if(K<=N)
.
ceq ter-ite (Tanterior, K, N, X) = Tanterior if(K>N) .
.....
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í:
Ejemplo 102
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 conseno-ite 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í:
85
Naturalmente 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.
Capítulo 8: Definición Recursiva de Operadores.
161
eq siguiente(Ta, X, K) = Ta * (-1) * (X^2 / ((2 * K) * (2 * K – 1))) .
eq coseno(X, N) = coseno-ite(1., - X * X / 2., 1, N, X) .
ceq coseno-ite(Total, Ta, K, N, X) =
coseno-ite(Total +Ta, siguiente(Ta, X, (K + 1)), (K + 1), N, X) if(K<=N) .
ceq coseno-ite (Total, Ta, K, N, X) = Total if(K>N) .
.....
Ejemplo 103
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 la última pareja, término positivo +
término negativo, adicionen a la sumatoria un valor despreciable, 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 un el operador Fib(n).
8.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.
Ejemplo 104
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 (fib n)
(if (= n 0) 0
(if (= n 1) 1
(+ (fib (- n 1)) (fib (- n 2)) )
)
)
)
La definición correspondiente en MAUDE es la siguiente:
La forma del proceso que determina esta definición es sin duda la de un proceso recursivo.
Ejemplo 105
Capítulo 8: Definición Recursiva de Operadores.
163
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 XX], 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 tempo de ejecución asociado
a valores moderados de n.
8.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
simplemente en mantener almacenado dos números consecutivos para calcular con ellos el
siguiente.
Ejemplo 106
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:
(define (fib n)
(if (= n 0) 0 (fib-iter 1 0 n))
)
Ejemplo 107
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 )
;;(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.
Ejemplo 108
Una versión más avanzada del operador fib(n) es basado en la Matriz de Fibonacci que puede calcularse
por medio de una algoritmo del orden O(ln n). El algoritmo se basa eb el cálculo de la Matriz de
Fibonacci ([Link] )
n
1 1 fib(n + 1) fib(n)
1 0 = fib(n) fib(n − 1
Capítulo 8: Definición Recursiva de Operadores.
165
n
El cálculo de A , 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.
86
Que de hecho no siempre es posible.
Capítulo 8: Definición Recursiva de Operadores.
166
• 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.
- 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 )
Capítulo 8: Definición Recursiva de Operadores.
167
7- ¿por que no es necesario que en la solución MAUDE del Ejemplo 85 todas las
ecuaciones sean condicionales?.
8. Elabore la versión en SCHEME de los ejemplos Ejemplo 93, Ejemplo 98, y Ejemplo 99.
9- Elabore la versión que procede de atrás para adelante para el caso del Ejemplo 83 y del
Ejemplo 85.
10- Elabore un código en MAUDE que defina los operadores _! (que calcula el factorial) y
_^_ (que eleva un número a una potencia) referidos en el Ejemplo 100.
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 en el Ejemplo 100 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 100, primero cambiando la
ecuación eq coseno(X, N) = coseno-ite(1, 1, N, X) por la ecuación eq coseno(X, N)
= coseno-ite(0, 1, N, X) y luego por la ecuación eq coseno(X, N) = coseno-ite(1, 2,
N, X).
15- En el operador que calcula los términos de la serie del coseno definido en el Ejemplo
101, se usó la expresión Tanterior * (-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)!
17- En la definición del operador coseno(X) del Ejemplo 103:
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 104, 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)
Capítulo 8: Definición Recursiva de Operadores.
168
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.
Capítulo 8: Definición Recursiva de Operadores.
169
0
n+xn e
....................
.....................
(k+1)+...
k+xk e
.....e
...
7+ ....
6+x6 e
5+x e
5
4+x4 e
3+x3 e
2+x e
2
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_______ .
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 9
Valores y Tipos Compuestos.
Capítulo 9: Valores y Tipos Compuestos.
172
9.1 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, sin embargo, 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 decimos
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 operaciones que le permitan acceder y manipular las
partes que lo componen, 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 datos87, 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.
87
En este trabajo usaremos la denominación “tipo abstracto” en el sentido citado, sin desmedro de asociarle la teoría en
una lógica ecuacional que define las propiedades de operaciones definidas (aplicables a cualquier álgebra que sea modelo
de la teoría).
Capítulo 9: Valores y Tipos Compuestos.
173
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 dichas entidades,
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”88 (“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 componentes89.
• Los componentes pueden ser de diferentes tipos (incluyendo tipos compuestos).
• Se puede acceder a los componentes por medio de un mecanismo que
denominaremos “vía de acceso”.
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. Para ello debe proveer lo siguiente:
• Un mecanismo para “construir” valores estructurados a partir de los valores de
sus componentes.
• 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 datos 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.
Ejemplo 109
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”.
88
Por referencia al hecho de que tradicionalmente constituyen la unidad básica de lectura/escritura de datos en archivos.
89
Aunque no necesariamente fijo.
Capítulo 9: Valores y Tipos Compuestos.
174
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.
Ejemplo 110
La declaración de nuestro tipo Vector en SCHEME es como sigue:
Ejemplo 111
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 .
Ejemplo 112
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 V1 (make-Vector 2 5 ))
(define V2 (make-Vector 3 4 ))
Ejemplo 113
Capítulo 9: Valores y Tipos Compuestos.
177
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í:
Ejemplo 114
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í:
Ejemplo 115
Los operadores selectores para las componentes del vector se pueden declarar y definir fácilmente en
Capítulo 9: Valores y Tipos Compuestos.
178
MAUDE con los mismos elementos que se usan para definir y declarar cualquier otro operador:
Ejemplo 116
Para definir operadores sobre el tipo definido en SCHEME se deben usar los operadores selectores y
constructores previamente definidos para el tipo.
Así, sobre nuestro tipo vector podemos definir los operadores siguientes:
Ejemplo 117
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.
Ejemplo 118
De aplicarse el operador a operandos incorrectos se produce un error de ejecución en el cuerpo del
operador al aplicar los selectores a un tipo inadecuado, así:
...
1 ]=> (suma_vectores 6 9)
;Error: XXXXXXX
Ejemplo 119
Para definir operadores sobre instancias de un tipo propuesto por el programador es necesario y
suficiente usar los constructores, así:
Ejemplo 120
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
verdadera posición de cada objeto. Además hace evidente la imposibilidad para acceder a
ciertos elementos de la lista directamente. En la figura 4.1, podemos observar, por ejemplo,
dos maneras de combinar los números 1, 2, 3, y 4 utilizando parejas.
Capítulo 9: Valores y Tipos Compuestos.
183
1 2 3 4 1 2 3 4
Ejemplo 121
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).
Los siguientes constructores forman parejas de enteros:
sort Pairs_Int .
op _|_ : Int Int -> Pairs_Int [ctor] .
op _ _ : Int Int -> Pairs_Int [ctor] .
op _,_ : Int Int -> Pairs_Int [ctor] .
No nos preocuparemos por definir operadores de selección, ya que para obtener las partes de una pareja
tomaremos ventaja de poder usar más de un operador al lado derecho de la ecuación.
Para ilustrar lo anterior presentamos a continuación un operador que tome como argumento a una pareja
y de como resultado a otra pareja que tenga los componentes en orden ascendente.
90
Justificado por el hecho de que una conexión entre más de dos componentes pueden representarse por varias conexiones
entre dichos componentes.
Capítulo 9: Valores y Tipos Compuestos.
185
9.4 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 (Vg. 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.
9.4.1 Declaración de Listas.
Para darle categoría de tipo a una lista el lenguaje debe poder ofrecernos construcciones
que lo permitan.
[Link] 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:
• <objeto> es el valor que se desea verificar como lista.
[Link] 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
Capítulo 9: Valores y Tipos Compuestos.
186
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 [Link].
Ejemplo 122
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 .
’()
Ejemplo 123
La construcción para una lista que contiene 1, 2, 3 y 4 sería entonces como sigue:
(cons 1
(cons 2
(cons 3
(cons 4 ’()))))
Que visualizada por medio de cajas luciría de la manera siguiente:
1 2 3 4
91
Tala 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 9: Valores y Tipos Compuestos.
187
El siguiente operador construye una lista a partir de sus componentes.
(list <objeto1> <objeto2> ... <objetoN> )
Donde:
• <objeto*> Son las componentes de la lista. De no existir ninguno se construye
una lista vacía.
El siguiente operador construye una lista cuyos componentes son todos iguales.
(make-list <numero> <objeto2> )
Donde:
• <objeto> Es el componente que se repite.
• <número> Es el número de veces que se repite el componente <objeto>.
Ejemplo 124
La lista del ejemplo anterior puede construirse de forma más simple como sigue:
(list 1 2 3 4)
Ejemplo 125
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í:
Ejemplo 126
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 nil))
Donde debe notarse que se usó el valor nil, para determinar el final de la cadena.
Ejemplo 127
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í:
El efecto del atributo ecuacional assoc, es que dos listas con los mismos componentes son
consideradas iguales sin importar cuales son sus sublistas92.
Ejemplo 128
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 reescrita, antes de
que tenga lugar cualquier otra reescritura, al otro operando de la operación.
Ejemplo 129
92
Los atributos ecuacionales assoc e idem no pueden ser usados juntos.
Capítulo 9: Valores y Tipos Compuestos.
190
De aparecer las listas siguientes como operando de un operador:
(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 se considerará la posibilidad de tener listas que siempre terminan en nilInt
como ocurre con las listas en SCHEME y con el objeto de contrastar los dos enfoques. En varios de los
ejemplos se solicitará al lector que indique el efecto de considerar cda uno de los enfoques. La forma de
construir una lista que mimifique las listas nativas en SCHEME, se presenta como ejemplo en la sección
10.4.4
La declaración de la lista arriba presentada no indica, sin embargo, cuales son los
componentes elementales de la lista, que no sean el que representa a la lista vacía, en
nuestro caso los miembros del sort Int. Para indicar que los enteros son las componentes
elementales de nuestra lista usaremos una relación de contención entre sorts, la relación de
subsort (sección [Link]).
Ejemplo 130
Para definir cuales son las componentes elementales de la lista, declararemos que el conjunto de los
enteros está contenido en el conjunto de las listas de enteros, así:
Ejemplo 131
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í:
(define (largo L)
( if (null? L) 0 (+ 1 (largo (cdr L))) )
)
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 (+ (cua (car L)) (sum-cua (cdr L))) )
)
Donde el operador cua, fue definido en el Ejemplo 52.
Ejemplo 132
En la definición siguiente el operador pertenece? verifica si un valor dado aparece en la lista,
recorriéndola en orden directo. Nótese que el proceso se interrumpe cuando se agota la lista sin haber
encontrado el elemento buscado o cunado se encuentra el elemento buscado aun cuando no se haya
agotado la lista:
(define (pertenece? E L)
(if (null? L) #f
(if (= (car L) E) #t
(pertenece? E (cdr L))
Capítulo 9: Valores y Tipos Compuestos.
192
)
)
)
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.
Ejemplo 133
En la definición siguiente el operador largo obtiene el tamaño de la lista, recorriéndola en orden directo
y usando un proceso iterativo, así:
(define (largo_ite T L)
( if (null? L) T (largo_ite (+ 1 T) (cdr L)) )
)
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.
Ejemplo 134
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í:
(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 lo definiremos en un
ejemplo más adelante. Allí mostraremos que el orden del proceso que determina es O(N) (en lugar de
ser O(1).
Capítulo 9: Valores y Tipos Compuestos.
193
El número de reescrituras requeridas para llevar a cabo el proceso definido en el ejemplo
anterior es el tamaño de la lista más 1. En este caso, sin embargo, las operaciones en cada
reescritura del proceso no son O(1) ya que el cdr_inverso debe llevar a cabo tantas
reescrituras como elementos tenga la lista para lleva a cabo su tarea. Así, el número de
reescrituras total es N+(N-1)+(N-2)+(N-3)+.....+1, por lo que el orden del proceso es ahora
O(N2).
[Link] Recorridos básicos sobre Listas en MAUDE.
Los recorridos en orden directo e inverso en MAUDE son planteados de una forma muy
simple, por medio de procesos que en cada paso de reescritura visiten el elemento de la lista
que corresponda, y le den al paso siguiente, la sublista que quede al suprimir el elemento
visitado para que repita la acción. El proceso se interrumpe cuando la lista sobre la que se
va a repetir la acción es la lista vacía, o cuando se obtiene el resultado esperado.
La diferencia con el SCHEME es que, en lugar de usar operadores de selección para
obtener el elemento y la sublista requeridos en cada paso, usaremos el proceso de
emparejamiento.
Ejemplo 135
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.
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), el orden de los procesos definidos para el
pero de los casos, es O(N) siendo N el tamaño de la lista.
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 ganancia en el
orden obtenida es 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.
9.4.4 Selección sobre listas.
La utilidad de un compuesto iterado se fundamenta en la capacidad de acceder o
seleccionar componentes específicos y grupos de componentes específicos. En esta sección
ilustraremos la manera de llevar a cabo selecciones sobre la lista en el marco de los
lenguajes analizados.
Para ello declararemos y definiremos cuatro operadores elementales, a saber:
• Un operador que obtenga el elemento de la lista que ocupa una posición dada.
• Un operador que obtenga el primer elemento de la lista cuyo valor cumpla con
una condición dada.
• Un operador que obtenga el primer elemento de la lista que tenga una relación
dada con su antecesor.
• Un operador que obtenga los elementos de la lista cuyo valor cumpla con una
condición dada.
[Link] Selección sobre Listas en SCHEME.
Los dos primeros dos operadores, no son otra cosa distinta a un recorrido que se interrumpe
al encontrar la componente buscada, dando como resultado dicha componente. Se debe
Capítulo 9: Valores y Tipos Compuestos.
195
proveer, sin embargo, un valor especial de retorno en caso de que la componente buscada
no se encuentre en la lista.
Ejemplo 136
En la definición siguiente el operador i-esimo obtiene el componente de posición i en la lista,
retornando -1 en caso de que éste no exista.
(define (i-esimo I L)
(if (null? L) -1
(if (= I 1) (car L)
(i-esimo (- 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 halle-menor-a-v? obtiene el componente cuyo valor satisface la
condición de ser menor que un valor dado, retornando -1 en caso de que éste no exista.
(define (halle-menor-a-v I L)
(if (null? L) -1
(if (< (car L) I) (car L)
(halle-menor-a-v 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.
Ejemplo 137
En la definición siguiente, el operador halle-menor-a-anterior? obtiene el primer componente cuyo
valor es menor que el valor del componente anterior, retornando -1 en caso de que éste no exista.
(define (halle-menor-a-anterior L)
(if (or (null? L) (null? (cdr L)) ) -1
(if (< (car (cdr L)) (car L)) (car (cdr L))
(halle-menor-a-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.
Capítulo 9: Valores y Tipos Compuestos.
196
El cuarto operador, por su parte, no sólo debe recorrer la lista, sino que debe construir otra
usando el operador cons.
Ejemplo 138
En la definición siguiente, el operador select-menor-a-v obtiene una lista que contiene los elementos
de la lista dada cuyo valor es menor que un umbral dado.
(define (select-menor-a-v I L)
(if (null? L) ’()
(if (< (car L) I ) (cons (car L) (select-menor-a-v I (cdr L)))
(select-menor-a-v I (cdr L))
)
)
)
Ejemplo 139
En la definición siguiente, el operador select-menor-a-v 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.
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.
[Link] 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.
Ejemplo 140
En la definición siguiente el operador [i] obtiene el componente de posición i en la lista, retornando un
valor especial de error en caso de que éste no exista.
Capítulo 9: Valores y Tipos Compuestos.
197
op _[_] : ListInt Int -> Int .
op error-en-indice : -> Int .
vars I E : Int .
var L : ListInt .
eq E L [1] = E .
ceq E L [I] = L [I – 1] if(I =/= 1) .
eq E [1] = E .
ceq E [I] = error-en-indice if(I =/= 1) .
eq nilLint [I] = error-en-indice .
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.
En la definición siguiente el operador {1Xen_/ X<_} obtiene el componente cuyo valor satisface la
condición de ser menor que un valor dado, retornando un valor especial de error en caso de que éste no
exista.
Para acceder a dos valores consecutivos de la lista se usará, como siempre, la unificación.
Ejemplo 141
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.
El cuarto operador, por su parte, debe construir otra usando el operador constructor.
Ejemplo 142
En la definición siguiente, el operador {*Xen_/X<_} obtiene una lista que contiene los elementos de la
Capítulo 9: Valores y Tipos Compuestos.
198
lista dada cuyo valor es menor que un umbral dado.
La especificación del operador del ejemplo anterior, que define un proceso iterativo no
ofrece dificultad alguna en MAUDE.
Ejemplo 143
En la definición siguiente, el operador {*Xen_/X<_} del ejemplo anterior es definido de manera que
genere un proceso iterativo.
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.
9.4.5 Modificadores de la lista
Debido a que los compuestos iterados suelen incluir grandes cantidades de componentes,
no es práctico usar directamente los constructores del tipo para especificar dichas
componentes. Es, por ello, necesario contar con operadores que permitan, tanto incluir
componentes en el iterado a medida que se requieran, como excluir componentes del
iterado en el momento en que ya no sean útiles.
Por otro lado, los iterados se usan como repositorios de datos durante largos períodos de
tiempo. Durante estos períodos no sólo es necesario incluir y excluir componentes en el
iterado, sino que también es necesario modificar las componentes existentes.
A los operadores encargados de incluir, excluir y modificar los componentes del iterado,
los hemos denominado “operadores modificadores” del iterado.
Capítulo 9: Valores y Tipos Compuestos.
199
En esta sección ilustraremos la manera de efectuar modificaciones sobre una lista en el
marco de los lenguajes analizados, por medio de los operadores siguientes:
• Un operador que incluya un nuevo componente en una posición dada de la lista.
• Un operador que excluya (o “borre”) el componente que ocupa una posición
dada de la lista.
• Un operador que excluya de la lista los componentes que satisfacen una
condición dada.
• Un operador que elimine de la lista los componentes repetidos, dejando sólo una
ocurrencia de un valor particular.
• Un operador que adicione al final de una lista los elementos de otra lista dada.
[Link] Modificadores de listas en SCHEME.
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. El lector debe notar que, en todos los
casos, el operador simplemente describe o “declara” la composición de la lista resultante.
Nótese, además, que el resultado de la operación debe ser siempre una lista.
Ejemplo 144
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 0) L
(if (= I 1) (cdr L)
(cons (car L) (borre (- I 1) (cdr L)))
)
)
)
)
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 (= E (car L)) (borre-e (cdr L))
Capítulo 9: Valores y Tipos Compuestos.
200
(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).
Ejemplo 145
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
132.
(define (pode L)
(if (null? L) L
(if (pertenece? E (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.
Ejemplo 146
En la definición siguiente, el operador append obtiene una lista que contiene los elementos de dos listas
dadas.
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.
[Link] 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.
Ejemplo 147
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.
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.
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.
Ejemplo 148
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.
9.4.6 Transformación de la lista
Algunos de los procesos útiles sobre la lista se facilitan si el orden de las componentes
cumplen ciertas condiciones. En particular la selección de un componente con base en su
Capítulo 9: Valores y Tipos Compuestos.
203
valor (o el valor de algunos de sus subcomponentes en el caso de que el componente sea a
su vez un compuesto), se puede llevar a cabo de forma más rápida si los elementos de la
lista se encuentran ordenados por su valor (o por el valor del subcomponente).
Es, en consecuencia, necesario contar con operadores que lleven a cabo diversos tipos de
transformaciones sobre la lista, y ente ellas, la recolocación de sus componentes en un
orden dado.
En esta sección ilustraremos la manera de llevar a cabo transformaciones sobre la lista en el
marco de los lenguajes analizados. Para ello declararemos y definiremos los siguientes tres
operadores elementales, a saber:
• Un operador que recorra la lista intercambiando los elementos que se hallen en
desorden.
• Dos operadores que ordenen los componentes de la lista en de forma ascendente
para el valor de los componentes.
[Link] Transformación de listas en SCHEME.
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.
Ejemplo 149
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 ser del O(1) el proceso en cada paso del recorrido,
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.
Ejemplo 150
En la definición siguiente, el operador buble-sort lleva a cabo un ordenamiento de la lista evocando de
forma repetida el operador intercambie definido arriba.
Capítulo 9: Valores y Tipos Compuestos.
204
(define (buble-sort L)
(if (or (null? L) (null? (cdr L)))
L
(buble-sort-ite L (largo L))
)
)
(define (buble-sort-ite L N)
(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.
Ejemplo 151
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-a-v (car L) (cdr L) ))
(cons (car L) (quick-sort (select-mayor-igual-a-v (car L) (cdr L))))
)
)
)
Donde se usaron los operadores de selección select-menor-a-v y select-mayor-igual-a-v, definidos,
el primero en el Ejemplo 139, y el segundo dejado como ejercicio al lector.
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 fuentes93.
[Link] Transformación de listas en MAUDE.
93
Consultar por ejemplo en [Link]
Capítulo 9: Valores y Tipos Compuestos.
205
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.
Ejemplo 152
En la definición siguiente, el operador <-> recorre la lista en orden inverso intercambiando los
elementos que estén en desorden.
Por llevar a cabo un recorrido simple, y ser del O(1) el proceso en cada paso del recorrido,
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.
Ejemplo 153
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.
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]).
Capítulo 9: Valores y Tipos Compuestos.
206
Ejemplo 154
En la definición siguiente, el operador Q>> lleva a cabo un ordenamiento de la lista por el método del
“Quick Sort”.
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).
Ejemplo 155
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:
(cons 5 (cons
(cons 2 (cons
(cons 1 (cons nil nil))
(cons 3 (cons nil nil))
)
)
(cons 9 (cons
(cons 7 (cons nil nil))
nil
)
)
)
)
Ejemplo 157
El operador get_root, obtiene la raíz de un árbol suministrado como operando, así:
A diferencia de lo que ocurre con la lista, no existe en SCHEME un operador que valide si
un objeto es un árbol. Esto se debe a que el árbol no es una estructura nativa en lenguaje y
a que éste no tiene la capacidad de declarar nuevos tipos.
[Link] Construcción del Árbol en MAUDE.
La introducción de un nuevo tipo en MAUDE comienza con la declaración de un nombre
para el tipo. El nuevo tipo debe contar con al menos un constructor, y una manera de
representar el árbol vacío. El constructor constituye, en este caso, la representación del
árbol.
La definición del constructor se apoyó en un enfoque similar al usado en SCHEME, por lo
que referimos al lector a la sección correspondiente.
Ejemplo 158
Capítulo 9: Valores y Tipos Compuestos.
209
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í:
sort BTreeInt .
op nilBTInt -> BTreeInt [ctor] .
op <_>_ _ : Int BTreeInt BTreeInt -> BTreeInt [ctor] .
Por ejemplo el árbol de la figura se representaría por medio del término siguiente:
<7>
(<3> (<2> (< 1 > nilBTInt nilBTInt)
nilBTInt
)
(<4> nilBTInt
(< 6 > nilBTInt nilBTInt)
)
)
(<8> nilBTInt
(<15> (<13> nilBTInt nilBTInt)
nilBTInt
)
)
3 8
2 4 15
1 6 13
Ejemplo 159
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? A E)
(if (null? A) #f
(if (= (get-root A) E) #t
(if (< E (get-root A))
(esta-en? (get-ArI A) E)
(esta-en? (get-ArD A) E)
)
)
)
)
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 as que parten de cada nodo del árbol tengan los
mismos elementos, constituyéndose en un árbol perfectamente “balanceado”94. 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 árbol95. En este caso el orden del proceso
de localización de un elemento es, en el peor de los casos O(log2N).
[Link] Localización de un Componente del Árbol en MAUDE.
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.
94
Caso en el que el número de componentes del árbol debe ser una potencia de N.
95
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 9: Valores y Tipos Compuestos.
211
Ejemplo 160
El operador _in?_ verifica si un entero dado se encuentra en el árbol, así:
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.
9.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.
[Link] 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.
Ejemplo 161
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
Capítulo 9: Valores y Tipos Compuestos.
212
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.
[Link] 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.
Ejemplo 162
El operador _in_ inserta en el árbol un entero dado, así:
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.
Ejemplo 163
El operador largo definido en la sección [Link] puede aplicarse tanto a listas de enteros como a listas de
vectores (ver sec. [Link]), así:
Y pueden ser usados incluso en listas cuyos componentes son de diferentes tipos.
Ejemplo 164
El operador largo definido en la sección [Link] puede aplicarse tanto a listas de enteros y vectores (ver
sec. [Link]), 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
operadores polimórficos (ver XXX). 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 tipo96.
Ejemplo 165
96
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 (Vg. 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 10: Programación Paramétrica y Relaciones entre Tipos
220
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-vector (cdr LV) V) ))
(integer? (car LV))
(cons (+ (car LV) V ) (sume-valor-lista-vector (cdr LV) V) ))
)
)
)
Ejemplo 166
La definición de los operadores sum_pi_rec(n) y cont_frac_rec(n), presentados en la sección [Link]
para calcular el valor de las fórmulas siguientes.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
221
∞
1 1
∑ (4n − 3)(4n − 1) y
2
n =1
12 +
3
22 + 2
3 +O
Siguen un patrón típico de los procesos que denominamos de “acumulación”, tratados en la sección 8.5.
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.
Ejemplo 167
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.
Ejemplo 168
Los operadores que calculan las acumulaciones, definidos en el ejemplo anterior, podrían haberse
definido usando una función equivocada, así:
Ejemplo 169
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.
Ejemplo 170
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) ))
)
Ejemplo 171
En [Abelson 85 sección 1.3.4], se ilustra el uso de una expresión lambda en la definición de un
operador que, al ser evocado, da como resultado a un operador que aproxima la derivada de una función,
así:
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.
Ejemplo 172
Aunque una evocación de la función siguiente:
97
La 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 10: Programación Paramétrica y Relaciones entre Tipos
225
En las secciones que siguen se describen la manera de definir módulos paramétricos,
teorías, y vistas, junto con la manera de usarlos para instanciar los módulos paramétricos.
Para una descripción más detallada se remite al lector a [Clavel 2007, sec 6.3].
10.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 [Link].
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, al sort S de 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 con
parámetros diferentes, pero 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 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 las diferentes instancias del
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
226
módulo, se hace necesario poder distinguir este sort para dos instancias del módulo
parametrizado (incluidas en otro módulo). Para distinguir entre estos sorts basta con
cualificar el nombre del sort, dentro del módulo parametrizado, con el nombre de los
parámetros de los que depende en todas las ocurrencias de dicho nombre, y con cualificar el
nombre del sort, dentro de los módulos que incluyen al módulo parametrizado, con el
nombre de las vistas con que se instancian los parámetros. Un sort depende de un
parámetro si se relaciona con un sort de la teoría asociada al parámetro (Vg. 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).
Ejemplo 173
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 es cualificado
con el único parámetro del módulo. Así, de incluirse conjuntos de enteros y de reales, instanciando el
sort parametrizado, el sort de los conjuntos de enteros y de los conjuntos de reales se distinguen por su
cualificación, así:
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.
98
Pueden ser usados al nivel del metalenguaje para llevar a cabo demostraciones de teoremas de forma controlada.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
228
Ejemplo 174
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
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.
Ejemplo 175
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
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 .
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
229
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-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 XXXX) 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> .
op <plantilla-fuente> : <sorts-fuente-dominio> -> <sort-fuente-rango>
to <plantilla-destino> .
op <termino-fuente> to term <termino-destino> .
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.
99
Es 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 10: Programación Paramétrica y Relaciones entre Tipos
231
• <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 lo 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
especificados100.
• 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 destino101. Entre los operadores correspondientes de la
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 del destino, interpretados en la fuente bajo
las proyecciones definidas en la vista, se cumplen en la fuente. La satisfacción de estos
axiomas, sin embargo, no es verificada por el intérprete.
Así como MAUDE ofrece de forma nativa, en el archivo [Link], una serie de
teorías, ofrece también una serie de vistas a dichas teorías.
100
En [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 XXXX). No presenta, sin embargo, ejemplo alguno
de esta circunstancia.
101
De hacerlo se genera un mensaje de advertencia.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
232
Ejemplo 176
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.
10.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.
Ejemplo 177
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} .
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
233
..
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.
Ejemplo 178
Para incluir en un módulo parametrizado a 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
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
inclusor a las correspondientes teorías del módulo incluido. 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
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
234
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.
Ejemplo 179
En el módulo siguiente tomado de [Clavel 2007, sec 6.3.4]102 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.
Ejemplo 180
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.
102
Se toma incompleto para inducir al lector a consultar la referencia.
103
Esta restricción parece ligarse al intérprete, más bien que al leguaje mismo.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
235
fmod PAIR-SET{X :: TOSET, Y :: TRIV} is
protecting PAIR{TOSET, Y} {X} .
…
op _<_ : Pair{TOSET, Y}{X} Pair{TOSET, Y}{X} -> Bool . .
...
endfm
Al incluir el módulo parametrizado PAIR se instanció el primer parámetro con la vista a TOSET, para
luego poder enlazarlo con el primer parámetro del módulo inclusor, mientras que el segundo parámetro
se enlazó directamente al correspondiente en el módulo inclusor. Esta instancia de PAIR, sin embargo,
mezcla una vista a una teoría con un enlace a un parámetro del módulo inclusor, lo que constituye un
instanciamiento incorrecto.
La versión correcta de este módulo es la siguiente.
Ejemplo 181
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í:
.fmod LIST{X::TRIV}
protecting INT .
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
236
sort List{X} .
subsort X$Elt < List{X} .
..
..
endfm
Para obtener una lista de enteros, basta crear un módulo que instancie el módulo paramétrico usando una
vista de los enteros a TRIV, así:
.fmod LIST-INT
.protecting LIST{Int} .
endfm
Donde Int es el nombre de una vista de INT a TRIV.
Ejemplo 182
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
237
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í:
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 errados104 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.
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.
10.4.2 Sobrecarga de operadores en subtipos.
Dada la posibilidad de sobrecargar los operadores (ver [Link].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
66 ). Este tipo de sobrecarga es denominado “sobrecarga ad-hoc” (“ad-hoc overloading”
[Clavel 2007, sec 3.6]).
104
Por 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 10: Programación Paramétrica y Relaciones entre Tipos
238
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 o
“sobrecarga de subsorts” (“subsort overloading” [Clavel 2007, sec 3.6]).
La razón de sobrecargar 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 kind105.
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.
10.4.3 Preregularidad
Bajo la relación de subsort, un término puede pertenecer a varios sorts. Así:
• Una constante pertenece al sort con que fue declarada y a todos los supersorts de
dicho sort.
• Un término de la forma f(t1,t2,...tn), donde ti pertenece al sort si y existe una
declaración para el operador de la forma, f : s1 s2 ... sn -> s, tiene como sort a s
y todos los supersorts de s.
• Un término de la forma f(t1,t2,...tn), donde ti pertenece al sort si y existen varias
declaraciones para el operador de la forma, f : s´1 s´2 ... s´n -> s´, tales que si
sea s´i o subsort de s´i, tiene como sorts los varios s´ junto con sus supersorts.
Nótese que para el último caso, es posible que el conjunto de sorts del término tenga más de
un sort minimal.
Ejemplo 183
El siguiente ejemplo, tomado de [Clavel 2007, sec 3.8] muestra esta situación, así:
sorts A B C D .
105
Pudiendo ser los kind de cada pareja diferentes.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
239
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
satisfecha106.
10.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 [Link] y en 10.3.5.
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.
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.
Ejemplo 184
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:
106
Tal 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 10: Programación Paramétrica y Relaciones entre Tipos
240
...
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).
Ejemplo 185
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.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
241
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.
Ejemplo 186
La declaración del módulo paramétrico que provee la lista ordenada genérica, denominado LIST-STBL,
requiere que los componentes de la lista sean ordenables. Por ello exige a su único parámetro satisfacer
la teoría TOSET, que define un orden total bajo el operador _>_.
El módulo se apoya en el módulo que define la lista genérica presentado en el Ejemplo 181, el módulo
LIST. 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-STBL.
fmod LIST-STD{X::TOSET}
protecting LIST{TOSET}{X} .
sort StdList{X} .
subsort StdList{X} < List{TOSET}{X} .
vars N : X$Elt .
vars SL : StdList{X} .
mb nil : StdList{X} .
cmb (N : SL) : StdList{X} if (SL==nil) or (N <= head(SL)) .
Ejemplo 187
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í:
fmod HET-LIST is
sort List .
op nil : -> List .
op _ _ : Universal List -> List [ctor poly (1)] .
endfm
P Q P ⇒Q ¬P∨Q
V V V V
V F F F
107
Antes de continuar, hay que reconocer una omisión en el presente texto cometida al simbolizar la equivalencia entre
dos fórmulas mediante el símbolo =. Esto se hizo así por simplicidad, pero lo cierto es que existe todo un arsenal de
símbolos que llaman del metalenguaje. Es decir, sirven para expresar aspectos del lenguaje como la equivalencia, o
también que una fórmula es tautología o contradicción. Por ejemplo para denotar equivalencia se utiliza el símbolo ≡, o
para expresar que una fórmula G es una tautología se escribe ╞G. Sin embargo en éste texto no utilizaremos nada de esto
tratando de mantener al mínimo la simbología
Capítulo 11: Formas Normales y Lógica Clausal
249
F V V V
F F V V
P Q P ⇒Q ¬P∨Q (P⇒Q)⇔(¬P∨Q)
V V V V V
V F F F V
F V V V V
F F V V V
Es un error garrafal y muy común pensar que la equivalencia es lo mismo que el símbolo
⇔. Si es cierto que tienen cierta afinidad, pero sola y exclusivamente aquella dada por la
definición del párrafo anterior. No basta que para una interpretación la expresión
⇒Q)⇔
(P⇒ ⇔(¬P∨ ∨Q) sea verdadera para que podamos decir que P⇒ ⇒Q es equivalente a
∨Q.
¬P∨
Será de gran utilidad una provisión adecuada de fórmulas equivalentes, a la hora de
transformar una fórmula cualquiera en una equivalente que tenga la forma que deseamos.
Antes de presentarlas, cabe recordar que la fórmula F, es una que es falsa siempre y V una
que es verdadera siempre. Todas las equivalencias presentadas a continuación pueden ser
demostradas utilizando tablas de verdad. Además de las presentadas, existen una infinidad
de equivalencias, de las que tomamos las más útiles para nuestros propósitos. La tabla
presentada se organiza de la manera presentada en [Grassmman 97].
Equivalencia Nombre común
1 P⇔Q = (P⇒Q)∧(Q⇒P) Definición equivalencia
2 P⇒Q = ¬P∨Q Definición implicación
3 (a)P∨Q = Q∨P (b)P∧Q = Q∧P Leyes Conmutativas
4 (a)P∨(Q∨R) = (P∨Q)∨R (b)P∧(Q∧R) = (P∧Q)∧R Leyes asociativas
5 (a)P∨(Q∧R) = (b)P∧(Q∨R) =
Leyes distributivas
(P∨Q)∧(P∨R) (P∧Q)∨(P∧R)
6 (a)P∨F = P (b)P∧V = P Leyes de identidad
7 (a)P∨V = V (b)P∧F = F Leyes de dominación
8 (a)P∨¬P = V (b)P∧¬P = F Leyes del medio excluido y de
Capítulo 11: Formas Normales y Lógica Clausal
250
contradicción
9 ¬(¬P) = P Ley de doble negación
10 (a)¬(P∨Q) = ¬P∧¬Q (b)¬(P∧Q) = ¬P∨¬Q Ley de De Morgan
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 (P∨Q)∨R,
pueden ser omitidos, es decir, podemos escribir P∨Q∨R. Más en general, podemos
escribir P1∨P2∨...∨Pn sin ambigüedad, donde P1,P2,...,Pn son fórmulas.
P1∨P2∨...∨Pn es verdadero si y solo si, al menos uno de los Pi (1≤i≤n) es
verdadero. De la misma manera, podemos escribir P1∧P2∧...∧Pn que es verdadero
si y solo si todos los Pi (1≤i≤n) son verdaderos.
3. Debido a la conmutatividad de ∨ y de ∧, el orden en que aparecen los Pi en
P1∨P2∨...∨Pn o en P1∧P2∧...∧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.
11.2.2 Formas Normales.
El método de deducción automática de teoremas, hacia el que nos dirigimos, requiere que
las expresiones lógicas estén en forma normal conjuntiva. Esta, no es más que una forma
estándar, de rescribir una expresión lógica que permite generalizar procesos.
Llamaremos literal a un átomo o la negación de un átomo.
Una formula A se dice estar en forma normal conjuntiva (para abreviar FNC) si y solo si,
tiene la forma P1∧P2∧...∧Pn donde cada P1,P2,...,Pn es una disyunción de
literales.
Ejemplo 188
Por ejemplo, si P, Q, y R son átomos, todas las siguientes fórmulas están en forma normal conjuntiva:
(P∨¬Q∨R)∧(¬P∨Q)∧(¬R∨P)
P∧Q
F
En la segunda, P se considera una disyunción de literales con un solo literal, lo mismo que Q .
Para toda fórmula en lógica proposicional, existe una fórmula en FNC, equivalente a ella.
Esto no lo probaremos, pero exhibiremos un proceso que permite encontrar una FNC
equivalente para cualquier fórmula.
Capítulo 11: Formas Normales y Lógica Clausal
251
Basta seguir los siguientes pasos aplicando las equivalencias presentadas en la sección
11.2:
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.
Ejemplo 189
Veamos un par de ejemplos para ilustrar el proceso de normalización:
¬((Q⇒P)∧¬R) XX
= ¬((¬Q∨P)∧¬R) Por def. de ⇒
= (¬(¬Q∨P)∨R) Por ley de De Morgan y doble negación
= ((Q∧¬P)∨R) Por ley de De Morgan y doble negación
= (Q∨R)∧(¬P∨R) Por ley distributiva y conmutativa
(Q∨R)∧(¬P∨R) es una FNC, equivalente a ¬((Q⇒P)∧¬R). Nótese que la segunda derivación del
proceso requiere la ley de doble negación pues ¬((¬Q∨P)∧¬R) = (¬(¬Q∨P)∨¬(¬R)) que por doble
negación es equivalente a (¬(¬Q∨P)∨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∧(Q⇒R))⇒S
= (P∧(¬Q∨R))⇒S Por def. de ⇒
= ¬(P∧(¬Q∨R))∨S Por def. de ⇒
= (¬P∨¬(¬Q∨R))∨S Por ley de De Morgan
= (¬P∨(Q∧¬R))∨S Por ley de De Morgan
= ((¬P∨Q)∧(¬P∨¬R))∨S Por ley distributiva
= ((¬P∨Q)∨S)∧((¬P∨¬R)∨S) Por ley distributiva
= (¬P∨Q∨S)∧(¬P∨¬R∨S) Por ley asociativa.
De donde se puede concluir que (¬P∨Q∨S)∧(¬P∨¬R∨S), es una FNC para (P∧(Q⇒R))⇒S.
Así como existe una FNC, también existe una forma normal disyuntiva que se define así:
Una formula A se dice estar en forma normal disyuntiva si y solo si, tiene la forma
P1∨P2∨...∨Pn donde cada P1,P2,...,Pn es una conjunción 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.
(∀x)A[x]∨(∀x)C[x] ≠ (∀x)(A[x]∨C[x])
(∃x)A[x]∧(∃x)C[x] ≠ (∃x)(A[x]∧C[x])
Ejemplo 190
Por ejemplo, si P, Q, y R son átomos, todas las siguientes fórmulas están en forma normal conjuntiva:
(∀x)A[x]∨(∀x)C[x]
= (∀x)A[x]∨(∀z)C[z] (reemplazando todas las ocurrencias de x en (C[x] por z.)
= (∀x) (∀z) (A[x]∨C[z]) (Por 11(a), donde z no aparece en A[x].)
Donde cada (Qixi), i=1,...,n, es (∀xi) o bien (∃xi), y M es una fbf que no contiene
ningún cuantificador.
Capítulo 11: Formas Normales y Lógica Clausal
254
Ejemplo 191
Por ejemplo, éstas son algunas fbfs en forma normal Prenex:
∀x(H(x)⇒M(x))
∀x∀y(I(s(x,y),s(y,x)))
(∀x)(∀y)(∃z)(P(x,y)⇒R(z))
Lo que sigue son los pasos, generales para llevar cualquier fbf a una forma normal prenex
equivalente usando las formulas de equivalencia presentadas arriba.
1. Eliminar todas las ⇒ y ⇔, utilizando las equivalencias 1 y 2.
2. Si la fórmula resultante contiene cualquier subexpresión compuesta negada, llevar
las negaciones hacia adentro, utilizando las equivalencias 9, 10, 12(a) y 12(b), hasta
que solo aparezcan átomos negados.
3. Llevar los cuantificadores hacia afuera utilizando las equivalencias 11 y 13, y si es
necesario renombrando variables como se ejemplificó en el Ejemplo 190.
Ejemplo 192
Veamos un par de ejemplos para ilustrar el proceso:
(∀x)P(x)⇒(∃x)Q(x)
= ¬(∀x)P(x)∨(∃x)Q(x) Por definición ⇒
= (∃x)(¬P(x))∨(∃x)Q(x) Por eq. 12(a)
= (∃x)(¬P(x)∨Q(x)) Por eq. 13(b)
(∀x)(∀y)((∃z)(P(x,z)∧P(y,z))⇒(∃u)Q(x,y,u))
= (∀x)(∀y)(¬((∃z)(P(x,z)∧P(y,z)))∨(∃u)Q(x,y,u)) Por definición ⇒
= (∀x)(∀y)((∀z)(¬P(x,z)∨¬P(y,z))∨(∃u)Q(x,y,u)) Por eq. 12(b) y 10
= (∀x)(∀y)(∀z)(∃u)(¬P(x,z)∨¬P(y,z)∨Q(x,y,u)) Por eq. 11(a)
Se recomienda verificar especialmente porqué el último paso es posible, revisando la equivalencia 11(a)
y las condiciones para aplicarla.
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 11.2.2.
(¬P∧Q)⇒R
P⇒((Q∧R)⇔S)
(¬P∧(Q⇒P)∨P)∨(¬P⇔(P∧¬Q))
Capítulo 11: Formas Normales y Lógica Clausal
255
¬(P⇒Q)∧(P∨(P∧Q∧R))∧¬S
3. Demostrar las equivalencias 11 a la 14, siguiendo la idea de demostración semántica que
se utilizó para demostrar la equivalencia 12(a).
4. Cuales de las siguientes afirmaciones son ciertas, cuales no y porqué:
(∀x)P(a) = P(a)XX(∀x)P(F(a),x) = P(F(a),x)
(∃x)P(x)∧(∃x)Q(x) = (∃x)(∃y)(P(x)∧Q(y))
(∀x)(P(x)⇒(∃y)R(y))∨(∀x)Q(x,a) =(∀x)(∀y)((P(x)⇒(∃y)R(y))∨Q(y,a))
(∀x)(P(x)⇒(∃y)R(y))∨(∀x)Q(x,a) =(∀x)(∀z)((P(x)⇒(∃y)R(y))∨Q(z,a))
(∀x)(P(x)⇒(∃y)R(y))∧(∀x)Q(x,a) =(∀x)((P(x)⇒(∃y)R(y))∧Q(x,a))
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)))
6. Expresar la fórmula ((P⇒¬Q)∧¬(Q∨P∨R)∧S)∨¬R∨(¬(T⇒P)⇒(¬R∧¬Q)) en forma clausal.
(recuerde que se debe llevar a FNC antes.)
Capítulo 12
Resolución en Lógica Clausal
Capítulo 12: Resolución en Lógica Clausal
258
12.1 Introducción.
La resolución sirve para fundamentar un método para probar que una fórmula en FNC
(Forma normal Conjuntiva) es contradictoria.
La resolución puede ser usada para probar que G es consecuencia lógica de F1,F2, ...,Fn.
En efecto, en el Capítulo 3 vimos que el problema de dicha consecuencia lógica, se puede
reducir al de probar que (F1∧F2∧...∧Fn∧¬G) es una contradicción.
Además puesto que en el Capítulo anterior, se vio que cualquier fórmula puede ser
convertida a otra en FNC equivalente a ella, el principio de resolución puede ser usado para
soportar un método general de demostración desde aserciones en FNC.
(P 1,1 ) ( ) (
∨ P1, 2 ∨ K ∨ P1,n1 ∧ P2,1 ∨ P2, 2 ∨ K ∨ P2,n2 ∧ K ∧ Pk ,1 ∨ Pk , 2 ∨ K ∨ Pk ,nk )
Donde los Pi,j son literales. En el resto de éste módulo, las fórmulas en FNC las
representaremos de la siguiente manera:
{{P 1,1 }{ } {
, P1, 2 ,K, P1,n1 , P2,1 , P2, 2 ,K, P2,n2 ,K, Pk ,1 , Pk , 2 ,K, Pk ,nk }}
{
Donde los Pi,j son literales; A un conjunto de literales P1,1 , P1, 2 ,K, P1,n1 lo llamaremos }
“cláusula” (que representa una disyunción de literales); Y al conjunto de cláusulas lo
llamaremos forma clausal (que representa una conjunción de cláusulas).
Por último para representar la cláusula vacía {} utilizaremos el símbolo .
12.2.2 Resolventes
Sea C1, C2 y R cláusulas. A R se le llama resolvente de C1 y C2 si hay un literal L∈C1, tal
que ¬L∈C2 y R tiene la forma:
Ejemplo 193
Los resolventes de las siguientes parejas de cláusulas:
Capítulo 12: Resolución en Lógica Clausal
259
{Q,¬R,P} y {R,¬P}
{Q,¬R,P} y {R,¬P}
{P} y {¬P}
Son, respectivamente las cláusulas siguientes:
{Q, P, ¬P}
{Q,¬R, R}
Donde el lector debe notar que la misma pareja de cláusulas puede tener dos 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.
• 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.
Capítulo 12: Resolución en Lógica Clausal
260
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}.
12.2.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 , 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 (P∧F=F).
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 probando la contradicción, así que lo que
sigue es demostrar éste hecho que se conoce como completitud del método.
[Link] El teorema de Resolución
Y finalmente:
Res*(G) = U Re s n
(G )
n≥0
Ejemplo 194
Por ejemplo, sea:
G = {{¬A,¬B,¬D},{¬C,A},{C},{B},{¬G,D},{G}}
Entonces
0
Res (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
Capítulo 12: Resolución en Lógica Clausal
261
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}, }.
Al intentar encontrar Res4(G) de forma análoga, nos encontramos con que no se pueden encontrar más
resolventes, de manera que:
4 3
Res (G) = Res (G).
Finalmente, se puede concluir que
3 4 *
Res (G) = Res (G) = ... = Res (G).
Nótese que ∈Res3(G). De manera que Res3(G) es una contradicción, y según el Teorema 12-1 Res3(G)
es equivalente a G, de manera que también G es contradictoria.
f =(∀x1)(∀x2)...(∀xr)M(x1,x2,...,xr)
Donde M es una fbf sin cuantificadores. Como se mencionó también en la sección 11.2.2,
M puede ser llevada a FNC como si fuera una fórmula de la lógica de proposiciones.
( ) ( )
M = P1,1 ( x1 ,K, xr ) ∨ K ∨ P1,n1 ( x1 ,K, xr ) ∧ K ∧ Pk ,1 ( x1 ,K, xr ) ∨ K ∨ Pk ,nk ( x1 ,K, xr )
donde los Pi,j(x1,...,xr), son literales. Es decir, fbfs sin cuantificadores ni conjunciones ni
disyunciones, que dependen posiblemente de alguna de las variables x1,...,xr.
Ahora supongamos un dominio de interpretación D={a,b}, con tan solo 2 elementos. La
equivalencia 14(a) de la sección 11.3.1, muestra como un cuantificador universal se puede
expandir como una conjunción de todos los casos posibles. En este caso f sería equivalente
a:
D={a,b,f(a),f(b),g(a),g(b),f(f(a)),f(f(b)),f(g(a)),f(g(b)),g(f(a)),g(f(b)),g(g(a)),g(g(b)),...}
Al considerar que todos los posibles términos construidos de ésta manera son distintos entre
si, se garantiza que D es el más grande de todos los dominios posibles para la teoría.
Sinteticemos en éste punto para no perder el sentido. Hemos mostrado tres hechos:
1. En la sección 11.3.2 mostramos que cualquier fbf puede llevarse a una forma
normal Prenex, con su matriz en FNC. Es decir, una fbf con todos los
cuantificadores a la izquierda, cuantificado una fórmula sin cuantificadores en FNC.
2. Si en la forma normal Prenex no aparecen cuantificadores existenciales, dado un
dominio específico finito, la totalidad de la fbf puede verse como una FNC en
lógica de proposiciones.
3. Dado un conjunto de fbfs, existe un dominio, en el cual se puede identificar
cualquier otro dominio.
Lo único que queda pendiente son los cuantificadores existenciales en la forma normal
Prenex. El problema se puede solucionar utilizando funciones de Skolem, pero en lo que
sigue simplemente nos olvidaremos del problema ya que en PROLOG no hay
cuantificadores existenciales. Un tratamiento completo se puede encontrar en [Chang 73,
capítulo 4]
Con estas tres ideas en mente, el camino queda listo para sustentar el sentido del método
que presentaremos a continuación.
12.3.2 Notación
Al problema que nos enfrentamos es probar que la fbf G, es consecuencia lógica de un
conjunto de fbfs F1,F2, ...,Fn. Para desarrollar la resolución en la lógica de predicados, así
como lo hicimos al presentar la resolución en la lógica proposicional, transformaremos
nuestro problema a probar que (F1∧F2∧...∧Fn∧¬Q) es contradictoria. Además
supondremos que (F1∧F2∧...∧Fn∧¬Q) está en forma normal Prenex, con matriz en FNC y
108
Jacques Herbrand (1908-1931), matemático francés. Sus trabajos se concentraron en la lógica matemática. Murió a la
edad de 23 años en un accidente. (ver [Link]
Capítulo 12: Resolución en Lógica Clausal
264
que no tienen cuantificadores existenciales. De manera que el método que desarrollaremos
a continuación es un método para probar que:
(( ) ( ))
f = (∀x1 )(∀x2 )K(∀xr ) P1,1 ( x1 ,K, xr ) ∨ K ∨ P1, n1 ( x1 ,K, xr ) ∧ K ∧ Pk ,1 ( x1 ,K, xr ) ∨ K ∨ Pk ,nk ( x1 ,K, xr )
12.3.3 Unificación
En la subsección 12.2.2 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(x)),R(x)}
A primera vista, no hay ningún literal en C1 que sea complementario a uno en C2. Sin
embargo, si sustituimos x por f(a) en C1 y x por a en C2, obtenemos:
C1’: {P(f(a)),Q(f(a))}
C2’: {¬P(f(a)),R(a)}
Que son instancias de C1 y C2 respectivamente (en el universo de Herbrand), 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)}
Aunque de manera más general, si substituimos x por f(x) en C1, obtenemos
C1*: {P(f(x)),Q(f(x))}
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(x)) en C2. Luego, podemos obtener el siguiente
resolvente de C1* y C2
C3: {Q(f(x)),R(x)}
Es claro que existe una infinidad de formas diferentes de instanciar C1 y C2 para encontrar
un resolvente. Sin embargo C3 tiene la característica muy especial de que cualquier otro
Capítulo 12: Resolución en Lógica Clausal
265
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. Consideramos que la presentación formal es muy engorrosa, colmada de
definiciones y nomenclatura complicada que confunde, mientras que la idea intuitiva es
sumamente sencilla y fácil de aplicar para los humanos. Pese a todo esto es necesario 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θ.
• El unificador más general θ de L1 y L2, es un unificador tal que, cualquier otro
unificador de L1 y L2 es una instancia de θ. Es decir, θ es el mgu (most general
unifier) de L1 y L2 sii para todo unificador σ de L1 y L2, existe una sustitución µ tal
que (L1σ)µ=L1θ.
Ejemplo 195
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 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.
Capítulo 12: Resolución en Lógica Clausal
266
• 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í. 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, y
como lo que se persigue es unificar literales de cláusulas diferentes no tendremos éste
problema. Pero cuidado, al interior de una cláusula no se puede hacer tal gracia, el lector no
debe olvidar que el efecto de una sustitución influencia toda la cláusula.
12.3.4 Resolventes
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 12.2.2 para
la lógica proposicional, excepto por la unificación.
Sean C1 y C2 dos cláusulas sin variables en común. Sean L1∈C1 y L2∈C2 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.
Ejemplo 196
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(y)}.
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 , agregando resolventes.
Ejemplo 197
Los siguientes predicados PROLOG tienen constantes como argumentos:
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))
[Link] 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.
Ejemplo 198
Las siguientes cláusulas son hechos en PROLOG:
capital(colombia, bogota) .
humano(socrates) .
sumar(X, 0, X) .
derivada(X, X, 1) .
vertical(linea(punto(X, Y), punto(X,Z))) .
[Link] 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.
Ejemplo 199
Las siguientes cláusulas son reglas en PROLOG:
Mortal(X) :- humano(X) .
sumar(X, s(Y), s(Z)) :- sumar(X, Y, Z).
tia(X,Y) :- hermana(X,Z), padreOmadre(Z,Y) .
Capítulo 13: Lenguajes Lógicos
275
13.3.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 “:-“.
Ejemplo 200
Las siguientes son consultas en PROLOG:
:- mortal(socrates), humano(platon) .
:- vertical(linea(punto(1, 2), punto(1, 3))) .
:- sumar(10, 8, RESPUESTA) .
g(a) .
g(b) .
h(b) .
k(Y)
Y = _01
f(_01), g(_01), h(_01)
Figura 13.1. Primer paso en la deducción para la consulta K(Y).
Para satisfacer ésta nueva meta, el intérprete busca de nuevo un hecho o la cabeza de una
regla que unifique con el primero de los términos de la nueva meta. En éste caso encuentra
que mediante el reemplazo de _01 por a, se puede unificar la meta f(_01),g(_01),h(_01)
con f(a), para obtener una nueva meta, g(a),h(a). Luego, sin necesidad de ningún
reemplazo, el primer término de la meta se puede unificar con el hecho g(a), para obtener la
nueva meta h(a). La representación hasta éste punto se puede observar en la Figura 13.2.
.
Capítulo 13: Lenguajes Lógicos
278
k(Y)
Y = _01
f(_01), g(_01), h(_01)
_01 = a
g(a), h(a)
h(a)
Figura 13.2. Paso intermedio en la deducción para la consulta K(Y).
Pero no hay manera de satisfacer ésta última meta. De manera que el intérprete reconoce
haber cometido un error y verifica si en algún punto existía la posibilidad de unificar de
manera diferente. Esto lo hace regresando un paso en el camino mostrado en la Figura 13.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),g(_01),h(_01) con f(b), para obtener una nueva meta g(b),h(b). Este proceso se
conoce como “backtracking” o retroceso.
Luego, y sin necesidad de ningún reemplazo, el primer término de la meta se puede unificar
con el hecho g(b), para obtener la nueva meta h(b). Y finalmente ésta meta se puede
unificar con el hecho h(b), para obtener la meta vacía o la cláusula vacía en términos del
principio de resolución. En la Figura 13.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
:- 5 is 3+2 .
Mejor aun, se puede colocar una variable como primer argumento del predicado is, por
ejemplo:
:- X is 3+2 .
Capítulo 14: Programación en Lógica de Predicados.
283
Y el intérprete responde X=5. Sin embargo el segundo argumento del predicado is debe
estar completamente instanciado en el momento de ejecutarse. Por ejemplo, ante consultas
como
:- 3+2 is X .
:- 5 is 3+Y .
El intérprete responde con un error de instanciación. Teniendo esto en cuenta, se pueden
usar las reglas para levara a cabo cálculos.
Ejemplo 201
La regla siguiente transforma grados centígrados a Fahrenheit:
c_a_f(C, F) :- F is C * 9 / 5 + 32
Ante la consulta siguiente:
:-c_a_f(37,F) .
El intérprete responde F=99.
Pero, de nuevo, no será posible usar este predicado en sentido contrario. Al plantear la consulta:-
c_a_f(C,99) el intérprete emite un error de instanciación.
Ejemplo 202
Los dos programas siguientes definen el cálculo del factorial usando las estrategias estudiadas en los
lenguajes funcionales, así:
El código siguiente define un proceso recursivo:
%%----------Factorial Recursivo:
factorial(0,1) .
factorial(N, F) :-
N > 0,
N1 is N-1,
factorial(N1, F1),
F is N * F1 .
El código siguiente define un proceso iterativo:
%%----------Factorial Iterativo:
factorial(N,F):- factorial(N,F,1).
factorial(0, F, F) .
factorial(N, F, P):-
NI is N-1,
NP is P*N,
Capítulo 14: Programación en Lógica de Predicados.
284
factorial(NI, F, NP).
Es importante tener en cuenta que el orden de los predicados en el programa debe ser tal que, durante el
proceso, cuando se llegue a una meta con el predicado is, todos las variables del lado derecho del is
deben tener un valor definido. El lector debe seguir de forma cuidadosa e proceso de resolución para
entender éste punto.
14.3 Listas
Como ha sido común ya a través de este curso, analizaremos a continuación el manejo de
listas. En PROLOG así como en los lenguajes funcionales las listas una construcción
sintáctica y no una estructuración en la memoria como en lenguaje C.
Una lista puede ser, o bien el átomo [] que representa la lista vacía o un término complejo
con encabezado y dos argumentos, que representan respectivamente la cabeza y la cola de
la lista. Entonces una lista compuesta por los enteros 1, 2 y 3 se escribiría así:
.(1, .(2, .(3, [])))
Que tendría la forma sintáctica
.
.
1
2 .
3 []
Aunque normalmente se utiliza una notación especial para listas, en la que la lista anterior
se escribiría:
[1, 2, 3]
La notación especial para listas, en el caso de que la cola sea una variable se ejemplifica
mediante:
[X|L] ó [a,[b|L]]
Que representan
.
. a .
X L b L
y
Respectivamente.
Capítulo 14: Programación en Lógica de Predicados.
285
Nótese que esta notación no adiciona ningún nuevo poder al lenguaje; Tan solo lo hace más
legible. Por ejemplo las dos listas ejemplificadas arriba podrían ser igualmente escritas
como
miembro(X,[X| _]) .
miembro(X, [_| L]) :- miembro(X, L) .
Analicemos en primer lugar el sentido declarativo de las sentencias. La primera es un hecho
que dice “un elemento X es miembro de una lista cuya cabeza es X.” La segunda es una
regla que dice “Un elemento X es miembro de una lista si es miembro de la cola de la
lista.” Sin lugar a dudas estas dos afirmaciones tienen sentido, sin embargo, no basta que
tenga sentido declarativo para que sean funcionales en PROLOG. De manera que
analicemos como responde ésta base de conocimientos ante la consulta
:- miembro(a, [a,b,c]) .
De modo inmediato, la consulta unifica con el hecho miembro(X,[X,_]), haciendo X=a, de
manera que el intérprete responde yes. Nótese que ésta consulta también unifica con la
cabeza de la regla miembro(X, [_, L]) :- miembro(X, L), sin embargo, no se debe
olvidar que el intérprete busca de arriba hacia abajo, de manera que en efecto unificará con
el hecho. Ahora supongamos que se le plantee la consulta
:- miembro(c, [a,b,c]) .
Entonces es imposible unificar con el hecho y por tanto se unifica con la cabeza de la regla,
haciendo X=c, y L=[b,c], de manera que se genera una nueva meta miembro(c,[b,c]).
De nuevo es imposible unificar con el hecho, así que el intérprete acude de nuevo a la regla
para obtener una nueva meta miembro(c,[c]), y entonces sí es posible unificar con el
hecho haciendo X=c, y el intérprete responde yes.
Es común que los predicados definidos en PROLOG, se puedan usar de varias maneras, por
ejemplo podríamos también plantear la consulta
:- miembro(X, [a,b,c]) .
Capítulo 14: Programación en Lógica de Predicados.
286
Que corresponde a la pregunta “¿Quien es miembro de la lista [a,b,c]?”. El
comportamiento del intérprete sería:
X=a ;
X=b ;
X=c ;
no
Lo que sucede en éste caso es que el intérprete unifica la meta miembro(X,[a,b,c]) con
el hecho miembro(X,[X,_]), para obtener una respuesta afirmativa a la consulta mediante
el reemplazo X=a. Luego, al forzar el backtracking, en intérprete unifica con la cabeza de la
regla, y luego de nuevo con el hecho, para obtener X=b. En la Figura 14.1, se puede apreciar
el árbol de búsqueda completo.
Miembro(X,[a,b,c])
X = a X = _01
Miembro(_01,[b,c])
_01 = b _01 = _02
Miembro(_02,[c])
_02 = c _02 = _03
Miembro(_03,[])
Figura 14.1. Árbol de búsqueda para la consulta miembro(X,[a,b,c]) y subsecuentes
retrocesos
miembro(X,[X, _]) .
miembro(X, [_, L]) :- miembro(X, L) .
Ahora debe ser claro que el hecho no es más que el caso extremo encargado de detener la
recursión que genera la regla. El valor de PROLOG yace en parte sobre la flexibilidad de
sus consultas que permiten utilizar un predicado en diferentes sentidos, como en éste
ejemplo que permite no solo verificar si un elemento es miembro de una lista, sino que
también se puede usar para listar uno a uno los elementos de una lista. El siguiente ejemplo
explota aun más las posibilidades de las consultas.
Capítulo 14: Programación en Lógica de Predicados.
287
14.3.2 Ejemplo: concatenar
El predicado concatenar, recibe tres listas como argumento, y se satisface si la tercera es
el resultado de pegar las dos primeras una seguida de la otra. En PROLOG bastan las
siguientes sentencias para proveer al predicado concatenar con dicha funcionalidad:
concatenar([], L, L) .
concatenar([H| T], L1, [H| L2]) :- concatenar(T, L1, L2) .
Que se leen en su orden: “La lista vacía concatenada a una lista es la misma lista”; Y “al
concatenar una lista no vacía cuya cabeza en H y cuya cola es T con la lista L1, obtenemos
una lista cuya cabeza es la misma H y cuya cola es L2, si L2 es el resultado de concatenar T
con L1.” Es de nuevo notable lo claro del sentido declarativo de las sentencias. Y será una
constante en PROLOG encontrar programas que evidencian su funcionalidad entre sus
líneas, lo cual no debe sorprender por sus cimientos que se apoyan sobre la lógica. Sin
embargo, al escribir los programas no se puede olvidar el proceso de derivación, para lograr
la funcionalidad deseada. En otras palabras, la riqueza expresiva de un programa PROLOG
depende del programador, que logre soldar lo declarativo con lo funcional.
Veamos ahora el proceso que se genera al plantear la consulta:
:- concatenar([1, 2, 3], [4, 5, 6], L) .
concatenar([1,2,3], [4,5,6], L)
L = [1|_01]
concatenar([2,3], [4,5,6], _01)
_01 = [2|_02]
concatenar([3], [4,5,6], _02)
_02 = [3|_03]
concatenar([], [4,5,6], _03)
_03 = [4,5,6]
L1 = []
L2 = [1, b, 2, d] ;
L1 = [1]
L2 = [b, 2, d] ;
L1 = [1, b]
L2 = [2, d] ;
L1 = [1, b, 2]
L2 = [d] ;
L1 = [1, b, 2, d]
L2 = [] ;
no
La búsqueda es ilustrada en la Figura 14.3, de donde se puede deducir la instanciación
presentada atrás.
Lo que sigue es explotar las posibilidades que nos ofrece el predicado concatenar, para
declarar nuevos predicados. Por ejemplo, definamos un Predicado prefijo, que reciba dos
listas como argumentos, y que se satisfaga cuando la primera de las listas sea un prefijo de
la segunda, por ejemplo, [], [1], [1,2,3] y [1,2,3,4,5] son prefijos de [1,2,3,4,5].
Basta la siguiente sentencia para definirlo:
Capítulo 14: Programación en Lógica de Predicados.
289
prefijo(P, L) :- concatenar(P, _, L) .
Igualmente podríamos definir otro predicado sufijo análogo de la siguiente manera:
sufijo(S, L) :- concatenar(_, S, L) .
Finalmente, y a lo que queríamos llegar, definamos un predicado sublista que reciba dos
listas como argumentos y se satisfaga cuando la primera sea sublista de la segunda. Por
ejemplo, [], [1,2], [3,4,5], [2,3,4,5] son sublistas de [1,2,3,4,5]. La siguiente línea
define la funcionalidad del predicado sublista.
:- invertir(R, [1,2,3]) .
El intérprete responde R=[3,2,1], como se ilustra en la Figura 14.5. Se recomienda que el
lector reconstruya la variable R a partir del árbol de búsqueda para comprobar la veracidad
de la afirmación. La manera natural de utilizar invertir se puede apreciar en la Figura
14.4.
Cabe anotar en éste punto, que el orden de la segunda y tercera sentencias tienen gran
importancia pues si se cambiara su orden, la consulta
:- invertir(R, [1,2,3]) .
Llevaría al intérprete a un ciclo infinito. Se recomienda intentarlo. Esto además es un efecto
típico de PROLOG, que debido al orden estricto de su búsqueda, la influencia en el
Capítulo 14: Programación en Lógica de Predicados.
290
comportamiento de un predicado debido al orden de las sentencias que lo definen es
crucial.
invertir([1,2,3],R)
R = _01
invertirAcc([1,2,3],[],_01)
_01 = _02
invertirAcc([2,3],[1],_02)
_02 = _03
invertirAcc([3],[2,1],_03)
_03 = _04
invertirAcc([],[3,2,1],_04)
_04 = [3,2,1]
invertir(R,[1,2,3])
R = _01
invertirAcc(_01,[],[1,2,3])
_01 = [_02|_03]
invertirAcc(_03,[_02],[1,2,3])
_03 = [_04|_05]
invertirAcc(_05,[_04|[_02]],[1,2,3])
_05 = [_06|_07]
invertirAcc(_07,[_06|[_04|[_02]]],[1,2,3])
_07 = []
_06 = 1
_04 = 2
_02 = 3
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) .
Por último definiremos un predicado quicksort, que toma dos listas como argumentos y
se satisface cuando la segunda es igual a la primera organizada de menor a mayor. Para esto
debemos introducir, los predicado =< y > que vienen con PROLOG. Estos se pueden usar
con notación infija, por ejemplo es válido escribir 3>2, que es lo mismo que >(3,2). El
problema con estos predicados (y con todos los predicados numéricos en PROLOG) es que
sus argumentos deben ser números al momento ejecución. Por ejemplo, la consulta
:- 3>X .
Arroja un error de instanciamiento. Mientras que dada la regla
A(X,Y) :- X>Y
Ante la consulta A(3,2) el intérprete responde yes.
Para quien no recuerda la filosofía del algoritmo quicksort, la idea es que la lista
desorganizada se divide en dos, cada una de las listas resultantes se ordena utilizando el
mismo algoritmo y luego se hace lo que se conoce como un “merge”. El merge consiste en,
dadas dos listas ordenadas, fusionarlas en una lista también ordenada. Empecemos entonces
por construir un predicado merge, que tome tres listas como argumento, de manera que se
satisfaga cuando la tercera lista sea el resultado de fusionar las dos primeras de manera
ordenada.
merge(A, [], A) .
merge([], B, B) .
merge([A|Ta], [B|Tb], [A|M])= :- A=<B, merge(Ta, [B|Tb], M) .
merge([A|Ta], [B|Tb], [B|M])= :- A>B, merge([A|Ta], Tb, M) .
Nótese que la meta A=<B (o A>B) tan solo aparecerá con argumentos concretos. Lo que
sigue es construir un predicado partir, que nos sirva para dividir en dos la lista
desorganizada. Hay muchas maneras de partir una lista, la que escogimos, consiste en
tomar elementos de la lista intercalados. Es decir, la lista [7,3,9,10,4,1,2] quedará
partida en [7,9,4,2] y [3,10,1]. El primer argumento será la lista completa, y los otros
dos serán las dos listas resultantes al partir la lista original.
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) .
Capítulo 14: Programación en Lógica de Predicados.
293
14.4 Ejemplo extra, acertijo de la zebra
El siguiente ejemplo 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.
¿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.
Capítulo 14: Programación en Lógica de Predicados.
294
:- 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.
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
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.
[Link] Atributos
El segundo componente de la plantilla de las clases del diagrama UML es la lista de los
“atributos” de la clase.
Los atributos definen las propiedades estáticas de los objetos y determinan la respuesta a las
operaciones de consulta u observación. Los atributos se asocian a rótulos o nombres
específicos en el objeto, y toman sus valores dentro de un dominio (o tipo de datos)
específico. El tener ciertos atributos específicos es propio de todos los objetos de la clase y
el tener ciertos valores específicos para cada uno de los atributos, es propio de cada objeto.
Un atributo puede tomar una sólo valor dentro de su dominio, en cuyo caso es mono-
valuado, o puede tomar varios valores dentro de su dominio, en cuyo caso es multi-valuado.
El valor de sus atributos determina el “estado” del objeto. Nótese que aunque dos objetos,
tengan el mismo valor para sus atributos, es decir se hallen el mismo “estado”; no son el
mismo objeto109.
109
Para distinguirlos se asume que ellos tienen un valor único de identificación o “Object Identification” (OID). El OID
puede estar asociado a algunos de los atributos (constantes) del objeto, como en el caso de las claves de las entidades en
una Base de Datos, o puede estar asociado al lugar particular de la memoria que ocupa durante la ejecución de un
programa.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
302
Los atributos se pueden clasificar en básicos, y derivados, así también como en escalares y
“objeto valuados”. Al ser creado los atributos básicos toman un valor inicial predefinido.
Los valores de los atributos básicos van cambiando a medida que le ocurren eventos al
objeto. Los valores de los atributos derivados son calculados a partir del valor de los
atributos básicos.
En la plantilla de la clase del diagrama de clases UML, se especifican las siguientes
propiedades para cada atributo:
• Nombre
• Tipo
• Valor inicial
• Número mínimo y máximo de valores posibles
• Si es básico o derivado.
[Link] Operaciones.
El tercer componente de la plantilla de las clases del diagrama de clases UML, es la lista de
los “métodos” de la clase.
Los métodos son operaciones que se lleva a cabo con base en los atributos de los objetos de
la clase. Ellas pueden constituir tanto consultas, como eventos. Cuando el software se
codifica en lenguajes procedurales, las operaciones se proyectan en la construcción a
“funciones”, o “subprográmas”.
En el diagrama de clase se presenta para cada uno de los métodos su “perfil”. El perfil de
un método consta de los siguientes elementos:
• Nombre
• Tipo del valor de retorno si es una función.
• Lista de argumentos con el tipo de cada argumento.
15.2.2 Relaciones entre Objetos.
Las relaciones entre los objetos son inherentes a su naturaleza y determinan sus modos de
colaboración. Todos los objetos de una clase tienen el mismo tipo de relaciones, por lo que
en el diagrama de clases las relaciones entre los objetos se representan como relaciones
entre sus correspondientes clases.
A continuación se describen los tipos de relación que se representan en el diagrama de
clases.
110
Se diferencian de los operadores “sobrecargados” en que estos pueden llevar a cabo una tarea diferente
(semánticamente hablando) y tener un perfil diferente, en cuanto tipo y número de argumentos.
111
Un ejemplo clásico de este tipo de proceso abstracto es el de concebir un dibujo como un compuesto de instancias de la
clase abstracta “figura”. Una acción sobre el dibujo puede ser la de “inflarlo”, que se lleva a cabo inflando cada una de
sus figuras. Para ello “inflar” del dibujo, evoca el método “inflar” de la clase abstracta “figura” sobre cada una de sus
figuras. El método que se ejecuta en cada figura debe, sin embargo, ser el suyo propio y no el de la clase abstracta (que
posiblemente no tiene cuerpo). Para ello basta con que el método “inflar” de las figuras sea polimórfico.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
305
15.2.3 Ejemplo de un Diagrama de Clases.
Para ejemplificar el diagrama de clases y los complementos lógicos, usaremos un ejemplo
basado en el presentado en [Coad 97, pg. 97], que denominaremos “La bodega de
Wally”112.
[Link] Descripción del área de aplicación.
El área de aplicación del software es la de una bodega de almacenamiento de diferentes
artículos, que recibe órdenes de compra y las despacha en uno o varios envíos, así:
Ejemplo 203
La bodega está conformada por islas que son rectángulos rodeados por pasillos de acceso. Las islas
poseen estanterías donde se hallan los cajones con los artículos del almacén. Los cajones se identifican
con dos números separados por un guión, el primero corresponde a la isla y el segundo el cajón.
Los artículos son recibidos en el muelle de recepción con la correspondiente orden de compra, en la cual
los artículos son chequeados con relación a la orden. Luego se toma una estiba en la que se colocan los
artículos, y se busca por la bodega, si existe un cajón con el mismo artículo o vacío para ser colocar los
artículos en esté cajón.
Las órdenes de venta se despachan tomado los diferentes artículos de la orden en el carro de despacho,
luego los artículos se colocan en cajas y bolsas que se llevan al camión para que sean llevados al sitio
del pedido. En razón del tamaño de las órdenes y la disponibilidad de espacio en el carro, las órdenes de
venta se surten con uno o varios despachos.
Es necesario conocer los inventarios en todo momento para volverlos a reponer, esto se realiza mirando
el comportamiento de las órdenes o contando manualmente los artículos de los cajones, normalmente se
cuenta una isla por día.
Es también necesario llevar un control estricto sobre el despacho de las órdenes para garantizar que estas
se satisfacen completamente, sin que se despache mercancía no solicitada o en un número mayor al
solicitado.
Ejemplo 204
En breve, el diagrama de clase describe los elementos del área de aplicación de la manera siguiente:
• La estructura de composición de las unidades de almacenamiento físico de la bodega, en cuanto
la clase Isla agrega la clase Cajon, cada una con su número respectivo de identificación.
• El hecho de que en un cajón hay cierta cantidad de un artículo es descrito por la clase relación
CajonLineaArticulo, que relaciona el cajon con el artículo registrando el número de unidades
del artículo que hay en el cajón.
112
Bajo la simplificación presentada en [Ramirez 99].
Capítulo 15: Complementos Lógicos al Diagrama de Clases
306
• Las ordenes de compra y sus respectivos despachos son descritas por la clases Orden y
ListaTomar, cuyas componentes son sus respectivas líneas OrdenLineaArticulo y
TomarListaArticulo que las relacionan con los artículos registrando las cantidades de artículos
pedidos y despachados respectivamente.
Organizacion Comprador
nombre numero
Direccion
Isla
numero
encuentreCanjonDeEstiba( )
rataDeproximaIslaParaContar( ) Orden ListaTomar
numero fecha
direccionEnvio
fechaEnvio remuevaArticulosdelCajon( )
construirListaTomar( )
Cajon
nombre
1+
estaVacio( ) OrdenLineaArticulo 1+
adicionArticulo( ) CantidadOrdenada TomarListaLineaArticulo
estaDisponibleParaEstiba( ) cantidadNesecitada
ConstruirTomarListaLineaArticulo( ) cantidadTomada
remuevaArticulodelCajon( )
confirmarCantidadTomar( )
Articulo
CajonLineaArticulo
numero
Cantidad descripcion
upc
estaCanditadDisponible( )
decrementeCantidad( ) encontrarCajonParaCantidad( )
muevaParaCajon( ) descargarDesdeEstibaACajon( )
construirListaTomarLineaArticulo( )
cuantohayDisponible( )
113
Tomada de [Ramirez 99]
Capítulo 15: Complementos Lógicos al Diagrama de Clases
307
Flujo de Información
Modelo de la Empresa
Construcción
114
Tal como refiere [Ramirez 99], en la parte superior tenemos el modelo resultante del análisis, y en las partes inferiores
encontramos sucesivamente los modelos resultantes del diseño y de la construcción. El ancho de la pirámide representa la
extensión y complejidad del modelo. Como observamos en la pirámide, el modelo del análisis es el de menor grado de
detalle. A medida que descendemos en la pirámide los modelos tendrán un mayor grado de detalle. Los modelos del
diseño y construcción deben, en efecto, especificar en detalle los elementos propios de la ejecución del software en la
plataforma de implantación. La altura de la pirámide es la que intuitivamente nos muestra la complejidad del desarrollo, a
mayor altura mayor incremento en las capas de detalles. La base corresponderá a la cantidad de líneas de código que
tenga el programa En cada una de las caras estarán los diferentes tipos de modelos prescritos por la metodología que se
use.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
308
Es preferible que todos los elementos del dominio problema queden especificados en los
modelos de la parte superior de la pirámide, o sea en el análisis, donde serán más obvios y
no serán oscurecidos por los detalles de la implementación, que serán introducidos en las
fases de diseño y construcción. El flujo ideal de la información proveniente del problema
es, entonces, el que se muestra en la figura siguiente.
Flujo de
Información
Modelo de
Área de la
aplicación
Análisis del
Diseño del
Constru
115
Si bien esta restricción es obvia, y poco probable que se pase por alto en la programación, es claro que constituye
información relevante del área de la aplicación que no se consigna en el análisis.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
310
15.4 Complementación del Diagrama de Clases.
En esta sección se muestra como complementando el diagrama de clases con
especificaciones en un formalismo lógico, se pueden agregar las restricciones faltantes.
No siendo el tema de este Capítulo el estudio de los lenguajes propuestos para este
propósito, presentaremos las restricciones en un formalismo lógico clásico. Así, usaremos
fórmulas del álgebra elemental para expresar los términos que sean necesarios en las
restricciones, y la notación clásica de conjuntos, en particular la notación de construcción
de conjuntos116, para representar selecciones sobre conjuntos.
15.4.1 Mejoras al Diagrama de Clases para el Ejemplo.
Para incluir las derivaciones y restricciones faltantes, se hicieron modificaciones al
diagrama de clases del ejemplo, que ahora luce de la manera que sigue:
Ejemplo 205
El diagrama de clases del Ejemplo 204 modificado para incluir los elementos faltantes es el siguiente
[Ramirez 99]:
Clases del Sistema
TomarArticulos TomarLista
TomarListaLineaArticulo
/ cantidadTomar fecha
/ cantidadDespachada
1+ 1+
remuevaArticulodelCajon( ) remuevaArticulosdelCajon( ) 1+
confirmarCantidadTomar( ) 1+
OrdenLineaArticulo Orden
/ CantidadOrdenada numero
/ CantidadTotalDespachada direccionEnvio
/ Cantidad PorDespachar 1+
fechaEnvio
ConstruirTomarListaLineaArticulo( ) construirListaTomar( )
1+ 1+
CajonEnIsla
cantidad ArticuloEnCajon
cantidad
estaCanditadDisponible( )
decrementeCantidad( )
muevaParaCajon( )
______________________________________________________________________________________________________________________________
1+ Comprador
Isla Cajon Articulo numero
numero nombre / numero Direccion
descripcion
encuentreCanjonDeEstiba( ) estaVacio( ) upc
rataDeproximaIslaParaContar( ) adicionArticulo( )
estaDisponibleParaEstiba( ) encontrarCajonParaCantidad( )
descargarDesdeEstibaACajon( )
Organizacion persona
Clases basicas construirListaTomarLineaArticulo( )
cuantohayDisponible( ) nombre nombre
cedula
En breve, los cambios sobre el diagrama anterior son los que siguen:
116
El conjunto de las X pertenecientes a algún conjunto C que cumplen la condición Φ(..)
es denotado por { xεC | Φ(x)}, ver [Link]
Capítulo 15: Complementos Lógicos al Diagrama de Clases
311
• Siguiendo los alineamientos en [Ramirez 99], las clases se agruparon por tipo colocando en la
parte inferior las clases que representan los elementos físicos del área de aplicación (Isla,
Cajon, Articulo, Comprador), encima de ellas las clases que las relacionan (CajonEn Isla,
ArticuloEnCajon117), y en la parte superior las clases que representan los eventos que afectan
118
las de más abajo (Orden, TomarLista , etc...).
• La relación entre la clase que representa la orden de compra (Orden) y la que representa los
despachos (TomarLista), es ahora de uno a muchos para representar el hecho de que una
orden puede entregarse en varios despachos.
• Se incluyó una relación de uno a muchos entre la clase que representa las líneas de la orden
(OrdenLineaArticulo) y la clase que representa las líneas del despacho
(TomarListaLineaArticulo), para representar que toda línea en un despacho debe
corresponder a una línea de la orden.
• Se incluyó la clase TomarArtículos con una relación de uno a muchos a la clase que representa
las líneas del despacho (TomarListaLineaArticulo) para representar el hecho de que es posible
tomar artículos de más de una caja para efectuar un despacho.
• Se incluyeron una serie de atributos derivados para representar los diferentes totales de artículos
en los despachos y la relación de estos con las cantidades ordenadas. Así:
TomarListaLineaArtí[Link] es la cantidad despachada de un artículo,
que acumula los artículos tomados de varias cajas;
[Link] es la cantidad total despachada en varios
despachos para un artículo de la orden, etc...
15.4.2 Derivaciones.
Las derivaciones son restricciones simples que usan el predicado de igualdad para asociar
un atributo con el término que calcula su valor. Este término debe utilizar como operandos
los nombres de los atributos de los que dependa el atributo derivado. Estos atributos deben
residir ya sea en la misma clase en la que está el atributo derivado, o en clases que puedan
alcanzarse desde la misma por caminos de asociación.
[Link] Notación
Las derivaciones se plantean con base en la construcción siguiente:
<referencia_a_atributo> = <termino>
Donde:
• <referencia_a_atributo> Es una referencia al valor de un atributo.
• <termino> Es un término basado en constantes y valores de otros atributos.
Las referencias al valor de un atributo se llevan a cabo desde la óptica del objeto que lo
posee bajo la notación siguiente:
<objetos> . <nombre_de_atributo>
117
CajonLineaArticulo en el diagrama anterior.
118
ListaTomar en el diagrama anterior.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
312
Donde:
• <objetos> Es una referencia a uno o a varios objetos.
• <nombre_atributo> Es el nombre del atributo a cuyo valor se desea referir.
Dado que una clase describe las propiedades de un conjunto de objetos, el nombre de la
clase será usado para referirse al conjunto formado por las instancias de la clase.
Ejemplo 206
El nombre siguiente:
OrdenLineaArticulo
Hace referencia a todas las líneas de todas las órdenes del problema tratado en el Ejemplo 205.
Ejemplo 207
El predicado siguiente:
[Link] = 0
Es una simplificación de.
∀(xεOrdenLineaArticulo) ([Link] = 0)
Ejemplo 208
Los predicados de igualdad siguientes definen el valor del atributo derivado
CantidadTotalDespachada referida en el ejemplo anterior:
[Link] = 0
if([Link] == ø)
[Link] =
Σ(xεOrdenLineaArticulo. TomarListaLineaArticulo) [Link]
if([Link] =/= ø)
119
El uso del conjunto como tal, en operaciones sobre conjuntos será implícito en el sentido del operador. Esto implica
que se evitarán sobrecargar los operadores sobre elementos aplicándolos a conjuntos.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
313
Donde el lector debe notar que [Link] refiere al conjunto de
objetos de la clase TomarListaLineaArticulo que esta asociado con el objeto en referencia de la clase
OrdenLineaArticulo , y que el símbolo ø representa el conjunto vacío.
15.4.3 Restricciones.
Las restricciones que restringen los valores de los atributos de forma absoluta, o en relación
con los valores de otros atributos diferentes, usan predicados de diversa índole. Los
argumentos de estos predicados serán términos que al igual que los términos de las
derivaciones utilizarán como operandos los nombres de los atributos que participan en la
restricción.
Al igual que para las derivaciones, situaremos las restricciones en una clase determinada
para representar que ella se aplica a todos los objetos de dicha clase. En consecuencia los
atributos que participan en la restricción deben residir ya sea en la clase en que se localiza
la restricción, o en clases que puedan alcanzarse desde ella por caminos de asociación.
[Link] Notación
Las restricciones se plantearán, en general, por medio de predicados bajo notación infija o
bajo notación prefija, así:
P(<lista_de_terminos>)
(<termino>)P(<termino>)
Donde:
• <lista_de_terminos> Es una lista de <termino> separados por “,”.
• <termino> Es un término basado en constantes y valores de otros atributos.
Al igual que para el caso de las derivaciones, cuando un predicado Φ(x) se aplica a todos
los elementos de un dominio C, es decir:
∀(xεC) Φ(x)
Usaremos la notación simplificada siguiente:
Φ(C)
[Link] Restricciones en el Ejemplo
Las restricciones más elementales afectan sólo el valor de un atributo.
Ejemplo 209
Una restricción obvia, es que el número de artículos ordenados sea positivo:
[Link] >= 1
Los atributos derivados simplifican la mayoría de las restricciones que aparecen explícitas
en la el área de la aplicación.
Ejemplo 210
Capítulo 15: Complementos Lógicos al Diagrama de Clases
314
Es una regla clara del problema no despachar más de lo ordenado, así:
Ejemplo 211
La asociación obligatoria de la clase TomarLista a la clase Orden, obliga a que todos los despachos se
refieran a una orden. La asociación obligatoria de la clase TomarListaLineaArticulo a la clase
OrdenLinea Articulo, por su parte, obliga a que todas las líneas de los despachos se refieran a una línea
de las órdenes.
Sin embargo, si una línea de un despacho corresponde a una línea de una orden, es de esperarse que el
despacho al que pertenece la línea de despacho se refiera a la misma orden que contiene la línea de la
orden a la que corresponde la línea del despacho. Esta restricción puede expresarse de la forma
siguiente:
[Link] ==
[Link]
El lector debe notar que esta restricción se presenta desde la óptica de una línea en un despacho, y por lo
tanto debe colocarse en dicha clase.
Ejemplo 212
Las líneas de las órdenes se asocian con un artículo que representa el artículo ordenado en dicha línea.
Las líneas de los despachos que corresponden a una línea de orden específica, por otro lado, deben
asociarse con tomas de cajas que contienen el mismo artículo.
La restricción siguiente garantiza esta circunstancia:
[Link] ==
[Link]. [Link]
Que corresponde a una restricción de la clase TomarArticulos.