0% encontró este documento útil (0 votos)
180 vistas332 páginas

Lógica Ecuacional en Desarrollo de Software

Cargado por

dbalzate
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
180 vistas332 páginas

Lógica Ecuacional en Desarrollo de Software

Cargado por

dbalzate
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Aplicaciones de la Lógica al Desarrollo del

Software.
Volumen I: Lógica ecuacional y
lenguajes funcionales.

.op cont-frac : int -> float .


.op cont-frac-rec : int int ->
float .

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) .
.....

Fernando Arango Isaza


Julio de 2008
Datos de catalogación bibliográfica

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

TABLA DE CONTENIDO ...............................................................................................................................I

PREFACIO A LA ENTREGA COMO TRABAJO DE AÑO SÁBATICO ............................................ VII

AGRADECIMIENTOS ................................................................................................................................. IX

CAPÍTULO 1.................................................................................................................................................... 1

EVOLUCIÓN DE LOS LENGUAJES DE PROGRAMACIÓN ................................................................. 1


1.1 INTRODUCCIÓN. ............................................................................................................................... 2
1.2 LA MÁQUINA DE ESTADOS............................................................................................................... 3
1.2.1 Vista Interna del Computador .................................................................................................... 3
1.2.2 El Proceso Computacional......................................................................................................... 5
1.2.3 La Máquina de Estados. ............................................................................................................. 5
1.3 LOS PROGRAMAS DEL COMPUTADOR............................................................................................... 6
1.4 LENGUAJES DE PROGRAMACIÓN ...................................................................................................... 8
1.5 EVOLUCIÓN DE LOS LENGUAJES DE PROGRAMACIÓN. ...................................................................... 9
1.5.1 Primera Generación: Lenguaje de Máquina............................................................................. 9
1.5.2 Segunda Generación: Lenguaje Ensamblador. ....................................................................... 10
1.5.3 Tercera Generación: Lenguajes Procedurales........................................................................ 11
1.5.4 Cuarta Generación: SQL y Lenguajes Declarativos. .............................................................. 14
1.5.5 Quinta Generación: Uso de la Lógica..................................................................................... 15
CAPÍTULO 2.................................................................................................................................................. 17

EVOLUCIÓN DE LOS PARADIGMA ARQUITECTÓNICOS ............................................................... 17


2.1 INTRODUCCIÓN .............................................................................................................................. 18
2.2 PARADIGMA DE INSTRUCCIONES.................................................................................................... 19
2.3 PARADIGMA DE FUNCIONES (O PROCESOS). ................................................................................... 19
2.3.1 Función..................................................................................................................................... 19
2.3.2 Arquitectura.............................................................................................................................. 21
2.3.3 Proceso de desarrollo............................................................................................................... 25
2.4 PARADIGMA DE ENTIDADES (O ESTRUCTURACIÓN DE DATOS). ..................................................... 26
2.4.1 Estructuración de los datos en archivos................................................................................... 26
2.4.2 Problemas con los Archivos y Aparición de los Gestores de Bases de Datos. ......................... 27
2.4.3 Modelo Relacional.................................................................................................................... 28
2.4.4 Arquitectura Relacional. .......................................................................................................... 28
2.5 PARADIGMA DE OBJETOS. .............................................................................................................. 31
2.5.1 Concepto Clásico de Objeto. .................................................................................................... 31
2.5.2 Relaciones entre objetos........................................................................................................... 32
2.5.3 Aplicaciones como Sistemas Dinámicos................................................................................... 33
2.5.4 Arquitectura Objetual............................................................................................................... 34
2.6 PARADIGMA DE AGENTES. ............................................................................................................. 36
CAPÍTULO 3.................................................................................................................................................. 37

ELEMENTOS DE LÓGICA MATEMÁTICA............................................................................................ 37


3.1 INTRODUCCIÓN .............................................................................................................................. 38
3.2 LA LÓGICA COMO UNA DISCIPLINA DE RAZONAMIENTO. .............................................................. 38
3.2.1 Lógica en la antigua Grecia. .................................................................................................... 39
3.2.2 La lógica en la matemática moderna. ...................................................................................... 40
Tabla de Contenido. ii
3.3 LA LÓGICA COMO UNA DISCIPLINA DE LOS LENGUAJES. ............................................................... 41
3.3.1 Sintaxis. .................................................................................................................................... 41
3.3.2 Semántica. ................................................................................................................................ 42
3.4 LÓGICA BÁSICA DE PROPOSICIONES. .............................................................................................. 42
3.4.1 Proposiciones. .......................................................................................................................... 43
3.4.2 Sintaxis. .................................................................................................................................... 43
3.4.3 Semántica. ................................................................................................................................ 46
3.5 LÓGICA DE PREDICADOS ................................................................................................................ 52
3.5.1 Predicados................................................................................................................................ 53
3.5.2 Sintaxis. .................................................................................................................................... 53
3.5.3 Semántica. ................................................................................................................................ 55
3.6 CONSECUENCIA LÓGICA. .........................................................¡ERROR! MARCADOR NO DEFINIDO.
3.7 TIPOS DE PREDICADOS Y LÓGICAS ASOCIADAS. .............................................................................. 56
3.8 EJERCICIOS PROPUESTOS. .............................................................................................................. 57
CAPÍTULO 4.................................................................................................................................................. 61

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

SISTEMA DE REESCRITURA DE TÉRMINOS (SRT) ........................................................................... 79


5.1 INTRODUCCIÓN. ............................................................................................................................. 80
5.2 ALCANCE DE LOS SRTS.................................................................................................................. 80
5.3 ESPECIFICACIÓN EN LÓGICA ECUACIONAL MULTISORT................................................................. 83
5.3.1 Signatura Multisort................................................................................................................... 83
5.3.2 Términos. .................................................................................................................................. 84
5.3.3 Ecuaciones................................................................................................................................ 86
5.4 SRT ............................................................................................................................................... 87
5.4.1 Substitución, Particularización y Emparejamiento. ................................................................. 87
5.4.2 Ocurrencias y Reemplazos. ...................................................................................................... 89
5.4.3 Relaciones de Reescritura entre términos de la especificación................................................ 90
5.4.4 Propiedad de Church-Roser: Confluencia y Terminancia. ..................................................... 91
5.5 SEMÁNTICA POR CLASES DE EQUIVALENCIA.................................................................................. 92
5.6 RESUMEN DEL CAPÍTULO................................................................................................................ 92
5.7 EJERCICIOS PROPUESTOS................................................................................................................ 93
CAPÍTULO 6.................................................................................................................................................. 95

ELEMENTOS BÁSICOS DE LOS LENGUAJES FUNCIONALES. ....................................................... 95


6.1 INTRODUCCIÓN .............................................................................................................................. 96
6.2 ORIGEN Y ENFOQUE GENERAL DE LOS LENGUAJES SELECCIONADOS.............................................. 96
6.2.1 LISP .......................................................................................................................................... 96
6.2.2 MAUDE .................................................................................................................................... 97
Tabla de Contenido. iii
6.3 IMPLEMENTACIONES E INTERFACES. .............................................................................................. 98
6.3.1 Interfaz del programa............................................................................................................... 98
6.3.2 Interfaz del intérprete. ............................................................................................................ 100
6.3.3 Implementaciones de los lenguajes seleccionados. ................................................................ 100
6.4 OPERADORES NATIVOS Y EXPRESIONES SIMPLES. ........................................................................ 103
6.4.1 SCHEME ................................................................................................................................ 103
6.4.2 MAUDE .................................................................................................................................. 105
6.5 EJERCICIOS PROPUESTOS. ............................................................................................................ 105
CAPÍTULO 7................................................................................................................................................ 107

OPERADORES DEFINIDOS ..................................................................................................................... 107


7.1 INTRODUCCIÓN ............................................................................................................................ 108
7.2 JUSTIFICACIÓN. ............................................................................................................................ 108
7.2.1 Reuso de código...................................................................................................................... 108
7.2.2 Arquitectura de operadores.................................................................................................... 109
7.3 DEFINICIÓN DE LOS OPERADORES................................................................................................. 109
7.3.1 Definición de Operadores en SCHEME. ................................................................................ 110
7.3.2 Definición de Operadores en MAUDE................................................................................... 112
7.4 SELECCIÓN FUNCIONAL................................................................................................................ 113
7.4.1 Selección en SCHEME. .......................................................................................................... 114
7.4.2 Selección en MAUDE. ............................................................................................................ 115
7.5 TIPO DEL OPERADOR Y DE SUS OPERANDOS .................................................................................. 116
7.5.1 Perfil de los Operadores en SCHEME. .................................................................................. 117
7.5.2 Perfil de los operadores en MAUDE...................................................................................... 119
7.6 ESTRATEGIA DE EVALUACIÓN...................................................................................................... 124
7.6.1 Estrategia de Evaluación en SCHEME. ................................................................................. 126
7.6.2 Estrategia de evaluación en MAUDE..................................................................................... 126
7.7 MEDIO AMBIENTE Y ASIGNACIÓN................................................................................................. 127
7.7.1 Medio ambiente en SCHEME. ................................................................................................ 128
7.7.2 Medio ambiente en MAUDE................................................................................................... 133
7.8 ESTRUCTURA DE LOS PROGRAMAS. .............................................................................................. 134
7.8.1 Estructura de los programas en SCHEME............................................................................. 134
7.8.2 Estructura de los programas en MAUDE............................................................................... 135
7.9 EJERCICIOS PROPUESTOS. ............................................................................................................ 138
CAPÍTULO 8................................................................................................................................................ 139

DEFINICIÓN RECURSIVA DE OPERADORES. ................................................................................... 139


8.1 INTRODUCCIÓN. ........................................................................................................................... 140
8.2 EJEMPLOS DE DEFINICIÓN RECURSIVA DE OPERADORES. ............................................................ 141
8.2.1 Sumatoria. .............................................................................................................................. 141
8.2.2 Raíz cuadrada por el método de Newton_Rapson.................................................................. 143
8.3 LA FORMA DEL PROCESO DE CÁLCULO. ....................................................................................... 145
8.4 LA EFICIENCIA DEL PROCESO DE CÁLCULO. ................................................................................. 148
8.5 ACUMULANDO. ............................................................................................................................ 152
8.5.1 Forma del proceso: acumulador asociativo y no asociativo.................................................. 152
8.5.2 Control a la precisión y eficiencia del cálculo: aproximación al coseno............................... 157
8.6 SERIE DE FIBONACCI. ................................................................................................................... 161
8.6.1 Proceso recursivo................................................................................................................... 162
8.6.2 Proceso iterativo..................................................................................................................... 163
8.7 UTILIDAD DE LA RECURSIÓN ........................................................................................................ 165
8.8 EJERCICIOS PROPUESTOS.............................................................................................................. 166
CAPÍTULO 9................................................................................................................................................ 171
Tabla de Contenido. iv
VALORES Y TIPOS COMPUESTOS. ...................................................................................................... 171
9.1 INTRODUCCIÓN ............................................................................................................................ 172
9.2 TIPOS COMPUESTOS ESTRUCTURADOS ......................................................................................... 172
9.2.1 Declaración de tipos compuestos estructurados. ................................................................... 174
9.2.2 Construcción de instancias del tipo compuesto estructurado. ............................................... 175
9.2.3 Selección de componentes de un valor compuesto estructurado. ........................................... 177
9.2.4 Definición de operadores sobre tipos compuestos estructurados........................................... 178
9.3 TIPOS COMPUESTOS ITERADOS .................................................................................................... 180
9.3.1 Conexión entre componentes: Pares. ..................................................................................... 181
9.3.2 Estructura del iterado: conjuntos, listas, árboles y grafos..................................................... 184
9.4 GESTIÓN DE LISTAS...................................................................................................................... 185
9.4.1 Declaración de Listas............................................................................................................. 185
9.4.2 Constructores de la lista......................................................................................................... 186
9.4.3 Recorridos básicos sobre listas. ............................................................................................. 190
9.4.4 Selección sobre listas.............................................................................................................. 194
9.4.5 Modificadores de la lista ........................................................................................................ 198
9.4.6 Transformación de la lista...................................................................................................... 202
9.5 GESTIÓN DE ÁRBOLES.................................................................................................................. 206
9.5.1 Ejemplo: Árbol Binario de Búsqueda..................................................................................... 206
9.5.2 Construcción del Árbol Binario de Búsqueda. ....................................................................... 207
9.5.3 Localización de un Componente en el Árbol Binario de Búsqueda........................................ 209
9.5.4 Inserción de un Componente en el Árbol Binario de Búsqueda. ............................................ 211
9.6 EJERCICIOS PROPUESTOS .............................................................................................................. 212
CAPÍTULO 10.............................................................................................................................................. 217

PROGRAMACIÓN PARAMÉTRICA Y RELACIONES ENTRE TIPOS............................................ 217


10.1 INTRODUCCIÓN ............................................................................................................................ 218
10.2 ABSTRACCIÓN DE TIPO Y OPERADOR EN SCHEME. ..................................................................... 218
10.2.1 Abstracción de tipo en SCHEME....................................................................................... 218
10.2.2 Abstracción de operadores en SCHEME........................................................................... 220
10.3 ABSTRACCIÓN DE TEORÍAS EN MAUDE. ..................................................................................... 224
10.3.1 Parametrización de módulos. ............................................................................................ 225
10.3.2 Definición de teorías.......................................................................................................... 227
10.3.3 Creación de vistas.............................................................................................................. 229
10.3.4 Creación de instancias de módulos paramétricos. ............................................................ 232
10.3.5 Lista parametrizada en MAUDE. ...................................................................................... 235
10.4 RELACIONES ENTRE TIPOS. .......................................................................................................... 236
10.4.1 “Kinds” y Gestión de Errores. .......................................................................................... 236
10.4.2 Sobrecarga de operadores en subtipos.............................................................................. 237
10.4.3 Preregularidad .................................................................................................................. 238
10.4.4 Ecuaciones de Membresía y de Membresía Condicional .................................................. 239
10.4.5 Operadores Polimórficos y listas heterogéneas. ............................................................... 242
10.5 EJERCICIOS PROPUESTOS .............................................................................................................. 242
CAPÍTULO 11.............................................................................................................................................. 247

FORMAS NORMALES Y LÓGICA CLAUSAL...................................................................................... 247


11.1 INTRODUCCIÓN. ........................................................................................................................... 248
11.2 FORMAS NORMALES EN LÓGICA DE PROPOSICIONES. ................................................................... 248
11.2.1 Equivalencia Semántica..................................................................................................... 248
11.2.2 Formas Normales. ............................................................................................................. 250
11.3 FORMAS NORMALES EN LÓGICA DE PREDICADOS. ....................................................................... 251
11.3.1 Equivalencia Semántica..................................................................................................... 252
11.3.2 Forma normal Prenex........................................................................................................ 253
Tabla de Contenido. v
11.4 EJERCICIOS PROPUESTOS. ............................................................................................................ 254
CAPÍTULO 12.............................................................................................................................................. 257

RESOLUCIÓN EN LÓGICA CLAUSAL.................................................................................................. 257


12.1 INTRODUCCIÓN. ........................................................................................................................... 258
12.2 RESOLUCIÓN EN LÓGICA PROPOSICIONAL. ................................................................................... 258
12.2.1 Notación............................................................................................................................. 258
12.2.2 Resolventes ........................................................................................................................ 258
12.2.3 Demostración por introducción de resolventes. ................................................................ 260
12.3 RESOLUCIÓN EN LÓGICA DE PREDICADOS.................................................................................... 262
12.3.1 Ideas intuitivas; Base para el principio de resolución ...................................................... 262
12.3.2 Notación............................................................................................................................. 263
12.3.3 Unificación ........................................................................................................................ 264
12.3.4 Resolventes ........................................................................................................................ 266
12.4 EJERCICIOS PROPUESTOS.............................................................................................................. 267
CAPÍTULO 13.............................................................................................................................................. 269

LENGUAJES LÓGICOS ............................................................................................................................ 269


13.1 INTRODUCCIÓN. ........................................................................................................................... 270
13.2 EL LENGUAJE PROLOG VISTO DESDE LA LÓGICA....................................................................... 270
13.2.1 Cláusulas de Horn y programa PROLOG. ........................................................................ 270
13.2.2 Consultas en PROLOG ...................................................................................................... 271
13.2.3 Forma Prenex de un programa PROLOG ......................................................................... 271
13.3 SINTAXIS Y NOMENCLATURA DEL PROLOG................................................................................ 272
13.3.1 Términos. ........................................................................................................................... 272
13.3.2 Predicados. ........................................................................................................................ 273
13.3.3 Programa: Hechos y Reglas. ............................................................................................. 273
13.3.4 Consultas ........................................................................................................................... 275
13.3.5 Ejemplo de PROLOG......................................................................................................... 275
13.4 PROCESAMIENTO POR RESOLUCIÓN SDL. .................................................................................... 276
13.4.1 Búsqueda de la prueba y árboles de búsqueda.................................................................. 276
13.5 EJERCICIOS PROPUESTOS .............................................................................................................. 279
CAPÍTULO 14.............................................................................................................................................. 281

PROGRAMACIÓN EN LÓGICA DE PREDICADOS ............................................................................ 281


14.1 INTRODUCCIÓN ............................................................................................................................ 282
14.2 CÁLCULOS ARITMÉTICOS. ............................................................................................................ 282
14.3 LISTAS ......................................................................................................................................... 284
14.3.1 Ejemplo: miembro.............................................................................................................. 285
14.3.2 Ejemplo: concatenar.......................................................................................................... 287
14.3.3 Ejemplo: invertir................................................................................................................ 289
14.3.4 Otros ejemplos de listas..................................................................................................... 290
14.4 EJEMPLO EXTRA, ACERTIJO DE LA ZEBRA ..................................................................................... 293
14.5 EJERCICIOS PROPUESTOS .............................................................................................................. 295
CAPÍTULO 15.............................................................................................................................................. 299

COMPLEMENTOS LÓGICOS AL DIAGRAMA DE CLASES............................................................. 299


15.1 INTRODUCCIÓN. ........................................................................................................................... 300
15.2 EL DIAGRAMA DE CLASES. .......................................................................................................... 300
15.2.1 Plantillas de Clases. .......................................................................................................... 301
15.2.2 Relaciones entre Objetos. .................................................................................................. 302
15.2.3 Ejemplo de un Diagrama de Clases................................................................................... 305
Tabla de Contenido. vi
15.3 INCOMPLETITUD DEL DIAGRAMA DE CLASES............................................................................... 306
15.3.1 Completitud del análisis. ................................................................................................... 307
15.3.2 El Diagrama de Clases Como un Conjunto de Restricciones............................................ 308
15.3.3 Necesidad de Nuevas Restricciones................................................................................... 309
15.3.4 Restricciones Faltantes en el Ejemplo. .............................................................................. 309
15.4 COMPLEMENTACIÓN DEL DIAGRAMA DE CLASES. ....................................................................... 310
15.4.1 Mejoras al Diagrama de Clases para el Ejemplo.............................................................. 310
15.4.2 Derivaciones. ..................................................................................................................... 311
15.4.3 Restricciones...................................................................................................................... 313
15.5 EJERCICIOS PROPUESTOS.............................................................................................................. 314
BIBLIOGRAFÍA. ......................................................................................................................................... 317
PREFACIO A LA ENTREGA COMO TRABAJO DE AÑO SÁBATICO

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

En las “Aplicaciones de la Lógica al Desarrollo de Software” se ha pretendido reunir y


presentar de una forma coherente un subconjunto de maneras de usar la lógica matemática
en el contexto del desarrollo de software, con especial énfasis en la programación.
La intención del libro, más que enciclopédica, es la de juntar el material de forma
coherente, reconociendo que existen un número básico de ideas y principios de la lógica
matemática, que se expresan de diferente forma en muchos de lenguajes que se usan en el
desarrollo del software.
La orientación del trabajo es, entonces, la de presentar primero los principios y luego sus
diferentes expresiones en los lenguajes. Con ello el autor espera que el lector fije su
atención en aspectos de fondo, más bien que en los aspectos de forma. Detrás de este
enfoque está la creencia de que los aspectos de fondo son unos relativamente pocos
elementos o “principios”, y que ellos se expresan en los lenguajes de análisis, diseño y
programación, en relativamente muchas formas sintácticas. Así, cuando el lector deba
afrontar un nuevo (y supuestamente “mejor”) lenguaje le bastará con reconocer, en el
marco de su sintaxis, los principios que conoce y los que no conoce, para, concentrándose
en estos últimos, apropiarse rápidamente del lenguaje. Un beneficio derivado de esta idea
es poder evitar que el lector se quede “estancado”, de por vida, en una lenguaje específico
(que asimiló desde los aspectos de forma), en un mundo altamente cambiante como es el de
los lenguajes del software.
El trabajo puede verse como una reorganización y ampliación del libro “Elementos Básicos
de Los Lenguajes Declarativos” (Fernando Arango Isaza y Daniel Cabarcas, ISBN: 958-
97945-1-3) del que toma varios elementos. Sin embargo, de este trabajo subsisten sin
mayores cambios sólo los capítulos relativos a la lógica de proposiciones y de predicados,
junto con su aplicación al lenguaje PROLOG (aproximadamente 3 de 14 capítulos).
Con base en las ideas planteadas arriba, la organización del trabajo es la siguiente:
• La Parte I, presenta primero la evolución de los lenguajes (Capítulo 1) y de los
paradigmas de desarrollo (Capítulo 2), para enfatizar la importancia cada vez
creciente del uso de la lógica en el desarrollo de software. Se introducen
entonces los principios básicos de la lógica de proposiciones y de la lógica de
predicados (Capítulo 3), señalando la necesidad de clasificar las lógicas por el
caracter de sus predicados y/o sus fórmulas para dar cuenta de las varias
familias de lenguajes que de ellas se derivan.
• La Parte II presenta la lógica ecuacional y los lenguajes relacionados, los
lenguajes funcionales. En esta parte se dedica una gran cantidad de material a
mostrar la manera de construir software con este tipo de lenguaje. La
importancia de este enfoque radica en que las estrategias de programación que
se usan en estos lenguajes se usan también en los que se analizan en las otras
partes del trabajo. Se enfatizan, en particular el uso de la recursión y su
aplicación a la gestión de estructuras iteradas, las estrategias de parametrización,
y los problemas de eficiencia e imprecisión que se asocian con la forma del
programa. Se presenta primero los elementos de la lógica ecuacional (Capítulo
4) y la forma de usarla como lenguaje de programación (Capítulo 5). Luego se
Prefacio. ix
analiza de forma comparativa el uso de los lenguajes funcionales, así: Se
presentan brevemente los lenguajes y sus operadores nativos (Capítulo 6), los
elementos necesarios para definir nuevos operadores (Capitulo 7), las formas de
usar la recursión y los procesos que determina (Capitulo 8), La utilización de
valores compuestos y tipos definidos (Capítulo 9), y, por último las formas de
crear módulos paramétrica y de aprovechar las relaciones entre los tipos
(Capítulo 10).
• La parte III presenta la lógica clausal y se estudian los elementos del lenguaje
PROLOG. En esta parte los elementos de programación se reducen a mostrar
las diferencias del uso del lenguaje PROLOG frente a los lenguajes funcionales
y a presentar las aplicaciones del lenguaje al manejo y organización de datos.
• La parte IV presenta, primero, la manera de “formalizar” el paradigma de
orientación por objetos, fundamentando el “cambio de estado” en el marco de la
lógica dinámica; para luego, mostrar el uso de la lógica para apoyar el
modelamiento en las fases de análisis y diseño bajo el lenguaje UML.

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.

1.2 La Máquina de Estados.


El “computador” u “ordenador”, es una máquina cuya función básica es la de llevar a cabo
operaciones de cómputo y/u ordenamiento sobre elementos de datos.
Desde la óptica de quién usa el computador, la tarea del computador será la de recibir los
datos necesarios para los cómputos, ejecutar los cómputos y devolver los resultados de la
ejecución. Ella es, pues, una tarea de transformación de “datos de entrada” por “datos de
salida”, o mas simplemente de “datos” por “resultados”5.

El computador tiene, además, la capacidad de llevar a cabo la "misma" transformación para


cualquier ocurrencia de los datos, a la que la que dicha transformación se aplique. En cada
caso de transformación el usuario someterá al computador la ocurrencia de datos que sea de
su interés particular.
La “transformación” que lleva a cabo el computador, no es fija, ni única, ni definida
durante su fabricación. A un computador, se le pueden definir múltiples y diversas
operaciones de transformación, entre las que el usuario escoge según sus necesidades
particulares. Decimos, entonces, que el computador es “programable”. A una
transformación específica, definida y expresada de forma que pueda ser utilizada por el
computador la denominamos un “programa de computador”. La utilización de uno de
los programas disponibles en un computador la denominamos la “ejecución” del programa.
1.2.1 Vista Interna del Computador
El computador le da soporte a la función básica de transformar los datos en resultados, con
base en la acción conjunta de los elementos físicos que lo componen. De forma muy
simplificada los elementos de una máquina de tipo “Von Newmann” son los siguientes:
Dispositivos de Entrada/Salida: Son los elementos que permiten el intercambio de datos
con el computador. Entre estos dispositivos se encuentran la pantalla, el teclado, el
“Mouse”, las impresoras, los dibujadores electrónicos, las lectoras de códigos de barra, los
sensores etc..
Dispositivos de Procesamiento: Los procesadores son los elementos activos del
computador, encargados de llevar a cabo las diferentes acciones de cómputo y
ordenamiento con los datos del proceso. Ellos se encargan de mover los datos entre los
diferentes lugares internos de almacenamiento, de efectuar los cómputos, de controlar la

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.

1.3 Los Programas del Computador.


Un programa de computador es la especificación de un conjunto de procesos
computacionales que el computador puede llevar a cabo cuando el usuario así lo solicite.
Son relevantes a nuestra discusión las siguientes características generales de todo
programa:
• Un programa es un conjunto de información que define un conjunto de
procesos. Esta información debe suministrársele al computador antes de
ejecutarse la transformación para un caso particular de datos.
• El programa especifica el proceso para un conjunto, posiblemente grande, de
ocurrencias de posibles datos de entrada o “casos”. En una ejecución del
programa, se lleva a cabo un proceso particular de transformación de los
muchos descritos por el programa.
• El programa define el proceso computacional adecuado para cada una de dichas
ocurrencias, sin que de ello se derive que este proceso sea necesariamente el
mismo para todas ellas.
Ya que un proceso computacional puede descomponerse en secuencias de operaciones
elementales del procesador, y que estas pueden representarse con secuencias de
instrucciones de máquina, un proceso podrá siempre especificarse por medio de un
conjunto de secuencias de instrucciones de máquina. En efecto, todo proceso puede
representarse por una sola secuencia de instrucciones de máquina, y toda secuencia de
instrucciones de máquina determina un proceso específico.
La capacidad de un programa para representar un conjunto (relativamente grande) de
procesos diferentes, se apoya en su habilidad para representar las similitudes y las
diferencias que existen entre ellos. En efecto, todos los procesos representados por un
programa, se asemejan en que se componen de los mismos subprocesos, y se diferencian en
la secuencia y número de veces en que llevan a cabo estos subprocesos. Así, un programa
se compone, por un lado, de secuencias de instrucciones asociadas con los subprocesos, y,
por el otro, secuencias de instrucciones que selecciona y secuencian los subprocesos que se
llevan a cabo en un caso particular
Así, las instrucciones de máquina con las que se definen los subprocesos, junto con las
instrucciones de máquina que permiten secuenciarlos se constituyen, entonces, en un
"lenguaje de programación" que puede ser utilizado para especificar (o "escribir")
programas. Al escribir programas con este lenguaje, el programador debe, simplemente,
definir una lista de instrucciones tomadas del lenguaje ciñéndose a las reglas previamente
definidas para especificarlas e ingresarlas a la máquina.
El lenguaje de máquina es, sin embargo, poco útil para escribir programas. Esto se debe, a
que las instrucciones de máquina son muy elementales por lo que se requieren cientos de
miles de ellas para constituir un programa verdaderamente útil, a que el uso de las
instrucciones de máquina requiere de conocimientos relativos a la arquitectura específica de
Capítulo 1: Evolución de los Lenguajes de Programación
7
la máquina en que se va a ejecutar el programa, a que operaciones que ocurren de forma
repetitiva se deben reescribir una y otra vez, a que los programas no son transportables
entre las máquinas, y en fin a cientos de razones....
Para aliviar este problema, los computadores modernos ofrecen una vasto conjunto de
programas preelaborados que apoyan tanto al usuario como a los demás programas, en las
tareas de control de la máquina, asignación de recursos, manipulación de archivos,
elaboración y ejecución de programas, control de dispositivos, etc..
Bajo estos programas, el usuario y los programas interactúan con la máquina en lenguajes
mucho más amigables y sofisticados que el lenguaje de máquina.
Una posible clasificación de los programas que operan en una máquina es la que se muestra
a continuación:
• BIOS: Son los programas de más “bajo nivel” y que se comunican directamente con la
máquina física. Ellos encapsula las conexiones básicas de la máquina liberando a los
demás programas de tener que conocer las particularidades físicas, específicas a cada
máquina (o al menos de cada familia de máquinas). Son programas usualmente
“quemados” en ROM y residen, por tanto, permanentemente en la memoria de acceso
directo. Parte de la BIOS se ejecuta automáticamente al encender la máquina. La BIOS
tiene como tarea, entre otras cosas, la de llevar a memoria el Sistema Operativo
(“cargarlo” a memoria), y la de ofrecerle un lenguaje para la gestión de los dispositivos
de la máquina.
• Sistema Operativo (SO): Son programas que se comunican con el usuario de la
computadora a través de un conjunto de comandos o de un interfaz gráfico (compuesto
por menús, formas de dialogo, botones etc..), y se comunican con la computadora a
través de conjuntos de comandos relacionados con la BIOS11. El usuario controla la
operación del computador, utilizando el lenguaje que le ofrece el SO. Con él, puede
indicarle que lleve a cabo tareas tales como las de ejecutar los programas (llevándolos a
la memoria y "entregándoles" el procesador), manipular archivos (copiándolos,
borrándolos, renombrándolos etc..), controlar el uso de los dispositivos, dividir el tiempo
del procesador entre las tareas, coordinar los hilos de proceso, restringir el acceso de los
usuarios a datos y a programas, conectarse con otras computadoras a través de redes,
etc.. De especial importancia son los servicios que el SO le ofrece a los programas para
facilitarles y controlar el uso de los recursos del computador.
• Traductores de Lenguajes de Programación: Con frecuencia considerados parte del
SO, existen diversos traductores que le permiten al usuario escribir sus propios
programas, para especificar las operaciones que le sean de interés. Estos traductores
implementan "lenguajes de programación" de muy diversa índole, que tienen como
objetivo fundamental el hacer fácil la escritura de programas. Aunque existen mucha
estrategias para lograr "hacer fácil la escritura de programas", todos los lenguajes tienen
en común, el que liberan al programador del uso de un lenguaje muy especializado y

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.

1.4 Lenguajes de Programación


Los lenguajes de Programación tienen como objeto, facilitar la elaboración de los
programas que soportan el nivel lingüístico aplicativo. Son implementados por medio de
programas traductores, que reciben del usuario (o “programador”) la especificación del
programa en el lenguaje de programación, o programa “fuente”, y la traducen al lenguaje
ofrecido para los programas por el SO, o programa “objeto”.
Desde que fue reconocida la importancia de los lenguajes de programación, se ha dedicado
una gran cantidad de esfuerzo e investigación a la concepción de “buenos lenguajes”, y a la
construcción de sus traductores correspondientes. Es así que existen ahora miles de
lenguajes de programación12, que cubren una gama muy amplia de características, y de
formas de abordar el problema. Definir una clasificación para los diversos lenguajes no es,
tampoco, tarea fácil13.
En este texto no se pretende afirmar que un lenguaje o familia de lenguajes es
uniformemente mejor que los demás. Esto es válido no sólo porque el mejor lenguaje para
un propósito no necesariamente es el mejor para otro propósito diferente, sino también
porque cuando aparecen nuevas características que se muestran útiles, tarde o temprano,
son incorporadas en los lenguajes existentes14.

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”).

1.5 Evolución de los lenguajes de Programación.


Al efecto de definir el papel de la lógica matemática en el marco de la informática y, en
particular, en el marco de los lenguajes de programación, presentaremos a continuación una
clasificación clásica de los lenguajes, que se apoya en su desarrollo histórico. En esta
clasificación clasifica el desarrollo de los lenguajes de forma muy sumaria por
“generaciones”. Así, cada generación de lenguajes marca la aparición de cambios radicales
en la manera de concebir el problema de la programación.
1.5.1 Primera Generación: Lenguaje de Máquina.
Tal como se indicó antes el conjunto de patrones que definen las instrucciones de máquina
constituye un lenguaje de programación que puede ser utilizado para escribir programas.
Aunque el lenguaje de máquina puede ser usado directamente por el programador, lo usual
es que los programas traductores de lenguajes de mas “alto nivel” traduzcan el programa
escrito en dicho lenguaje a lenguaje de máquina.
Todo programa debe, en efecto, ser traducido a lenguaje de máquina antes de ser ejecutado.
Una vez traducido el programa al lenguaje de máquina debe ser colocado en una memoria
de acceso directo para ser ejecutado15. Una vez que el programa ha sido colocado en la
RAM, para ejecutarlo, se procede a “cargar” en un lugar especial de la memoria del
procesador la dirección de la primera instrucción del programa16. El procesador lleva
entonces cabo, de forma cíclica, un proceso de “cargar” la instrucción que hay en la
dirección de programa que posee, ejecutar dicha instrucción, y aumentar la dirección de
programa en el tamaño de una instrucción de máquina17 para pasar, en el siguiente ciclo, a
la instrucción siguiente del programa.
Las instrucciones de máquina llevan usualmente a cabo tareas simples, tales como traer
datos de la RAM a la memoria interna del procesador (o “registros”), llevar a cabo
operaciones simples sobre los datos contenidos en sus registros dejando el resultado en

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:

mov %r1 %r2

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.

2.2 Paradigma de Instrucciones.


Muchos programas fueron concebidos sin tener en cuenta noción alguna de modularización
o arquitectura. Por lo tanto, el nivel básico de granularidad de los modelos lo constituyen
las instrucciones del lenguaje de programación, en particular los de 3era generación (y
hacia abajo).
Los modelos visuales son representaciones más legibles del ensamblaje de instrucciones del
lenguaje que constituye un programa, tales como los diagramas de lógica usados en la
programación FORTRAN y COBOL.

2.3 Paradigma de Funciones (o Procesos).


El desarrollo de software basado en el libre uso de los conceptos que fundamentan los
lenguajes de programación, se asoció rápidamente con los problemas de calidad asociados a
la complejidad del software [Dijkstra 68].
En el primer esfuerzo para resolver estos problemas se introdujeron los conceptos relativos
a la programación estructurada y a la “arquitectura de funciones”.
2.3.1 Función.
Bajo este paradigma el software es concebido con base en el concepto de función. Así,
toda pieza de software es la especificación de una función que lleva a cabo una proyección
de un conjunto de “datos” en un conjunto de “resultados”.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
20
Función

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.

Serían definiciones adecuadas las Serían definiciones inadecuadas las


siguientes: siguientes:

“calcula el sueldo del empleado” “lleva a cabo primero tal operación y


“calcula el área de acero de la viga” luego aquella”
“valida que haya disponibilidad “calcula el área de acero de la viga y
presupuestal para el gasto” deja almacenado un factor que será
• utilizado para ahorrar tiempo en el
calculo del acero en las columnas”

• Fórmulas matemáticas: Para describir la manera como se relacionan los datos


de entrada con los resultados.

Disponible(i) = max(0,∑j=1,i (Ingresos(j) - Egresos(j)))

Una especificación formal por restricciones de la raíz cuadrada de un número real es la


siguiente:
2
y=raiz(x) {x∈ℜ / x =y}
Donde:
“x” es un elemento del rango.
“ℜ” representa el conjunto de los números reales.
“y” es un elemento del dominio.

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

• Las “mini especificaciones” describen las funciones componentes de una


función específica, la forma como se repiten secuencian y seleccionan en
ejecución dichas componentes, y la forma como ellas se intercambian los datos,
por medio de una especificación textual que modela la codificación de la
función específica en el lenguaje de programación.
El pseudo código para el programa que calcula la raíz podría ser como sigue sería el
siguiente:
Lea x,y,dx 2
Hasta que |x -y|≥ dx
cambie x por ((y/x)+x)/2
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
25
2.3.3 Proceso de desarrollo.
Bajo el paradigma de funciones el método de desarrollo de software se fundamenta en el
proceso siguiente:
• Fase de análisis: Definición y especificación de la función principal del
software.
• Fase de diseño: Definición y especificación de las funciones constituyentes y de
sus formas de cooperación para las funciones de la aplicación, progresando
desde las funciones de más alto nivel hasta las funciones de más bajo nivel
(Top-Down).
• Fase de Programación: Especificación de las funciones en el lenguaje de
programación.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
26
2.4 Paradigma de Entidades (o Estructuración de Datos).
Desde sus inicios los lenguajes 3GL concibieron la estructura de los datos que se mantienen
almacenados en disco para uso permanente, desde la óptica del concepto de archivo. El
archivo es esencialmente un “periférico” con el que el programa intercambia datos. En un
archivo los datos se ven como una cadena de bytes organizados en registros que
constituyen la unidad básica de información de intercambio entre el programa y los
periféricos.
2.4.1 Estructuración de los datos en archivos.
El enfoque de los 3GL da lugar a una arquitectura de los datos consistente en un conjunto
de archivos separados compuestos por listas de registros cada uno con un conjunto de datos
organizado en un árbol disjunto de los demás.
Un registro está, a su vez, compuesto por una serie ítems de dato asociados a un tamaño y
representación específica. Para leer o escribir un registro, el programa partía del
conocimiento de su estructura y composición, que debía aparecer en el programa en la
forma de una instrucción declarativa. Ejemplos de estas instrucciones son las
declaraciones de registros del COBOL, y las instrucciones struct del C.

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.

• El modelo de casos de uso que muestra los procesos principales de la aplicación,


desde la óptica de las interacciones de los entes externos con la misma.

Evaluación
del TDG
Nombrar Jurado
Calificador

Ingresar
Disponibilidad Horaria

Director
Escuela

Consultar Citación Realizar Citación

Jurado Estudiante
Calificador

Elaborar Acta Consultar


Especial Acta/Acta Especial
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
36
2.6 Paradigma de Agentes.
Sin entrar en detalles sobre este paradigma, que pertenece más al campo de la Inteligencia
Artificial, podemos señalar que adiciona al paradigma de los objetos conceptos propios de
las organizaciones humanas de cooperación, tales como, conocimiento, objetivos o
intenciones, coordinación de tareas con otros agentes, capacidad de viajar, etc..
Capítulo 3
Elementos de Lógica Matemática
Capítulo 3: Elementos de Lógica Matemática.
38
3.1 Introducción
En el primer capítulo del trabajo se adujo que los lenguajes de programación han
evolucionado de lenguajes orientados a describir en detalle el proceso computacional que
ocurre al ejecutar un programa, a lenguajes orientados a describir las características
relevantes del área de aplicación.
En el marco de esa evolución se destacó el uso de la lógica matemática como lenguaje de
programación. La lógica permite describir las características el área de aplicación con base
en aserciones o restricciones, facilita la construcción de instancias que modelan el área de
aplicación satisfaciendo las restricciones, y, por último, permite obtener respuestas a
consultas relativas a dichas instancias con base en demostraciones soportadas en la lógica.
En el segundo capítulo se mostró que para representar adecuadamente áreas de aplicación
cada vez más complejas, los lenguajes de programación han venido incorporando tipos de
conceptos o “categorías conceptuales” cada vez más cercanas a las del lenguaje natural. A
un conjunto específico de dichas categorías se le denominó “paradigma arquitectónico”. El
software elaborado con base en uno de dichos paradigmas posee una forma de
modularización estrechamente ligada al paradigma, por lo que éste constituye la base de
todo método de desarrollo.
Para que sea posible usar la lógica matemática como un lenguaje de especificación en el
marco de un método moderno de desarrollo es, entonces, necesario que ella pueda dar
cuenta de los conceptos involucrados en los diferentes paradigmas arquitectónicos. En
efecto, diferentes lógicas dan soporte a diferentes paradigmas.
El enfoque de este trabajo es, en efecto, el de presentar cada tipo específico de lógica, para
luego mostrar los lenguajes que de ellas se derivan. Así, en la parte II se presenta la lógica
ecuacional como base de los lenguajes funcionales, en la parte III se presenta la lógica
clausal como base de los lenguajes lógicos, y en la parte IV se presentará28 la lógica
dinámica como base de los lenguajes orientados por objetos.
En este capítulo se introducen los conceptos básicos de la lógica y de la teoría de
demostración. En él se presentan primero los elementos de la lógica proposicional, para
luego presentar los conceptos de la lógica de predicados.

3.2 La Lógica Como una Disciplina de Razonamiento.


La capacidad de convencer a quien escucha de la veracidad de un argumento emitido, es
una propiedad fundamental de todo proceso de comunicación humana.
Tal vez la forma más simple de convencer, es la de soportarse en la confianza o “autoridad”
que el oyente atribuye al emisor. Esta es probablemente la forma que prevalece en la
mayoría de las relaciones humanas, incluyendo la gran mayoría de los procesos educativos
y, por supuesto, en todo lo relativo a las relaciones personales y la religión.

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

[Link] Árbol sintáctico


La naturaleza recursiva de los criterios formativos, hace posible que un conector ensamble
tanto variables proposicionales, como a otras proposiciones complejas. En consecuencia,
una proposición compleja puede tener múltiples conectores y múltiples variables
proposicionales.
Para poder establecer la semántica de una proposición con varias variables y conectores, es
necesario saber cuales son las proposiciones (simples o complejas) que ensambla cada
conector; es decir, se debe conocer cual es la estructura de composición de la proposición.
Una manera de visualizar la estructura de composición de una fbf es representarla por
medio de un “árbol sintáctico”. El árbol sintáctico de una fbf se define de la manera
siguiente:
• El árbol representa toda la fbf.
• Cada fbf que aparece como componente de la fórmula global es representada
por una rama del árbol.
• Cada conector de la fbf corresponde a un nodo del árbol.
• Cada aparición de una variable proposicional en la fbf corresponde a una hoja
del árbol.
• De cada conector se desprenden una rama por cada una de las fbf que conecta el
conector.
• De los conectores monádicos (¬ ) se desprende una sola rama. De los
conectores diádicos (∨, ∧, ⇔, ⇒) se desprenden dos ramas.
• Las ramas que se desprenden de un conector deben ser disjuntas.

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

[Link] Uso de paréntesis.


Para clarificar las fbf que conecta cada conector se pueden usar paréntesis. Los paréntesis
encierran las diferentes subfórmulas de la fórmula, clarificando su relación con los
conectores.

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))))

[Link] Uso de un orden de precedencia entre los operadores.


Para evitar la sobre utilización de paréntesis se define un orden de precedencia entre los
conectores. Los de mayor precedencia se asocian con las fbf mas elementales que tengan
adjuntas formando fbfs mas complejas, con las que, luego, se asocian los conectores de
menor precedencia.
Usaremos el siguiente orden de precedencia (los de mayor precedencia aparecen primero en
la lista):
¬, ∧, .∨, .⇒,.⇔

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:

Y el árbol sintáctico de la fbf anterior bajo el orden de precedencia siguiente:


⇒, ¬, ∨, ∧, ⇔
Sería el que se muestra a continuación:

[Link] Uso de el sentido de asociatividad entre los operadores.


Cuando aparece una secuencia de conectores iguales (de igual precedencia) se debe definir
un sentido de asociatividad. En este caso será -> (izquierda a derecha) o <- (derecha a
izquierda). Asumiremos que la flecha indica el orden en que se los conectores se van
asociando con las fbfs que los rodean.
En lo que sigue asumiremos que nuestros conectores diádicos (⇒,∨, ∧, ⇔) asocian de
izquierda a derecha (->), mientras que nuestro único conector monádico (¬), asocia de
derecha a izquierda (<-).

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”.

[Link] Semántica de las fbfs complejas.


El valor de verdad de una fórmula compleja puede hallarse a partir del valor de verdad de
sus átomos constituyentes aplicando la semántica de los conectores. Esto se debe hacer de
forma progresiva desde las subfórmulas más elementales hasta cubrir las más complejas y
eventualmente toda la fbf. Este proceso equivale a colocarle el valor de verdad a cada
operador del árbol sintáctico.
El valor de verdad de un fbf cualquiera es en consecuencia dependiente de la interpretación
Λ). Así, se F es una fbf a su valor de verdad en Λ lo llamaremos Λ(F).

Definición:
Dada una interpretación Λ, y una fbf F. Si Λ(F) es true diremos que F “cumple bajo
Λ”, o que Λ “satisface a F”, o que:
Λ es modelo de F.
Simbólicamente: Λ |= F
Definición:
Una interpretación Λ es modelo de un conjunto de fórmulas, si es modelo de cada
formula del conjunto.
Simbólicamente: Λ |= F1, F2, …, Fn

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

[Link] Consecuencia lógica y Equivalencia Semántica.


Tal como se refiere en [Chang 73]41, en gran medida, el propósito de la lógica como
lenguaje, es establecer con claridad cuando una argumentación o deducción tiene sentido.
Para ello debemos clarificar primero el concepto de “argumentar” o “deducir”. Estos dos
conceptos pueden asociarse fácilmente a dos conceptos más precisos: la “consecuencia
lógica” y la “equivalencia semántica”.
Definición:
Dadas las fbfs F1,F2, ...,Fn, y una fórmula G, se dice que G es consecuencia lógica de
F1,F2, ...,Fn, si y solo si para cualquier interpretación en la cual F1∧ ∧F2∧ ∧Fn es
∧...∧
verdadero, también G es verdadero.

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.

P Q R P⇒Q Q⇒R (P⇒Q)∧(Q⇒R)∧P ((P⇒Q)∧(Q⇒R)∧P)⇒R (P⇒Q)∧(Q⇒R)∧P∧¬R


V V V V v V V F

v V F V F F V F
v F V F v F V F
v F F F v F V F
F V V V v F V F
F V F V F F V F
F F V V v F V F
F F F V v F V F

En la tabla no solo hay una sino tres 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:

P Q R ¬P ¬P∨Q R∧(¬P∨Q) ¬R P⇔¬R (P⇔¬R)⇒R∧(¬P∨Q) P∨((P⇔¬R)⇒R∧(¬P∨Q))


V V V F V V F F V V
V V F F V F V V F V
V F V F F F F F V V
V F F F F F V V F V
F V V V V V F V V V
F V F V V F V F V V
F F V V V V F V V V
F F F V V F V F V V

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.

3.5 Lógica de Predicados


Como se mencionó anteriormente, el cálculo de predicados o la lógica de primer orden
como también se conoce, no es más que una ampliación de la lógica de proposiciones para
lograr un mayor detalle y así poder expresar razonamientos más complejos.
Se vio que la lógica de proposiciones tiene como unidad básica la proposición, y no se
proveen mecanismos para analizar al interior de la proposición. Es precisamente allí donde
radica la diferencia con el cálculo de predicados, que provee los elementos y la sintaxis
necesaria para entender lo que compone una proposición.
Capítulo 3: Elementos de Lógica Matemática
53
3.5.1 Predicados.
Vale la pena ejemplificar, para entender como, la estructura del lenguaje, se acomoda a las
necesidades expresivas.
“Todos los hombres son mortales”
“Sócrates es hombre”
“Sócrates es mortal”
En lógica de proposiciones serían 3 proposiciones distintas sin ninguna conexión entre
ellas. En cálculo de predicados, diremos lo siguiente:
H(x) es un predicado que se entiende como “x es hombre” y M(x) como “x es mortal”;
Sócrates es un objeto que representaremos con la letra s. Las tres frases en lógica de primer
orden se escribirían así:

(∀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:

Considere la fbf: (∀x)(∃y)P(x,y)


Y consideremos la siguiente interpretación:
D = {1, 2}
P(1,1) P(1,2) P(2,1) P(2,2)
V F F V

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.

3.6 Tipos de predicados y lógicas asociadas.


Si bien las nociones generales de predicado y de Fórmula Bien Formada, son suficientes
para llevara cabo cualquier tipo de afirmación sobre los elementos de una área de
aplicación, los criterios de demostración que se aplican de forma general a los mismos, no
son suficientes para cubrir todos los modos de razonamiento usados en la matemática.
Existen, en efecto, criterios de demostración que son propios de ciertos tipos de predicados
y fórmulas bien formadas. Así, una clasificación más detallada de los predicados y de las
fbfs permitirá introducir nuevos criterios particulares para lógicas más especializadas que la
de predicados en general. Una característica de estas lógicas será la de permitirnos llevar a
cabo de forma automática procesos de demostración específicos.
Es en este sentido que clasificaremos la lógica en los tipos que se muestran a continuación:
• En la Lógica Ecuacional los predicados afirman la igualdad entre expresiones
matemáticas o “términos”. De esta lógica surgen una familia importante de
lenguajes lógicos denominados “Lenguajes Funcionales”. Estos lenguajes dan
cuenta de forma natural de la arquitectura de funciones y son el objeto de
estudio de la Parte II del trabajo.
• En la Lógica Clausal las fbfs tiene una forma específica denominada
“cláusula”. Cuando las cláusulas son “Cláusulas de Horn”, es posible
automatizar los procesos de demostración por reducción al absurdo. De esta
lógica surgen los lenguajes “clausales” y la mayoría de los demostradores
automáticos de teoremas. Estos lenguajes dan cuenta de forma natural de la
arquitectura de datos y son el objeto de estudio de la parte III del trabajo.
• En la Lógica Nodal se introducen predicados que describen el efecto de los
eventos que ocurren en el tiempo sobre los elementos del área de aplicación.
Bajo esta lógica se pueden describir sistemas dinámicos, dando cuenta natural
de la arquitectura de objetos. El estudio de esta lógica y de los lenguajes
Capítulo 3: Elementos de Lógica Matemática
57
asociados será (en versiones futuras del trabajo) el objeto de estudio de la parte
IV del trabajo.

3.7 Ejercicios Propuestos.


1. Desarrollar la tabla de verdad para cada una de las definiciones de los conectivos lógicos.
(¬P, P∧Q, P∨Q, P⇒Q y P⇔Q).
2. Determinar cuales de las siguientes son proposiciones atómicas y cuales no. Si no lo son,
descomponerlas en sus átomos constituyentes y rescribirlas como fórmulas de la lógica
proposicional.
a) “Si la humedad es alta y la temperatura es alta, entonces uno no se siente
confortable.”
b) “Todos los artistas están locos.”
c) “Héctor es rico y José es rico y también feliz.”
d) “La colombina que se comió Camila era roja o naranjada.”
e) “Si Héctor está en la piscina, entonces José debe de estar también en la piscina.”
3. Dibujar el árbol sintáctico para la siguiente declaración de la lógica de proposiciones: “Si
no me equivoco, Héctor estaba en la piscina y José no, de manera que Sandra dijo
mentiras.”
4. Construir la tabla de verdad y el árbol sintáctico para las siguientes fórmulas:
a) (¬P∧(Q⇒P)∨P)∨(¬P⇔(P∧¬Q))

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:

a) ¬P es consecuencia lógica de (P⇒Q) y ¬Q.


b) P⇒Q es consecuencia lógica de (p⇒(Q⇒R)) y Q.
c) R es consecuencia lógica de p∨Q, P⇒R y Q⇒R.
9. Convertir la siguiente argumentación a lógica de proposiciones y demostrar su validez:
“Si Josef esta arrestado es porque es culpable o alguien lo ha calumniado. Por tanto, si Josef
no es culpable, alguien debió haber calumniado a Josef.”
Parte II: Lógica
Ecuacional y
Lenguajes
Funcionales.
Capítulo 4
Lógica Ecuacional
Capítulo 4: Lógica Ecuacional
62
4.1 Introducción.
En el Capítulo 3 se presentaron los conceptos básicos de la lógica de predicados. Allí se
definió un predicado como una proposición atómica que afirmaba algo sobre una o varias
entidades de un dominio de interpretación.
En ese capítulo también se introdujeron las construcciones sintácticas “constante” y
“variable” de la lógica de predicados, como medio para referirse a las entidades sobre las
que recae una afirmación (aquel sobre las que el predicado predica).
En este capítulo se introduce primero la construcción “termino” de la lógica de predicados,
como aquella que permite, de forma general, referirse a las entidades sobre las que recaen
las afirmaciones basadas en predicados. Mostraremos que las construcciones “constante” y
“variable” son términos atómicos y que existen términos compuestos obtenidos uniendo
otros términos por medio de la construcción “operador”. Se muestra entonces, que los
operadores se corresponden con funciones definidas en el dominio de interpretación y que
el uso de términos le permite a la lógica de predicados hacer afirmaciones sobre dichas
funciones.
Caracterizado el concepto de operador, se procede a introducir el predicado de “igualdad”
como un medio para afirmar que dos términos, no necesariamente idénticos desde el punto
de vista sintáctico, son iguales o “dan lo mismo” desde el punto de vista semántico.
El capítulo termina mostrando como los términos y el predicado de igualdad dan lugar a la
Lógica Ecuacional que se orienta a la caracterización formal de funciones definidas sobre
conjuntos específicos y da soporte formal al razonamiento en la matemática.

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:

(+ _ _ _ ...)

4.2.2 Criterios formativos de los términos.


Un término es ya sea atómico o es la aplicación de un operador a un conjunto de términos
(diferente a sí mismo). Para determinar si una construcción de la lógica es un término,
basta con validar si satisface los criterios formativos de la lógica para los términos:
CRITERIOS FORMATIVOS PARA LOS TÉRMINOS:
• Una constante o una variable es un término.
• La aplicación de un operador a un conjunto de términos en concordancia con
su plantilla es un término.
• Ninguna otra construcción constituye un término
La aplicación de un operador a un conjunto de términos está en concordancia con la
plantilla del operador, si los términos que agrupa corresponden en número y tipo a los
definidos por la plantilla, y la colocación relativa de cada término con respecto al operador
es la que indica la plantilla.
Capítulo 4: Lógica Ecuacional
65
Nótese que la aplicación de un operador es recursiva, permitiendo que los términos a los
que se aplica sean instancias del mismo u otro operador. Esto implica que el conjunto de
posibles términos que se pueden construir en una lógica sea, en general, de tamaño infinito.

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))

4.2.3 Términos Complejos


Dada la naturaleza recursiva de los criterios formativos para los términos, estos pueden
tener cualquier grado de complejidad y por tanto un gran número de operadores. Es de
vital importancia evitar ambigüedades al determinar los operandos de cada operador en un
término complejo.
DEF:
Subtérmino – Un operando de alguno de los operadores de un término complejo.
[Link] Árbol Sintáctico.
Una manera de visualizar los operandos de cada operador en un término complejo es
representarlo como un árbol dirigido en el que los que los operadores son nodos de donde
se desprenden los operandos como ramas. En este árbol el operador más externo
Capítulo 4: Lógica Ecuacional
66
corresponderá a la raíz y sus operandos a cada una de las ramas principales. Si uno de estos
operandos es a su vez un subtérmino complejo, la rama que le corresponde tendrá un nodo
asociado al operador de dicho subtérmino con ramas asociadas a sus operandos. Esta
estructura se repite hasta llegar a operandos atómicos que son representados como las hojas.
DEF:
Árbol sintáctico de un término- Es un árbol dirigido que representa los operandos
de cada operador, asociando cada operador a un nodo del árbol y sus operandos a
árboles disjuntos que parten de dicho nodo.

Ejemplo 13
Árbol que representa el término 1/(-(4*7))

1 -

4 7

[Link] Asociatividad y precedencia de operadores.


En los ejemplos anteriores se usaron paréntesis o una notación “estándar” para delimitar los
operandos de cada operador, evitando la ocurrencia de subtérminos contiguos a más de un
operador. Cuando esto último ocurre, no quedan delimitados los operandos de cada
operador, existiendo una ambigüedad con respecto al árbol sintáctico que le
correspondiente al término.

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.

(1 + (3 * ( 4 * ((5 + 1 ) / ((1 + 3) * 3)))))

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.

Si bien el uso de paréntesis facilita la identificación del operador principal de los


subtérminos, también es cierto que su uso excesivo dificulta su escritura debido a la
necesidad de mantenerlos balanceados.
Una manera de evitar la ambigüedad sin uso excesivo de paréntesis, es la introducción de
los conceptos siguientes:
• Precedencia entre operadores: La precedencia entre los operadores es una
propiedad de los operadores monádicos y diádicos con notación infija. La
precedencia entre operadores toma un valor en el dominio de los naturales.
Capítulo 4: Lógica Ecuacional
68
• Asociatividad: La asociatividad es una propiedad de los operadores monádicos y
diádicos con notación infija. La asociatividad toma un valor en el conjunto
{derecha->izquierda, izquierda->derecha} (<- y ->). Es importante notar
que la posición del operador monádico respectoa a su operando determina su
sentido de asociatividad, ya que este debe ir siempre en la dirección que va del
operando al operador.
• Precedencia entre asociatividades: Es un orden impuesto a los elementos del
conjunto donde toma valor la asociatividad. Es decir se le da más precedencia a
la asociatividad “derecha->izquierda” que a la ”izquierda->derecha” o
viceversa.
En un término (o subtérmino) donde aparecen en secuencia varios operadores de tipos
diferentes, los operadores de mayor precedencia (es decir lo que tienen un valor de
precedencia menor), se asocian primero con las unidades que los rodean formando una
unidad (o subtérmino), luego los operadores de precedencia siguiente se asocian con estas
unidades formando sus propias unidades, luego los operadores de precedencia siguiente se
asocian con estas últimas unidades, y así sucesivamente, hasta cubrir todos los operadores
del término.
En un término (o subtérmino) donde aparecen en secuencia varios operadores de un mismo
tipo, los operadores localizados en el extremo señalado por la dirección de asociatividad del
operador asocian primero con los operandos que los rodean formando una unidad (o
subtérmino), luego el operador localizado antes, en la dirección de asociatividad, se asocia
con las unidades que lo rodean formando su propia unidad, y así sucesivamente, hasta
cubrir todos los operadores del subtérmino.

Ejemplo 17
Dando el siguiente orden de precedencia y sentido de asociatividad a los operadores aritméticos:

Operador Descripción Precedencia Asociatividad


_ ^_ Eleva a potencia 1 ->
_↓ Invierte el número 1 ->
⊥_ Trunca los decimales al entero 1 <-
anterior
-_ Cambia el signo 1 ->
_*_ Multiplica 2 ->
_/_ Divide 2 ->
_+_ Suma 3 ->
_-_ Resta 3 ->

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.

Termino equivalente interpretación


3^4^5 * 4 + 7 / -3 + 5 Término original
((3^4)^5) * 4 + 7 / (-3) + 5 Asocian los operadores de precedencia 1, note
el efecto de la dirección de asociatividad en el
operador ^
(((3^4)^5) * 4) + (7 / (-3)) + 5 Asocian los operadores de precedencia 2
(((((3^4) * 4) + (7 / (-3))) + 5) Asocian los operadores de precedencia 3, note
el efecto de la dirección de asociatividad en el
operador +

En un término (o subtérmino) donde aparecen ambigüedades relativas a la aplicación de


operadores monádicos, esta debe resolverse con base en la precedencia entre los sentidos de
asociatividad.

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)))).

4.2.4 Interpretación de los Operadores.


Los términos le permiten a los predicados referirse a los elementos del dominio de
interpretación de una gran diversidad de formas. En particular las siguientes:
• Las constantes permiten hacer referencias explícitas a elementos particulares del
dominio de interpretación.
• Las variables permiten hacer referencias, de forma conjunta, a todos o a algunos
de los elementos del dominio de interpretación.
Capítulo 4: Lógica Ecuacional
70
• Los operadores permiten referirse de forma indirecta a los elementos del
dominio de interpretación usando referencias a otros elementos con los que
mantienen una relación.

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) )

4.3 Lógica Ecuacional


La lógica ecuacional es una rama de la lógica de predicados que se ocupa del razonamiento
sobre las propiedades de las funciones. Para ello se apoya en el predicado de igualdad de
términos definidos sobre los operadores asociados con dichas funciones.
La lógica ecuacional da soporte formal al razonamiento en matemáticas que se ocupa de las
propiedades de las funciones definidas sobre las entidades matemáticas (Vg. los números).
A continuación se presenta primero el predicado de igualdad, luego los criterios de
demostración que se le asocian, y por último el carácter de las teorías construidas con base
en dicho predicado.
4.3.1 El predicado de igualdad.
El predicado de igualdad permite afirmar que dos términos que pueden ser sintácticamente
diferentes, son iguales o “dan lo mismo” desde el punto de vista semántico.

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.

El concepto de igualdad puede caracterizarse por medio de tres propiedades fundamentales


a saber:
• Reflexión: Un elemento puede ser considerado igual a sí mismo.
• Transitividad: Si un elemento es considerado igual a otro y este último es
considerado igual a un tercero, entonces el primer elemento puede también ser
considerado igual al tercero.
• Substituibilidad: Un elemento puede ser substituido por otro igual a él en el
marco del criterio en el que fue definida la igualdad.

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.

El predicado de igualdad es fundamental para describir las propiedades de las funciones


que ocurren en el dominio de interpretación.

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)

4.3.2 Criterios de Demostración.


Además de los criterios de demostración de la lógica de Predicados, en la lógica Ecuacional
se pueden usar cinco criterios adicionales que son consecuencia directa de las propiedades
inherentes al concepto de igualdad. Estos criterios o leyes de demostración de la lógica
Ecuacional son los siguientes:
1. Ley reflexiva: Un término es igual a sí mismo.
t=t
2. Ley simétrica: Si un término es igual a otro, el otro es igual al primero.
t1 = t2
––––––
t2 = t1
3. Ley transitiva: Si un término es igual a otro, y este es igual a un tercero, entonces el
primero es igual al tercero.
t1 = t2, t2=t3
–––––––––––
t1 = t3
4. Ley de substitutividad de funciones:
t1 = t1’, t2=t2’, t3=t3’, ...
––––––––––––––––––––––––––––
f(t1, t2, t3, ...) = f(t1’, t2’, t3’, ...)
5. Particularización de variables a términos: De una fbf cuantificada universalmente se
deriva cualquier particularización de las variables a términos del mismo Sort, con las
variables de los términos cuantificadas universalmente.

∀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

[Link].2 Perfil de los operadores.


Nombre y perfil de los operadores utilizados en los términos complejos.

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)

[Link] Demostración en Lógica Ecuacional.


Capítulo 4: Lógica Ecuacional
74
La incorporación de teoremas en la lógica ecuacional se lleva a cabo por un proceso de
demostración apoyado en una “derivación”.
DEF:
Derivación – Una derivación es una secuencia de términos t0,t1,t2,...,tn que
pueden demostrarse iguales por razonamiento ecuacional.
Para simplificar la especificación de la igualdad entre todos los términos de una derivación
usaremos la notación siguiente:
NOTACION:
La expresión t0=t1, representa la aserción siguiente:
∀v0 ∀v1 ∀v2... ∀vm (t0=t1)
La expresión t0=t1=t2=...=tn, representa las aserciones siguientes:
∀v0 ∀v1 ∀v2... ∀vm (t0=t1)
∀v0 ∀v1 ∀v2... ∀vm (t1=t2)
...
∀v0 ∀v1 ∀v2... ∀vm (tk-1=tk) para todo 1≤k<n
Donde v0,v1,v2,.. ,vm son las variables involucradas en los términos.

Nótese que aplicando las leyes reflexiva, simétrica y transitiva de la Lógica


Ecuacional, es fácil demostrar que la aserción siguiente es consecuencia lógica de
las anteriores:

∀v0 ∀v1 ∀v2... ∀vm (ti=tj) para 0≤i<n y 0≤j<n.


La eliminación de los cuantificadores obliga, sin embargo, a declarar cuales
identificadores deben ser considerados como variables en los términos (y estarán por
tanto cuantificados) y cuales deben ser considerados como constantes. En lo que
sigue se declararan de forma explícita las variables para evitar confusión.
En una demostración en Lógica Ecuacional se construyen de forma paulatina las aserciones
representadas por la expresión t0=t1=t2=...=tn. Para ello se parte del termino t0, y se
demuestra la aserción t0=t1, luego la aserción t1=t2, a continuación t2=t3 y así
sucesivamente hasta llegar a tn-1=tn. Se dice, entonces que la aserción t0=tn, es un teorema
de la teoría obtenido por razonamiento ecuacional.
La aserción que dan lugar a un nuevo término ti de la derivación, se demuestran aplicando
de forma rigurosa razonamiento ecuacional. Para aplicar este razonamiento se siguen los
pasos siguientes
1. Subtérmino: Se selecciona un subtérmino (s) de ti-1 a ser substituido.
2. Axioma: Se selecciona un axioma(A), o teorema, de la teoría para que
determine la substitución.
3. Emparejamiento: Se define una substitución (σ) de las variables de A
que haga uno de sus lados (lm) idéntico a s (σ(lm) ≡ s ).
4. Remplazo: Se remplaza en ti-1 a s por el otro lado del axioma (ln) luego
de que se le haya aplicado la substitución, para dar como resultado el
nuevo término de la derivación (ti ≡ Ssσ(ln)(ti-1) ).
Nótese que por la ley de substitutividad de funciones, el nuevo término actual, resultante de
la reescritura, es semánticamente igual al anterior.
Capítulo 4: Lógica Ecuacional
75
Ejemplo 28
En la teoría asociada a los enteros de los ejemplos anteriores se puede demostrar la aserción siguiente:

- -x = x

Por medio del razonamiento ecuacional que se ilustra en la tabla siguiente:

Derivación Axioma Substitución Particularización


- -X (1) X ← - -X - -X + 0 = - -X
- -X + 0 (3) X←X -X + X = 0
- -X + (-X + X) (4) X ← - -X; - -X + (-X + X) = (- -X + -X ) + X
Y ← -X;
Z←X
(- -X + -X) + X (3) X ← -X - -X + -X = 0
0+X (2) X←X 0+X=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.

Es importante notar que si bien cada paso de la derivación es producto de la aplicación


rigurosa de los criterios y axiomas de la teoría, la selección adecuada de los que serán
aplicados en cada paso es fundamental para obtener la derivación. Para llevar a cabo esta
selección no hay, sin embargo, criterio alguno y es guiada básicamente por la intuición del
matemático con base en el resultado previsto para la demostración.
De lo anterior se induce que el matemático parte, en general, de una premonición del
resultado de toda demostración antes de intentarla. A un resultado previsto pero no
demostrado en matemáticas se le denomina una “conjetura”. Es normal que el desarrollo
de las matemáticas se apoye en conjeturas, que son producto del contacto que tienen los
matemáticos con su área de conocimiento particular.

4.4 Resumen del Capítulo.


Los operadores son símbolos de la lógica de predicados que se utilizan para construir
términos complejos agrupando términos más simples denominados operandos. Los
términos más elementales o “atómicos” son las constantes y las variables.
El perfil de los operadores determina el nombre del operador, el sort de los términos
resultantes de su aplicación, el número de operandos que agrupa (aridad del operador), el
sort de los operandos, y la forma como se ensamblan los operandos con el operador para
formar los términos complejos.
Capítulo 4: Lógica Ecuacional
76
En la forma “estándar” de ensamblaje, el nombre del operador va primero y los operandos
le siguen encerrados entre paréntesis y separados por coma. En notación infija, el nombre
del operador puede aparecer en medio de los operandos a los que aplica.
En un término complejo, con varios operadores y operandos, es fundamental poder
determinar los operandos de cada operador. El “árbol sintáctico” representa de forma
gráfica los operandos que corresponden a cada operador.
Mientras que en notación estándar no se presentan ambigüedades al reconocer los
operandos de cada operador, en notación infija es necesario usar ya sean términos
completamente parentetizados, o contar con un orden de precedencia entre los operadores,
un sentido de asociatividad para cada operador, y un orden de precedencia entre los
sentidos de asociatividad para evitar las ambigüedades.
Los operadores se asocian a funciones definidas en el dominio de interpretación. Los
términos permiten a los predicados referirse a los elementos del dominio de interpretación a
través de las relaciones que tienen con otros términos. Las propiedades de estas relaciones
pueden igualmente ser especificadas por medio de predicados sobre términos.
El predicado de igualdad permite afirmar que dos términos que pueden ser sintácticamente
diferentes deben ser considerados iguales desde el punto de vista semántico. Las tres
propiedades que caracterizan el predicado de igualdad son la reflexión, la transitividad y el
principio de substituibilidad.
A una lógica de predicados basada en el predicado de igualdad se le denomina “Lógica
Ecuacional”. La lógica ecuacional adiciona cinco criterios de demostración a la lógica de
predicados, a saber: la ley reflexiva, la ley simétrica, la ley transitiva, la ley de
sustituibilidad de funciones, y la ley de particularización de variables a términos.
Una teoría en lógica ecuacional está compuesta de declaraciones de sorts, declaraciones de
operadores, axiomas ecuacionales, y teoremas ecuacionales. Las demostraciones en lógica
ecuacional se basan en construcción de derivaciones. Las derivaciones son secuencias de
términos que pueden demostrarse iguales por razonamiento ecuacional.

4.5 Ejercicios propuestos.


1. Árbol sintáctico en notación estándar.
Sea Σ={F, G, H, I} el conjunto de operadores de un alfabeto, con 3, 2, 1 y 0 argumentos
respectivamente.
a. Dibuje el árbol que representa el siguiente término:
t = G(F(I,G(x,H(I)),F(y,H(z),I)),H(G(x,G(I,z))))
b. Haga una lista de los operandos que aparecen en el término.

2. Árbol sintáctico en notación infija completamente parentetizado.


3. Árbol sintáctico en notación infija con precedencia y asociatividad entre operadores.
4. Variaciones al ejercicio anterior.
Capítulo 4: Lógica Ecuacional
77
5. Definición de variables en C.
6. Un campo, se define en matemáticas como un conjunto F dotado de 2 operaciones, la
operación “+” y la operación “*”, que cumplen 7 axiomas conocidos como los axiomas de
campo.
Una teoría en lógica ecuacional para campos es la siguiente:

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.

f. Demostrar que –(a+b) = -a+(-b).


Capítulo 5
Sistema de Reescritura de
Términos (SRT)
Capítulo 5: Sistemas de Reescritura de Términos.
80
5.1 Introducción.
En el Capítulo 4 se introdujeron los conceptos de término y de igualdad entre términos, y
con base en ellos los conceptos básicos de la lógica Ecuacional. Se mostró que una
demostración en está lógica, se apoya en la construcción (o derivación) de una secuencia de
términos sintácticamente diferentes que pueden demostrarse iguales (semánticamente) por
razonamiento ecuacional. Una derivación parte, en efecto, de un término inicial que es
reescrito a otro diferente, y este a otro nuevo, y así sucesivamente hasta llegar al término
final deseado.
Desde la óptica de la computación, una derivación en lógica Ecuacional puede verse como
un proceso que transforma un conjunto de datos, contenidos en el término inicial, a un
conjunto de resultados, contenidos en el término final. En consecuencia, si se automatiza el
proceso de derivación, es posible asimilar una teoría en lógica Ecuacional a un programa de
ordenador, y una derivación en dicha teoría a una ejecución del programa.
En este capítulo se presenta una manera sencilla de automatizar la derivación en lógica
Ecuacional. Para ello presentaremos el concepto de Sistema de Reescritura de Términos, o
por simplicidad SRT. Un SRT es, en efecto, una teoría en lógica Ecuacional que satisface
ciertas propiedades permitiendo automatizar algunos de los procesos de demostración en la
teoría.
En las secciones que siguen, se discute primero el carácter y alcance que tiene la
automatización del proceso de derivación que da soporte a los SRTs, para luego presentar,
en términos de la teoría de lenguajes, los conceptos que constituyen el soporte teórico de
dicha automatización.
En los capítulos siguientes mostraremos un conjunto de lenguajes declarativos, que, bajo
diversas formas sintácticas, permiten definir SRTs. A estos lenguajes se les conoce de
forma genérica como lenguajes Funcionales.

5.2 Alcance de los SRTs.


Antes de presentar los conceptos relativos a los SRTs, es importante señalar los diferentes
enfoques y grados de automatización del proceso de derivación posibles, así como los
beneficios que de ellos se derivan. Con ello el lector podrá comprender mejor las
limitaciones y usos de los lenguajes funcionales cubiertos en el texto.
El factor que más influye en el carácter y utilidad de una automatización del proceso de
derivación, es la manera como se automatizan los pasos del proceso que dependen más
fuertemente de la intuición del matemático. En efecto, una automatización adecuada de
estos pasos estaría potenciando la intuición del matemático, y una automatización
inadecuada la estaría coartando.
Los pasos de la derivación más ligados a la intuición del matemático, son aquellos en los
que debe seleccionar, entre las varias posibles formas de continuar el proceso, aquella que
conduce de forma más rápida al resultado deseado. Si se examina cuidadosamente una
derivación, podrá verse que la determinación del término siguiente a uno cualquiera en la
derivación es uno de dichos pasos. Esto se debe a que el término que sigue a otro en la
derivación, no es, en general, señalado de forma única por el razonamiento ecuacional,
Capítulo 5: Sistemas de Reescritura de Términos
81
dejando al matemático la decisión de seleccionar el adecuado. Para ver esto es suficiente
con notar dos puntos importantes, a saber:
• La obtención de un nuevo término de la secuencia se apoya en la selección de
un axioma (o teorema), una particularización y un subtérmino del término
actual, tales que al aplicar la particularización al axioma (o teorema), uno de sus
lados sea idéntico al subtérmino seleccionado.
• En general, existen múltiples tercetas axioma (o teorema) / particularización /
subtérmino que cumplen con esta condición.
La selección adecuada de dicha terceta, y en consecuencia del término siguiente en cada
paso de la derivación es, además, un factor crítico para el éxito del proceso. En efecto, de
no escogerse correctamente dicho término, la derivación puede tomar un rumbo que no
conduce al término final deseado, o tomar un rumbo que conduce a un término intermedio
ya obtenido antes convirtiéndose en un proceso cíclico interminable.

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:

Derivación Axioma Substitución Particularización


- -X (1) X ← - -X - -X + 0 = - -X
- -X + 0 (3) X←X -X + X = 0
- -X + (-X + X) (1) X ← - -X; - -X + 0 = - -X
(- -X + 0) + (-X + X) (4) X ← (- -X + 0); (- -X + 0) + (-X + X) =
Y ← -X; ((- -X + 0) + - X) + X
Z←X
((- -X + 0) + - X) + X (1) X←X 0+X=X
((- -X + 0) + - X) + ( X + 0 )

O un camino cíclico trivial como el que se muestra a continuación:

Derivación Axioma Substitución Particularización


- -X (1) X ← - -X - -X + 0 = - -X
- -X + 0 (1) X←--X - -X + 0 = - - X
- -X (1) X ← - -X - -X + 0 = - -X
- -X + 0 (1) X←--X - -X + 0 = - - X
- -X

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.

5.3 Especificación en Lógica Ecuacional Multisort.


Una especificación en una Lógica Ecuacional Multisort {(S,∑),X,E}, que en lo que sigue se
denominará “especificación multisort”, está compuesta por un alfabeto (S,∑) denominado
“signatura multisort”, un conjunto de variables X con los que se define un lenguaje ΓΣ(X)
cuyas frases se denominan “términos”, y un conjunto de predicados de igualdad E,
definidos sobre los términos, que se denominan “ecuaciones”.
En lo que sigue se describen en detalle los elementos de la especificación junto con sus
relaciones, y se presentan los criterios formativos de cada una de sus construcciones.
5.3.1 Signatura Multisort.
Una signatura Multisort (S, ∑ ) , está compuesta por los elementos siguientes:
• Un alfabeto compuesto por dos conjuntos de símbolos: Un conjunto de símbolos
de sort S y un conjunto de símbolos de operación ∑ .
• Una relación definida sobre los símbolos del alfabeto, que asocia varios
símbolos de sort a cada símbolo de operación.
La relación entre los símbolos del alfabeto, define una clasificación de los símbolos de
operación en familias (no necesariamente disjuntas45) indexadas por los elementos del
conjunto S*×S, así:
∑ = { ∑s,s | s∈S*, s∈S }
Para un operador σ∈∑s,s se dice que su rango es <s,s>, su aridad es s, y el sort de su valor
o coaridad es s.
DEF:
Constante –Una constante es un operador cuyo rango es <ε,s>, donde ε representa
la cadena vacía en S*.

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í:

Operador Rango Aridad Coaridad


- < int , int > int int
+ < int int , int int
int > int
0 < ε , int > ε int
s < int , int > int int

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

Cuando sea necesario hacer referencia al conjunto de variables involucradas en un


término, estos se representarán por expresiones de la forma (x:s)t.
donde t∈ΓΣ(X), y todas las variables representadas en (x:s) son subtérminos de t y

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 }

Sea la relación entre los conjuntos X y S , la que sigue:

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.

En el marco de un SRT, todas las ecuaciones simples (xl)l=(xr)r y condicionales (xl)l=(xr)r


if (xu1)u1=(xv1)v1,.... (xun)un=(xvn)vn de la especificación multisort, deben satisfacer la
condición siguiente:
(xr ∪ xu1 ∪ xv1.∪.xun ∪ xvn) ⊂ (xl)l
Capítulo 5: Sistemas de Reescritura de Términos
87
En otras palabras en el lado derecho de la ecuación (incluyendo la parte condicional) no
deben aparecer variables que no estén en el lado izquierdo. Esta condición garantiza que un
término base, a ser calculado por reescritura, se transforma siempre a otro término base
durante la derivación.

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))

Si l=r ó es una ecuación simple de la especificación y σ, es una sustitución, a la ecuación


σl=σr se le denomina una particularización de la ecuación l=r . De igual manera si l=r if
u1=v1,....un=vn es una ecuación condicional de la especificación, a la ecuación σl=σr if
σu1=σv1,.... σun=σvn se le denomina una particularización de la ecuación l=r if
u1=v1,....un=vn. Nótese que por los criterios de demostración de la lógica ecuacional (ver
4.3.2 ), la aserción representada por una particularización de una ecuación, es consecuencia
lógica de la aserción representada por la ecuación.

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)

Emparejamiento: Dado un término l∈ΓΣ(X) de una especificación multisort y un término


cualquiera t∈ΓΣ(Y) de la especificación, decimos que l empareja con t, si existe una
substitución σ tal que la σl=t pueda demostrarse en el marco de la especificación. Nótese
que si σl es el mismo t (σl≡t), la demostración es trivial por aplicación del criterio de
reflexión (ver 4.3.2 ).

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)))

5.4.2 Ocurrencias y Reemplazos.


Un término que sigue a otro en una derivación, es obtenido del anterior substituyendo uno
de sus subtérminos por otro término del mismo sort. Una manera de hacer referencia al
subtérmino que es substituido es usar una ocurrencia.
Una ocurrencia es una secuencia de enteros que identifica un subtérmino de un término,
señalando el camino que debe recorrerse en el árbol sintáctico, partiendo de la raíz, para
llegar al nodo correspondiente al operador principal del subtérmino. Los números enteros
de una ocurrencia indican el camino al subtérmino, señalando, en cada nodo del árbol
sintáctico (del término original), la rama que debe transitarse para llegar al nodo (o
subtérmino) deseado. Así, el primer número de la secuencia indica cual de las ramas que
parte de la raíz debe transitarse llegando a otro subtérmino, el número siguiente indica la
rama del nodo correspondiente al subtérmino que debe transitarse, y así sucesivamente. Al
agotarse los números se habrá llegado a un subtérmino específico (o a un nodo con el
operador principal de dicho subtérmino).
DEF:
Ocurrencia – Una ocurrencia n un término es una secuencia de números naturales,
que denotan un camino en el árbol sintáctico del término.
NOMENCLATURA:
Si t es un término y u una ocurrencia, se denotará por t|u al subtérmino de t que se
encuentra al recorrer en el árbol el camino denotado por u. Con el símbolo ε
denotaremos la ocurrencia vacía y por tanto t|ε denota a t.

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))))

5.4.3 Relaciones de Reescritura entre términos de la especificación.


Reescritura: En un SRT se dice que un término t reescribe a un término t’ usando la
ecuación l=r, (o la ecuación l=r if u1=v1,....un=vn) si hay una ocurrencia u, tal que el
subtérmino t|u empareja con l bajo una substitución σ , (t|u = lσ), y t’ es el término
resultante de remplazar en t el subtérmino t|u por el término σl, (t’= t[u ← rσ]). Para el
caso de las ecuaciones condicionales se debe satisfacer además la condición de que las
aserciones representadas por las ecuaciones u1=v1,....un=vn puedan demostrarse ciertas en el
marco de la especificación47.

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:

A(S(0), 0) →R S(0) (con u=ε y σ(x)=S(0))


M(x,S(A(M(x,0),0))) →R M(x,S(M(x,0))) (con u=2.1 y σ(x)=M(x,0))

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.

5.5 Semántica por Clases de Equivalencia.


La semántica de una especificación multitipo esta dada en el contexto de las álgebras
multisort con signatura.
Una álgebra multisort con signatura (S,∑) consiste en un conjunto de soporte As por cada
sort s∈S y una función As,sσ : As→As por cada símbolo de operación σ∈∑s,s... En un
álgebra multisort con signatura, es posible definir el significado de un término de la
signatura y establecer si se satisface, o no, una ecuación cualquiera de la signatura.
La semántica básica de una especificación multisort (S, ∑, E) esta dada por el conjunto de
álgebras multisort con signatura (S,∑) que son modelo de las ecuaciones E. Las álgebras se
relacionan entre sí por medio de homomorfismos que permiten proyectar un álgebra en otra
preservando la estructura. La semántica inicial de la especificación (S, ∑, E) está dada por
una álgebra inicial, módulo homomorfismos, de las álgebras de la semántica básica de la
especificación.
Una posible representación concreta de una de tales álgebras, Γ∑,E puede obtenerse del
álgebra de términos base Γ∑ de la signatura, imponiéndole una relación de congruencia
basada en el significado de los términos base, en las álgebras de la semántica básica de la
especificación. Bajo esta congruencia dos términos base cualquiera son considerados
equivalentes (semánticamente) si tienen el mismo significado en todas las álgebras de la
semántica básica. El álgebra formada por las clases de congruencia constituye en un
álgebra inicial de la especificación.

5.6 Resumen del capítulo.


Si se automatiza el proceso de derivación, las teorías en la lógica ecuacional pueden ser
consideradas como programas, y las derivaciones en dichas teorías como ejecuciones de
dichos programas. Los Sistemas de Reescritura de Términos constituyen una
automatización adecuada de la derivación en lógica Ecuacional, y son la base de los
lenguajes funcionales.
El factor de mayor trascendencia al automatizar la derivación, es la manea como se
remplaza la intuición del matemático al momento de decidir la secuencia de términos de la
derivación. Los SRTs optan por hacer esta decisión innecesaria primero, usando los
axiomas como reglas de reescritura en un único sentido, y segundo, limitando al
programador a usar sólo teorías en las que no puedan existir secuencias infinitas de
derivación (o terminancia) ni secuencias divergentes de derivación (o confluencia). A estas
dos propiedades de la teoría se les conoce como asumpciones de Church-Rosser.
Capítulo 5: Sistemas de Reescritura de Términos
93
Formalmente una especificación en una Lógica Ecuacional Multisort, o especificación
multisort, {(S,∑),X,E}, está compuesta por la signatura multisort (S,∑), las variables X, los
“términos” ΓΣ(X), y las “ecuaciones” E.
La signatura Multisort, provee unos símbolos de sort S, unos símbolos de operación∑, y
una relación entre ellos que define el sort de los operandos, o aridad del operador, y el sort
del valor resultante de su aplicación, o coaridad del operador. Las constantes son
operadores que tienen como aridad el valor ε (no tienen operandos).
Las variables X son un conjunto finito de símbolos x1,x2,x3,....,xn cada uno asociado a cada
símbolo de S denominado su tipo.
Los términos son las frases de un lenguaje, ΓΣ(X), definido sobre el alfabeto {(S,∑),X} que,
al igual que las variables, tienen un tipo. Los criterios formativos de los términos son los
siguientes: 1- Las variables son términos. 2-Las constantes son términos. 3-Los símbolos
de operación aplicados a términos son términos.
Las ecuaciones E pueden ser simples o condicionales. Las ecuaciones simples son
construcciones de la forma l=r, donde l y r son términos del mismo sort. 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. Las ecuaciones simples representan predicados de igualdad sobre
términos. Las ecuaciones condicionales representan predicados de la forma l=r ⇐ (u1=v1 ∧.
u2=v2... ∧ un=vn). Ambos tipos de predicados están cuantificados universalmente en las
variables de sus términos.
La aplicación de las ecuaciones como reglas de reescritura sobre términos esta definida y
regulada por los conceptos de substitución, particularización, emparejamiento, y
reemplazo. Una substitución mapéa un conjunto de variables a términos del mismo sort;
una substitución de un término es una aplicación de una substitución a sus variables. Una
particularización de una ecuación es otra ecuación obtenida aplicando una substitución a
los términos de la primera. Un término empareja a otro si se le puede aplicar una
substitución que lo haga igual al otro. Un reemplazo es un término obtenido de otro
cambiando uno de sus subtérminos por otro del mismo sort.
En un SRT un término está en una relación de reescritura con otro término, bajo una
ecuación del SRT, si es un reemplazo obtenido del otro, cambiándole un subtérmino que
empareja con el lado derecho de la ecuación, bajo una cierta substitución, por el lado
izquierdo de la particularización de la ecuación asociada a la misma la substitución.
En una derivación efectuada con un SRT el término que le sigue a otro tiene con él una
relación de reescritura. El SRT selecciona como siguiente uno cualquiera entre los que
tienen con él dicha relación. Las propiedades de terminancia y confluencia de la
especificación garantizan que cualquier selección sea apropiada.
La semántica básica de una especificación multisort ((S,∑), E) esta dada por el conjunto de
álgebras multisort con signatura (S,∑) que son modelo de las ecuaciones E.

5.7 Ejercicios propuestos.


2. Sea Σ={F, G, H, I} el conjunto de operadores de un alfabeto, con 3, 2, 1 y 0 argumentos
respectivamente.
Capítulo 5: Sistemas de Reescritura de Términos.
94
c. Dibuje el árbol que representa el siguiente término:
t = G(F(I,G(x,H(I)),F(y,H(z),I)),H(G(x,G(I,z))))
d. Determine si los siguientes son subtérminos de t y en caso afirmativo especifique en
que ocurrencia aparecen: G(I,G(H(I))); F(y,H(z),I)); G(I,z); H(z,I); y.
e. Muestre el resultado de realizar los siguientes reemplazos: t[2.1←H(I)]; t[[Link]←
G(I,I)]; t[1← x];
f. ¿Existe alguna diferencia real entre lo que hace una sustitución y el resultado de un
reemplazo?, si su respuesta es afirmativa, ¿cual?
3. Dado el conjunto de reglas de reescritura R={G(x,x)→x, H(H(x))→H(x), H(I)→I} para Σ
definido en el ejercicio 2, y los términos t = G(H(H(I)),H(x)), s = G(H(H(I)),H(I)):
a. Obtener el conjunto de todas las ocurrencias de s y t.
b. Determinar cuales de tales ocurrencias pueden ser reemplazadas, utilizando cual
regla y bajo que substitución.
c. Obtener todas las secuencias de reescritura a partir de t y de s.
d. ¿Podría usted determinar si el SRT conformado por (Σ, R), es terminante y/o
confluente?
4. Sea Σ={A, M, S, 0} el conjunto de operadores de un alfabeto, con 2, 2, 1 y 0 argumentos
respectivamente (A y M son operadores binarios, S es unario y 0 es una constante). Los
siguientes elementos pertenecen a Ter(Σ): 0; S(0); A(x, 0); A(M(x,y),y); A(M(x,y),z);
M(x,S(A(y,0))).
Considere el SRT formado por el conjunto de operadores Σ={A, M, S, 0}, con 2, 2, 1, y 0
argumentos respectivamente (sección 5.4.1), y las siguientes reglas de reescritura:
1)
A(x,0) → X
2) A(x,S(y)) → S(A(x,y))
3) M(x,0) → 0
4) M(x,S(y)) → A(M(x,y),x)
a. Muestre, paso a paso, como se puede reescribir el término t =
M(S(S(0)),A(S(0),S(0)), hasta llegar a S(S(S(S(0)))).
b. Podría usted encontrar otra deducción a partir del término t, para obtener el mismo
resultado que en a.
c. ¿Podría usted determinar si éste SRT es confluente?
d. ¿Podría usted determinar si éste SRT es terminante?
e. ¿Podría usted encontrar la relación entre el presente SRT y la aritmética de números
naturales?
Capítulo 6
Elementos Básicos de Los
Lenguajes Funcionales.
Capítulo 6: Elementos básicos de los Lenguajes Funcionales.
96
6.1 Introducción
En el capítulo anterior se presentaron formalmente los Sistema de Reescritura de Términos
(o SRT) como un enfoque a la automatización de la derivación, que permite utilizar una
teoría lógico ecuacional como un programa de ordenador. Allí se mostró que en un SRT,
los axiomas de la teoría son utilizados como reglas unidireccionales de reescritura que se
aplican de forma automática a cada uno de los términos en una cadena de derivación para
generar el término siguiente.
En este capítulo se introducen varios lenguajes de programación que pueden considerarse
como implementaciones particulares de un SRT. En efecto, los programas escritos en estos
lenguajes, pueden asimilarse a un conjunto de axiomas en una lógica ecuacional y, la
ejecución de estos programas, como un proceso de derivación que se desenvuelve usando
de forma automática los axiomas como reglas unidireccionales de escritura.
En las secciones que siguen se presentan el enfoque y los elementos básicos de cada uno de
los lenguajes analizados. La presentación se hará siempre de forma comparativa
introduciendo primero cada concepto de forma abstracta, para mostrar luego su
implementación en cada uno de los lenguajes. Con ello el lector no sólo podrá apropiarse
de los conceptos sin atarlos a una forma sintáctica específica, sino que podrá observar las
ventajas y desventajas relativas de los diferentes lenguajes y sus implementaciones.

6.2 Origen y Enfoque general de los lenguajes


seleccionados.
Los diferentes lenguajes funcionales en uso han surgido en diferentes contextos históricos y
geográficos, que de alguna manera determinan su carácter particular. A continuación se
presentan estos contextos para los lenguajes funcionales que usados en el texto.
6.2.1 LISP
El lenguaje LISP (LISt Processing) es el más antiguo de los lenguajes funcionales y
comparte con el FORTRAN el honor de ser el más antiguo de los lenguajes de
programación de alto nivel en uso.
Tal como nos lo presenta McCarty mismo [McC 97], los elementos del LISP fueron
concebidos por John McCarthy durante el verano de 1956 en el marco de un proyecto de
investigación en inteligencia artificial del Colegio Dartmouth48. McCarthy estaba
interesado en un lenguaje de programación apto para trabajar directamente con
representaciones simbólicas del mundo como alternativa a un lenguaje orientado a describir
cálculos matemáticos. Este lenguaje, según concebía McCarthy, estaría soportado en frases
de un formalismo lógico-matemático y en un programa capaz de llevar a cabo
razonamiento sobre dichas frases.
Es claro de la descripción de McCarty que el LISP fue influenciado tanto por los elementos
recién introducidos en el FORTRAN, orientado a llevar a cabo cálculos matemáticos, como
por las facilidades para el procesamiento de listas mostradas en el marco del programa

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.

6.3 Implementaciones e interfaces.


Tanto para poder escribir los programas funcionales como para ejecutarlos, es necesario
contar con un programa que reciba las instrucciones del programa, verifique su consistencia
y lo convierta a un programa ejecutable compilándolo o interpretándolo. Estos programas,
que denominaremos indistintamente como las “implementaciones” o los “interpretes” del
lenguaje, deben proveer mecanismos efectivos de comunicación programa-usuario e
interprete-programador. A estos mecanismos los denominaremos las “interfaces” del
interprete y del programa.
En lo que sigue caracterizaremos brevemente dichas interfaces para presentar luego las que
ofrecen las implementaciones utilizadas en este trabajo
6.3.1 Interfaz del programa.
Los lenguajes funcionales son fundamentalmente lenguajes orientados a definir programas
de cálculo. En efecto, los programas escritos en lenguajes funcionales, son básicamente
artefactos que le ofrecen al usuario la posibilidad de someter al computador, de forma libre,
términos a ser calculados.

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 ]=> (define (cua X) (* X X))


;Value: cua
1 ]=> (define a 1)
;Value: a
Un término a ser calculado se somete al SCHEME en notación polaca inversa:

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 :

1 ]=> (load “[Link]”)


;Loading “[Link]” -- done
;Value: sqrt
De no existir el archivo, el comando es respondido con un cambio a un estado de error.

1 ]=> (load “aaaa”)


;Unable to find file “aaa” because: File does not exist.
Capítulo 6: Elementos básicos de los Lenguajes Funcionales.
102
;To continue …
;…
2 error>
Para revertir el estado de error, el usuario debe pulsar la combinación de teclas <control>g)
Los comandos siguientes solicitan cambiar el directorio de trabajo y la carga de un archivo:
1 ]=> (cd “MIT Scheme”)
;Value: 9 #[pathname “e:\\...\\mit scheme”]
1 ]=> (load “[Link]”)
;Loading “[Link]” -- done
;Value: sqrt

[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]:

Maude> fmod SIMPLE-NAT is


sort Nat .
op zero : -> Nat .
op s_ : Nat -> Nat .
op _+_ : Nat Nat -> Nat .
vars N M : Nat .
eq zero + N = N .
eq s N + M = s (N + M) .
endfm
Un término a ser calculado se somete al Maude 2 a través del interfaz:

Maude> reduce in SIMPLE-NAT : s s zero + s s s zero .


reduce in SIMPLE-NAT : s s zero + s s s zero .
rewrites: 3 in 0ms cpu (0ms real) (~ rews/sec)
result Nat: s s s s s zero
Capítulo 6: Elementos básicos de los Lenguajes Funcionales
103
En esta interacción las líneas que comienzan con Maude> corresponden a ingresos de texto por parte
del usuario y las demás líneas corresponden a respuestas del sistema.
Los módulos de programa pueden incluirse, también, desde un archivo:

Maude> load [Link]

6.4 Operadores nativos y Expresiones simples.


Todos los intérpretes considerados en este trabajo, ofrecen un conjunto de operadores ya
implementados, que el usuario puede usar para llevar a cabo cálculos. A estos operadores
los denominaremos en lo que sigue operadores “nativos”. Aunque el conjunto de
operadores nativos es una característica propia de cada implementación, todas las
consideradas en este trabajo ofrecen operadores para llevar a cabo cálculos con números
enteros, números reales y cadenas de texto.
La sintaxis de los términos a ser formados con estos operadores varía, sin embargo, según
sea el lenguaje considerado. Dentro de cada lenguaje, por otro lado, existen regularidades
sintácticas que deben respetarse no sólo para los operadores nativos, sino también para los
operadores definidos por el usuario. Estas características pueden ilustrarse fácilmente por
medio de ejemplos simples de cálculo en el marco de los operadores nativos.
En esta sección presentaremos de forma verbal los elementos básicos de la sintaxis de cada
lenguaje y los ilustraremos con el uso de operadores elementales.
6.4.1 SCHEME
Una característica del LISP es el uso generalizado de listas de componentes separados por
espacios en blanco y encerrados en paréntesis. Las componentes de una lista pueden ser
ítems elementales o una lista. Los ítems elementales no van entre paréntesis.
En consecuencia, todos los textos del SCHEME están constituidos por listas de listas de
ítems elementales, encerradas (las listas) en paréntesis.
El primer elemento de una lista (el que sigue al paréntesis de apertura) es, además
considerado como un operador que se aplica a los demás elementos de la lista que
constituyen sus operandos. Esta notación es conocida como notación “prefija” o notación
“Polaca”.
La notación prefija de LISP tiene la ventaja de que el operador se puede aplicar a varios
operandos de manera natural. Además la estructura de los términos es fácilmente
descifrable para el intérprete o compilador. Es, sin embargo, difícil de leer para los
humanos, y no es raro encontrar errores lógicos derivados de la colocación equivocada de
paréntesis.

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>.

(or <e1> <e2>...<en>)


(not <e>)
Estas Se evalúan, de manera similar al and, pero expresando un OR lógico, y la negación.
A manera de ejemplo, podríamos definir el mayor o igual de cualquiera de estas dos
formas:
Capítulo 6: Elementos básicos de los Lenguajes Funcionales
105
6.4.2 MAUDE
Maude, por otro lado, permite el uso de notación infija con gran flexibilidad, lo que mejora
la legibilidad de los programas. Además su notación prefija, similar a la de los lenguajes
procedurales, completa un cuadro muy amplio de posibilidades.

6.5 Ejercicios Propuestos.


Capítulo 7
Operadores Definidos
Capítulo 7: Operadores Definidos
108
7.1 Introducción
En el capítulo anterior se presentaron los lenguajes funcionales que serán analizados en este
trabajo. Se indicó, además, que sus respectivas implementaciones ofrecían una serie de
operadores nativos que pueden ser usados por el usuario para llevar a cabo cálculos.
Si dichas implementaciones ofrecieran sólo estos operadores, no tendrían más poder que la
de una calculadora de bolsillo. La potencia real de los lenguajes declarativos, en efecto,
surge de las facilidades que tienen para proponer nuevos operadores y nuevos tipos de dato
con sus operadores asociados.
En éste capítulo se presentan los elementos básicos disponibles por los diferentes lenguajes
para proponer nuevos operadores. En principio, nos ocuparemos del planteamiento de
operadores sencillos en el marco de los tipos de datos nativos al lenguaje. Dejaremos para
capítulos posteriores el problema del planteamiento de nuevos tipos de dato y de sus
operaciones asociadas.
Con base en el planteamiento de estos operadores sencillos, sin embargo, se discutirán
asuntos importantes como la definición del operador, el operador de selección, el manejo de
los tipos, el medio ambiente de evaluación y la asignación, las estrategias de evaluación, y
la estructuración de los programas.
Aunque la discusión se extiende a las características particulares de cada lenguaje, contará
con un factor unificador consistente en asimilar la definición de los operadores al
planteamiento de axiomas en el marco de una teoría lógica-ecuacional.

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.

7.3 Definición de los operadores.


Una pregunta esencial en el planteamiento de un operador es la de como indicarle al
intérprete lo que debe hacer para llevar a cabo el cálculo de un término basado en un
operador definido por el usuario. En otras palabras la pregunta es: ¿como indicarle al
intérprete el significado de un operador definido?.
Capítulo 7: Operadores Definidos
110
En los lenguajes funcionales la respuesta a este problema es muy sencilla, asì:
• El intérprete debe remplazar el término que involucra al operador definido, por
otro término diferente (que puede incluir el mismo u otros operadores
definidos), y proceder a remplazar, de forma sucesiva, los tèrminos resultantes
en el remplazo, hasta llegar a un tèrmino final que involucre solo operadores
nativos.
• El término que debe remplazar la evocación del operador es derivado del que
aparece al lado derecho de un axioma de igualdad de la forma t1 = t2, que tiene
al lado izquierdo un término que empareja con la evocación del operador (ver
[Link]). Antes del remplazo, al término del lado derecho se le deben aplicar las
substituciones de variables que fueron requeridas para el emparejamiento.
En otras palabras en un lenguaje funcional, el significado de un operador definido en el
programa se indica con axiomas de igualdad, y el inicio del cálculo no es otra cosa que la
aplicación de un paso de derivación en una demostración en lógica ecuacional (ver [Link]).
A continuación mostraremos en cada lenguaje las construcciones con las que se definen los
operadores, asimilándolas a una forma particular de un axioma de igualdad. Señalaremos
en cada caso las extensiones y restricciones que el lenguaje impone a dicho axioma y las
consecuencias de las mismas.
7.3.1 Definición de Operadores en SCHEME.
En el lenguaje SCHEME la capacidad de definir operadores se asocia a la capacidad de
rotular elementos del programa, o “definir” el significado de los rótulos55 usados en el
programa [Abelson 1985 1.1.2].

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 ]=> (define pi 3.141542)


;Value: pi
Es posible usar el rótulo como un substituto del valor definido:

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:

1 ]=> (cua 10)


;Value: 100.

[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 ]=> (define (sum_cua x y) (+ (cua x) (cua y))


;Value: sum_cua
Se usa cua para definir el operador sum_cua.:

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:

1 ]=> (define (sum_cua x y) (+ (cua x) (cua y)))


;Value: sum_cua
1 ]=> (sum_cua 3 4)
;Value: 25
1 ]=> (define (sum_cua x y) (* (cua x) (cua y)))
1 ]=> (sum_cua 3 4)
;Value: 144

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().

7.4 Selección funcional.


Los operadores binarios nativos presentados en el capítulo anterior, representan funciones
elementales aplicables a todos los elementos del dominio correspondiente. Con estos
operadores y la posibilidad de definir términos complejos, es posible definir operadores
asociados con funciones más complejas (que componen funciones elementales) que son
aplicables a todos los elementos del dominio resultante de la composición.

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))

7.4.2 Selección en MAUDE.


El lenguaje MAUDE ofrece diversos mecanismos de selección que incluyendo operadores
de selección y selección entre axiomas. En este numeral se examinan estos mecanismos en
detalle.
[Link] Emparejamiento como mecanismo de selección.
Dado que en MAUDE la definición de un operador puede llevarse a cabo por medio de
varias ecuaciones, es posible asociar cada ecuación a condiciones particulares de los
argumentos llevando a cabo un proceso de reescritura diferente en cada caso.

Ejemplo 57
La función |x| puede definirse en MAUDE con base en la selección asociada al emparejamiento:

eq abs( x ) = abs_aux(x, x > 0.) .


eq abs_aux(x, true) = x .
eq abs_aux(x, false) = - x .
El lector debe notar que las dos ecuaciones usadas para definir la función auxiliar aux() son mutuamente
excluyentes permitiéndole al intérprete seleccionar una de ellas en cada caso. De no ser excluyentes, el
intérprete indicaría la ambigüedad al momento de llevar a cabo la reescritura.

[Link] Ecuaciones condicionales.


En MAUDE es posible asociar cada ecuación a una condición que debe cumplirse para ser
considerada en el proceso de reescritura. Estas ecuaciones denominadas “condicionales” se
definen por medio de construcciones de la forma siguiente:
Capítulo 7: Operadores Definidos
116
ceq < t1 > = < t2 > if < t3 >
donde:
• El término t3 debe evaluar a true para que la ecuación t1 = t2 sea considerada en
un paso de reescritura.

Ejemplo 58
La función |x| puede definirse en MAUDE con base en ecuaciones condicionales:

eq abs( x ) = x if(x > 0.) .


eq abs( x ) = - x if(x <= 0.) .
Nótese de nuevo que las condiciones asociadas a las ecuaciones deben se excluyentes, o de lo contrario
podría presentarse un caso de ambigüedad.

Es por supuesto posible combinar el mecanismo de selección que ofrece el emparejamiento,


con el que ofrece la ecuación condicional.

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.) .

[Link] Operador de selección.


MAUDE ofrece de forma nativa un operador if funcional. La forma general del if en
MAUDE es la siguiente:
if <predicado> then <consecuencia> else <alternativa> fi
Para evaluar una expresión if..fi, 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.

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 .

7.5 Tipo del operador y de sus operandos


Los valores involucrados en un proceso, se almacenan en la memoria del computador bajo
una representación específica (basada en dígitos binarios), y pueden participar en conjuntos
específicos de operaciones. No todos los valores, sin embargo, se almacenan de igual
manera ni pueden participan en las mismas operaciones. Es entonces posible clasificar los
Capítulo 7: Operadores Definidos
117
valores en diversos “tipos de valor”, considerando que todos los valores que se almacenan
de igual manera y pueden participar en las mismas operaciones son de un mismo tipo.
Puesto que todo valor manipulado en un programa debe ser representado, almacenado y
operado de alguna manera, no existen lenguajes atipados.
Algunos lenguajes, sin embargo, obligan al programador a “declarar” el tipo de los valores
que serán asociados con los operadores definidos en el programa. Estos lenguajes se
denominan “fuertemente tipados”. Otros lenguajes, en contraste, le ahorran al
programador el tener que declarar dichos tipos. Estos lenguajes son denominados
“débilmente tipados”. Entre los lenguajes funcionales existen lenguajes fuertemente
tipados y lenguajes débilmente tipados.
Las principales razones para declarar los tipos son:
5. El intérprete puede controlar desde la escritura del programa que los
operadores serán aplicados sólo a los tipos apropiados de operandos.
6. El programador puede reusar los mismos símbolos de operación en
diferentes operadores (o “sobrecargar” los operadores).
7. El lenguaje puede definir nuevos operadores con notación infija.
Los tipos asociados a un operador hacen, por otro lado, parte del perfil del operador (ver C4
sección 2.1). En consecuencia, la manera como se define el perfil de los operadores, se
asocia a la necesidad de declarar o no declarar los tipos.
En esta sección, el problema de la declaración de tipos, se analizará, para los diferentes
lenguajes, con base en los mecanismos que ofrecen para definir el perfil de los operadores
propuestos.
7.5.1 Perfil de los Operadores en SCHEME.
Tal como se indica en [Hanson 2002, secc. 1.3], los tipos en SCHEME son latentes, en
lugar de ser manifiestos, en el sentido de que el tipo se asocia a los valores pero no a las
variables que los representan. En consecuencia, en SCHEME no se declaran tipos
específicos para las variables siendo posible asociarlas a un valor de cualquier tipo.
Esto se traduce en que, al proponer los operadores, el programador no puede indicar ni el
tipo de sus argumentos ni el tipo del resultado de su aplicación. Así, sólo al momento de
llevarse a cabo el cálculo asociado al operador, se verifica que este pueda aplicarse a los
operandos (ya calculados); y, de no ser esto posible, el intérprete genera un mensaje de
error.

Ejemplo 61
La definición siguiente define un operador cuyo resultado no tiene un tipo determinado:

1 ]=> (define (op1 x) (if (= x 1) 1 “abb”))


;Value: op1
1 ]=> (op1 1)
;Value: 1
1 ]=> (op1 2)
;Value: “abb”
Capítulo 7: Operadores Definidos
118
Que posibilita escribir términos que al calcularse podrían dar lugar a errores de ejecución:

1 ]=> (+ 1 (op1 1) )
;Value: 2
1 ]=> (+ 1 (op1 2) )
;The object “abb” passed as the second argument to integer-add, is not the correct type.
; To continue.......

En otras palabras el SCHEME es un lenguaje débilmente tipado.


Por no ser importantes al momento de proponerse un nuevo operador, ni el tipo de los
operandos ni el tipo del valor resultante de su aplicación, el SCHEME no requiere de una
construcción distinta a la definición para definir el perfil de los operadores.
El perfil usado en la evocación de los operadores en SCHEME, por su parte, se ciñe a una
única notación prefija (ver [Hanson 2002, secc. 1.4]) con el formato siguiente:
(<op> <argumentos_actuales>)
Donde:
• <op> es el nombre del operador.
• <argumentos_actuales> es una secuencia de términos, separados por espacios, que
constituyen los operandos del operador.
El número de términos usados como argumentos actuales en la evocación, debe coincidir
con el número de las variables usadas como argumentos formales en la definición. A
efectos del cálculo, cada variable de la definición se asocia con el término de igual posición
en la evocación.
De lo anterior es claro que, La “sobrecarga” de operadores no es sencilla en SCHEME ya
que esta se debe apoyar en el tipo de los argumentos para identificar el operador que esta
siendo evocado.

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:

1 ]=> (define (+ x y) (string-append x y))


;Value: +
1 ]=> (+ 1 1)
;The object 1, passed as an argument to string-append, is not a string.
; To continue.......

Para permitirle a un operador establecer el tipo de sus operandos (y actuar en consecuencia) el


SCHEME ofrece una serie de operadores nativos, así los operadores siguientes:

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.

7.5.2 Perfil de los operadores en MAUDE


Tal como se indicó antes (ver 6.2.2) MAUDE da soporte a una lógica ecuacional de sorts
ordenados. Esto significa que MAUDE considera los valores que manipula como
pertenecientes a conjuntos, denominados sorts, sobre los que se definen funciones
representadas por los operadores. Así, al proponer un nuevo operador, el programador debe
indicar los sorts que constituyen el dominio y el rango de la función asociada.
En otras palabras MAUDE es un lenguaje fuertemente tipado.
[Link] Declaraciones de sort y relaciones de subsort en MAUDE
Los identificadores de sort deben corresponder ya sea a los de los tipos nativos o a los de
los tipos propuestos por el programador. La proposición de tipos de datos en el marco de
un programa será tratada en más detalle en el Capítulo 9.
Los identificadores de los sort propuestos por el programador se deben introducir por
medio de construcciones de la forma siguiente:
sort <identificador_de_sort> .
sorts <lista_de_identificadores_de_sort> .
Donde:
• <identificador_de_sort> Es un identificador que en adelante será asociado a un
sort.
• <lista_de_identificadores_de_sort> Es una lista de <identificador_de_sort>
separados por espacio.
Es posible definir relaciones de contención56 entre los sorts usando la relación de subsort.
La relación de subsort entre dos sorts diferentes, se indica en MAUDE por medio de la
construcción siguiente [Clavel 2007, sec 4.4.3]:
subsort <sort_contenido> < <sort_continente> .
Donde:
• <sort_continente> Es el sort que contiene como subsort a <sort_contenido>.
• <sort_contenido> Es el sort que es contenido como subsort en
<sort_continente>.

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:

op cua : float -> float .


op sumcua : float float -> float .
op abs : float -> float .
vars x y : float .
eq cua(x) = x * x .
eq sumcua(x, y) = cua(x) + cua(y) .
eq abs( x ) = if (x > 0.) then x else (- x) fi .

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:

op op1 : int -> int .


var x : int .
ceq op1(x) = 1 if ( x == 1) .
ceq op1(x) = ´a if ( x =!= 1) 1 .
No es posible, tampoco, definir el operador con base en unos argumentos de tipo equivocado:

var y : string .
ceq op1(y) = 1 .
Ni, por supuesto, evocarlo con argumentos errados:

> rew abs(´a) .

[Link].1 Notación Prefija y Notación Infija


Los identificadores que constituyen la <plantilla> del operador pueden contener ocurrencias
del caracter “_”. La existencia de estas ocurrencias determina que las aplicaciones del
operador podrán usar notación infija (ver 4.2.3). Los “_” en <plantilla> deben coincidir, en
número, con los identificadores de sort en <sorts_dominio>, e indican los lugares en <plantilla>
donde se deben colocar operandos en una aplicación cualquiera del operador. Cada
operando en una aplicación debe, además, pertenecer al sort referido en <sorts_dominio> en
la misma posición del “_” que el operando remplaza en <plantilla>.
Capítulo 7: Operadores Definidos
122
Cuando no hay ocurrencias del caracter “_” en <plantilla>, las aplicaciones del operador sólo
pueden usar notación prefija (ver 4.2.3). En este caso, en una aplicación del operador debe
aparecer primero el nombre del operador y luego, entre paréntesis, una secuencia de
operandos separados por coma. Cada operando debe, además, pertenecer al sort que se
halla referido en <sorts_dominio> en la misma posición que tiene el operando en la
secuencia.

Ejemplo 64
La notación infija permite usar símbolos más adecuados para identificar el operador:

op |_| : float -> float .


vars x : float .
eq |x| = if (x > 0.) then x else (- x) fi .

[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 .

[Link].3 Sobrecarga de Operadores.


En MAUDE, operadores diferentes pueden tener la misma <plantilla>. En este caso el
intérprete identifica el operador, que corresponde a una aplicación, por el tipo de los
operandos. Es decir en MAUDE si es posible sobrecargar los operadores usando la misma
plantilla con diferentes sorts para el dominio y el rango.

Ejemplo 66
Dos operadores distintos pueden tener el mismo símbolo de operación, así:

op _+_ : string string -> string .


vars x y : string .
eq x + y = concat(x,y) .

[Link].4 Atributos Ecuacionales


Dentro de los posibles <atributos_del_operador>, se encuentran los denominados “atributos
ecuacionales”. Los atributos ecuacionales son una manera implícita para declarar ciertos
tipos de axiomas ecuacionales que de otra forma causarían no terminancia en el proceso de
reescritura [Clavel 2007, sec 4.4.1].
Capítulo 7: Operadores Definidos
123
Por ejemplo el atributo ecuacional comm permite especificar que un operador binario es
conmutativo, y que, en consecuencia, el orden de los operandos no afecta el resultado de
aplicar la operación.

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:

op _+_ : Nat3 Nat3 -> Nat3 [comm] .


vars N3 : Nat3 .
eq N3 + 0 = N3 .
La ecuación que sigue es implícita y no es necesario incluirla en la especificación, debido a la propiedad
conmutativa del operador declarada con el atributo ecuacional comm.

eq 0 + N3 = N3 .

En lo que sigue se introducirán otros atributos ecuacionales cuando sean pertinentes al


tópico tratado.
[Link].5 Precedencia y gathering
Dado que en MAUDE es posible definir operadores con notación infija, es muy alta la
probabilidad de que se presenten conflictos al momento de establecer el árbol sintáctico de
los términos (ver [Link] ).
Como alternativa al uso de paréntesis como medio para evitar estos conflictos, MAUDE,
ofrece los dos mecanismos siguientes [Clavel 2007, sec. 3.9]:
• Precedencia: Por medio del atributo del operador prec se puede definir la
precedencia del operador:

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:

op _+_ : Nat Nat -> Nat [prec 33] .


op _*_ : Nat Nat -> Nat [prec 31] .
De no indicarse en la declaración del operador, Maude 2 asigna a la precedencia un valor de defecto.
Para ver la precedencia de los operadores nativos y el valor de defecto remitimos al lector a la referencia
citada arriba.

• “Gathering”: El patrón de gathering permite controlar el valor de la


precededencia de los operadores que sean usados como operandos del operador.
Para ello se usa un atributo del operador que asocia una letra a cada uno de los
operandos del operador, así: E indica que el (operador del) operando debe tener
una precedencia menor o igual que el operador; e indica que el (operador del)
operando debe tener una precedencia menor que el operador; & indica que el
operando pude tener cualquier precedencia.
Capítulo 7: Operadores Definidos
124
Ejemplo 69
En la declaración siguiente [Clavel 2007, sec. 3.9], se le da a los operadores suma y multiplicación un
valor de precedencia y un patrón de gathering determinado:

op _+_ : Nat Nat -> Nat [prec 33 gather (E e)] .


op _*_ : Nat Nat -> Nat [prec 31 gather (E e)] .

7.6 Estrategia de Evaluación.


Dado un término complejo a ser calculado por el intérprete de un lenguaje funcional, cabe
siempre la pregunta siguiente: ¿en que orden se van a aplicar los axiomas asociados con los
(varios) operadores del término durante el proceso de reescritura?. Nótese que este orden
es esencialmente arbitrario ya que el proceso de reescritura puede, en principio, aplicarse a
cualquiera de los subtérminos del término a ser calculado59.
Desde el punto de vista del árbol sintáctico, esto significa que el mecanismo de derivación
debe reescribir los operadores del término complejo en una secuencia específica,
decidiendo, por ejemplo, si reescribe primero los operadores de la parte inferior del árbol, si
reescribe primero los operadores de la parte superior del árbol, si los reescribe en algún
orden diferente predeterminado, o si reescribe varios operadores en forma paralela (o
simultánea).
Para definir este orden, cada implementación usa una “estrategia de evaluación”. Son
estrategias típicas, la de calcular primero los operandos de los operadores antes de abordar
el cálculo de los mismos, que ha sido denominada “evaluación en orden aplicativo”60, y la
de abordar primero el cálculo de los operadores antes de abordar el cálculo de los
operandos, que ha sido denominada “evaluación en orden normal”61 [Abelson 1985 sec.
1.1.3].

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:

eq fun(a,b) = if (a > b) then (a / b + a) else (b / a + b) fi .

> 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:

eq fun(a,b) = if (b != 0) then (a / b) else (9999999999) fi .


Capítulo 7: Operadores Definidos
126
eq fun(4,0) .

Al proceder en orden aplicativo, la ejecución termina de forma anormal, al llevar a cabo la división por
cero, mientras que si se procede en orden 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?.

La manera como se determina el orden de aplicación de los axiomas en el proceso de


reescritura de términos complejos basados en operadores nativos o definidos es, en
consecuencia, un asunto importante en el lenguaje. En lo que sigue examinaremos la
estrategia de evaluación utilizada en cada uno de los lenguajes estudiados e indicaremos el
grado en que el usuario puede influir en ella.
7.6.1 Estrategia de Evaluación en SCHEME.
En general, las expresiones complejas en SCHEME se evalúan en dos pasos, para cada
aplicación de un operador, primero se evalúan todos los operandos (o subexpresiones), y
luego se evalúa el operador con los valores resultantes de la evaluación previa de los
operandos [Abelson 85 sec 1.1.3]. La evaluación de los operandos de un operador se lleva
a cabo en un orden no predeterminado por el lenguaje [.
En otras palabras en SCHEME se usa un orden de evaluación aplicativo, para todos los
operadores nativos y definidos.
Para evitar que esto ocurra en el caso de los operadores de selección, se opta por
considerarlos como “formas especiales”, en lugar de ser considerados como
procedimientos, negándoles, así, el carácter operador en propiedad. Las formas especiales
son nativas al lenguaje y no pueden ser propuestas por el programador. Para cada forma
especial el lenguaje determina, entonces, el orden en que se procede al llevar a cabo la
derivación.
7.6.2 Estrategia de evaluación en MAUDE
Tal como se refiere en [Clavel 2007, sec 4.4.7], MAUDE le permite al programador
determinar la estrategia de evaluación, de forma particular, para cada uno de los operadores
que propone.
La estrategia de evaluación se indica utilizando el siguiente identificador de propiedad
dentro de la sección de <atributos_del_operador> en la especificación del perfil del operador
(ver sec. “Perfil de los Operadores en MAUDE”):
strat( <orden_de_evaluación > 0 )
Donde:
• <orden_de_evaluación > es una lista de enteros, separados por espacio, cada
uno señalando por su posición a uno de los operandos del operador. Los
operandos del operador se reescriben en el orden en que son señalados en la
lista. La lista no requiere señalar a todos los operandos y puede incluso ser
vacía (en cuyo caso no se escribe).
Capítulo 7: Operadores Definidos
127
• 0 señala al operador propiamente dicho e indica el orden en que se debe
reescribir en relación con la reescritura de sus operandos. En efecto, los
operandos no señalados en <orden_de_evaluación > se evalúan luego de la
evaluación del operador.

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.

7.7 Medio ambiente y asignación.


El valor resultante del cálculo de un término sometido al intérprete de un lenguaje
funcional, depende de los operadores y de los valores que componen el término. Tanto los
operadores como los valores, son referidos, en el término, por medio de símbolos o rótulos.
Los símbolos que representan operadores deben corresponder ya sea con los asociados a los
operadores nativos del lenguaje o con los asociados a los operadores propuestos por el
programador.
Los símbolos que representan valores deben asociarse a valores específicos de los dominios
sobre los que operan los operadores. En otras palabras los términos a ser evaluados deben
ser términos base de la teoría. Así, en principio, estos símbolos deben ser símbolos literales
del lenguaje que, como se explico en el capítulo anterior, son las constantes de la teoría
lógico ecuacional asociada con el programa.
En algunos lenguajes es posible, sin embargo, representar valores en el término por medio
de símbolos diferentes a los literales del lenguaje. Estos símbolos son tradicionalmente
denominados como “variables” en el lenguaje en un sentido similar al utilizado en los
lenguajes procedurales62. Para que sea posible usar una variable en un cálculo, sin

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 ]=> (define pi21 )


;Value: pi21
Pero usarlo sin que tenga un valor asociado es un error:

1 ]=> (* 2 pi21)
;Unassigned variable: pi21.
; To continue.......

[Link] Cambio del Significado de un Símbolo.


A un símbolo que ya pertenece al medio ambiente se le puede reasociar un nuevo valor por
medio de una forma especial de asignación [Hanson 2002, sec. 1.2.3].

Ejemplo 76
A la variable definida pero no asignada del ejemplo anterior se le puede asociar un (nuevo) valor con la
forma especial set!.

1 ]=> (set! pi21 (* 2 pi))


;Unespecified return value
1 ]=> pi21
;Value: 6.28

Que no puede, sin embargo, ser usada para aumentar el medio ambiente

1 ]=> (set! pi212 (* 2 pi))

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.......

[Link] Medio ambiente de una evocación.


Al llevarse a cabo el cálculo de la evocación de un operador, los operandos se evalúan en el
medio ambiente en que se llevó a cabo la evocación. Luego de evaluados los operandos, se
reescribe la evocación usando el (único) axioma asociado al operador. El término
resultante de la reescritura se calcula, a su vez, en el medio ambiente en que se definió el
operador extendido con la asociación de las variables que constituyen los argumentos
formales a los valores de los operandos ya evaluados (ver [Hanson 2002, sec. 1.2.4]).

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))

Se lleva a cabo en el medio ambiente E1 = {x/4, y/2}


Cuando se usa el operador definido los, operandos se evalúan en el medio ambiente de la evocación.
Así, en la evocación siguiente:

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

[Link] Extensiones locales del medio ambiente durante el cálculo.


Capítulo 7: Operadores Definidos
131
Si bien el medio ambiente en el que se reescribe una evocación de un operador es el
resultante de extender el medio ambiente de su definición, con los argumentos formales
valorados, la reescritura misma del operador puede extender aun más dicho medio
ambiente.
Esto ocurre cuando se usan las formas especiales let, let* y letrec, al definir el operador.
Estas formas especiales son, en efecto, operadores que pueden ser usados para formar las
expresiones que constituyen el <cuerpo> de la definición del operador (es decir el lado
derecho del axioma).
El perfil de estos tres operadores es idéntico y se ajusta a la forma general siguiente (ver
[Hanson 2002, sec. 2.2]:
(let ((<simbolo> <valor>) ..... ) <cuerpo>)
Donde:
• <simbolo> es un símbolo a ser introducido en el medio ambiente. De existir ya
el símbolo en medio ambiente, el nuevo lo oculta sin destruirlo ni reasignarlo.
• <valor> es un término que se evalúa en el medio ambiente de la evocación del
let67, para determinar el valor asociado a la variable en el medio ambiente
extendido por el let.
• <cuerpo> es la expresión que será evaluada luego de la extensión del medio
ambiente introducida por el let, y que, usualmente, incluye referencias a los
nuevos símbolos definidos.

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)))

Que resulta luego de la reescritura del operador en el término siguiente:

(let ( (x (+ x y)) (y z) ) (+ x y))


A ser evaluado en el medio ambiente E3 = {x/4, y/3, z/4}.

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 ]=> (define (set_x y) (set! x y))

Y las evocaciones siguientes:

1 ]=> (set! y 1)
...
1 ]=> (define z 3)
...
1 ]=> (set_x 8)

1 ]=> (f (+ 2 y ) (+ z y)))

;Value: 15

[Link] Faceta Procedural del SCHEME


La posibilidad que tiene el SCHEME, de usar, en el axioma que define un operador,
variables en el término de la derecha que no son variables del término de la izquierda, sino
que provienen del medio ambiente de la definición, viola la condición impuesta en la
sección 5.3.3: “Ecuaciones” (ver Capítulo 5) para los SRT.
La principal consecuencia de esta violación, es que el valor de una evocación del operador
ya no depende solamente del valor de los operandos, y de la manera como se combinan
(expresada en la definición), sino que depende, también, del estado del medio ambiente de
la definición en el momento de la evocación. Si se tiene en cuenta que este estado puede
Capítulo 7: Operadores Definidos
133
ser modificado arbitrariamente con el uso del operador set!, se puede deducir que el valor
de una evocación depende de la secuencia de evocaciones que se hayan efectuado entre el
momento de la definición del operador y el momento de su utilización.
Esta característica es la marca de clase de los lenguajes procedurales, quienes definen los
programas especificando las secuencias de cambios al medio ambiente que se llevan a cabo
durante una ejecución (ver [Arango 97 C 8]).
La posibilidad de que un programa especificado en SCHEME tenga características
procedurales es adicionalmente aumentada por la posibilidad de que el <cuerpo> de los
operadores define y let sean a su vez una secuencia de expresiones bajo la forma general
siguiente:
(<expresion> ..)
Donde:
• <expresion> es una construcción que puede ser un literal, una referencia a una
variable, una forma especial o una evocación de un procedimiento (u operador)
[Hanson 2002, sec. 1.4].
• .. Significa que puede haber mas de una instancia de <expresion>.
El cálculo de una secuencia de expresiones se lleva a cabo en el orden en que se escriben, al
igual que ocurre en una secuencia de “instrucciones” en un lenguaje procedural68. Además
puesto que una <expresion> puede ser una forma especial, esto significa que dentro del
cuerpo puede haber evocaciones de define y de let a cualquier nivel de profundidad69 (ver
sección “Estructura de los Programas en SCHEME” (sec. 7.8.1)).
Sin entrar más en los aspectos procedurales del SCHEME, diremos que en lo que sigue
evitaremos un estilo de programación procedural, y nos limitaremos a usar el lenguaje de
un modo meramente declarativo.
7.7.2 Medio ambiente en MAUDE.
El medio ambiente en el que se lleva a cabo el cálculo de un término sometido al intérprete,
está definido por los módulos del programa, previamente incluidos en el mismo. Estos
módulos definen un conjunto de operadores evocables que se reconocen con base en su
perfil. El perfil de los operadores, cada uno de ellos asociados a los axiomas que les
proveen su significado, constituyen el medio ambiente en el que se realizan los cálculos.
Con base en este medio ambiente, el intérprete analiza los términos sometidos para
descubrir cuales de los operadores definidos son evocados en el mismo. Identificadas las
evocaciones, el intérprete trata de reescribirlas usando los axiomas, y siguiendo un orden
ajustado a las estrategias de evaluación de los operadores.
La reescritura de cada evocación se lleva a cabo siguiendo el procedimiento descrito en la
sección 5.4.1 “Substitución, Particularización y Emparejamiento”. Durante este proceso,

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.

7.8 Estructura de los programas.


Tal como se describió al principio del capítulo, la correcta modularización de los programas
es un factor importante para la calidad del software. A través de ella se apoyan factores de
calidad como son la legibilidad, la mantenibilidad, la corrección y la reutilizabilidad
[Meyer 98].
En Las subsecciones que siguen se presentan los mecanismos que ofrecen los diferentes
lenguajes analizados par la modularización de los programas.
7.8.1 Estructura de los programas en SCHEME
En SCHEME la unidad fundamental de modularización es la definición de los operadores o
“procedimientos”.
Un programa, en efecto, es un conjunto de definiciones de procedimiento que pueden ser
utilizados como operadores en los términos a ser sometidos a cálculo por el usuario. Un
procedimiento, usualmente, evoca otros procedimientos del programa, y estos, a su vez,
pueden evocar otros procedimientos y así sucesivamente. De esta manera el SCHEME
soporta de forma directa la descomposición progresiva de operadores en operadores72 que,
usada de forma adecuada, simplifica las especificaciones apoyando la legibilidad de los
programas.
Dado que el <cuerpo> del operador de definición de funciones, o define (ver “Definición
de Operadores en SCHEME”, sección 7.3.2), puede ser una secuencia de <expresion>, y
que el define mismo es una <expresion>; es entonces posible definir operadores dentro de
otros operadores. Estos operadores internos son sólo visibles dentro del operador en que se
definen (ver “Extensiones Locales del Medio Ambiente Durante el Cálculo”, sección
[Link]), permitiéndole a un operador estructurar sus propios componentes sin conflictos de
nombre con operadores de igual nombre definidos en otros lugares diferentes. Esta

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:

1 ]=> (define (sum_cua x y) ( (define (cua x) (* x x)) (+ (cua x) (cua y))))


;Value: sum_cua

Para apoyar la reusabilidad, SCHEME permite que un conjunto de definiciones se escriba


en un archivo de texto para luego ser incluidas en el medio ambiente en el que se necesiten.
De esta manera un archivo con definiciones se convierte en un módulo o “paquete” de
mayor nivel que el operador mismo.

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:

1 ]=> (define (sum_cua x y) ((load “[Link]”)) (+ (cua x) (cua y))))


;Value: sum_cua

7.8.2 Estructura de los programas en MAUDE


La unidad básica de estructuración de los programas en MAUDE es el módulo. Un módulo
contiene los elementos sintácticos y las aserciones que definen una teoría en lógica.
Existen dos tipos fundamentales de módulos en MAUDE: los “módulos funcionales” (o
“functional modules”), con los que se le da soporte a una lógica denominada “lógica
ecuacional con membresía” (o “membership equational logic”), y los “módulos
sistémicos” .(o “system modules”), con los que se le da soporte a una lógica denominada
“lógica de reescritura” (o “rewriting logic”)73 (ver [Clavel 2007, secs 1.2 y 3.2]).
Los módulos funcionales definen uno o varios tipos de datos (o sorts) relacionados junto
con las operaciones sobre dichos tipos. Estas definiciones se llevan a cabo en el marco de
la lógica ecuacional con membresía, por lo que un módulo funcional es la especificación de
una teoría en dicha lógica. Los módulos sistémicos extienden los funcionales para
especificar teorías en lógica de reescritura.
Un programa en MAUDE está compuesto por uno o varios módulos interrelacionados. Las
relaciones entre los módulos se definen por medio de operaciones entre módulos. Los tipos

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].

7.9 Ejercicios Propuestos.


1- Realizar los siguientes ejercicios de la sección 1.1. de [Abelson 85], que está disponible
en línea en la WWW (ver referencia)
1.1, 1,2, 1.3, 1.4,1.5
2- Escriba un procedimiento en SCHEME que lleve a cabo los cálculos siguientes:
- El área de un trapecio, dado el tamaño de las bases y la altura.
- La longitud de una recta dada las coordenadas cartesianas de sus dos extremos.
- El área de un triangulo dadas las coordenadas cartesianas de sus tres lados.
3- Escriba un procedimiento en SCHEME que determine lo siguiente:
- Si la raíz de una ecuación cuadrática de coeficientes dados tiene, o no, componente
imaginaria.
- Si con tres lados de longitudes L1, L2 y L3 se puede o no formar un triangulo.
- Si un triangulo cuyos lados tienen longitudes L1, L2 y L3 es o no rectángulo.
- Si tres números dados A, B, C se hallan en orden ascendente.
Capítulo 8
Definición Recursiva de
Operadores.
Capítulo 8: Definición Recursiva de Operadores.
140
8.1 Introducción.
Definir con base en lo definido es una práctica común tanto en el lenguaje común como en
la matemática. Este tipo de definición es denominada “recursiva”76.

Ejemplo 82
Considere la siguiente definición de “ancestro”:

Un ancestro es el padre o un ancestro del padre

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}

Esta ecuación puede escribirse fácilmente en un lenguaje funcional, como la definición de


un operador que lleve a cabo la sumatoria de los cuadrados de los números comprendidos
entre dos enteros que constituyen sus argumentos.

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:

op Σn2_ _. : int int -> int .


vars I J : int .
eq Σn2 I J = (I * I) + Σn2 (I + 1) J .
.....

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

Para evitar esta circunstancia, basta con notar que:


i

∑n
n =i
2
= i 2 {3}

Con lo que se hace evidente que la substitución recursiva definida por la ecuación {2} debe
interrumpirse cuando el índice inferior de la sumatoria sea igual al superior, caso en el que
la reescritura debe usar la substitución sugerida en la ecuación {3}.
Este cambio en la ecuación a ser utilizada puede expresarse fácilmente en los lenguajes
funcionales, usando alguna forma de selección.

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í:

op Σn2_ _. : int int -> int .


vars I J : int .
ceq Σn2 I J = (I * I) + Σn2 (I + 1) J if(i<j) .
eq Σn2 I I = (I * I) .
.....

Que conduce a un proceso de reescritura que da como resultado el valor deseado:

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

En este punto es importante resaltar la importancia de la estrategia de evaluación asociada


al operador de selección, así: Para garantizar la terminancia del proceso de reescritura
debe evitarse la reescritura de la opción no seleccionada por dicho operador.
8.2.2 Raíz cuadrada por el método de Newton_Rapson.
En [Abelson 85 sección 1.1.7], se presenta un bello ejemplo de la definición recursiva en
SCHEME de un operador orientado al cálculo de la raíz cuadrada de un número por el
método de Newton-Rapson, que presentaremos aquí en sus elementos esenciales.
Podemos definir sin ambigüedades, en términos matemáticos, la raíz cuadrada de un
número real cualquiera x, como el número real y, mayor o igual a cero, tal que y2=x, o
simbólicamente:
x = { y ∈ R / y > 0 ∧ y 2 = x} {4}
Sin embargo, a diferencia del ejemplo de la sección anterior, ésta definición no nos da luces
para concebir un procedimiento que obtenga la raíz cuadrada de un número ya que no
facilita un tratamiento algebraico que “despeje” la incógnita. Para darle solución a este tipo
de problemas, es usual utilizar métodos de aproximación que partan de una solución trivial
inicial y, en iteraciones sucesivas, la transformen a una muy cercana a la solución
verdadera.
El método de Newton Rapson es uno de dichos métodos de aproximación y es aplicable al
caso de la raíz cuadrada. El método dice que si tenemos una aproximación y a la raíz
cuadrada de x, podemos encontrar una mejor si promediamos y con x/y. Por ejemplo,
supongamos que queremos encontrar la raíz cuadrada de 3 y supongamos que nuestra
aproximación inicial es 1.

Aproximación x/y Promedio


1 (3/1) = 3 (3+1)/2 = 2
2 (3/2) = 1.5 (1.5+2)/2 = 1.75
1.75 (3/1.75) = 1.7143 (1.7143 + 1.75)/2 = 1.7321
1.7321...
Tabal 8.1: calculo de raíz por Newton Rapson

Si asimilamos cada línea de la tabla a un término, y la secuencia de líneas a la secuencia de


términos de un proceso de reescritura que parte de la primera línea, podemos representarla
en un lenguaje funcional por medio de una ecuación.

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:

op √_ _. : float float -> float .


vars Y X : float .
eq √ Y X = √ (((X / Y) + Y) / 2) X .
.....

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:

op √_ _. : float float -> float .


vars Y X : float .
ceq √ Y X = Y . if(abs(Y * Y – X) > 0,001) .
ceq √ Y X = √ (((X / Y) + Y) / 2) X if(abs(Y * Y – X) <= 0,001) .
Capítulo 8: Definición Recursiva de Operadores.
145
.....

Al usar este procedimiento obtendríamos, por ejemplo:

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….

8.3 La forma del Proceso de Cálculo.


La importancia de la manera como se desenvuelve el proceso de cálculo para un operador
definido, puede intuirse comparando los procesos de reescritura correspondientes al cálculo
de los operadores definidos en la sección anterior (Ejemplo 86 y Ejemplo 88).
En el caso del operador que calcula la suma de los cuadrados comprendidos entre dos
enteros dados (Ejemplo 86), es fácil observar que, durante la substitución, el término de la
derivación crece hasta alcanzar un tamaño máximo y luego comienza a reducirse para
alcanzar la respuesta deseada. Este crecimiento se debe a que la operación suma que se
introduce en cada paso de reescritura, no puede llevarse a cabo inmediatamente debido a
que no se dispone del valor del segundo sumando. Este sumando, en efecto, se calcula
primero para la última operación suma introducida, que, al ser efectuada, obtiene el
segundo sumando de la penúltima suma introducida, y así sucesivamente. Nótese también
que el tamaño máximo alcanzado por el término durante el proceso depende del valor de
los argumentos, ya que el proceso de substitución debe introducir j-i veces la operación
suma antes de que se pueda calcular el último sumando.
En el caso del operador que obtiene la raíz cuadrada de un número dado, (Ejemplo 88), es
fácil observar que, durante la substitución el término de la derivación no crece como el del
caso anterior, sino que se mantiene estable manteniendo el mismo número de operadores y
operandos, hasta que se obtiene la respuesta deseada. Esta estabilidad se debe a que el
efecto neto de cada paso de substitución es el de cambiar el valor del primer argumento de
la evocación de la función, haciéndolo evolucionar hasta el valor deseado.
A los procesos que presentan inestabilidad en la memoria se le ha denominado proceso
recursivo mientras que A los procesos que presenta estabilidad se les ha denominado
proceso iterativo [Abelson 85 1.2.1]. El lector debe notar que esta definición refiere a la
forma como se comporta el proceso en la memoria y no a la forma como se define el
operador (que arriba denominamos definición recursiva)77.

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:

(define (sum_n2 i j ) (suma_it 0 i j ))


(define (suma_it total i j )
(if (> i j ) total
(suma_it (+ total (* i i )) (+ i 1 ) j )
)
)
La versión iterativa del operador de sumatoria en MAUDE, es como sigue:

op Σn2_ _. : int int -> int .


op sum_ite. : int int int -> int .

vars I J Total : int .


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) .
.....
Capítulo 8: Definición Recursiva de Operadores.
148
Al concebir la solución recursiva nos apoyamos en la propiedad asociativa de la suma, por
lo que cabe pensar si ésta es posible para fórmulas que no tengan dicha propiedad (ver
segundo ejemplo de la sección siguiente). La respuesta es de nuevo SI: basta con mirar el
problema de atrás hacia adelante, así:
j j −4
sum = ∑ n 2 =∑ n 2 + (( j − 3) 2 + (( j − 2) 2 + (( j − 1) 2 + j 2 ))) {7}
n =i n =1

Con lo que es fácil concebir la definición del ejemplo siguiente:

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:

op Σn2_ _. : int int -> int .


op sum_ite. : int int int -> int .

vars I J Total : int .


eq Σn2 I J = sum_ite(0, I, J) .
ceq sum_ite(Total, I, J) = sum_ite((Total + J * J), I, (J - 1)) if(i<=j) .
ceq sum_ite(Total, I, J) = Total if(i>j) .
.....

Al contrastar iteración con recursión debemos ser cuidadosos en no confundir la noción de


proceso recursivo con la de definición recursiva de un operador. En efecto en un lenguaje
funcional tanto los procedimientos iterativos como los recursivos se derivan de un operador
definido de forma recursiva. Lo importante es recordar de nuevo, que cuando nos
referimos a una definición recursiva, nos estamos refiriendo al hecho sintáctico de que la
definición del operador hace referencia al mismo operador. Pero cuando decimos que un
proceso sigue un patrón recursivo, estamos hablando acerca de como evoluciona el proceso
que la definición determina, y no acerca de la sintaxis de la definición.
Esta distinción se hace más difícil debido a que la mayoría de las implementaciones de
lenguajes procedurales, (como Pascal o C) están diseñadas de manera que cualquier
definición recursiva genera un proceso recursivo que consume una cantidad de memoria
que crece con el número de llamadas al procedimiento. Como consecuencia, en estos
lenguajes, es necesario usar construcciones especiales como do, for, o while para
implementar procesos iterativos sobrecargando la sintaxis del lenguaje.

8.4 La eficiencia del proceso de Cálculo.


Para todos los operadores definidos en el Capítulo anterior, se cumple que el número de
reescrituras y cálculos elementales que se efectúan en una evocación cualquiera, está
acotado superiormente por el número de operaciones especificadas en su definición y en la
definición de los operadores que participan en el proceso de reescritura. En consecuencia,
Capítulo 8: Definición Recursiva de Operadores.
149
el tiempo que se tarda el cálculo asociado a una evocación de estos operadores es, entonces,
independiente del valor de los operandos79.

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)

Figura 2.2. El proceso de construcción de sum-pi recursivo.

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)

Figura 2.3. El proceso de construcción de cont-frac recursivo.


Capítulo 8: Definición Recursiva de Operadores.
154
La relación entre las funciones auxiliares y el término genérico es como se muestra en la
tabla siguiente:

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:

(define (sum_pi n) (sum_pi_rec 1 n))


(define (cont_frac n) (cont_frac_rec 1 n))
Que se desprenden de la primera afirmación, y dejan definida Sn y Fn que eran lo que realmente
queríamos definir.

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í:

op sum-pi : int -> float .


op sum-pi-rec : int int -> float .

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
.....

.op cont-frac : int -> float .


.op cont-frac-rec : int int -> float .

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.

[Link] Acumulador iterativo


Ahora definiremos un operador que determine un proceso iterativo para el cálculo de ambas
funciones.
Al observar de nuevo el proceso iterativo para la sumatoria de los cuadrados (sección 8.3),
vemos que en cada paso es necesario hallar un valor que se acumula mediante la operación
suma. Para la sumatoria no será difícil imaginar como se puede hacer esto mismo, así:
1 1
primero calculamos y lo acumulamos sumando, luego calculamos y los
1⋅ 3 5⋅7
1
acumulamos sumando, luego y acumulamos, y así sucesivamente hasta encontrar y
9 ⋅ 11
1
acumular .
(4n − 3) ⋅ (4n − 1)

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í:

op sum-pi : int -> float .


op sum-pi-ite : float int int -> float .

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) .
.....

Sin embargo en el caso de la fracción, no es tan obvia la manera de definir el operador


iterativo. Nótese que, por no ser la división un operador asociativo, no es posible llevar a
cabo el cálculo de forma paulatina almacenando en la memoria valores intermedios si
empezamos a hallar valores de arriba hacia abajo.
Capítulo 8: Definición Recursiva de Operadores.
156
1 1
Así, como valor, no es útil para hallar y este a su vez no es útil para encontrar
12 2 2
1 + 2
2
1
, pues haría falta la estructura completa para colocar el nuevo valor en el lugar
2 2
1 +
3
22 +
32
apropiado de la expresión.
Esta posibilidad, sin embargo, aparece claramente si llevamos a cabo el proceso de
acumulación de abajo hacia arriba (en lugar de atrás hacia delante como en el caso de la
sumatoria de los cuadrados).
Así, como se muestra en la figura 2.1, empezaremos por encontrar el último cociente,
correspondiente a n y luego con ese cociente hallaremos el penúltimo cociente,
correspondiente a n-1, haciendo la suma y división necesaria. El proceso continuará hasta
que encontremos el valor de la primera fracción.
1
Fn =
2
12 +
3
22 +
n−2
O+
n −1
(n − 2) 2 +
n
(n − 1) 2 +
n2

Figura 2.1. El proceso de construcción de cont-frac iterativo.

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í:

op cont-frac : int -> float .


op cont-frac-ite : float int -> float .

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.

Nótese que ambos procedimientos comparten la misma estructura:


Capítulo 8: Definición Recursiva de Operadores.
157
“Si se cumple la condición extrema se devuelve lo que se lleva acumulado;
De lo contrario se hace una llamada recursiva, cuyo primer argumento
depende del acumulado y del nuevo término que se calcula a partir del
contador”.
En el caso de la sumatoria, el segundo argumento de la llamada recursiva es el contador
más uno y el tercer argumento es el mismo valor máximo. Para la fracción, el segundo
argumento de la llamada recursiva es el contador menos uno pues, como se aclaró atrás, la
acumulación se hace de abajo hacia arriba. Lo único que los diferencia realmente, es el
primer argumento de la llamada recursiva que para la sumatoria es:
1
acumulado + {12}
(4contador − 3)(4contador − 1)
y para el cociente es:
contador
{13}
contador 2 + acumulado
8.5.2 Control a la precisión y eficiencia del cálculo: aproximación al coseno
Para desarrollar la capacidad de definir operadores útiles, el programador debe tener
conciencia de los peligros potenciales de sus especificaciones. En esta sección
examinaremos problemas asociados a la precisión y a la eficiencia con base en un ejemplo.
El problema será el de definir un operador que obtenga una aproximación al coseno de x
utilizando series de potencias.
El coseno de un ángulo x en radianes se puede expresar como la serie

(−1) n x 2 n
cos( x) = ∑ {14}
n=0 (2n)!
Si truncamos esta serie a una de sus sumas parciales, obtenemos una aproximación con un
error que depende del último término de la suma:
n
(−1) k x 2 k x2 x4 x6 (−1) n x 2 n
cos( x) ≈ S n = ∑ =1− + − +K+ {15}
k =0 (2k )! 2! 4! 6! (2n)!
El primer acercamiento a la definición del operador, será el de construir un procedimiento
que efectúe la suma hasta el n-ésimo término de la serie mediante un proceso iterativo,
siguiendo el modelo desarrollado en la sección anterior.

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:

op coseno : float int -> float .


op coseno-ite : float int int float -> float .

vars Total X : float .


vars K N : int .
Capítulo 8: Definición Recursiva de Operadores.
158
eq coseno(X, N) = coseno-ite(1., 1, N, X) .
ceq coseno-ite(Total, K, N, X) =
coseno-ite((Total + (-1)^K * X^(2 * K) / (2 * K)!), (K + 1), N, X) if(K<=N)
.
ceq coseno-ite (Total, K, N, X) = Total if(K>N) .
.....
Suponiendo que existen procedimientos predefinidos _! (que calcula el factorial) y _^_ (que eleva un
número a una potencia).

Esta definición, sin embargo, NO LOGRA SU COMETIDO, induciendo, de hecho, al


cálculo de valores errados para el coseno(x,n). En lo que sigue abordaremos los
problemas que evitan que el cálculo sea correcto y eficiente, y propondremos caminos de
solución.
[Link] Primer cambio: Precisión en término.
Si reflexionamos sobre la manera como se indica en la definición, el cálculo de los términos
que se suman en cada iteración del proceso,
2
   x 4   x 6   x8   (−1) n x 2 n 
(+ 1),  − x ,  + ,  − ,  + , K ,   {16}
 2!   4!   6!   8!   (2n)! 
Podemos identificar un problema potencial que, en general, induce al intérprete a lleva a
cabo un cálculo equivocado.
En efecto, para calcular un término se induce a calcular de forma independiente la potencia
y el factorial, por medio de los subtérminos X^(2 * K) y (2 * K)!. No hace falta ser
demasiado agudo, para notar que el resultado de estos cálculos produce valores demasiado
grandes para la capacidad de almacenamiento de los valores en la mayoría de las
implementaciones. Las limitaciones en la capacidad de almancenamiento de valores se
traducen en algunos casos en el almacenamiento de valores equivocados y en otros en una
pérdida de precisión84.
Así a, pesar de que el valor de X^(2 * K) / (2 * k)!) es pequeño, en general, no se calcula
correctamente debido a que el cálculo de sus dos componentes ya presenta errores de
precisión. Las expresiones matemáticas, pese a ser claras y concisas, no son
necesariamente la mejor manera, ni siquiera la más exacta, de calcular.
Para eliminar los problemas de precisión en el cálculo del término optaremos por mantener
los valores intermedios del proceso de cálculo lo mas pequeños posibles. Para ello nos
apoyaremos en el hecho de que, por un lado, los términos de la secuencia {16}, van
disminuyendo su valor hasta un punto en que se hace despreciable, y que, por otro, es fácil
ver que cada término puede encontrarse a partir del anterior, mediante un producto
adecuado.

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

A continuación, expresemos los términos de la sucesión {ak} de la manera que queremos:


a0 = 1
x2 {18}
ak = (−1)ak −1
(2k − 1)(2k )
Para mayor ilustración miremos de nuevo los términos de la sucesión, expresados de esta
manera:
 x 2   x2 ⋅ x 2   x 4 ⋅ x 2   x6 ⋅ x 2   x 2( n−1) ⋅ x 2 
(+ 1),  − ,  + ,  − ,  + ,K,  (−1)  {19}
 (1 ⋅ 2)   2!⋅(3 ⋅ 4)   4!⋅(5 ⋅ 6)   6!⋅(7 ⋅ 8)   [2(n − 1)]!⋅[(2n − 1) ⋅ (2n)] 
Que son exactamente los términos mostrados arriba, expresados de una manera que hace
obvio que cada término “contiene” al anterior. Así, en general, el término de orden k se
obtiene del termino de orden k-1, multiplicándolo por (-1) para cambiarle el signo, y
multiplicándolo por x2/((2k-1)(2k)), para obtener la potencia y el factorial adecuados.
Con esto en mente, podemos definir un operador que calcule el término de forma precisa.

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:

op termino : float int -> float .


op ter-ite : float int int float -> float .

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í:

op coseno : float int -> float .


op coseno-ite : float int int float -> float .

vars Total X : float .


vars K N : int .
eq coseno(X, N) = coseno-ite(1., 1, N, X) .
ceq coseno-ite(Total, K, N, X) =
coseno-ite(Total + termino(X,K), (K + 1), N, X) if(K<=N) .
Capítulo 8: Definición Recursiva de Operadores.
160
ceq coseno-ite (Total, K, N, X) = Total if(K>N) .
.....

[Link] Segundo cambio: mejora en la eficiencia.


Si bien resolvimos el grave problema de precisión que tenía la solución inicial, todavía
queda un problema de eficiencia.
En efecto, si analizamos el número de veces que se lleva a cabo la reescritura, para el caso
de la última versión iterativa, podemos ver que durante el proceso de acumulación de los
términos de la serie, esta se lleva a cabo N+2 veces, al igual que en el caso del cálculo de la
suma de los cuadrados.
Sin embargo el cálculo propio de cada término de la serie requiere de K+2 reescrituras
siendo K el índice del término en la serie. Así, una aproximación al número total de
reescrituras, es el siguiente:
Nro_Reescrituras = 2+3+4+......N+(N+1)+(N+2) = (N+4)*(N+2)/2 {20}
Este número contrasta con el requerido para el cálculo de la suma de los cuadrados que era
básicamente N+2. En términos de la teoría del orden, se puede demostrar que ahora
nuestro algoritmo es del orden de N2, cuando los que teníamos antes eran del orden de N.
Para corregir este problema de eficiencia, modificaremos la última definición del operador
coseno(x,n), acoplando el cálculo de los términos de la serie, con el cálculo de la serie
misma. A esta estrategia la denominaremos “tejer” las definiciones de los operadores
termino(n), y coseno(x,n) del Ejemplo 101. La especificación separada de los operadores
y su posterior tejido, facilita una estrategia de programación en la que las diferentes partes
del programa se conciben de forma independiente, para juntarlas luego de que se han
definido y probado85.
La posibilidad de llevar a cabo este tejido se debe a que en el proceso definido por
termino(n) van apareciendo de forma sucesiva los términos de la serie, y en el proceso de
coseno(x,n) se van utilizando de forma sucesiva dichos términos. Así, si se acoplan el
cálculo de los términos con su uso, se pueden fundir las dos iteraciones en una sola.

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í:

op siguiente : float int -> float .


op coseno : float int -> float .
op coseno-ite : float float int int float -> float .

vars Total X Ta : float .


vars K N : int .

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) .
.....

[Link] Tercer cambio: precisión en la condición de salida


En las definiciones del operador coseno anteriores usamos un número de iteraciones fijo
suministrado por el usuario como argumento al operador. Sin embargo, un mejor criterio
para detener el proceso de aproximación, sería poder decir si la aproximación es
suficientemente buena, según algún criterio.
Se puede demostrar que los términos de la sucesión tienden a cero cuando n tiende a
infinito. Entonces, lo que haremos será detener el proceso cuando la última pareja (término
positivo + término negativo), sumen un valor cercano a cero. Así, podemos estar seguros
que la aproximación es buena, pues, el resto de término no sumados, no aportan mucho.

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í:

op siguiente : float int -> float .


op coseno : float -> float .
op coseno-ite : float float float int float -> float .

vars Total X Ta Taa : float .


var K : int .
eq siguiente(Ta, X, K) = Ta * (X^2 / ((2 * K) * (2 * K – 1))) .
eq coseno(X) = coseno-ite(0., 1., X * X / 2., 2, X) .
ceq coseno-ite(Total, Taa, Ta, K, X) =
coseno-ite( Total + Taa - Ta,
siguiente(Ta, X, K),
siguiente(siguiente(Ta, X, K), X, K + 1),
(K + 2),
X
) if(abs(Taa-Ta) >= 0.000001) .
ceq coseno-ite (Total, Taa ,Ta ,K, X) = Total if(abs(Taa-Ta) < 0.000001) .
.....
Nótese que ahora el proceso acumula los términos de 2 en 2 con el índice correspondiendo al primer
término no sumado.

8.6 Serie de Fibonacci.


En esta sección nos apoyaremos en el problema de calcular los elementos de la serie de
Fibonacci, para resaltar el efecto que tiene el enfoque usado al definir un operador, en la
eficiencia del proceso de reescritura.
Capítulo 8: Definición Recursiva de Operadores.
162
En efecto, para este problema, la eficiencia del proceso pude ir desde un número de
reescrituras del orden de n!, hasta un número de reescrituras del orden de log2(n). Y, si
bien este cambio tan dramático de orden no siempre es posible, el ejemplo ilustra la
necesidad de analizar con seriedad el problema de la eficiencia del operador.
En la serie de Fibonacci se parte de los número 0 y 1 y, a partir de ellos, el término
siguiente se obtiene sumando los dos anteriores, así:

0, 1, 1, 2, 3, 5, 8, 13, 21, ...


En general los números de Fibonacci pueden ser definidos 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:

op fib : int -> int .


vars Total X Ta Taa : float .
var N : int .
eq fib(0) = 0 .
eq fib(1) = 1 .
eq fib(N) = fib(N – 1) + fib(N – 2) if(N > 1) .

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))
)

(define (fib-iter a b contador)


(if (= contador 1) a
Capítulo 8: Definición Recursiva de Operadores.
164
(fib-iter (+ a b) a (- contador 1))
)
)
La definición correspondiente en MAUDE es la siguiente:

op fib : int -> int .


op fib-iter : int int int -> int .
vars K N Na Naa : int .
eq fib(0) = 0 .
eq fib(1) = 1 .
eq fib(N) = fib-iter(0, 1, N) if(N > 1) .
eq fib-iter(Faa, Fa, K) = fib-iter(Fa, Faa+Fa, (K – 1)) if(N > 0) .
eq fib-iter(Faa, Fa, 0) = Fa .

Que genera un proceso significativamente más eficiente, así:

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

Que sin duda corresponde a un proceso iterativo.

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.

8.7 Utilidad de la recursión


Aunque los ejemplos anteriores muestran una clara superioridad, en lo que a eficiencia se
refiere, de los procesos iterativos sobre los procesos recursivos, no debemos concluir que la
definición de procesos recursivos es inútil.
En primer lugar, de los ejemplos anteriores, debe ser claro que es significativamente más
fácil concebir una solución recursiva que concebir una iterativa. Prueba de esto es la
naturalidad con que surge el primer procedimiento para calcular los números de Fibonacci,
en contraste con el segundo que genera el proceso iterativo más eficiente, pero un poco más
difícil de idear y entender, y ni que hablar de la dificultad asociada al último algoritmo
presentado.
La recursión es, en efecto, un instrumento útil para entender y diseñar la solución a
problemas específicos. Una vez concebida una solución recursiva, se puede intentar
transformarla a una versión iterativa86.
El ejemplo del cambio de moneda presentado por Abelson en [Abelson 1985 sección 1.2.2],
que resumimos a continuación, es una muestra patente del poder de la recursión como
medio para hallar una solución a un problema.
El problema es el siguiente: ¿De cuantas maneras diferentes se puede cambiar una cantidad
de dinero dada, si contamos con monedas de 20, 50, 100, 200 y 500 pesos?.
Este problema tiene una solución relativamente sencilla, si se plantea de forma recursiva.
Este planteamiento se basa en los hechos siguientes:
• El conjunto £(V,20 a 500) formado por todas las maneras de cambiar V con las
cinco denominaciones, puede ser dividido en dos conjuntos separados: las maneras
que emplean al menos una moneda de 500 y las maneras que no utilizan ninguna
moneda de 500.

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.

8.8 Ejercicios propuestos.


1- Siga paso a paso el proceso de cálculo en MAUDE, bajo la definición de los operadores
Σn2 y √ propuestos en Ejemplo 83, Ejemplo 84, Ejemplo 85 (use los comandos referidos en
[Clavel 2007, sec. 16.5]
2- Defina en MAUDE el operador Σn2 y √ del Ejemplo 85 y del Ejemplo 89, usando el
operador de selección,
if <predicado> then <consecuencia> else <alternativa> fi

3- Lleve a cabo el Ejercicio 1.6 referido en [Abelson 85 sec. 1.1.7]


4- Escriba un procedimiento SCHEME para llevar a cabo el cálculo siguiente:
- La suma de los n primeros números naturales pares.
n
1
- La sumatoria de los inversos de los n primeros números naturales: ∑i
i =1

- 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)).

c- Reescriba la definición de siguiente para que se cambie:


siguiente(Ta, K), siguiente(siguiente(Ta, K), (K + 1)) por siguiente(Ta, K, 1), 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

21- Escriba un procedimiento SCHEME que obtenga un valor aproximado de x usando el


procedimiento siguiente (partición binaria):
- El valor de x para x >1 está comprendido en el intervalos [1 , x].
- Este intervalo se puede reducir a la mitad evaluando si el punto medio del intervalo es
mayor o menor que el valor buscado. Si es menor puede reducirse el intervalo desechando
la segunda mitad. Si es mayor puede reducirse el intervalo desechando la primera mitad.
- El intervalo de incertidumbre de la respuesta puede reducirse tantas veces como se desee
usando el procedimiento anterior. Una vez que el intervalo se reduzca a un valor muy
pequeño, una buena aproximación a la respuesta es el punto medio del intervalo reducido.
22- Escriba un procedimiento SCHEME que obtenga un valor aproximado de x usando
el procedimiento del punto anterior pero que opere tanto para x > 1 como para x ≤ 1.

23. Escribir un procedimiento, con tres argumentos, x, n y m, que calcule:


n  i m e j −1 
∑ x ∑

i =1 


j =1 (i + j )! 

¿Que proceso genera el procedimiento escrito? Reescriba el procedimiento, de manera que


genere un proceso diferente.
24. Escriba un procedimiento que calcule
n
k 3 −1
∏ 3
k =2 k + 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.

9.2 Tipos Compuestos Estructurados


Es usual que en el contexto de un problema nos encontremos con entidades u objetos, cuya
descripción implique un conjunto determinado de valores (escalares o compuestos). Un
punto en el plano, por ejemplo, es descrito por dos números reales correspondientes a sus
coordenadas cartesianas; una fecha es descrita por medio de tres enteros correspondientes al

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.

9.2.1 Declaración de tipos compuestos estructurados.


Para darle categoría de tipo propuesto por el programador, a todos los posibles datos
estructurados que representen vectores, y poder distinguirlos de otros datos estructurados
con igual o diferente estructura, el lenguaje debe poder ofrecernos una construcción que
permita declarar el nuevo tipo.
[Link] Declaración de un tipo compuesto estructurado en SCHEME.
El MIT_SCHEME le da soporte a los tipos compuestos estructurados, por medio del
registro. El SCHEME provee, para la manipulación de registros, los dos mecanismos
siguientes:
• Un conjunto de operadores nativos que incluyen los siguientes: un operador para
declarar un tipo específico de registro, un operador para definir operadores
encargados de construir valores de un tipo de registro previamente declarado, y
un operador para definir operadores encargados de obtener, o seleccionar, las
componentes de valores de un tipo de registro previamente declarado [Hanson
2002, sec. 10.4].
• La forma especial define-structure que da soporte a la definición del tipo y de
los operadores constructores y selectores en una sola operación [Hanson 2002,
sec. 2.10].
En lo que sigue nos referiremos solamente al primer mecanismo de soporte a los registros,
ya que el segundo sólo adiciona “azúcar sintáctico” al primero.
Para declara un tipo registro el SCHEME ofrece el operador nativo siguiente:
(make-record-type <nombre_del_tipo> <lista_de_nombres_de_campos>)
Donde:
• <nombre_del_tipo> es un string que identifica el tipo.
• <lista_de_nombres_de_campo> es una lista de símbolos que se asocian con
cada una de las componentes de los valores del tipo estructurados.
La evocación del operador make-record-type da como resultado un valor del tipo nativo
compuesto record-type, que será usado en la definición de los demás operadores del tipo.
Nótese que es este valor, y no el nombre del tipo, el elemento que representa el tipo en las
operaciones que siguen.
Para definir un operador que permite verificar si un valor es del tipo definido, el SCHEME
ofrece el operador nativo siguiente:
(record-predicate <record_type> )
Donde:
Capítulo 9: Valores y Tipos Compuestos.
175
• <record_type> es el valor obtenido como resultado de declarar el tipo con el
operador make-record-type.
La evocación del operador record-predicate da como resultado un operador que acepta
como argumento un valor, y devuelve #t (verdadero) si el valor es del tipo estructurado
declarado y #f (falso) si no lo es. En adición al predicado de tipo creado con el operador
record-predicate, el SCHEME ofrece de forma nativa el predicado de tipo record?, que
permite establecer si un valor es de algún tipo estructurado creado con el operador make-
recor-type.

Ejemplo 110
La declaración de nuestro tipo Vector en SCHEME es como sigue:

(define Vector (make-record-type “Vector” ‘(Vi Vj )))


Donde asociamos el valor de tipo record-type, resultante de evocar a make-record-type con el rótulo
Vector, para poder usar dicho valor en las operaciones siguientes.
El predicado de tipo para el nuevo tipo se define de la manera siguiente:

(define Vector? (record-predicate Vector ))

Donde el predicado de tipo definido es asociado al identificador Vector?.

[Link] Declaración de un tipo compuesto estructurado en MAUDE


La declaración de un nuevo tipo en MAUDE se lleva a cabo declarando simplemente a 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 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 .

9.2.2 Construcción de instancias del tipo compuesto estructurado.


Para tener valores de tipo Vector, es necesario construirlos partiendo de sus componentes
reales.
[Link] Construcción de instancias del tipo compuesto estructurado en
SCHEME.
El MIT_SCHEME le da soporte a la construcción de los valores de un tipo compuesto
estructurado, por medio del operador nativo siguiente:
(record-constructor <record_type> [<lista_de_nombres_de_campos>] )
Capítulo 9: Valores y Tipos Compuestos.
176
Donde:
• <record_type> es el valor obtenido como resultado de declarar el tipo con el
operador make-record-type.
• <lista_de_nombres_de_campo> es una lista opcional con elementos de la
<lista_de_nombres_de_campo> usada al declarar el tipo con el operador
make-record-type. Si esta lista se omite, se asume que es igual a la dada al
declara el tipo.
La evocación del operador record-constructor da como resultado un operador constructor,
que acepta como argumentos valores para las componentes incluidas en la
<lista_de_nombres_de_campo> y da como resultado un valor del tipo registro descrito
por el argumento <record_type>.

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 make_Vector (record-constructor Vector ))


Donde asociamos el símbolo make_Vector al operador resultante de evocar record-constructor, para
poder usar dicho operador al crear instancias de Vector, así:

(define V1 (make-Vector 2 5 ))
(define V2 (make-Vector 3 4 ))

Donde V1 y V2 hacen referencia a los vectores particulares V1 = 2i+5j y V2 = 3i+4j.

[Link] Construcción de instancias del compuesto estructurado en MAUDE


En el lenguaje MAUDE se obtienen los valores de un tipo declarado cualquiera, por medio
de operadores “constructores”. Un constructor para un sort, es un operador que tiene como
codominio a dicho sort y no es reescrito por ecuación alguna en la teoría. Así, cuando se
somete al intérprete una evocación de un constructor para ser calculada, el valor resultante
es la misma evocación.
Los términos base de operadores constructores representan elementos específicos del
codominio del operador, y son el resultado final del cálculo de términos de su tipo en una
teoría.
El constructor para un tipo compuesto estructurado recibe como argumentos los valores
componentes, actuando como un aglutinante para dichas componentes.
En MAUDE, los constructores se declaran de la misma forma que se declaran los demás
operadores, pero a diferencia de ellos no se les asocia con definición alguna. Dentro de los
atributos del operador se debe incluir el atributo ctor para facilitar operaciones de
depuración y demostración de teoremas sobre la especificación [Clavel 2007, sec 4.4.3].

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í:

op _i+_j : float float -> Vector [ctor] .


Permitiendo que los vectores V1 y V2 se representen de forma tradicional por medio de los términos
siguientes:
2i+5j y 3i+4j

9.2.3 Selección de componentes de un valor compuesto estructurado.


Para acceder a los valores de las componentes de un valor compuesto estructurado es
necesario contar con mecanismos de selección para dichos componentes.
[Link] Selección de componentes en SCHEME.
El MIT_SCHEME le da soporte a la selección de los componentes de un tipo compuesto
estructurado, por medio del operador nativo siguiente:
(record-accessor <record_type> <nombre_de_campo> )
Donde:
• <record_type> es el valor obtenido como resultado de declarar el tipo con el
operador make-record-type.
• <nombre_de_campo> es un elemento de la <lista_de_nombres_de_campo>
usada al declarar el tipo con el operador make-record-type.
La evocación del operador record-accessor da como resultado un operador selector, que
acepta como argumento un valor del tipo compuesto estructurado descrito en
<record_type> y da como resultado el valor del componentes referido por
<nombre_de_campo> .

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í:

(define get_Vi (record-accessor Vector ‘Vi ))


(define get_Vj (record-accessor Vector ‘Vj ))
Donde asociamos los símbolos get_Vi y get_Vj a los operadores selectores resultantes de evocar
record-accessor, para poder usar dichos operadores al manipular instancias de Vector.

[Link] Selección de componentes en MAUDE


En MAUDE, es posible declarar y definir operadores selectores de la misma forma que se
declaran los demás operadores.

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:

op get-Vi : Vector -> float .


op get-Vj : Vector -> float .
vars X Y : float .
eq get-Vi(X i+Y j ) = X .
eq get-Vj(X i+Y j ) = Y .

Donde es importante notar que la definición de los operadores selectores, se apoya en la


posibilidad de que los términos del lado izquierdo de un axioma tengan más de un
operador. Esta simple propiedad del lenguaje hace innecesario la inclusión de operadores
nativos especializados en este tipo de definición.
Como se verá en la sección siguiente, esta misma propiedad hará innecesario el uso de los
selectores en la mayoría de las situaciones en que se debe acceder a las componentes de un
valor compuesto.
9.2.4 Definición de operadores sobre tipos compuestos estructurados.
Un tipo de datos propuesto por el programador adquiere su máxima utilidad, sólo cuando se
cuenta con operadores que operen con los valores del tipo. Al usar estos operadores el
usuario del tipo, se abstrae de considerar (e incluso conocer) los detalles con los que se
lleva a cabo dichas operaciones. Ellas se convierten, entonces, en gránulos gruesos de
programación que simplifican enormemente los programas.
[Link] Definición de operadores sobre tipos compuestos estructurados en
SCHEME.
Tal como se ilustra en [Abelson 85, sección 2.1.1] para definir, en el lenguaje SCHEME,
los procedimientos operadores de un tipo abstracto de datos, son necesarios y suficientes
los procedimientos constructores y selectores del tipo que esta siendo definido.

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:

(define (suma_vectores V1 V2)


(make_Vector (+ (get_Vi V1) (get_Vi V2)) (+ (get_Vj V1) (get_Vj V2)) )
)

(define (producto_escalar_vectores V1 V2)


(+ (* (get_Vi V1) (get_Vi V2)) (* (get_Vj V1) (get_Vj V2)) )
)

(define (igual_vectores V1 V2)


(and (= (get_Vi V1) (get_Vi V2)) (= (getVj V1) (getVj V2)) )
)
Capítulo 9: Valores y Tipos Compuestos.
179
Dados estos operadores se pueden lleva a cabo operaciones sobre valores previamente
creados.

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í:

1 ]=> (define V1 (make-Vector 2 5 ))


...
1 ]=> (define V2 (make-Vector 3 4 ))
...
1 ]=> (suma_vectores V1 V2)
...

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

[Link] Definición de operadores sobre tipos compuestos estructurados en


MAUDE.
Una vez declarado el tipo y los constructores del tipo, es una tarea fácil declarar y definir
operadores que actúen sobre instancias del tipo.

Ejemplo 119
Para definir operadores sobre instancias de un tipo propuesto por el programador es necesario y
suficiente usar los constructores, así:

op _+_ : Vector Vector -> Vector .


op _*_ : Vector Vector -> float .
op _==_ : Vector Vector -> Bool .
vars X1 Y1 X2 Y2 : float .
eq (X1 i+Y1 j ) + (X2 i+Y2 j ) = (X1 + X2) i+(Y1 + Y2) j .
eq (X1 i+Y1 j ) * (X2 i+Y2 j ) = (X1 * X2) + (Y1 * Y2) .
eq (X1 i+Y1 j ) == (X2 i+Y2 j ) = (X1 == X2) and (Y1 == Y2) .
Los operadores pueden ser usados para llevar a cabo cálculos, así:

Maude> rew (2i+5j) + (3i+4j) .


XXXXX
XXXX
Capítulo 9: Valores y Tipos Compuestos.
180

[Link].1 Completitud suficiente en MAUDE


Una propiedad importante de la definición de operadores sobre tipos definidos es la
denominada Completitud Suficiente (“Sufficient Completeness” [Clavel 2007, sec. 4.4.3]).
Esta propiedad implica que la definición de los operadores es tal, que el resultado de
calcular un término base de uno de dichos operadores, es un término de un operador
constructor del tipo asociado con el operador. Esta propiedad no es otra cosa que la
garantía de que los operadores se definieron correctamente.
El intérprete de Maude2 ofrece un chequeador de Completitud Suficiente, que permite la
verificación automática de la definición de los operadores sobre un tipo propuesto por el
programador.

9.3 Tipos Compuestos Iterados


Es usual que en el contexto de un problema nos encontremos con entidades u objetos
compuestos por múltiples elementos que se relacionan entre sí. Son ejemplos de este tipo
de entidades, los valores de un vector N dimensional, los clientes de un banco, los
prestamos de los clientes del banco, los estudiantes de un curso, los predios de un
municipio, los ítems almacenados en un árboles binarios de búsqueda en una base de datos,
etc...
Una característica importante de este tipo de entidades es que para describirlas es necesario,
no solo mantener la información que describe cada uno de sus componentes individuales,
sino que es necesario mantener también información de las relaciones que existen entre
dichas componentes.
Para tratar con estas entidades es, igualmente, de gran utilidad considerar estos conjunto de
valores como unidades de valor o datos compuestos. La mayoría de los lenguajes de
programación ofrecen, en efecto, facilidades para manipular este tipo de compuesto. Estas
facilidades van desde, estructuras básicas que permiten definir secuencias de componentes
en memoria o en disco (Vg. “arreglos”, “vectores” o “archivos” ), hasta construcciones
para que programador defina estructuras de diversos grados de complejidad en memoria y
en disco (Vg. creación de objetos complejos en el “heap” y acceso a los mismos por medio
de “punteros”, en la forma de “listas enlazadas”, “árboles” o “grafos”, y/o estructuras
complejas de datos o “tablas” interrelacionadas, almacenados en archivos, y con un
lenguaje de acceso a los mismos en la forma de “bases de datos”).
En lo que sigue nos referiremos a este tipo de compuesto como “datos compuestos
iterados”. Caracterizaremos los valores compuestos iterados por las siguientes
propiedades:
• Son compuestos que tiene un número no determinado de componentes.
• En cada compuesto los componentes son considerados del mismo tipo
(pudiendo ser, a su vez, tipos compuestos).
• Se puede acceder a los componentes individuales por medio de un mecanismo
que denominaremos “vía de acceso”.
Capítulo 9: Valores y Tipos Compuestos.
181
• Entre las componentes de un compuesto y, posiblemente, entre las componentes
de varios compuestos pueden ocurrir conexiones o “relaciones”.
Para facilitar los cálculos con datos iterados el lenguaje debe ofrecer, también, mecanismos
para darle la categoría de tipo de dato todos los conjuntos de valores iterados que tengan la
misma estructura de composición. Para ello debe proveer lo siguiente:
• Un mecanismo para “construir” de forma progresiva valores iterados a partir de
los valores de sus componentes.
• Un mecanismo para obtener o “seleccionar” los valores de las componentes a
partir de un valor iterado.
• La posibilidad de definir operadores que actúen sobre datos del tipo iterado.
En esta sección presentaremos las facilidades ofrecidas por cada uno de los lenguajes
analizados, para crear y manipular datos compuestos iterados. Para ello nos apoyaremos en
iterados con relaciones específicas entres sus componentes. Para estos tipos de iterados
presentaremos, en cada lenguaje, las construcciones que permiten crearlos y manipularlos,
haciendo énfasis en la manera de definir los elementos referidos en el párrafo anterior.
9.3.1 Conexión entre componentes: Pares.
Para componer datos se debe contar con mecanismos de agregación y desagregación. El
mecanismo de agregación más simple es el que junta o “pega” dos valores para formar una
pareja. El mecanismo de desagregación mas simple es el que toma una pareja y obtiene o
“selecciona” sus componentes.
La importancia del mecanismo de manipulación de parejas, es que a partir de él se
implementan los mecanismos para manipular estructuras más complejas, y en particular las
iteradas.
En esta sección se presenta, entonces, la manera de agregar y desagregar parejas en los
lenguajes analizados.
[Link] Parejas en SCHEME.
En concordancia con la estrategia general del lenguaje, el SCHEME ofrece los operadores
nativos especializados a la gestión de parejas que se describen a continuación
Un operador encargado de formar una pareja.
(cons <objeto_1> <objeto_2> )
Donde:
• <objeto_1> y <objeto_2> son dos valores de tipos arbitrarios, que pueden
incluir valores compuestos, que constituyen las componentes de la pareja.
Dos operadores encargados de obtener de una pareja su primer o segundo componente,
respectivamente.
(car <pareja> )
(cdr <pareja> )
Donde:
• <pareja> es el valor compuesto de tipo pareja del que se desea obtener las
componentes.
Capítulo 9: Valores y Tipos Compuestos.
182
Dos operadores encargados de reasignar el primero o el segundo componente de una pareja
ya creada.
(set_car! <pareja> <objeto>)
(set_cdr! <pareja> <objeto>)
Donde:
• <pareja> es el valor compuesto de tipo pareja al que se le desea cambiar un
componente.
• <objeto> es el nuevo valor que tomará el componente a ser modificado.
Un operador que permite establecer si un valor es o no es una pareja.
(pair? <objeto> )
Donde:
• <objeto> es el valor que se desea verificar como pareja.

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í:

1 ]=> (define P1 (cons 2 5 ))


;Value: p1
1 ]=> (pair? P1)
;Value: #t
1 ]=> (car P1)
;Value: 2
1 ]=> (cdr P1)
;Value: 5
1 ]=> (define P2 (cons 3 4 ))
;Value: p2
1 ]=> (define P3 (cons P1 P2 ))
;Value: p3
1 ]=> (car (cdr P3))
;Value: 3
1 ]=> (car (cdr (car P3)))
; The object 5, passed as the first argument to car, is not the correct type.
; To continue....

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

(cons (cons 1 2) (cons (cons 1


(cons 3 4)) (cons 2 3))
4)
Figura 4.1. Dos maneras de combinar 1, 2, 3 y 4 usando parejas.

[Link] Parejas en MAUDE.


En concordancia con la estrategia general del lenguaje, el MAUDE la gestión de parejas
debe acomodarse a las construcciones asociadas a la definición de una teoría en lógica
ecuacional multisort con membrecía. En otras palabras, no se ofrecen construcciones
específicas para la gestión de parejas.
La gestión de parejas es, sin embargo, un problema de programación sencillo en el marco
de la declaración y definición de operadores.

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.

op ->_ : Pairs_int -> Pairs_Int .


ceq ->(I J) = I J if(J > I) .
ceq ->(I J) = J I if(J <= I) .

Es importante señalar que en MAUDE es innecesario contar con la diversidad de


operadores asociados en SCHEME a la gestión de parejas. En particular; prescindiremos
de los selectores substituyéndolos por referencias a las componentes en el lado izquierdo de
las ecuaciones que las requieran en su lado derecho; prescindiremos del predicado de tipo
ya que éste es verificado automáticamente por el intérprete; y por último, prescindiremos de
Capítulo 9: Valores y Tipos Compuestos.
184
la instrucción de asignación ya que no existen variables distintas a las cuantificadas en los
axiomas.
9.3.2 Estructura del iterado: conjuntos, listas, árboles y grafos.
Si bien, las conexiones entre los componentes de los compuestos iterados pueden
representar muy diversos tipos de relación en el dominio del problema, es posible clasificar
los compuestos iterados por la forma que toman estas relaciones.
En particular, si consideramos sólo compuestos iterados en los que se cumplan las
condiciones siguientes:
• Los elementos se conectan con un sólo tipo de conexión que agrupa dos
componentes diferentes90.
• La conexión es dirigida, en el sentido de que asigna un rol diferente a cada uno
de los dos elementos que conecta (Vg. anterior y siguiente, padre e hijo, etc...).
• Los elementos conectados pertenecen al mismo compuesto iterado.
Podemos clasificar los compuestos iterados en los tipos que siguen:
• LISTAS: Todos los componentes están conectados a otros dos componentes
diferentes, con excepción de dos que denominaremos componentes primero y
ultimo. En las conexiones de un elemento éste juega máximo una vez cada uno
de los roles de la conexión, que denominaremos rol de anterior y rol de
siguiente. El componente denominado primero no tiene el rol siguiente, y el
componente denominado último no tiene el rol de. Anterior.
• ÁRBOLES: Todos los componentes están conectados a componentes
diferentes, pudiendo estar conectados a uno o a varios componentes diferentes.
En las conexiones de un elemento éste juega sólo una vez uno de los roles de la
conexión, que denominaremos rol de hijo, pero puede jugar varias veces el otro
rol, que denominaremos rol de padre. Existe uno y sólo un elemento, que
denominaremos raíz, que no juega el rol de hijo. Pueden existir varios
elementos, que denominaremos hojas, que no juegan el rol de padre. A todos
los demás elementos los denominaremos nodos.
• GRAFOS: Todos los componentes están conectados a componentes diferentes.
Un componente puede estar conectado a uno o a varios componentes diferentes.
Un componente puede jugar varias veces cualquiera de los roles de la conexión.
La importancia de estos tipos de compuestos iterados, es que, por un lado, permiten
modelar diversos tipos de elementos en el dominio del problema, y por otro, se pueden
manipular con códigos estandarizados que se abstraen del significado específico de la
relación.
Así, en lo que sigue, nos limitaremos a ejemplificar la manera de gestionar compuestos tipo
LISTA y tipo ARBOL en cada uno de los lenguajes seleccionados. Con ello sentaremos las
bases suficientes para la gestión de los demás tipos, y mostraremos las capacidades
relativas de los diferentes lenguajes frente a este tipo de compuesto.

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 .

9.4.2 Constructores de la lista.


Para tener listas específicas, es necesario construirlas partiendo de sus componentes reales.
[Link] Constructores de listas en SCHEME.
Una lista en SCHEME es un par cuyo segundo componente es una lista. Así, una lista
puede ser definida de forma recursiva de la manera siguiente [Hanson 2002, sec. 7]:
• La lista vacía es una lista.
• Es también una lista, un par cuyo primer elemento, denominado cabeza, es un
componente básico, y cuyo segundo elemento, denominado cola, es una lista.
Para representar una lista vacía en MIT SCHEME se usa el símbolo siguiente91:

’()

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

SCHEME provee adicionalmente algunos operadores nativos para construir listas.

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)

El siguiente operador nativo permite establecer si un objeto es una lista:


(list? <objeto> )
Donde:
• <objeto> Es el objeto a ser verificado como lista.
El siguiente operador nativo permite establecer si un objeto es lista es la lista vacía:
(null? <objeto> )
Donde:
• <objeto> Es el objeto a ser verificado como lista vacía.
Antes de escribir operadores sobre listas, es fundamental entender el uso de los operadores
nativos asociados con parejas (car, cdr), como el medio para acceder a los diferentes
elementos de la lista.

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í:

(car (list 1 2 3 4))


;;1
(cdr (list 1 2 3 4))
;;(2 3 4)
(car (cdr (cdr (list 1 2 3 4))))
;;3
Capítulo 9: Valores y Tipos Compuestos.
188
Es importante entender, también, que sólo el uso adecuado del cons da lugar a listas.

Ejemplo 126
El constructor cons puede ser usado para insertar un elemento al principio de la lista así:

(cons 0 (list 1 2 3 4))


;;(0 1 2 3 4)
Sin embargo, si se utiliza de otra manera, el resultado no es una lista. Así, en la figura que sigue, se
muestra la estructura de cajas resultante de diversas formas de usar el cons.

1 2 3 2 3 1 2 3 4

(cons (list 1 2 3) (list 2 3)) (cons (list 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.

[Link] Constructores de listas en MAUDE


Tal como se explicó antes, en el lenguaje MAUDE se obtienen los valores de un tipo
declarado cualquiera, por medio de operadores constructores (ver [Link]). El constructor
para un tipo compuesto recibe como argumentos las partes del compuesto y actúa como un
aglutinante para dichas partes.
Una forma clásica de partir la lista es la de dividirla en su primer componente, la cabeza, y
la lista que le sigue, la cola, para aglutinarlos como un par de la misma forma que en el
SCHEME. En [Clavel 2007, sec 6.3.5] puede verse una definición de la lista bajo este
enfoque. Esta forma de partición tiene, sin embargo como desventaja, que los elementos de
la secuencia quedan incluidos en parejas diferentes formándose una estructura de
encapsulamiento de tantos niveles de profundidad como elementos tenga la secuencia (ver
Ejemplo 123). El efecto de este encapsulamiento será evidente a medida que se definan
operadores sobre la lista.
Capítulo 9: Valores y Tipos Compuestos.
189
En MAUDE es posible lograr que todos los elementos de la lista queden al mismo nivel de
profundidad, evitando el encapsulamiento arriba referido, usando los dos elementos
siguientes (ver [Clavel 2007, SEC 7.12.1]):
• Un constructor que aglutina dos listas en lugar de un elemento y una lista.
• Los atributos ecuacionales assoc y id dentro de los <atributos_del _operador>
(ver [Link]) en el constructor de la lista.

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í:

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint ] .

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í:

subsort Int < ListInt .


Esto equivale a decir que un entero puede considerarse como una lista de enteros, validando su uso
como componentes elementales de las listas de enteros.

9.4.3 Recorridos básicos sobre listas.


Un proceso básico sobre compuestos iterados es el de recorrer la estructura, visitando en
algún orden específico los componentes elementales que la forman. En esta sección
ilustraremos la manera de llevar a cabo recorridos en el marco de los lenguajes analizados.
Para ello declararemos y definiremos tres operadores elementales, a saber:
• Un operador que obtenga el tamaño de la lista, contando los componentes que
esta posee.
• Un operador que obtenga la suma de los cuadrados de los elementos de la lista.
• Un operador que determine si un valor específico se encuentra en la lista.
Con el objeto de ilustrar la capacidad de los lenguajes frente a la definición de recorridos,
mostraremos, además, la manera de especificar recorridos que visten las componentes de la
Capítulo 9: Valores y Tipos Compuestos.
191
lista en orden directo, es decir moviéndose del primer elemento hacia el último, y en orden
inverso, es decir moviéndose desde el último hacia el primero.
[Link] Recorridos básicos sobre Listas en SCHEME.
Los recorridos en orden directo en SCHEME son planteados de una forma muy simple, por
medio de procesos que en cada paso de reescritura procesan el elemento de la cabeza de la
lista, que puede ser obtenido usando el operador car, y le dan al paso siguiente la cola de la
lista, que puede ser obtenida usando el operador cdr, para que repita la acción.
Los dos primeros operadores deben recorrer toda la lista.

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.

El número de reescrituras requeridas en los procesos de recorridos del ejemplo anterior, es


fácil de obtener, si se tiene en cuenta que en cada reescritura del proceso definido el tamaño
de la lista se reduce en 1, y que el proceso se termina cuando la lista es vacía. De lo
anterior se deduce que el número de reescrituras definidas en el proceso no supera el
tamaño de la lista. Así, si tenemos en cuenta que las operaciones en cada reescritura del
proceso son O(1) y éstas se repiten N veces, siendo N el tamaño de la lista, podemos
deducir que el orden del proceso es O(N).
En el caso del tercer operador, el recorrido se interrumpe cuando se obtiene el resultado
esperado.

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 L) (largo_ite 0 L))

(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 L) (largo_ite 0 L))

(define (largo_ite T L)
( if (null? L) T (largo_ite (+ 1 T) (cdr_inverso L)) )
)
En esta definición se usó el operador cdr_inverso que tiene como objeto reducir la lista en un
componente pero suprimiendo el último, en lugar del primero. Este operador 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.

op >| _ | : ListInt -> Int .


op <| _ | : ListInt -> Int .
var I : Int .
var L : ListInt .
eq >| nilLint | = 0 .
eq <| nilLint | = 0 .
eq <| I | = 1 .
eq >| I | = 1 .
eq >| I L | = 1 + >| L | .
eq <| L I | = 1 + <| L | .
Donde es importante notar que el emparejamiento determina que se asocie el primero o último elemento
de la lista a la variable I (que solo se puede asociar a un entero), y la sublista restante a la variable L (que
debe asociarse al resto para emparejar).
Nótese también que es necesaria una ecuación particular para el caso de una lista con un sólo elemento,
ya que, como se explicó en el Ejemplo 129, en las listas cuyo elemento identidad es el nilLint se asume
que solo aparece nilLint en la lista vacía (a diferencia del SCHEME en el que el nilLint es siempre el
último elemento de la lista). Se deja al lector la taréa de definir el operador en orden inverso para el caso
en que la lista tiene siempre el nilLint al final.
En la especificación siguiente el operador Σn2_ obtiene la suma de los cuadrados de los componentes de
la lista, recorriéndola en orden directo.

op Σn2_ : ListInt -> int .


var I : Int .
var L : ListInt .
eq Σn2 I L = (I * I) + Σn2 L .
eq Σn2 I = (I * I) .
eq Σn2 nilLint = 0 .
.....
Capítulo 9: Valores y Tipos Compuestos.
194
En la especificació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 cuando se encuentra el elemento buscado aun cuando no se haya
agotado la lista:

op _in_ : int ListInt -> bool .


vars I E : Int .
var L : ListInt .
ceq E in I L = true if(E == I) .
ceq E in I L = E in L if(E =/= I) .
ceq E in I = true if(E == I) .
ceq E in I = false if(E =/= I) .
eq E in nilLint = false .
.....

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))
)
)
)

El proceso definido en la especificación del ejemplo anterior es claramente recursivo; es


posible definir, también, un proceso iterativo (que introduce un problema adicional).

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.

(define (select-menor-a-v I L) (select-menor-a-v_ite I L ’()))


(define (select-menor-a-v-ite I L LR)
(if (null? L) LR
(if (< (car L) I ) (select-menor-a-v-ite I (cdr L) (cons (car L) LR))
(select-menor-a-v-ite I (cdr L) LR )
)
)
)
Donde la lista resultante tiene los elementos debidos pero en el orden inverso al que tenían en la lista
original.

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.

op {1Xen_ /X<_} : ListInt Int -> Int .


op error-valor-inexistente : -> Int .
vars I E : Int .
var L : ListInt .
ceq {1Xen (E L) /X< I } = E if(E < I) .
ceq {1Xen (E L) /X< I } = {1Xen L /X< I } if(E >= I) .
ceq {1Xen (E) /X< I } = E if(E < I) .
ceq {1Xen (E) /X< I } = error-valor-inexistente if(E >= I) .
eq {1Xen nilLint /X< I } = error-valor-inexistente .

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.

op {X<X1en_} : ListInt Int -> Int .


op no-hay : -> Int .
vars I E : Int .
var L : ListInt .
ceq { X<X1en (I E L) } = E if(E < I) .
ceq { X<X1en (I E L) } = { X<X1en (E L) } if(E >= I) .
ceq { X<X1en (I E) } = E if(E < I) .
ceq { X<X1en (I E) } = no-hay if(E >= I) .
eq { X<X1en I } = no-hay .
eq { X<X1en nilLint } = no-hay .
Donde la condición de inexistencia debe contemplar tanto el caso de que la lista sea vacía como el caso
de que no haya sino un elemento haciendo imposible la comparación.

El cuarto operador, por su parte, debe construir otra usando el operador constructor.

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.

op {*Xen_/X<_} : ListInt Int -> Int .


vars I E : Int .
var L : ListInt .
ceq {*Xen (E L) /X< I } = E {*Xen L /X< I } if(E < I) .
ceq {*Xen (E L) /X< I } = {*Xen L /X< I } if(E >= I) .
ceq {*Xen (E) /X< I } = E if(E < I) .
ceq {*Xen (E) /X< I } = nilLint if(E >= I) .
eq {*Xen nilLint /X< I } = nilLint .

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.

op {*Xen_/X<_} : ListInt Int -> Int .


op {*Xen_/X<_ite_} : ListInt Int ListInt -> Int .
vars I E : Int .
vars L LR : ListInt .
eq {*Xen L /X< I} = {*X en L /X< I ite nilLint } .
ceq {*Xen (E L) /X< I ite LR} = {*Xen L /X< I ite (LR E)} if(E < I) .
ceq {*Xen (E L) /X< I ite LR} = {*Xen L /X< I ite LR} if(E >= I) .
ceq {*Xen (E) /X< I ite LR} = LR E if(E < I) .
ceq {*Xen (E) /X< I ite LR} = LR if(E >= I) .
eq {*Xen nilLint /X< I ite LR} = LR .
Donde la lista resultante tiene los elementos debidos en el orden correcto.

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.

(define (append L1 L2)


(if (null? L1) L2
Capítulo 9: Valores y Tipos Compuestos.
201
(if (null? L2) L1
(cons (car L1) (append (cdr L1) L2) )
)
)
)
Donde la evocación recursiva del operador definido tiene como único objeto profundizar en los niveles
de encapsulamiento de L1, para llegar al nivel donde aparece la lista vacía. Esta lista es, entonces,
substituida por la lista L2.

Para intuir el orden del proceso asociado al operador definido en el ejemplo anterior, es
suficiente con notar que la reescritura especificada en la definición se debe llevar a cabo
tantas veces como los niveles de encapsulamiento que existen en la primera lista. Así si se
acepta que las operaciones en cada paso de dicha reescritura se asocian con un proceso de
orden O(1), el orden total del proceso es O(N), siendo N el tamaño de la primera lista.
[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.

op _[_]<=_ : ListInt Int Int -> ListInt .


vars I J E : Int .
var L : ListInt .
ceq L[I]<=E = E L if(I <= 1) .
ceq (J L)[I]<=E = J L[I -1]<=E if(I > 1) .
ceq J[I]<=E = J E if(I > 1) .
ce nilLint[I]<=E = E .
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 _[_]<=[] suprime el componente localizado en una posición dada
de la lista, o no suprime ninguno si la posición dada es inválida.

op _[_]<=[] : ListInt Int -> ListInt .


vars I J E : Int .
var L : ListInt .
eq (J L)[1]<=[] = L .
eq J[1]<=[] = nilLint .
ceq (J L)[I]<=[] = J L[I -1]<=[] if(I > 1) .
ceq J[I]<=[] = J if(I > 1) .
eq nilLint[I]<=[] = nilLint .
En la definición siguiente el operador _not-in_ suprime los componentes que tienen un valor dado.

op _not-in_ : int ListInt -> ListInt .


vars I E : Int .
Capítulo 9: Valores y Tipos Compuestos.
202
var L : ListInt .
eq E not-in (E L) = E not-in L .
ceq E not-in (I L) = I (E not-in L) if(E =/= I) .
eq E not-in E = nilLint .
ceq E not-in I = I if(E =/= I) .
eq E not-in nilLint = nilLitnt .
.....
Donde el lector puede notar que el operador borra, no sólo la primera ocurrencia del elemento sino todas
ellas.
En la definición siguiente, el operador set_ elimina los elementos repetidos de la lista dejando la última
ocurrencia de cada elemento. Para ello usa el operador _in_ definido previamente en el Ejemplo 135.

op set : ListInt -> ListInt .


vars I E : Int .
var L : ListInt .
ceq set(E L) = set(L) if(E in L) .
ceq set(E L) = E set(L) if(not (E in L)) .
eq set(nilLint) = nilLint .
eq set(E) = E .
.....
Donde el lector puede notar que la definición del operador difiere de la del anterior sólo en que la
condición de eliminación del componente es diferente.

Dado que en la definición de los operadores del ejemplo anterior, se especifican reescrituras
similares a las de su correspondiente definición en SCHEME, los procesos asociados a
dichos operadores tiene el mismo orden que sus correspondientes en SCHEME.
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.

op _|_ : ListInt ListInt -> ListInt .


vars L1 L2 : ListInt .
eq L1 | L2 = L1 L2 .
Nótese que cuando se adicionan elementos a una lista vacía, o una lista vacía a otra lista cualquiera, no
se generan nilLint intermedios, debido a que estos juegan el rol del elemento identidad.

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.

op <->_ : ListInt -> ListInt .


vars I E : Int .
var L : ListInt .
eq <->nilLint = nilLint .
eq <-> E = E .
ceq <->( I E ) = E I if(E >= I) .
ceq <->( I E ) = I E if(E < I) .
ceq <-> (L E I ) = (<-> L I) E if(E < I) .
ceq <-> (L E I ) = (<-> L E) I if(E >= I) .
.....

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.

op B>>_ : ListInt -> ListInt .


op auxB>>_ : ListInt -> ListInt .
vars I E : Int .
var L : ListInt .
eq B>>nilLint = nilLint .
ceq B>>L = auxB>>(<-> L) if(L =/= nilLint) .
eq auxB>>(E L) = E auxB>>(<->L) .
eq auxB>>E = E .
Donde el lector puede notar que el algoritmo toma ventaja de que el proceso de intercambio, efectuado
en el orden inverso, siempre deja el elemento menor de la lista de primero.

Dado que la reescritura que evoca el intercambio se lleva a cabo N-1 veces, y que el
algoritmo de intercambio en si mismo tiene el orden O(M) siendo M el tamaño de la lista
que recibe (M va disminuyendo desde N hasta 1), el sort tendrá el orden O(N2).
El operador que implementa el “Quick Sort” es, en este caso, un ejemplo de simpleza y
expresividad (el lector puede comparar la especificación del ejemplo que sigue con la
correspondiente en C++ presentada en [Sylvan 2007]).
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”.

op Q>>_ : ListInt -> ListInt .


vars I E : Int . var L : ListInt .
eq Q>>nilLint = nilLint .
eq Q>>(E L) = (Q>>{*X en L /X< E}) E (Q>>{*X en L /X>= E}) if(L =/= nilLint) .
Donde se usaron los operadores de selección {*Xen_/X<_} y {*X en_/X>=_}, definidos, el primero en
el Ejemplo 143, y el segundo como ejercicio para el lector.

Tal como fue referido arriba el orden del Quick Sort es, en el peor de los casos O(N2) y en
el caso promedio es O(N logN).

9.5 Gestión de Árboles.


Al igual que para las listas, en el caso de los árboles se debe contar con operadores que
permitan construirlos, modificarlos, transformarlos y seleccionar sus componentes. En esta
sección nos limitaremos, sin embargo, a ilustrar de forma superficial la manera de construir
y manipular árboles usando los elementos de los lenguajes analizados. Esperamos, con
ello, que el lector adquiera las bases necesarias para llevar a cabo una especificación más
completa por su propia cuenta.
Para ilustrar la gestión de los árboles nos apoyaremos en el denominado “árbol binario de
búsqueda”. Para este árbol presentaremos un constructor, un modificador y un selector.
9.5.1 Ejemplo: Árbol Binario de Búsqueda.
El árbol binario de búsqueda es uno de los compuestos más usados en computación. Este
árbol fue concebido como un mecanismo para agilizar las operaciones de inserción y
localización de las componentes de un compuesto iterado, y, en alguna de sus variantes, es
piedra fundamental en todos los gestores de datos.
El árbol binario de búsqueda se puede caracterizar por una serie de condiciones que debe
cumplir, así:
• Es un iterado donde en cada componente hay un valor de identificación que es
único, diferenciándolo de los demás componentes. Los valores de identificación
tienen la característica de ser ordenables.
• Con excepción de las hojas, todos los componentes pueden ser padres de
máximo dos hijos diferentes, que llamaremos “izquierdo” y “derecho”. Cuando
un nodo tiene sólo un hijo, este puede ser tanto el izquierdo como el derecho,
dependiendo de su valor de identificación.
• Para todo componente se cumple que el hijo de la derecha, si existe, posee un
valor de identificación menor que el suyo propio, y el hijo de la izquierda, si
existe, posee un valor de identificación mayor que el suyo propio.
• Cada hijo es la raíz de un árbol (o sub-árbol), siendo disjuntos los árboles que
parten del hijo izquierdo y del derecho.
Capítulo 9: Valores y Tipos Compuestos.
207
9.5.2 Construcción del Árbol Binario de Búsqueda.
Para tener árboles binarios de búsqueda específicos, se debe contar con un constructor.
Para definir el constructor tomaremos ventaja de la naturaleza recursiva del árbol binario de
búsqueda, Así:
• Un árbol binario de búsqueda puede siempre verse como un componente (la
raíz) unido a dos árboles binarios de búsqueda disjuntos.
• Cuando alguno de los hijos de un componente falta, podemos considerar que el
árbol que se le asocia es un árbol vacío.
[Link] Construcción del Árbol en SCHEME.
Al igual que con la lista, los elementos del árbol deben ser unidos formando pares por
medio del operador cons. A la manera de unir los elementos del árbol (usando pares), la
denominaremos su “representación”.
Para formar el árbol con base en pares, usaremos la representación que se describe a
continuación:
• Un árbol binario de búsqueda es ya sea un componente, o un par que junta su
raíz con sus sub-árboles.
• Los sub-árboles se juntan en un par que tiene como su primer elemento el sub-
árbol de la derecha, y como su segundo elemento el sub-árbol de la izquierda.
• En caso de que alguno de los sub-árboles sea el árbol vacío este será
representado por medio de un nil.

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
)
)
)
)

Es importante notar que la forma de representar el árbol no es única, y, en consecuencia, es


una decisión de diseño, tomada al momento de concebir la especificación.
Con el objeto de facilitar la definición de los demás operadores incluiremos dos operadores
que crean un árbol.
Capítulo 9: Valores y Tipos Compuestos.
208
Ejemplo 156
El operador make-Btree, recibe un componente y dos árboles dando como resultado un árbol binario
de búsqueda elemental, que tiene al componente como su raíz, y a los dos sub-árboles como los sub-
árboles que parten de la raíz, así:

(define (make-BTree E AI AD) (cons E (cons AI AD)) )


El operador make-e-Btree, recibe un componente y da como resultado un árbol binario de búsqueda
elemental, que tiene al componente como su raíz., así:

(define (make-e-BTree E ) (cons E (cons nil nil)) )

Es importante anotar que el uso del operador make-BTree, no garantiza, en si mismo, la


creación de un árbol binario de búsqueda correcto, debido que no controla la satisfacción de
las condiciones relativas a los valores de identificación de los componentes. El uso de esta
operación queda, entonces, bajo la responsabilidad del usuario de la especificación.
Con el objeto de independizar la definición de los operadores de la representación escogida
para el árbol, incluiremos operadores para acceder a la raíz y a los sub-árboles que parten
de la raíz. Una discusión de la importancia de esta estrategia, aparece en [Abelson 85,
[Link]].

Ejemplo 157
El operador get_root, obtiene la raíz de un árbol suministrado como operando, así:

(define (get-root A) (car A) )


El operador get_ArI, obtiene el sub-árbol Izquierdo de un árbol suministrado como operando, así:

(define (get-ArI A) (car (cdr A)) )


El operador get_ArD, obtiene el sub-árbol derecho de un árbol suministrado como operando, así:

(define (get-ArD A) (cdr (cdr A)) )

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

Respetando el estilo que se ha venido usando en MAUDE, nos abstendremos de elaborar


constructores y selectores, de la manera usada en SCHEME. El precio a pagar es el de
hacer la especificación de los operadores dependiente de la representación. El uso de
notación infija disminuirá, sin embargo, el impacto de esta circunstancia.
9.5.3 Localización de un Componente en el Árbol Binario de Búsqueda.
Para localizar un componente en un árbol binario de búsqueda, con base en su valor de
identificación, se deben visitar una serie de componentes del árbol verificando si su valor
de identificación corresponde con el buscado. El primer componente visitado debe ser la
raíz del árbol. Si un componente visitado no es el buscado, se debe proceder a visitar ya
sea la raíz del sub-árbol derecho, o la raíz del sub-árbol izquierdo que están conectados al
componente. Se procede con la raíz del sub-árbol derecho cuando el valor de identificación
buscado es menor que el del componente, y en caso contrario, se procede con la raíz del
sub-árbol izquierdo. De llegarse a un sub-árbol vacío sin haber hallado el componente, es
porque éste no se encuentra en el árbol.
[Link] Localización de un Componente del Árbol en SCHEME.
Capítulo 9: Valores y Tipos Compuestos.
210
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.

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í:

op _in?_ : Int BTreeInt -> bool .


vars T TD TI : BtreeInt .
vars E ER : Int .
eq E in? nilBTInt = false .
eq E in? <E> TI TD = true .
ceq E in? <ER> TI TD = E in? TI if(E < ER) .
ceq E in? <ER> TI TD = E in? TD if(E > ER) .

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í:

op _in_ : Int BTreeInt -> BTreeInt .


vars T TD TI : BtreeInt .
vars E ER : Int .
eq E in nilBTInt = < E > nilBTInt nilBTInt .
eq E in <E> TI TD = <E> TI TD .
ceq E in <ER> TI TD = <ER> (E in TI) TD if(E < ER) .
ceq E in <ER> TI TD = <ER> TI (E in TD) if(E > ER) .

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.6 Ejercicios propuestos


Datos compuestos estructurados.
1- En [Abelson 85, sec. 2.1.1 ay 2.1.2] se definen directamente los constructores y
selectores de un tipo estructurado, con base en pares, sin utilizar el tipo registro ofrecido
pro el SCHEME. Lea las secciones correspondientes y lleve a cabo los ejercicios
siguientes: 2.1, 2.2, 2.3.
2- Aritmética de Intervalos: Realizar basándose en los lenguajes analizados, los siguientes
ejercicios de la sección 2.1.4 en [Abelson 85, sec. 2.1.4]: 2.7, 2.8, 2.9, 2.10, 2.11.
3- Punto en un plano: Elabore en MAUDE y en SCHEME un tipo compuesto estructurado
que represente un punto en un plano. Defina operadores para:
Hallar las coordenadas cartesianas del punto.
Hallar las coordenadas polares del punto.
Hallar la distancia entre dos puntos.
Hallar el área de un rectángulo cuyos vértices son dos puntos dados.
4- Rectángulo en un plano: Elabore en MAUDE y en SCHEME un tipo compuesto
estructurado que represente un rectángulo en un plano con base en dos puntos que
representen dos esquinas diagonalmente opuestas del rectángulo cualquiera que estas
sean. Elabore luego operadores para:
Hallar el área de un rectángulo.
Hallar el rectángulo más pequeño que incluye dos rectángulos dados.
Capítulo 9: Valores y Tipos Compuestos.
213
Desplazar el rectángulo una distancia dada representada por un punto.
Deformar el rectángulo con base en un desplazamiento de la esquina superior izquierda
representado por medio de un punto.
Determinar si un punto dado es o no es interior al rectángulo.
4- Rectángulo normalizado: Asuma que los puntos que representan el rectángulo en el
ejemplo anterior, son siempre su esquina superior izquierda y su esquina inferior
derecha. Reelabore para este caso los operadores del ejercicio anterior.
4- Elabore en SCHEME y MAUDE un tipo compuesto estructurado que registre la
información relativa a un estudiante del curso “Lenguajes Declarativos”.
Pares
1- SCHEME: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2.2]: 2.25, 2.26.
2- Dibuje la representación de cajas de las listas referidas y resultantes en los ejercicios
2.26, 2.27 de [Abelson 85, sec. 2.2.2]
Listas
Definición SCHEME
1-: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2]: 2.17, 2.18, 2.21, 2.22, 2.34.
Definición en MAUDE:
1- Cambie el constructor de listas en MAUDE por el siguiente:
op _ _ : Int ListInt -> ListInt [ctor ] .
Explique el efecto que tendría éste cambio en los ejemplos sobre listas en MAUDE.
Reelabore los ejemplos en MAUDE para que operen con base en el nuevo constructor.
2- Para un operador con el siguiente perfil:
op sel : ListInt -> Int .
Elabore ecuaciones que den como resultado los elementos siguientes:
Primer elemento de la lista.
Suma del primero y tercer elemento de la lista.
Diferencia entre el segundo elemento y el penúltimo.
Suma de las diferencias entre el primero y el último y el segundo y el penúltimo.
El valor del elemento del medio.
Listas de enteros:
Elabore en los lenguajes analizados operadores que lleven a cabo las tareas que se muestran
a continuación. Elabore operadores que lleven a cabo la tarea por medio de un proceso
tanto recursivo como iterativo. Indique el orden de cada una de las soluciones definidas.
1- Dados dos vectores X y Y de N componentes:
X1, X2, X3,....., Xi,..... Xn
Capítulo 9: Valores y Tipos Compuestos.
214
Y1, Y2, Y3,....., Yi,..... Yn
Correspondientes a las coordenadas cartesianas de los vértices de un polígono cerrado:
Hallar el perímetro aplicando la fórmula siguiente:
Perímetro = ∑i√ (Xi+1 – Xi)2 + (Yi+1 – Yi)2
Hallar el área aplicando la fórmula siguiente:
Area = ∑i (Yi * Xi+1 - Xi * Yi+1)
2- Suprima un elemento dado de un vector, contemplando la posibilidad de que dicho
elemento ocurra más de una vez en el vector.
3- Suprima las ocurrencias múltiples de los elementos en una lista, dejando como resultado
una lista con los mismos elementos pero sin ocurrencias múltiples.
15- Declare un operador podarDerecha, que elimine ocurrencias múltiples en una lista
dejando únicamente la primera ocurrencia, y defina su funcionalidad mediante
ecuaciones.
4- Intercale los elementos de dos listas ordenadas para obtener una nueva lista ordenada con
los elementos de las dos listas originales.
5- Separe los elementos de una lista en los que son menores que un valor dado (el umbral),
y los que son mayores o iguales que ese valor dado, dejando como respuesta dos listas,
una con los mayores que el umbral y otra con los menores que el umbral.
6- Suprimir un elemento de un vector, dada su posición en el vector.
7- Incluir un elemento en un vector delante del elemento que tiene una posición dada.
8- Sumarle a todos los elementos de un vector hasta una posición dada, un valor dado.
9- Sumarle un valor dado al primer componente de un vector que tenga un valor mayor que
otro valor dado.
14- Obtenga el valor de las fórmulas del ejercicio 5 del Capítulo 8, pero considerando que
los valores de x son obtenidos de forma sucesiva de una lista de valores dados.
11- Reconozca la existencia en una lista de un segmento dado, dejando como respuesta la
posición del carácter donde aparece el segmento.
13- Coloque la primera mitad de la lista luego de la segunda sin cambiar el orden de los
elementos de cada mitad.
Listas de Registros:
Para una lista de componentes, cada uno de ellos representando a un estudiante del curso
“Lenguajes Declarativos”, lleve a cabo las tareas siguientes en los diferentes lenguajes
analizados:
1- Defina el tipo de dato con su correspondiente constructor.
2- Elabore operadores para lleva a cabo las tareas siguientes.
Hallar el promedio de notas de los estudiantes del curso.
Capítulo 9: Valores y Tipos Compuestos.
215
Hallar la diferencia entre el promedio de notas de los estudiantes que están viendo el
curso por primera vez, y los estudiantes que están repitiendo el curso.
Seleccionar los M estudiantes con mejores notas, para un valor de M suministrado.
Para una lista de componentes, cada uno de ellos representando un punto en el plano, lleve
a cabo las tareas siguientes en los diferentes lenguajes analizados:
1- Defina el tipo de dato con su correspondiente constructor.
2- Elabore operadores para lleva a cabo las tareas siguientes.
Hallar el área y el perímetro de la poligonal cerrada formada por los puntos,
asumiendo que el último tramo de la poligonal une el último punto con el primero.
Determine si un punto dado se halla al interior de la poligonal cerrada formada por
los puntos.
Listas de listas
1-: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2]: 2.27, 2.28, 2.32.
2- Cree un constructor en MAUDE que le permita representar listas de listas, y lleve a cabo
los ejercicios del punto anterior con base en dicha representación.
Árbol Binario de Búsqueda.
[Link] un operador, que cuente el número de nodos de un árbol binario de búsqueda.
[Link] un operador que tome como argumentos un árbol y un identificador. En caso
de que el identificador se encuentre en el árbol, lo elimine del árbol. Hay que tener
cuidado con las posibles ramas que se desprenden del nodo a eliminar, estas deben ser
reubicada. La función debe retornar el árbol reorganizado.
3- Desarrolle un operador de inserción que mantenga el árbol balanceado.
Capítulo 10
Programación Paramétrica y
Relaciones entre Tipos.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
218
10.1 Introducción
Los operadores definidos por el usuario pueden verse como patrones de proceso aplicables
a las múltiples combinaciones posibles de valores para sus operandos. Los mecanismos de
selección no hacen otra cosa que ampliar el universo de dichas combinaciones, adaptando
cada proceso al carácter de los valores particulares que involucra.
Así, la capacidad de definir operadores, se constituye en un mecanismo para parametrizar
procesos abstrayéndolos de los valores (o datos) involucrados en los mismos.
Los valores involucrados no son, sin embargo, lo único que puede abstraerse en la
definición de un proceso. Es, en efecto, posible abstraerse también de los operadores
particulares y de los tipos de los valores particulares involucrados en el proceso.
La utilidad de abstraer el tipo de los valores, puede verse fácilmente si se consideran los
operadores asociados con los compuestos iterados. En una lista, por ejemplo, la definición
del operador que obtiene su tamaño es independiente del tipo de los elementos de la lista.
Si fuera posible abstraerse del tipo del componente, se podría definir este operador una sola
vez, de forma genérica, para todas las listas evitando hacerlo cada vez que se cambie el tipo
del componente.
La utilidad de abstraerse de un operador particular, puede verse fácilmente si se considera,
por ejemplo, la definición del operador que obtiene la raíz de una función por el método de
la partición binaria (ver ejercicio XXXX): La definición de dicho operador es, en efecto,
independiente de la función a la que se le desea calcular su raíz. Si fuera posible abstraerse
del operador que representa la función, se podría definir el operador que obtiene la raíz una
sola vez, de forma genérica, para todas las funciones evitando hacerlo cada vez que cambie
la misma.
En este capítulo presentaremos, para cada lenguaje analizado, los mecanismos de
abstracción que ofrecen al definir los operadores, con énfasis en la abstracción de operador
y de tipo.

10.2 Abstracción de tipo y operador en SCHEME.


En el lenguaje SCHEME se ofrecen dos mecanismos de abstracción distintos para el tipo y
para el operador.
El mecanismo de abstracción de tipo se apoya en el hecho de que el lenguaje es débilmente
tipado (ver sec. 7.5.1).
El mecanismo de abstracción de operadores se apoya en la posibilidad de usar argumentos
que representan operadores. Por medio de estos argumentos se parametriza la definición de
un operadores abstrayéndolo de (algunos de los) operadores referidos en la definición.
En las secciones siguientes presentaremos estos dos enfoques y analizaremos sus
consecuencias.
10.2.1 Abstracción de tipo en SCHEME.
Tal como se indicó en el Capítulo 7, los tipos en SCHEME son latentes, en lugar de ser
manifiestos, en el sentido de que el tipo se asocia a los valores pero no a las variables que
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
219
los representan [Hanson 2002, Ch 1]. En consecuencia, puesto que las variables se usan
como argumentos formales al definir los operadores, estos pueden, en principio, evocarse
con valores de cualquier tipo.
En particular los operadores definidos para las listas en SCHEME, pueden ser usados con
cualquier lista independientemente del tipo de sus componentes.

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í:

1 ]=> (define Vects (list (make-Vector 2 5 ) (make-Vector 3 4 )))


...
1 ]=> (largo Vects)
...2

1 ]=> (define Ints (list 2 5 ) )


...
1 ]=> (largo Ints)
...2

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í:

1 ]=> (define Ints-Vects (list (make-Vector 2 5 ) 4 ))


...
1 ]=> (largo Ints-Vects)
...2

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) ))
)
)
)

10.2.2 Abstracción de operadores en SCHEME.


Tal como se refiere en [Hanson 2002, sec. 1.3.1], en SCHEME los procedimientos son
considerados como objetos de primer orden. En consecuencia, al igual que los valores
pueden ser asociados con variables, pasados como argumentos, e incluso constituirse en
valores de retorno de otros procedimientos.
[Link] Operadores como argumentos de otros operadores.
Dado que los operadores son objetos de primer orden en SCHEME, es posible considerar
que algunos de los argumentos formales en la definición de un operador representan a un
operador en lugar de representar un valor. Esto implica que dentro de la definición este
argumento será usado como si fuera un operador, evocándolo con sus respectivos
operandos. Al evocarse el operador así definido, es, por supuesto, necesario suministrarle
un operador real como operando.
Siguiendo la línea de pensamiento de [Abelson 85 sección 1.1.3], ilustraremos lo anterior
apoyándonos en las oportunidades de abstracción que aparecen en los ejemplos de
acumulación previamente elaborados.

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í:

(define (aux-pi k S) (+ (/ 1 (* (- (* 4 k) 3) (- (* 4 k) 1))) S)


(define (aux-frac k S) (/ k (+ (* k k) S) )
El lector debe notar que estas funciones no son otra cosa que la definición en el lenguaje de
programación de las funciones auxiliares en las que se fundamentaron los operadores de la sección
[Link].
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
222
Finalmente los operadores que calculan las acumulaciones, pueden construirse con base en las funciones
genéricas y las auxiliares específicas, así:

(define (sum_pi n) (acumule-rec 1 n aux-pi))


(define (cont_frac n) (acumule-rec 1 n aux-frac))

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í:

(define (sum_pi n) (acumule-rec 1 n valor-absoluto))


Este error se detectaría sólo en el momento de usar el operador sum_pi, así:

1 ]=> (sum_pi 10)


...ERROR XXXXXXX

[Link] Expresiones lambda


En ocasiones, al usar procedimientos genéricos como el definido en el ejemplo anterior,
parece absurdo tener que definir procedimientos triviales con el único objeto de pasarlos
como argumentos.
El SCHEME ofrece la forma especial lambda para definir operadores sin nombre, que
pueden ser evocados directamente o usados como operandos al evocar un operador.
La forma general de la definición de un operador por medio de la forma especial lambda es
la siguiente [Hanson 2002, sec. 2.1]:
(lambda (<argumentos formales>) <cuerpo>)
Donde:
• <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.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
223
• <cuerpo> es un término o expresión que decide el valor de la aplicación del
procedimiento.
Un operador creado por medio de una expresión lambda puede ser evocado como
cualquier otro operador.

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.

1 ]=> ((lambda (x) (* x x)) 2)


;Value: 4
Donde la parte en negrita corresponde a la definición del operador, que es evocado con el número 2
como operando.

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) ))
)

[Link] Operadores como resultado de la evocación de operadores.


Por ser un objeto de primer orden en el lenguaje SCHEME, un operador puede ser obtenido
también como el resultado de la evocación de un operador.
Para indicar que el resultado de evocar un operador es un operador, basta con utilizar una
expresión lambda en la definición del operador.

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í:

(define (derivada f dx)


(lambda (x) (/ (- (f (+ x dx)) (f x) dx) )
)
Que de ser evocada genera una función que recibe como argumento a otra función.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
224

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:

(define (derivada f x dx)


(/ (- (f (+ x dx)) (f x) dx)
)
Produce el mismo resultado de una evocación de la función anterior, si un operador debe recibir la
función como argumento para evocarla varias veces con el mismo valor de dx, es más eficiente que
reciba la función que, fijado el valor de dx, no lo requiere como argumento.

10.3 Abstracción de teorías en MAUDE.


El lenguaje MAUDE provee un mecanismo de abstracción aplicable tanto los operadores
como a los sorts. Este mecanismo se apoya en parametrizar sus unidades de
modularización, es decir los módulos, entre los que se encuentran los módulos funcionales
analizados en los capítulos anteriores (ver sec 7.8.2).
La parametrización de módulos en MAUDE permite definir módulos abstrayendo algunos
de los módulos que el módulo definido incluye. Así, un módulo parametrizado puede
cambiar uno o varios de los módulos que incluye, cambiando el valor del parámetro que los
representa.
Puesto que dentro de un módulo se usan los operadores y sorts de los módulos incluidos, la
abstracción de estos módulos, implica la abstracción de operadores y sorts, que son el
objeto de nuestra discusión97.
Un módulo parametrizado puede ser instanciado asociando los parámetros con los módulos
reales a ser incluidos. MAUDE requiere, sin embargo, que un módulo real asociado con un
parámetro satisfaga una serie de condiciones definidas para el parámetro. Las condiciones
definidas para el parámetro son descritas en una “teoría”. Así, cada parámetro de un
módulo parametrizado se asocia con la teoría que debe satisfacer el modulo real asociado.
Para indicar la manera como los elementos de un módulo real satisfacen las condiciones de
una teoría, se debe crear una “vista” de la teoría sobre el módulo real. Entonces al
instanciar un módulo parametrizado se substituye cada parámetro con el nombre de una
vista del modulo real sobre la teoría del parámetro.

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.

fmod SET{X :: TRIV} is


sorts Set{X} .
subsorts X$Elt < Set{X} .
op empty : -> Set{X} .
op _,_ : Set{X} Set{X} -> Set [assoc comm id: empty] .

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 PAIR{X :: TRIV, Y :: TRIV} is


sort Pair{X, Y} .
op <_;_> : X$Elt Y$Elt -> Pair{X, Y} .
op 1st : Pair{X, Y} -> X$Elt .
...
...
endfm
Donde se muestra que al usar el sort Elt definido en TRIV, se debe cualificar con el nombre del
parámetro (X$Elt Y$Elt), en lugar de cualificarse con el nombre de la teoría. Con ello las distintas
cualificaciones pueden representar sorts distintos al momento de instanciar el módulo parametrizado.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
227
Así, de instanciarse el módulo parametrizado para formar una pareja de un entero y un real:

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.

10.3.2 Definición de teorías.


Las teorías definen las condiciones que debe satisfacer un módulo para poder ser incluido
en un módulo parametrizado a través de un parámetro. Una teoría define las propiedades
sintácticas y semánticas que debe satisfacer dicho módulo. Al igual que los módulos, las
teorías pueden ser “teorías funcionales” (o “functional theories”), o “teorías sistémicas”
(o “system theories”).
Las teorías funcionales le dan soporte a una lógica ecuacional con membresía (o
membership equational logic), pero, a diferencia de los módulos funcionales las teorías
funcionales pueden tener axiomas declarados con el atributo nonexec [Clavel 2007, sec
4.5.3]. Los axiomas con este atributo no son usados en el proceso de reescritura y sirven
para definir la semántica del módulo98 . Estos axiomas le permiten al módulo liberarse de
tener que satisfacer las condiciones de Church-Rosser, pero sin perjudicar la capacidad de
efectuar cálculos con los axiomas que si las cumplen.
Una teoría funcional se define de la manera siguiente:
fth <nombre> is <cuerpo_del_modulo> endfth
Donde:
• <nombre> Es el identificador de la teoría, usualmente colocado en letra capital.
• <cuerpo_del_modulo> Es el cuerpo de un módulo funcional tal como fue
descrito en la sección [Link], y con la posibilidad de incluir axiomas
cualificados con el atributo nonexec.
Una teoría puede importar otros módulos o teorías como submódulos o subteorías. Una
teoría no puede, sin embargo, ser importada por módulos. Las teorías sólo pueden ser
usadas en los parámetros de los módulos parametrizados, o importadas por otras
teorías. Además, si bien una teoría puede importar a otra teoría, sólo puede hacerlo por
medio de including. No pueden importarse teorías usando protecting o extending.
Con el objeto de ilustrar el uso de las teorías y sus relaciones, reproducimos aquí las teorías
presentadas en [Clavel 2007, sec 6.3.1], que ilustran la definición de los operadores de
orden _<_ y _<=_ para conjuntos total y parcialmente ordenados.

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.

MAUDE ofrece de forma nativa una serie de teorías, incluidas en el archivo


[Link].

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

Las teorías TOTAL-PREORDER y TOTAL-ORDER (tomadas de [Clavel 2007, sec 7.11.4]),


define las propiedades del operador _<=_ para conjuntos débilmente y totalmente ordenados:

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

10.3.3 Creación de vistas.


Para poder incluir un módulo en una instancia de un módulo parametrizado a través de un
parámetro, se debe primero crear una vista de la teoría asociada al parámetro sobre el
módulo a ser incluido, y luego usar la vista como argumento real de la instancia del módulo
parametrizado.
Una vista se define de la manera siguiente:
view <nombre> from <fuente> to <destino> is
<mapeo>
endv
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
230
Donde:
• <nombre> Es el identificador de la vista, donde es costumbre usar el mismo
nombre asociado con <destino>99.
• <fuente> Es el nombre asociado a una teoría, o una expresión que evalúe a una
teoría. En lo que sigue nos referiremos a ella como la fuente.
• <destino> Es el nombre asociado a un módulo o a una teoría, o una expresión
que evalué a uno de ellos. En lo que sigue nos referiremos a este módulo o
teoría como el destino.
• <mapeo> Es una serie de <expresiones-de-mapeo> separadas por espacios,
Las <expresiones-de-mapeo> proyectan los sorts y operadores de la teoría referida en
<fuente> a los sorts y operadores del módulo o teoría referida en <destino>.
La proyección de un sort se define por medio de una expresión de la forma siguiente:
sort <nombre-fuente> to <nombre-destino> .

Donde:
• <nombre-fuente> Es el nombre de un sort de la fuente.
• <nombre-destino> Es el nombre de un sort del destino.
Dada la proyección de un sort de la fuente a un sort del destino, el sort del destino será
representado por el sort de la fuente, tanto dentro de la fuente como dentro de los módulos
parametrizados que incluyen el destino a través del parámetro asociado a la fuente. Los
sorts que no aparezcan en el mapeo se asumen proyectados a los sorts de igual nombre.
Para cada sorts de la fuente debe existir un sort correspondiente en el destino. Además si el
sort S es subsort del sort T en fuente, los sorts correspondientes en el destino S´ y T´ deben
mantener la misma relación de subsort. Así, si dos sort S y T de la fuente pertenecen a un
mismo “kind” (ver 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:

view Bool from TRIV to BOOL is


sort Elt to Bool .
endv
Proyecta TRIV al módulo nativo BOOL creando una vista con nombre Bool. De forma idéntica existen
las vistas Nat, Int, Rat, Float, String y Qid.
La teoría DEFAULT se proyecta a una serie de módulos nativos [Clavel 2007, sec 7.11.2]. Así, la vista
siguiente:

view Nat0 from DEFAULT to NAT is


sort Elt to Nat .
endv
Proyecta DEAFAULT al módulo nativo NAT creando una vista con nombre Nat0. De forma idéntica
existen las vistas Int0, Rat0, Float0, String0 y Qid0.
La teoría STRICT-TOTAL-ORDER se proyecta a una serie de módulos nativos [Clavel 2007, sec
7.11.3]. Así, la vista siguiente:

view Nat< from STRICT-TOTAL-ORDER to NAT is


sort Elt to Nat .
endv
Proyecta STRICT-TOTAL-ORDER al módulo nativo NAT creando una vista con nombre Nat<. De
forma idéntica existen las vistas Int<, Rat<, Float< y String< .
La teoría TOTAL-ORDER se proyecta a una serie de módulos nativos [Clavel 2007, sec 7.11.4]. Así,
la vista siguiente:

view Nat<= from TOTAL-ORDER to NAT is


sort Elt to Nat .
endv
Proyecta TOTAL-ORDER al módulo nativo NAT creando una vista con nombre Nat<=. De forma
idéntica existen las vistas Int<=, Rat<=, Float<= y String<=0 .

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.

Es posible incluir en un módulo parametrizado una instancia de otro módulo parametrizado.


El modulo incluido puede, además, ser instanciado con parámetros del módulo inclusor.
Para ello es necesario que las teorías asociadas a los parámetros del módulo inclusor,
correspondan con las de los parámetros del módulo incluido. Al instanciamiento de
módulos parametrizados con los parámetros del módulo parametrizado que los incluye se le
denomina enlace de parámetros [Clavel 2007, sec 6.3.4].

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:

fmod SET-PAIR{X :: TRIV, Y :: TRIV } is


...
protecting SET{X} .
protecting SET{Y} .

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.

view TOSET from TRIV to TOSET is


endv

fmod SET-MAX{T :: TOSET} is


protecting SET{TOSET}{T} .
protecting BOOL .
op max : Set{TOSET}{T} -> T$Elt .
var E : T$Elt .
var S : Set{TOSET}{T} .
eq max(E, S) = if S == empty or max(S) < E then ....
….
endfm
Donde el módulo inclusor asoció su parámetro con TOSET con el objeto de usar el operador _<_
definido en dicho módulo.
Para poder instanciar el módulo paramétrico incluido SET{X::TRIV} con el parámetro T asociado a
TOSET, es necesario instanciarlo dos veces; la primera se lleva a acabo con una vista de TOSET a
TRIV (denominada TOSET), con lo que, el módulo SET{....} pasa de ser paramétrico en TRIV a ser
paramétrico en TOSET; este último módulo es, entonces, instanciado con el parámetro T del módulo
inclusor que es de tipo TOSET.
Nótese que el módulo paramétrico incluido no cambia, y sólo toma de TOSET los elementos que ésta
teoría tiene en común con TRIV. La mediación de la vista TOSET tiene, entonces, como único objeto
permitir enlazar los parámetros del módulo incluido y del módulo inclusor.
Nótese además, que los sort definidos en el modulo incluido (Vg. Set{..}), se cualifican con los
nombres de los parámetros usados en la instanciación (Vg. Set{TOSET}{T} ). Con ello se evitan
conflictos que podrían surgir si se usara más de una instancia del módulo incluido.

Si un módulo incluido tiene varios parámetros, no se debe mezclar en una substitución de


los parámetros, el uso de vistas a teorías y el uso de parámetros del módulo
inclusor103[Clavel 2007, sec 6.3.4].

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.

view TRIV from TRIV to TRIV is


endv

fmod PAIR-SET{X :: TOSET, Y :: TRIV} is


protecting PAIR{TOSET, TRIV} {X, Y} .

op _<_ : Pair{TOSET, TRIV}{X, Y} Pair{TOSET, TRIV}{X, Y} -> Bool . .
...
endfm

Al momento de incluir un módulo parametrizado, es posible renombrar algunos de sus


elementos de la forma referida en [Link]. El renombramiento puede llevarse a cabo antes o
después de instanciar el módulo. No es posible, sin embargo, renombrar los elementos de
las teorías asociadas a sus parámetros. Al renombrar los elementos del módulo
parametrizado se debe tener en cuenta, además, que los elementos cualificados con
parámetros (reales o formales) mantengan la cualificación. Para una descripción más
detallada de las condiciones que deben tenerse en cuenta al momento de renombrar los
elementos de un módulo parametrizado, el lector debe remitirse a [Clavel 2007, sec 6.3.4].
10.3.5 Lista parametrizada en MAUDE.
Como ejemplo de la parametrización en MAUDE, presentamos a continuación los
elementos básicos de la especificación de un módulo paramétrico en el que se define una
lista genérica con sus operaciones respectivas. Este módulo puede ser utilizado para crear
listas de elementos de cualquier tipo. Para una especificación más completa de una lista
genérica en MAUDE el lector debe consultar a [Clavel 2007, sec 7.12.4].

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} .

op nil : -> List{X} [ctor] .


op _ _ : List{X} List{X} -> List{X} [ctor assoc id: nil ] .

op | _ | : List{X} -> Int .


var I : X$Elt .
var L : List{X} .
.eq | nil | = 0 .
eq | I L | = 1 + >| L | .

op _[_] : List{X} Int -> X$Elt .


op error-en-indice : -> X$Elt .
var I : Int .
var E : X$Elt .
var L : List{X} .
eq E L [1] = E .
ceq E L [I] = L [I – 1] if(I =/= 1) .
eq nil [I] = error-en-indice .

..
..
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.

10.4 Relaciones entre Tipos.


La relación de contención entre tipos es declarada en MAUDE por medio de la instrucción
subsort (ver [Link] ).
Si bien la relación de subsort fue utilizada en [Link] y en 10.3.5 para declarar las
componentes elementales de la estructura iterada lista, ella puede también ser utilizada para
definir operadores asociados a funciones parcialmente definidas sobre un tipo, darle apoyo
a la recuperación de errores, indicar de forma más precisa el efecto de aplicar operadores
(ya definidos) a subconjuntos del tipo, y definir tipos con los elementos de un tipo ya
existente que satisfagan alguna condición.
En esta sección se presentan estos usos para la relación de subsort.
10.4.1 “Kinds” y Gestión de Errores.
La instrucción subsort permite definir jerarquías de contención entre tipos.

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í:

subsort Nat < Int < Rat .


Define una jerarquía de contención entre los tipos referidos, indicando que el conjunto de los Racionales
(Rat) contiene al conjunto de los Enteros (Int) y este a su vez, contiene al conjunto de los Naturales
(Nat).

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 .

sorts Camino Camino? .


subsort Arco < Camino < Camino? .
op nil : -> Camino .
op _;_ : Camino? Camino? -> Camino? [assoc id: nil].

var A : Arco .
vars C : Camino .

ops origen fin : Camino -> Nodo .


eq origen(A ; C) = origen(A) .
eq fin(C ; A) = fin(A) .

cmb A ; C : Camino if C == nil or fin(A) = origen(C) .

protecting INT .

op largo : Camino -> Int .


eq largo(nil) = 0 .
eq largo(A ; C) = s largo(C) .

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.

[Link].1 Lista Ordenable Paramétrica


La importancia de definir un subsort por medio de ecuaciones de membresía, radica en
poder definir operadores que puedan ser aplicados sólo a los miembros del sort que
satisfacen cierta condición.
Para ilustrar este punto presentamos a continuación la especificación de un módulo
paramétrico en el que se define una lista genérica ordenada. Este módulo puede ser
utilizado para introducir los algoritmos de ordenamiento junto con operadores que pueden
ser aplicados sólo a listas de elementos ordenados. Para una especificación más completa
de una lista genérica ordenada en MAUDE el lector debe consultar a [Clavel 2007, sec
6.3.6].

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)) .

op sort_list : List{TOSET}{X} -> StdList{X} .


..
..
op merge_list : StdList{X} StdList{X} -> StdList{X} .
..
..
endfm
Donde Int el operador sort_list lleva a cabo el ordenamiento de una lista ordenable, convirtiéndola en
una instancia de una lista ordenada, mientras que el operador merge_list lleva a cabo la intercalación de
dos lista ordenadas dando como resultado una lista ordenada.
Se omiten las definiciones de los operadores que el lector puede escribir con base en los presentados en
[Clavel 2007, sec 6.3.6]
Nota: La ecuación de memebrecía condicional interactúa con los atributos assoc e iter de forma
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
242
indeseable, por lo que no es seguro que se pueda usar con listas definidas de la manera presentada en
lugar de cabeza cola (ver [Clavel 2007, sec 13.12.8].

10.4.5 Operadores Polimórficos y listas heterogéneas.


Los operadores nativos if_then_else_fi, _==_ son ejemplos de operadores polimórficos,
que actúan sobre cualquier tipo de elemento.
En MAUDE se pueden definir operadores polimórficos usando el atributo poly. El atributo
poly tiene la forma siguiente:
poly (<lista_de_enteros>)
Donde:
• < lista_de_enteros > Es una lista de enteros que indica en cuales argumentos es
polimórfico el operador y si lo es en el resultado.
La ocurrencia de un 0 en <lista_de_enteros> indica que el operador es polimórfico en el
resultado, la aparición de números mayores que 0 indican que el operador es polimórfico en
el argumento que ocupa dicha posición. Para argumentos que no son constantes, la
aparición de un 0 debe estar acompañada, al menos, con otro número diferente de 0.
Sólo pueden ser polimórficos los operadores constructores (y los nativos). En la
declaración del operador polimórfico se debe colocar Universal en el tipo de los
argumentos en que el operador es polimórfico.

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

10.5 Ejercicios propuestos


Ejercicios: sección 1.3.* de (Abelson 1985)
1. La regla de Simpson, es otra manera para aproximar el valor de una integral definida
entre a y b. En términos matemáticos, dado un número natural par, n:
h
[ f (a) + f (a + h) + f (a + 2h) + K + f (a + nh)]
b
∫a
f ( x)dx ≈
3
Donde h=(b-a)/n. Defina un procedimiento que tome como argumentos, f, a, b, n y que
retorne el valor de la integral definida, aproximado mediante la regla de Simpson. Si lo hizo
sin utilizar el procedimiento sumatoria definido en la sección 3.2, reescríbalo para que
haga uso de tal abstracción.
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
243
2. El procedimiento sumatoria (sección 3.2) genera un proceso recursivo. El
procedimiento puede ser reescrito para que lleve a cabo un proceso iterativo. Muestre como
hacer esto llenando los campos faltantes en la siguiente definición:
(define (sumatoria termino a sig b)
(define (iter a resultado)
(if <??>
<??>
(iter <??> <??>)))
(iter <??> <??>))
3. Escriba un procedimiento productoria, análogo a sumatoria, que retorne el producto
de los valores de una función en puntos sobre un rango dado.
a. Muestre como definir factorial en términos del nuevo procedimiento productoria.
b. Use el procedimiento productoria, para encontrar una aproximación a π, usando
la fórmula
π 2 ⋅ 4 ⋅ 4 ⋅ 6 ⋅ 6 ⋅ 8K
=
4 3 ⋅ 3 ⋅ 5 ⋅ 5 ⋅ 7 ⋅ 7K
c. Si su procedimiento productoria genera un proceso iterativo, escriba uno que
genere un proceso recursivo. Y si su procedimiento genera uno recursivo, escriba
uno que genere un proceso iterativo.
4. Muestre que sumatoria y productoria son casos especiales de un concepto más
general llamado acumular, que combina una colección de términos, usando alguna función
general de acumulación:
(acumular combinador nulo termino a sig b)
Acumular toma como argumentos la misma especificación de rango y término que
sumatoria, junto con un procedimiento combinador (de dos argumentos) que especifica
como el término actual se combina con la acumulación de los términos precedentes; y nulo
es un valor que especifica la base a usar cuando terminen los valores.
a. Escriba el procedimiento acumular.
b. Reescriba los procedimientos sumatoria y productoria, en términos de
acumular.
5. En el ejercicio 3 del módulo 2, se planteo una fórmula general para una fracción continua
infinita.
a. Escriba un procedimiento frac-inf, con el siguiente perfil (frac-inf N D a b),
donde N y D son procedimiento, con argumento n, que calculan el n-ésimo valor para
Ni y Di, y [a, b] determina el rango de enteros.

b. Si su procedimiento frac-inf no utiliza acumular, reescríbalo de manera adecuada


para aprovechar tal abstracción. (Nota: es fundamental, mirar con cuidado, que tipo
de proceso genera acumular. Recuerde que si el proceso es iterativo, es necesario
acumular de atrás hacia adelante. Ver Sección 2.3 para más detalles).
Capítulo 10: Programación Paramétrica y Relaciones entre Tipos
244
c. Utilice el procedimiento frac-inf, para reescribir los procedimientos que calculan
fracciones continuas infinitas desarrollados en el módulo 2 (sección 2.3 y ejercicios
3.a y 3.b).
6. El método de Bisección de Bolzano sirve para encontrar raíces de la ecuación f(x)=0,
donde f es una función continua. Dados puntos a y b tales que f(a)<0<f(b), entonces f
tiene al menos un cero entre a y b. Para localizar el cero, sea c el promedio entre a y b; Si
f(c)>0, f tiene un cero entre a y c; si f(c)<0, f tiene un cero entre c y b. Continuando de
esta manera, podemos encontrar cada vez intervalos más pequeños con la seguridad de que
f tiene un cero allí. El proceso se detiene si f(c)=0 o si el intervalo es suficientemente
pequeño, en cuyo caso, se retorna el punto medio del intervalo.
a. Escriba un procedimiento con el siguiente perfil: (buscar-cero f pto-neg pto-
pos), que busque raíces de f(x)=0, suponiendo que f(pto-neg)<0<f(pto-pos).
b. En la sección 1.3.3 de (Abelson 1985), se puede encontrar este procedimiento.
Aunque Abelson hace uso de la forma especial let, no debe ser difícil comparar su
propio procedimiento con el del libro.
Parte III: Lógica de
Predicados y
Lenguajes
Clausales.
Capítulo 11
Formas Normales y Lógica Clausal
Capítulo 11: Formas Normales y Lógica Clausal
248
11.1 Introducción.
Las Formulas Bien Formadas de la lógica de predicados pueden ser de enorme complejidad
debido a la recursividad propia de los criterios formativos relativos a los conectores.
Simplificar las fórmulas será, entonces, útil desde el punto de vista de la lectura de las
aserciones.
En este capítulo se presenta, bajo el concepto de “equivalencia semántica” la posibilidad de
transformar las fbf a formas estandarizadas, que se han denominado “formas normales”. La
“forma normal conjuntiva” reduce las fbfs a la conjunción de “cláusulas”, por lo que
diremos que las fbfs están en “forma clausal”.
La importancia de esta forma normal es debida a que bajo ciertas condiciones una fbf en
forma clausal constituye una especificación lógica para la que es posible automatizar la
demostración, por reducción al absurdo, que establece que una cláusula (sometida como
“consulta”) es consecuencia lógica de la fórmula en forma clausal.
La automatización de este proceso da lugar a una familia de lenguajes cuyo principal
representante es el lenguaje PROLOG.

11.2 Formas Normales en lógica de proposiciones.


Para la lógica de proposiciones la equivalencia semántica está ligada sólo a la
interpretación de las proposiciones atómicas de la fbf.
11.2.1 Equivalencia Semántica.
La “equivalencia semántica” es un concepto clave en la lógica de predicados y en particular
en el propósito de automatizar la demostración automática de teoremas, las explicaciones
que siguen son basadas en gran medida en [Chang 73], donde el lector podrá hallar los
conceptos presentados en mayor profundidad.
Dos fórmulas G y H se dicen equivalentes, y se escribe G=H107, si y solo si, los valores de
verdad de G y H son los mismos bajo cualquier interpretación de G y H.
Por ejemplo, en la siguiente tabla de verdad, se puede verificar que P⇒Q es equivalente a
¬P∨Q. (con frecuencia omitiremos algunas columnas de las tablas de verdad, por
simplicidad.)

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

Otra forma de definir la equivalencia entre fórmulas es decir:


G=H si y solo si G⇔H es una tautología.
Esto se sigue de la definición del conectivo ⇔ que solo es verdadero cuando las dos
proposiciones que conecta tienen el mismo valor de verdad; y de la definición de tautología
que dice que una fórmula es tautología si y solo si es verdadera para cualquier
interpretación. Esto se puede apreciar al extender la tabla anterior, con una columna para
(P⇒Q)⇔(¬P∨Q).

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.

11.3 Formas Normales en Lógica de Predicados.


La definición de equivalencia lógica dada en la sección 11.2, también es válida para la
lógica de predicados.
Capítulo 11: Formas Normales y Lógica Clausal
252
Además, como las equivalencias presentadas en la misma sección provienen de la
definición de los conectores que son idénticos a los de la lógica de primer orden, tales
equivalencias también son válidas en la lógica de primer orden. Realmente lo que nos hace
falta son algunas equivalencias típicas que nos permitan manipular cuantificadores. Para
una explicación más detallada de los contenidos de esta sección el lector debe referirse a
[Chang 1973]
11.3.1 Equivalencia Semántica.
Sea A una fbf que contiene una variable libre x. Para enfatizar que la variable libre x
aparece en A, escribiremos A[x]. Sea B una fbf que no contiene la variable x. Entonces
tenemos las siguientes equivalencias, donde Q representa ∃ o ∀ indiferentemente:

Equivalencia Nombre común

11 (a) (Qx)A[x]∨B = (Qx)(A[x]∨B)


11 (b) (Qx)A[x]∧B = (Qx)(A[x]∧B)
12 (a) ¬((∀x)A[x]) = (∃x)¬A[x]
12 (b) ¬((∃x)A[x]) = (∀x)¬A[x]

El problema de éstas equivalencias es que no se pueden probar usando tablas de verdad,


sino que es necesario demostrarlo semánticamente. Veamos como se probaría, por ejemplo
12(a):
Sea I una interpretación arbitraria en un dominio D. Hay que considerar 2 casos:
1. Si ¬((∀x)A[x])es verdadera en I: entonces (∀x)A[x] es falsa en I. Esto quiere
decir, que existe un elemento e∈D tal que A[e] es falso, o lo que es lo mismo,
¬A[e] es verdadero. Por tanto, (∃x)¬A[x] es verdadero.
2. Si ¬((∀x)A[x])es falso en I: entonces (∀x)A[x] es verdadero en I. Esto quiere
decir, que A[x] es verdadero para todo x∈D, o lo que es lo mismo, ¬A[x] es
falso para todo x∈D. Por tanto, (∃x)¬A[x] es falso.

De manera similar, se pueden probar las otras.


Ahora, sean A y C fbfs que contienen la variable x. Entonces:

Equivalencia Nombre común

13 (a) (∀x)A[x]∧(∀x)C[x] = (∀x)(A[x]∧C[x])


13 (b) (∃x)A[x]∨(∃x)C[x] = (∃x)(A[x]∨C[x])

Que en palabras, quieren decir que el cuantificador universal y el existencial distribuyen


sobre ∧ y ∨, respectivamente. Sin embargo no es cierto, en general, que ∀ distribuya
sobre ∨, ni tampoco que ∃ distribuya sobre ∧. Es decir:
Capítulo 11: Formas Normales y Lógica Clausal
253
Equivalencia errada Nombre común

(∀x)A[x]∨(∀x)C[x] ≠ (∀x)(A[x]∨C[x])

(∃x)A[x]∧(∃x)C[x] ≠ (∃x)(A[x]∧C[x])

Sin embargo si fuera de nuestro interés (y lo será) sacar el cuantificador universal al


exterior de la fbf (∀x)A[x]∨(∀x)C[x], podemos reemplazar x en (∀x)C[x] digamos por z (o
cualquier otra letra) ya que el nombre de la variable no tiene importancia. Y entonces
aplicar 11(a) siempre y cuando z no aparece en A[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].)

De manera similar se puede hacer con (∃x)A[x]∧(∃x)C[x].


Hay dos equivalencias, que aunque no son propiamente lógicas, sino semánticas, es
importante introducirlas, no solo por su utilidad sino porque nos permiten conectar aun más
íntimamente la lógica proposicional con la de primer orden.
Sea A una fbf que contiene la variable libre x, y sea I una interpretación arbitraria en un
dominio D={a1, a2, a3...}. Entonces:

Equivalencia Nombre común

14 (a) (∀x)A[x] = A[a1]∧A[a2]∧A[a3]∧...


14 (b) (∃x)A[x] = A[a1]∨A[a2]∨A[a3]∨...
La idea que queremos dejar, al presentar éstas última equivalencia, es que cualquier fbf de
la lógica de primer orden, puede ser vista como una fórmula en lógica proposicional para
los casos en que el dominio D es finito.
11.3.2 Forma normal Prenex.
En lógica proposicional, se introdujeron dos formas normales (la forma normal conjuntiva
y disyuntiva). En la lógica de predicados, también hay una forma normal llamada “forma
normal Prenex”. Esta forma normal, así como las introducidas en lógica proposicional,
sirven para estandarizar el proceso de deducción lógica que veremos más adelante.
Una fbf se dice estar en una forma normal Prenex si y solo si la fbf tiene la forma:
(Q1x1)...(Qnxn)(M)

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.

11.4 Ejercicios Propuestos.


1. Demuestre cada una de las equivalencias de la tabla de equivalencias de la sección 10.2
utilizando tablas de verdad.
2. Para las siguientes fórmulas encuentre una fórmula en forma normal conjuntiva
equivalente a ella; Y luego una forma normal disyuntiva.

(¬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.

12.2 Resolución en lógica proposicional.


Empezaremos por presentar el método de resolución para la lógica de proposiciones, donde
se comprenderá la esencia del problema.
12.2.1 Notación
Ahora, para facilitar la presentación del método, introduciremos algo de notación. Una
fórmula en FNC es de la forma:

(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:

R = (C1 – {L}) ∪ (C2 – {¬L})

Donde: ‘–’ es la operación diferencia para conjuntos y ‘∪’ la operación unión de


conjuntos. De manera que R es el conjunto formado por los literales de C1 sin L y
los literales de C2 sin ¬L.

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.

El siguiente teorema servirá para mostrar que al agregar a un conjunto de cláusulas el


resolvente de dos de sus cláusulas, no se afecta el valor de verdad de la forma clausal. En
lo que resta de ésta sección aparecerán algunas demostraciones, que consideramos
importantes, no solo por el esfuerzo de precisar argumentos, sino para evidenciar la
efectividad del método que presentaremos más adelante.
TEOREMA 12-1: Sea G={C1,C2} una fórmula en FNC, representada como conjunto
de cláusulas. Sea R un resolvente de las cláusulas C1,C2. Entonces G’={C1,C2,R} es
equivalente a G.
Demostración: Para probarlo, empezaremos por suponer una interpretación que hace verdadero a G’ y
probaremos que también G es verdadero bajo tal interpretación. Luego supondremos una interpretación que
hace verdadero a G y probaremos que también G’ es verdadero bajo tal interpretación.
1. Sea I una interpretación que hace verdadero a G’. Como G’ es una conjunción de Cláusulas, I hace
verdadera todas las cláusulas de G’. En Particular hace verdadero a C1 y a C2. Por tanto G={C1,C2}
es también verdadero.
2. Sea I una interpretación que hace verdadero a G. De nuevo C1 y C2 son verdaderas bajo I, y tan solo
falta probar que R es verdadero bajo I para probar que G’ es verdadero bajo I. Sabemos que R tiene
la forma (C1–{L})∪(C2–{¬L}), con L∈C1 y ¬L∈C2. Se pueden dar dos casos, L es verdadero bajo I,
o ¬L es verdadero bajo I.
• Si L es verdadero bajo I: Recordemos que una cláusula es una disyunción de literales, por lo
que, para que una cláusula sea verdadera bajo I, basta que uno de sus literales sea verdadero bajo
I. De éste modo, como C2 es verdadero, existe un literal L’∈C2 tal que L’ es verdadero bajo I.
Pero como L es verdadero bajo I, ¬L es falso bajo I y por tanto L’≠ ¬L. De manera L’∈C2–{¬L},
de manera que L’∈(C1–{L})∪(C2–{¬L}) y por tanto R es verdadero bajo I.

• Si ¬L es verdadero bajo I: de manera análoga, existe L’∈C1–{L} que es verdadero bajo I, y que hace
a R es verdadero bajo I.
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

Sea G un conjunto de cláusulas, entonces Res(G) se define como:


Res(G) = G∪{R | R es un resolvente de dos cláusulas de G}

Más aun definamos:


Res0(G) = G
Resn+1(G) = Res(Resn(G)) (para n≥0)

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.

TEOREMA 12-2: Si un conjunto de cláusulas G es contradictorio, entonces


∈Res*(G).
Demostración: Supongamos que G es contradictoria, y mostremos que ∈Res*(G) (por inducción sobre el
número n de diferentes fórmulas atómicas en G).
Base: Si n = 0, como G es contradictoria, G = { }, y por tanto ∈Res*(G).
Hipótesis inductiva: Supongamos que para cualquier conjunto de cláusulas contradictoria G, que contiene n
fórmulas atómicas, es cierto que ∈Res*(G).
Paso inductivo: Sea G un conjunto de cláusulas que contiene n+1 fórmulas atómicas A1,A2,...,An,An+1. Hay dos
casos posibles, la fórmula atómica An+1 puede ser verdadera o falsa.
Supongamos que An+1 es falsa.
Llamemos G0 al conjunto de cláusulas resultante. Nótese que en las cláusulas donde aparecía el
literal An+1, como las cláusulas son disyunciones de literales, An+1 simplemente desaparece. Por otro
lado, en las cláusulas en que aparecía el literal ¬An+1, como éste es verdadero, la cláusula se hace toda
verdadera, de manera que la cláusula entera desaparece del conjunto.

–Por ejemplo H={{A1,¬A2,A3,A4},{A2,A3,¬A4},{¬A1,A3,¬A4},{A2,A3}} representa


(A1∨¬A2∨A3∨A4)∧(A2∨A3∨¬A4)∧(¬A1∨A3∨¬A4)∧(A2∨A3) de manera que su suponemos que A4 es
falsa, obtenemos (A1∨¬A2∨A3∨F)∧(A2∨A3∨V)∧(¬A1∨A3∨V)∧(A2∨A3), y sabemos que
(A1∨¬A2∨A3∨F)=(A1∨¬A2∨A3). Y además sabemos que (A2∨A3∨V)=V=(¬A1∨A3∨V). Demanera que
si reconstruimos la forma clausal obtenemos H0={{A1,¬A2,A3},{A2,A3}}.–
Nótese que G0 es contradictoria pues G lo es, y G0 no es más que una restricción de las posibles
interpretaciones de G. Pero como G0 solo tienen n fórmulas atómicas (A1,A2,...,An), podemos aplicar
la hipótesis inductiva, así que ∈Res*(G0).
Por la manera en que construimos G0 cualquier resolvente de sus cláusulas también se podría obtener
de las cláusulas de G0, excepto que posiblemente tendría el literal An+1. –Por ejemplo (siguiendo el
ejemplo anterior) {A1,A3}∈Res*(H0), que se obtuvo de las cláusulas {A1,¬A2,A3} y {A2,A3}. A partir
de las cláusulas correspondientes de H ({A1,¬A2,A3,A4} y {A2,A3}), se puede obtener el resolvente
{A1,A3,A4}.–
Capítulo 12: Resolución en Lógica Clausal
262
De manera que como ∈Res*(G0), siguiendo el camino de resolventes que llevó a , se puede
concluir que ∈Res*(G) ó {An+1}∈Res*(G).
Supongamos que An+1 es verdadera.
Llamemos G1 al conjunto de cláusulas resultante. Nótese que en las cláusulas donde aparecía el
literal ¬An+1, como las cláusulas son disyunciones de literales, ¬An+1 simplemente desaparece. Por
otro lado, en las cláusulas en que aparecía el literal An+1, como éste es verdadero, la cláusula se hace
toda verdadera, de manera que la cláusula entera desaparece del conjunto.......
Mediante un razonamiento análogo al del caso anterior se puede concluir que
∈Res*(G) ó {¬An+1}∈Res*(G).
Con estos dos resultados en la mesa, la conclusión es directa. En
el peor de los casos {An+1},{¬An+1}∈Res*(G) y es claro que en tal caso
también es cierto que ∈Res*(G).

12.3 Resolución en Lógica de Predicados.


La resolución presentada arriba para la lógica de proposiciones se extenderá, ahora, a la
lógica de predicados.
12.3.1 Ideas intuitivas; Base para el principio de resolución
Para poder aplicar el principio de resolución como lo conocemos a la lógica de predicados,
es necesario mejorar nuestra percepción de las fbfs. Sabemos que la diferencia real entre la
lógica de proposiciones y la de primer orden son los cuantificadores y las variables. Sin
embargo, en la sección 11.3.2, vimos que siempre es posible llevar los cuantificadores
hacia la izquierda. Ahora supongamos una fbf f que solo cuenta con cuantificadores
universales.

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:

f = M (a,K, a ) ∧ M (a,K, a, b ) ∧ M (a,K, a, b, a ) ∧ M (a,K, a, b, b ) ∧ M (a,K, a, b, a, a ) ∧ K ∧ M (b,K, b )


Es decir, f es igual a una conjunción de todas las posibles asignaciones de a y b a las variables
x1,...,xr, en M. Luego, sabiendo que M es una conjunción de disyunciones, lo que obtuvimos es
una fbf sin cuantificadores y sin variables, finalmente una fórmula en FNC de la lógica de
proposiciones.
El problema radica en que si recordamos la definición dada para consecuencia lógica en la
sección ¡Error! No se encuentra el origen de la referencia., que también rige la
consecuencia lógica en lógica de predicados, las condiciones se deben cumplir para
Capítulo 12: Resolución en Lógica Clausal
263
cualquier interpretación. Y no olvidemos que cualquier interpretación en lógica de primer
orden, implica también cualquier dominio. Luego hay infinitos posibles dominios y una
gran cantidad de posibles interpretaciones sobre cada dominio.
El primer paso hacia la solución del problema proviene de Herbrand108 en 1930, quien
redujo el problema al estudio de un solo dominio. Dada una serie de fbfs, el “universo de
Herbrand” como se le conoce a tal dominio es el más grande de todos los dominios
posibles, de manera que cualquier dominio que se pueda pensar, está incluido en el
universo de Herbrand.
Sin entrar en detalles, éste dominio se construye a partir de las constantes y las letras de
función que aparecen en la serie de fbfs, formando todos los posibles términos y
considerándolos diferentes. Por ejemplo si a, b son las constantes de una teoría y f, g las
letras de función (unarias), entonces el universo de Herbrand es:

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 )

Es una contradicción. Por tanto en adelante omitiremos los cuantificadores y simplemente


supondremos que cualquier variable que aparezca está cuantificada universalmente.
Además utilizaremos la notación de cláusulas y conjuntos de cláusulas introducida en la
subsección 12.2.1, de manera que f aparecerá así:

{{P (x ,K, x ),K, P (x ,K, x )},K, {P (x ,K, x ),K, P (x ,K, x )}}


1,1 1 r 1, n1 1 r k ,1 1 r k , nk 1 r

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:

(C1σ – L1σ) ∪ (C2σ – L2σ)


= ({P(a),Q(a)} – {P(a)}) ∪ ({¬P(a),R(y)} – {¬P(a)})
Capítulo 12: Resolución en Lógica Clausal
267
= {Q(a)} ∪ {R(y)}
= {Q(a),R(y)}
Es resolvente para C1 y C2.

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.

12.4 Ejercicios propuestos.


1. Sea θ={a/x,b/y,g(x,y)/z} una sustitución y E=P(h(x),z). Encontrar Eθ.
2. Determinar si las siguientes parejas de literales son unificables y en caso afirmativo
encontrar el unificador más general (mgu):
L1 = Q(a) y L2 = Q(b)
L1 = Q(a,x) y L2 = Q(a,a)
L1 = Q(a,x,f(x)) y L2 = Q(a,y,y)
L1 = Q(x,y,z) y L2 = Q(u,h(v,v),u)
L1 = P(x1,g(x1),x2,h(x1,x2),x3,k(x1,x2,x3)) y L2 = P(y1,y2,e(y2),y3,f(y2,y3),y4)
3. Encontrar todos los posibles resolventes (si existe alguno) de las siguientes parejas de
cláusulas:
C1 = {¬P(x),Q(x,b)} y C2 = {P(a),Q(a,b)}
C1 = {¬P(x),Q(x,x)} y C2 = {¬Q(a,f(a))}
C1 = {¬P(x,y,u),¬P(y,z,v),¬P(x,v,w),P(u,z,w)} y C2 = {P(g(x,y),x,y)}

• No olvide que las variables entre una cláusula y otra se consideran


independientes.
C1 = {¬P(v,z,v),P(w,z,w)} y C2 = {P(w,h(x,x),w)}
4. Demostrar mediante resolución que M(c,p(a,p(b,p(c,0)))) es consecuencia lógica de las
siguientes proposiciones:
F1: (∀x)(∀y)M(x,p(x,y))
F2: (∀x)(∀y)(∀z)( M(x,y) ⇒ M(x,p(z,y)) )
5. Para los siguientes conjuntos de cláusulas, encontrar los resolventes necesarios para
evidenciar lo contradictorio de las fórmulas.
{{P,Q,R},{¬Q,R},{P,¬R},{¬P,¬R},{¬P,R}}
{{P,¬R,S},{R,¬S},{P,¬Q,¬R},{P,S},{Q},{¬P,Q,R,¬S}}
Capítulo 13
Lenguajes Lógicos
Capítulo 13: Lenguajes Lógicos
270
13.1 Introducción.
Los lenguajes lógicos automatizan el proceso de demostración por reducción al absurdo
basado en la resolución, presentado en el Capítulo anterior.
Para lleva a cabo esta automatización, se debe simplificar el proceso de introducción de
resolventes de manera que no sea necesario expandir todos los posibles resolventes para
probar la contradicción de un conjunto de cláusulas. Esta simplificación es posible gracias
a que es suficiente con introducir una serie de resolventes que termine con la cláusula vacía
para demostrar la contradicción. En otras palabras, la eficiencia del proceso dependerá de
que tan rápido se introduzca dicha cláusula.
Al objeto de simplificar la exposición, en éste Capítulo se explora sólo la estrategia de
introducción de resolventes usada en lenguaje PROLOG.
Luego de presentar dicha estrategia, se procede a presentar los elementos del lenguaje y a
introducir unos pocos ejemplos básicos. Se dejan para el Capítulo siguiente
consideraciones más profundas sobre el uso del PROLOG como lenguaje de programación.

13.2 El Lenguaje PROLOG visto desde la Lógica.


Al problema que confronta un lenguaje lógico es probar que la fbf G, es consecuencia
lógica de un conjunto de fbfs F1,F2, ...,Fn probando que (F1∧F2∧...∧Fn∧¬G) es
contradictoria.
Para usar la resolución se debe partir de que (F1∧F2∧...∧Fn∧¬G) está en forma normal
Prenex, con matriz en FNC y que no tienen cuantificadores existenciales.
En el lenguaje PROLOG el conjunto de fbfs F1,F2, ...,Fn, constituye el progrma
propiamente dicho y la fbf G constituye una “consulta” a dicho programa.
Se supone además que la fbf (F1∧F2∧...∧Fn) no es contradictoria, en sí misma, por lo que
de haber contradicción en (F1∧F2∧...∧Fn∧¬G), esta debe ser introducida por la fbf ¬G.
13.2.1 Cláusulas de Horn y programa PROLOG.
El lenguaje PROLOG exige que las cláusulas del programa sean cláusulas de “Horn”.
En una cláusula de Horn hay un literal positivo (o no negado), y no hay más de un literal
positivo. Los demás literales de la cláusula, si existen, deben ser literales negativos. Así,
una cláusula de Horn tiene la forma siguiente:
∀x1∀x2∀x3.... ∀xn (A(...) ∨ ¬B1(...) ∨ ¬B2(...) ∨ ¬B3(...) ∨ ...)
Donde:
x1,x2,x3.... ,xn son variables ligadas a los cuantificadores.
A(...),B1(...),B2(...),B3(...), ... son predicados atómicos en los que pueden aparecer
variables cuantificadas.
Es fácil ver que estas cláusulas son semánticamente equivalentes a fbfs de una de las dos
formas, siguientes.
Capítulo 13: Lenguajes Lógicos
271
∀x1∀x2∀x3.... ∀xn (A(...)⇐ B1(...) ∧ B2(...) ∧ B3(...) ∧..)
∀x1∀x2∀x3.... ∀xn (A(...))
Así, un programa PROLOG es una conjunción de cláusulas de HORN que luce de la
manera siguiente:
∀x1,1∀x1,2∀x1,3.... ∀x1,n1 (A1(...)⇐ B1,1(...) ∧ B11,2(...) ∧ B1,3(...) ∧..) ∧
∀x2,1∀x2,2∀x2,3.... ∀x2,n2 (A2(...)⇐ B2,1(...) ∧ B2,2(...) ∧ B2,3(...) ∧..) ∧
∀x3,1∀x3,2∀x3,3.... ∀x3,n3 (A3(...)⇐ B3,1(...) ∧ B3,2(...) ∧ B3,3(...) ∧..) ∧
..
∀xk,1∀xk,2∀xk,3.... ∀xk,nk Ak(...) ∧
∀xk+1,1∀xk+1,2∀xk+1,3.... ∀xk+1,n(k+1) Ak+1(...) ∧
...

13.2.2 Consultas en PROLOG


Las consultas G en PROLOG son fbfs de la forma siguiente:
G = ∃x1∃x2∃x3... ∃xn ( G1(...)∧G2(...)∧G3(...)∧..)
Donde:
x1,x2,x3.... ,xn son variables ligadas a los cuantificadores.
G1(...),G2(...),G3(...), ... son predicados atómicos en los que pueden aparecer
variables cuantificadas.
Dicho en palabras, la consulta trata de establecer si existen valores para las variables que
involucra la conjunción de una serie de literales positivos, que hagan que ella sea
consecuencia lógica del programa. El objeto de someter la consulta, no es otro que el de
obtener estos valores para las variables.
Nótese que la consulta es semánticamente equivalente a la fbf siguiente:
G = ¬(∀x∀y∀x...( ¬G1∨¬G2∨¬G3∨..))
En otras palabras una consulta es una cláusula negada, donde todos los literales son
negados.
Nótese que puesto que la consulta se niega al momento de adicional la consulta al programa
para demostrar la contradicción de la fbf resultante, ella se convierte en la única cláusula de
la fbf resultante que no tiene literales positivos.
13.2.3 Forma Prenex de un programa PROLOG
Es importante hacer notar que por estar, en la formula (F1∧F2∧...∧Fn∧¬G), cada cláusula
cuantificada de forma independiente, y tener todas sus variables ligadas a sus propios
cuantificadores, las variables que aparecen en una cláusula son diferentes de las que
aparecen en otra, sin que exista ligadura alguna entre ellas. Es por esto que, desde ahora,
estas variables se pueden considerar de nombre diferente sin pérdida de generalidad.
Capítulo 13: Lenguajes Lógicos
272
Así, a pesar de que es usual que cuando se escribe un programa aparezcan las mismas
variables en distintas cláusulas, al momento de llevarse a cabo una unificación para
introducir un resolvente (ver 12.3.3 ), el intérprete se llevan a cabo cambios de nombres en
las variables repetidas entre cláusulas para evitar conflictos entre ellos (evitando
considerarlas como variables ligadas representando el mismo valor).
Así, utilizando las equivalencias expuestas en la sección 11.3.1, es fácil ver que todos los
cuantificadores que aparecen en la expresión (F1∧F2∧...∧Fn∧¬G) se pueden llevar hacia la
izquierda, dejando a la derecha una expresión sin cuantificadores y en forma normal
conjuntiva. En el Ejemplo 192 se muestra un caso de este proceso de transformación que es
fácil generalizar.

13.3 Sintaxis y nomenclatura del PROLOG


La sintaxis propia de la lógica de predicados es, sin duda, poco apropiada para ser usada
directamente como lenguaje de programación. Esto se debe, en primer lugar, a la
dificultad de usar los símbolos que representan los conectores y los cuantificadores, y en
segundo lugar, a que no es necesario especificar elementos que pueden ser sobreentendidos.
En esta sección se presentan las simplificaciones en la sintaxis que hacen viable el uso de la
lógica clausal como lenguaje de programación.
13.3.1 Términos.
Antes de entrar a detallar la forma especificar los predicados y las cláusulas, es necesario
establecer con claridad la manera de escribir los términos que constituyen los argumentos
de los predicados.
En PROLOG un término puede ser un átomo, un literal, una variable, o un término
complejo.
[Link] Átomos
Un átomo puede ser una cadena de caracteres, sin espacios, que comienza con una letra
minúscula. Los átomos se usan en PROLOG para representar elementos específicos de los
dominios de Interpretación. Por ejemplo juan es un átomo.
[Link] Literales
Los literales pueden ser números o cadenas de caracteres.
Un número es simplemente eso. Dependiendo del intérprete, esto incluye, enteros,
números de punto flotante, etc.
Las cadenas de caracteres se presentan encerrada entre comillas y puede incluir espacios.
Por ejemplo ´juan, o ‘Juan David’ son cadenas de caracteres.
Cuando hablemos de una constante nos estaremos refiriendo indiferentemente a un
número, a una cadena de caracteres o a un átomo.
[Link] Variables
Una variable es una cadena de caracteres que se caracteriza por comenzar con una letra en
mayúscula o el símbolo _. Por ejemplo A, Tio o _hombre son variables. También _ es una
variable pero tiene un significado especial. Se le llama una variable anónima, porque de
Capítulo 13: Lenguajes Lógicos
273
aparecer dos veces en una sentencia se interpreta como variables diferentes, lo que no
ocurre con la variable Tio por ejemplo.
[Link] Términos Complejos
Un término complejo (o también llamado estructura) consta de un encabezado seguido de
una secuencia de argumentos. El encabezado es un átomo, mientras que los argumentos
son cualquier término (incluyendo términos complejos). Los argumentos se encierran entre
paréntesis y se separan con comas, como un llamado a función típico. El número de
argumentos de un término complejo es llamado aridad. Por ejemplo el término complejo
padre(juan, X) tiene aridad 2, su encabezado es padre, y sus argumentos son juan y X.
Como es natural, PROLOG entenderá como diferentes a dos términos complejos que tan
solo se diferencian en su aridad.
13.3.2 Predicados.
Los predicados se escriben en PROLOG de la forma usual, es decir el nombre del
predicado va primero, seguido por la lista de los argumentos que le corresponden. Los
argumentos, a su vez, son término definidos de la manera referida antes.

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))

13.3.3 Programa: Hechos y Reglas.


Las cláusulas que constituyen un PROGRAMA se escriben teniendo en cuenta los
elementos de notación siguientes:
• Los cuantificadores se omiten quedando implícito que todas las variables en una
cláusula cualquiera, están cuantificadas universalmente (con ∀).
• El símbolo de implicación “⇐” se substituye por el símbolo “:-“
• El conector lógico ∧ que separa las cláusulas se substituye por “.”
Capítulo 13: Lenguajes Lógicos
274
• El conector lógico ∧ que separa los literales que constituyen la fbf que implica
en una cláusula, se substituye por “,”
Así, el patrón de programa presentado en la sección 13.2.1 luciría de la manera siguiente:
A1(...) :- B1,1(...), B11,2(...), B1,3(...) ,.. .
A2(...) :- B2,1(...), B2,2(...), B2,3(...),.. .
A3(...) :- B3,1(...), B3,2(...), B3,3(...),.. .
..
Ak(...) .
Ak+1(...) .
...

[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) .

Los predicados que conforman la consulta se denominan “metas” u “objetivos”.


13.3.5 Ejemplo de PROLOG
Tan solo hay tres construcciones básicas en PROLOG: Hechos, reglas y consultas. Una
colección de hechos y reglas es llamada una “base de conocimientos” (o base de datos) y
la programación en PROLOG se trata simplemente de escribir bases de conocimientos. La
forma de usar entonces un programa de PROLOG es mediante consultas que se le hacen a
la base de conocimientos. El siguiente ejemplo pretende introducir al lector en ésta
estrategia de programación.
hermana(ana,juan) .
hermana(ana,pedro) .
padreOmadre(jose,ana) .
padreOmadre(jose,juan) .
padreOmadre(jose,pedro) .
padreOmadre(pedro,esteban) .
tia(X,Y) :- hermana(X,Z), padreOmadre(Z,Y) .
Esta es una base de conocimientos que pretende explicar algunas relaciones de parentesco.
Por ejemplo la primera línea expresa que ana es hermana de juan; la tercera que jose es
padre o madre de ana; y la última expresa que X es tía de Y si X es hermana de Z y Z es
padre o madre de Y.
Una consulta sería:
:- hermana(ana,juan) .
A lo que un intérprete de PROLOG respondería: yes. Y obviamente frente a la consulta
siguiente:
:- hermana(ana,jose)

El intérprete de PROLOG respondería: no. Y obviamente frente a la consulta siguiente:


Otra consulta podrías ser la siguiente:
:- hermana(ana,X) .
A la que el intérprete de PROLOG respondería: yes. X = juan.
Capítulo 13: Lenguajes Lógicos
276
Finalmente a la consulta siguiente:
:- tia(ana,S) .
El intérprete respondería: yes. S = esteban.

13.4 Procesamiento por Resolución SDL.


El intérprete de PROLOG, utiliza el principio de resolución para probar que el programa
con la consulta negada es contradictorio. Gracias a lo restringido de las cláusulas a las que
se enfrenta el intérprete, veremos que para aplicar el principio de resolución no es necesario
computar todos los posibles resolventes del conjunto, sino tan solo unos pocos, muy
específicos, que llevan rápidamente hacia la contradicción.
Lo primero que hay que notar es que la base de conocimientos no debe ser contradictoria en
si misma. De allí que la contradicción debe provenir de la negación de la consulta, así que
tan solo es necesario computar resolventes que provengan de la consulta.
Además, nótese que una consulta tan solo contiene literales negados, mientras que un
hechos siempre es un literal positivo, y una regla cuenta con un solo literal positivo, la
cabeza específicamente. De manera que la búsqueda de resolventes queda restringida a los
hechos o las cabezas de las reglas que unifiquen con cada un literal de la consulta.
En el proceso de resolución SDL consiste en buscar siempre, para el primer literal de la
consulta, un hecho o la cabeza de una regla que unifiquen con tal literal. Ésta búsqueda se
hace en orden de arriba hacia abajo a través de la base de conocimientos., lo que se
conoce como resolución lineal. Tras encontrarlo, se conforma el resolvente incluyendo
posiblemente el cuerpo de la regla (si la unificación se dio con la cabeza de una regla) al
resto de la consulta, y se repite el proceso para ésta nueva cláusula. Si en un punto de éste
proceso no se encuentra con quien unificar el primer literal de la consulta, se retrocede un
paso en el proceso, y se busca una unificación diferente.
Si no está completamente claro el proceso, no hay porqué preocuparse, más adelante se
detallará el proceso y se ilustrará mediante ejemplos, por ahora solo se busca mostrar de
donde proviene éste método, que en últimas es bastante sencillo.
13.4.1 Búsqueda de la prueba y árboles de búsqueda
Aunque hemos hablando del método que emplea PROLOG para encontrar demostraciones
desde hace tres módulos, no sobra por fin concretarlo, siguiendo un ejemplo. Supongamos
la siguiente base de conocimientos:
f(a) .
f(b) .

g(a) .
g(b) .

h(b) .

k(X) :- f(X), g(X), h(X) .


Inicialmente supongamos que se le plantea la consulta:
Capítulo 13: Lenguajes Lógicos
277
:- g(b).
El intérprete inmediatamente encuentra una unificación con el cuarto hecho de la base y
responde yes.
Ahora supongamos que planteamos la consulta:
:- k(X) .
Probablemente el lector ya haya deducido que existe una única respuesta a esta consulta
que es X=b. Sin embargo, veamos cómo exactamente llega PROLOG a tal conclusión.
El intérprete busca a través de la base de conocimientos, de arriba hacia abajo un hecho o
la cabeza de una regla que unifique con la meta k(X). En este caso solo hay una
posibilidad, que es la cabeza de la única regla (k(X) :- f(X), g(X), h(X) .).
La unificación es evidente, pero para no crear confusión entre las variables de dos cláusulas
diferentes PROLOG crea una nueva variable digamos _01, para evidenciar que la variable
de la meta no es la misma que la de la regla. Entonces tenemos la consulta k(X) y la regla
k(_01) :- f(_01), g(_01), h(_01), con lo que, hecha la unificación bajo la substitución x/_01
y obtenido el resolvente, obtenemos una nueva meta f(_01),g(_01),h(_01). Esto se puede
entender de dos maneras. La primera es que éste es precisamente el resolvente que se
obtiene entre la consulta y la regla, como se ilustró en la sección anterior. La otra es
entendiendo directamente el significado de la consulta: Se le preguntó al intérprete si
“existía algún X que tuviera la propiedad k”; Luego él, encontró una regla que decía que
“un X tiene la propiedad k si también tiene las propiedades f, g y h”. De manera que ahora
el intérprete buscará un X, con estas últimas tres propiedades.
Las deducciones suelen ilustrarse mediante árboles como el de la Figura 13.1, donde
aparece la consulta o meta inicial y la transición a la siguiente meta mediante una línea
rotulada con el reemplazo realizado, sobre las variables de la meta.

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)

Figura 13.3. Proceso completo de resolución para la consulta k(Y).

A la representación de la Figura 13.3 se le llama árbol de búsqueda, y es una buena manera


ilustrar el proceso de resolución que emplea PROLOG. Los nodos contienen la meta a
satisfacer en un momento determinado de la búsqueda, mientras que las líneas muestran el
reemplazo necesario para encontrar un nuevo resolvente. Las hojas (es decir, los nodos
Capítulo 13: Lenguajes Lógicos
279
terminales), en caso de estar vacíos muestran un posible camino de solución, mientras que
si tienen una meta representan un punto de falla, donde fue necesario hacer backtracking.
Si seguimos las sustituciones desde la raíz hasta una hoja vacía, podemos desprender la
sustitución necesaria para encontrar tal solución. En nuestro caso tenemos Y por _01, y
luego _01 por b, es decir Y por b, que es precisamente la información que nos arrojaría el
intérprete de PROLOG.
Hay ocasiones en que existe más de una solución para una consulta planteada. Sería el caso
por ejemplo de la consulta
:- f(X)
En tal caso el intérprete entrega la primera solución con que se encuentra en la búsqueda.
Sin embargo, es posible pedirle otras soluciones forzando el backtracking después de una
respuesta utilizando el símbolo “;”. En nuestro caso por ejemplo, las siguientes líneas
muestran éste mecanismo y en la Figura 13.4 se expone el árbol completo.
X=a;
X=b;
no
Finalmente, cuando no se encuentra otra manera de satisfacer la consulta, el intérprete
responde no.

f(X)
X = a X = b

Figura 13.4. Árbol de búsqueda para la consulta f(X) y subsecuentes retrocesos.

13.5 Ejercicios propuestos


1. Dada la siguiente base de conocimientos:
ama(juan, maría) .
ama(pedro, claudia) .
ama(maría, julio) .
ama(julio, verónica) .
ama(verónica, hector) .
ama(hector, verónica) .
ama(claudia, hector) .
celoso(X,Y) :- ama(X,Z), ama(Y,Z)
Encontrar la respuesta ante la consulta:- celoso(X,Y), y construir el árbol de búsqueda
completo. ¿Está usted de acuerdo con la definición de celoso dada por la regla?

2. Considerar la siguiente base de conocimientos:


a(a1,1).
a(A,2).
a(a3,N).
Capítulo 13: Lenguajes Lógicos
280
b(1,b1).
b(2,B).
b(N,b3).

c(X,Y) :- a(X,N), b(N,Y).

d(X,Y) :- a(X,N), b(Y,N).


d(X,Y) :- a(N,X), b(N,Y).
Predecir la respuesta la las siguientes consultas:
:- a(X,2).
:- b(X,kalamazoo).
:- c(X,b3).
:- d(X,Y).
Construir un árbol de búsqueda completo para la consulta
:- c(X,Y).
Capítulo 14
Programación en Lógica de
Predicados
Capítulo 14: Programación en Lógica de Predicados.
282
14.1 Introducción
Lo que queda de nuestro estudio de PROLOG, es estudiar algunos ejemplos para exponer la
manera en que se usa para elaborar programas o bases de conocimiento. En general las
técnicas utilizadas serán las mismas que se usaron en los lenguajes funcionales, de manera
que haremos énfasis, en dos aspectos distintivos de PROLOG: La manera de ejecutar
mediante consultas, que ofrece gran versatilidad a los programas; y el sentido declarativo
de las bases de conocimientos. Al explotar éste último punto pretendemos que el lector lea
una base de conocimientos y de allí sea capaz de extraer para que sirve.
En adelante será común referiremos a un término complejo específico como un predicado.
No deben confundirse sin embargo: Un término complejo, es una construcción sintáctica de
PROLOG, fría y sin sentido; por otro lado, cuando hablemos de un predicado, nos
referiremos al significado del término complejo, a lo que representa de nuestro problema a
resolver, y a la manera en que debe comportarse frente a consultas. La construcción de un
predicado en PROLOG requiere con frecuencia de varias sentencias que lo definan, y que
lo provean de las capacidades para responder ante consultas.

14.2 Cálculos aritméticos.


PRLOLOG tiene la capacidad de llevar a cabo operaciones y cálculos aritméticos como los
que se efectúan en lenguajes funcionales. La programación, por otro lado, preserva el
enfoque básico ya aprendido en dichos lenguajes. El programador debe, sin embargo,
ajustarse no sólo a los nuevos elementos sintácticos, sino también al tener en cuenta el
orden en que se toman las metas (de izquierda a derecha), y el orden en que se examinan las
cláusulas (de arriba abajo).
La igualdad, en el sentido usado en los lenguajes funcionales, no encaja de buen gusto en el
esquema general de PROLOG. Las razones del problema no serán discutidas aquí, pero si
será necesario aclarar la manera en que deben y pueden ser usadas las expresiones
aritméticas.
PROLOG provee el predicado is, que sirve para evaluar expresiones aritméticas, así como
un paquete de operaciones típicas +, -, *, etc. que se emplean para definir términos
complejos. Por ejemplo frente a la consulta:
:- is(5, +(3,2)) .
El intérprete responde yes. También es posible escribir la misma consulta con notación
infija así:

:- 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.

PROLOG también provee predicados de comparación <, >, =< y >=


Es importante conocer estas características de PROLOG, pero no dedicaremos más
esfuerzo a tal propósito, pues la aritmética no es realmente el fuerte de PROLOG, para
terminar presentaremos un ejemplo sencillo.

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

.(X, L) y .(a, .(b,L))


Para una mayor ilustración se recomienda el capítulo 4 de [Blackburn 01], o seguir los
ejemplos a continuación.
14.3.1 Ejemplo: miembro
El predicado miembro, recibe dos argumentos, un elemento y una lista, en ese orden y se
satisface si el elemento pertenece a la lista. En PROLOG bastan las siguientes dos
sentencias para proveer al predicado miembro de tal funcionalidad:

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

Realmente en este punto, no debe requerir mucho esfuerzo, entender la manera de


programar en PROLOG, ya que no difiere de manera decisiva de lo que se hizo tanto en
LISP como MAUDE. Echemos de nuevo un vistazo a las declaraciones para el predicado
miembro.

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]

Figura 14.2. Árbol de búsqueda para la consulta concatenar([1,2,3], [4,5,6], L).


Al reconstruir la instanciación de variables a partir de la búsqueda ilustrada en la Figura
14.2 se obtiene: L = [1|_01]; _01 = [2|_02]; _02 = [3|_03]; _03 = [4,5,6]; de
donde
L = [1|[2|_02]]
L = [1|[2|[3|_03]]]
L = [1|[2|[3|[4,5,6]]]] = [1,2,3,4,5,6]
Que es el resultado que esperábamos. Sin embargo, el predicado concatenar, puede tener
más usos que simplemente obtener la lista que resulta de concatenar dos listas concretas.
Capítulo 14: Programación en Lógica de Predicados.
288
Por ejemplo, podemos usarlo también en sentido contrario, para partir una lista en dos. Al
plantear la consulta
:- concatenar(L1, L2, [1, b, 2, d])
Obtenemos lo siguiente del intérprete al forzar el backtracking.

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.

concatenar(L1, L2, [1, b, 2, d])


L1 = [] L1 = [1|_01]
L2 = [1, b, 2, d] L2 = _02
concatenar(_01, _02, [b, 2, d])
_01 = [] _01 = [b|_03]
_02 = [b, 2, d] _02 = _04
concatenar(_03, _04, [2, d])
_03 = [] _03 = [2|_05]
_04 = [2, d] _04 = _06
concatenar(_05, _06, [d])
_05 = [] _05 = [d|_07]
_06 = [d] _06 = _08
concatenar(_07, _08, [])
_07 = []
_08 = []

Figura 14.3. Árbol de búsqueda para la consulta concatenar(L1,L2,[1,b,2,d]) y subsecuentes


retrocesos

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.

sublista(SubL, Lista) :- prefijo(P, Lista), sufijo(SubL, P) .


Que se lee: “Una sublista de una lista es un sufijo de un prefijo de la lista.” Se recomienda
no escatimar esfuerzos para comprobar el sentido declarativo de ésta última afirmación y
subsecuentemente construir el árbol de búsqueda de una consulta que involucre el
predicado.
14.3.3 Ejemplo: invertir
Definamos ahora el predicado ivertir, que toma dos listas como argumentos y se satisface
si las listas están en orden inverso una de la otra. Para hacerlo utilicemos un acumulador,
como lo hemos hecho repetidamente en éste curso.

ivertir(L1, L2) :- inertirAcc(L1, [], L2) .


invertirAcc([], ACUM, ACUM) .
inertirAcc([H|T], ACUM, INV) :- invertirAcc(T, [H|ACUM], INV) .
La primera sentencia simplemente sirve para inicializar apropiadamente el proceso con un
acumulador vacío. La tercera sentencia, es la encargada de el paso iterativo de ir colocando
uno a uno los elementos de la primera lista en el acumulador, y la segunda sentencia (el
hecho) es el caso extremo que detiene el proceso cuando la primera lista se ha vaciado, por
acción de la segunda regla.
Sorprendentemente, pese a la apariencia procedural de éste predicado, que va llevando uno
a uno los elementos de la primera lista hacia el acumulador, el predicado invertir es
igualmente funcional en sentido contrario. Es decir, ante la consulta

:- 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]

Figura 14.4. Árbol de búsqueda para la consulta invertir([1,2,3], R).

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

Figura 14.5. Árbol de búsqueda para la consulta invertir(R, [1,2,3]).

14.3.4 Otros ejemplos de listas


Cada ejemplo es todo un mundo que simplemente abriremos para que el lector descubra.
Capítulo 14: Programación en Lógica de Predicados.
291
El predicado extraer1, toma tres argumentos, el primero es un elemento, y los otros dos
son listas. Se satisface cuando la última lista es igual a la primera tras extraer la primera
ocurrencia del elemento. Se define así:

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.

partir([], [], []) .


partir([A], [A], []) .
partir([A,B|T], [A|Ta], [B|Tb]) :- partir(T, Ta, Tb) .
Capítulo 14: Programación en Lógica de Predicados.
292
Finalmente, definamos el predicado quicksort:

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

Figura 14.6. Árbol de búsqueda incompleto, ilustrativo para la consulta zebra(N).


Capítulo 14: Programación en Lógica de Predicados.
295
14.5 Ejercicios propuestos
1. Construir el árbol de búsqueda para la consulta :-
sublista([2,3,4],[0,1,2,3,4,5]), dada la base de conocimientos definida a
través de la sección 14.3.2.

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.

3. Construir el árbol de búsqueda para la consulta agregar(3,[1,2,3,4],[1,3,2,3,4]),


según la base de conocimientos definida en la sección 14.3.4.

4. Construir dos árboles de búsqueda para la consulta factorial(4,F), basándose en cada


una de las implementaciones desarrolladas en el Ejemplo 202.

5. Resolver el taller de listas que se encuentra en la página web, utilizando predicados


definidos en PROLOG.

6. Construir el árbol de búsqueda para la consulta partir([7,3,9,10],L1,L2), según la


base de conocimientos definida en la sección 14.3.4.

7. Construir el árbol de búsqueda para la consulta quicksort([7,3,9,10],R), según la


base de conocimientos definida en la sección 14.3.4 (Si considera que puede predecir
adecuadamente el resultado de partir o merge, no es indispensable que desarrolle la
totalidad del árbol).
Parte IV: Lógica
dinámica y
lenguajes OO
Capítulo 15
Complementos Lógicos Al
Diagrama de Clases.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
300
15.1 Introducción.
Durante las fases de análisis y diseño del software, se determina la arquitectura de las
aplicaciones, identificando sus componentes y las relaciones entre dichas componentes.
Tal como se indicó en el Capítulo 2, el caracter de las componentes está determinado por el
paradigma arquitectónico que fundamenta el proceso de desarrollo.
En el marco del paradigma de Orientación por Objetos (ver 2.5), el componente primario de
una aplicación es el “objeto”. Bajo esta concepción, una aplicación esta constituida por un
conjunto de objetos que luego de ser creados, atraviesan por un conjunto de “estados” para
por último ser destruidos. En otras palabras los objetos tienen una “vida”.
Los objetos que constituyen una aplicación no son independientes sino que coexisten y
colaboran entre sí. Los objetos, en efecto, saben de la existencia de otros objetos y se
relacionan con ellos intercambiando “mensajes”. Al igual que lo hacen entre sí, los objetos
interactúa también con su “medio ambiente” (Vg. los usuarios de la aplicación, o una
máquina que controlan) a través de intercambio de mensajes.
En general, los mensajes son de dos tipos a saber: “consultas” y “eventos”. Las consultas
son básicamente referencias a las propiedades observables de un objeto, que son
respondidas con el valor de dicha propiedad. Los eventos son “ordenes” que, de ser
aceptadas, por el objeto, generan cambios en su vida. Estos cambios se ven reflejados, ya
sea en el valor de sus propiedades observables, y/o en su capacidad para aceptar nuevos
mensajes.
Las características comunes a un conjunto potencial de objetos (propiedades, posibles
estados, mensajes, eventos, relaciones, etc...), son definidas de forma conjunta por medio de
una “clase”. El “diagrama de clases” especifica de forma gráfica las clases que definen los
objetos de la aplicación.
Al nivel de la fase de análisis, las clases del diagrama de clases representan básicamente las
clases de objetos del área de aplicación del software. En este sentido todo software bajo el
paradigma de Orientación por Objetos es un modelo de su área de aplicación. Como
modelo, el diagrama de clases puede considerarse como un conjunto de restricciones que se
satisfacen en el área de la aplicación y que, en consecuencia, deben satisfacer también en el
software que la representa.
En este capítulo, primero, se examinan los elementos básicos del diagrama de clases
propuesto en el Lenguaje Universal de Modelamiento UML (o Unified Modelling
Language) [OMG UML]. A rol seguido se muestra como, para representar todas las
características del área de aplicación, que son relevantes al software, es necesario
complementar el diagrama con elementos propios de la lógica matemática.
Como es costumbre, el discurso, va de explicaciones meramente conceptuales a ejemplos
que ilustran la utilidad de los conceptos.

15.2 El Diagrama de Clases.


El diagrama de clases UML contiene tres tipos de elementos, a saber: plantillas de clases,
relaciones entre objetos, y relaciones entre clases.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
301
15.2.1 Plantillas de Clases.
Las clases de una aplicación software especifican las propiedades comunes de un grupo de
objetos. Ellas definen, en consecuencia, una categoría de clasificación para los objetos de
la aplicación.
Las clases se especifican por medio de una “plantilla” que permite crear objetos con
propiedades especificas. Los objetos que se crean con base en la plantilla de una clase se
denominan “ocurrencias o instancias” de la clase. Todo objeto, además, pertenece a una
clase (y lo sabe).
Las clases pueden, en consecuencia, verse tanto como una fabrica de objetos, o como el
conjunto de objetos fabricado. En [Booch 96, p120] se definen las clases en el contexto del
desarrollo OO, como: “Una clase es un conjunto de objetos que comparten un estructura
común y un comportamiento común. …Un solo objeto no es más que una instancia de una
clase.”
En la “plantilla de la clase” se especifican una serie de propiedades de los objetos que
varían según el lenguaje OO de que se trate. A continuación se muestran las propiedades
de la plantilla de las clases descritas en el diagrama de clases del lenguaje UML.

[Link] Nombre de la Clase


La plantilla de la clase en el diagrama de clases UML tiene como su primer componente al
nombre de la clase.
Toda clase tiene un nombre que la distingue. El nombre de cada clase debe ser único
dentro del “paquete” que la agrupa. En el marco de la aplicación el par {nombre de la
clase, nombre del paquete} debe ser único.

[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.

[Link] Asociaciones (“enlace con”).


Las asociaciones entre clases representan “enlaces” o “conexiones” entre objetos. Un
enlace puede conectar varios objetos de distintas clases. El número de objetos que conecta
un enlace es su aridad. Es costumbre usar en las aplicaciones sólo enlaces de aridad 2 (o
“binarios”), debido que los enlaces de aridades mayores pueden representarse con varios
enlaces binarios.
Las asociaciones pueden considerarse como clases, cuyas instancias son los enlaces que
representan. Así, las asociaciones puede, a su vez, tener atributos y métodos.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
303
Los enlaces, y en consecuencia las asociaciones, pueden ser “dirigidas” en el sentido de que
cada objeto participante tiene un “rol” definido en el enlace. El rol que cumple cada objeto
en la asociación es identificado con un rótulo o “nombre de rol”.
Un objeto puede aparecer en varios enlaces cumpliendo un rol determinado. El número de
veces que un objeto aparece cumpliendo un rol, se denomina la “cardinalidad del rol” para
el objeto. La asociación limita dicha cardinalidad indicando para cada rol una cardinalidad
mínima y una cardinalidad máxima. La cardinalidad de un rol para un objeto no puede ser
menor que la cardinalidad mínima ni mayor que la cardinalidad máxima indicadas para
dicho rol en la asociación.
Los objetos que están enlazados a otro objeto, cumpliendo un rol determinado, pueden
considerarse como el valor de un atributo del objeto al que están enlazados. El nombre de
este atributo es el nombre del rol que cumplen los objetos enlazados. Se dice entonces, que
éste atributo es “objeto valuado”, y que su número máximo y mínimo de valores,
corresponde a las cardinalidades máxima y mínima del rol.
En el diagrama de clase se presenta, para cada uno de las asociaciones, los siguientes
elementos:
• Nombre de la asociación.
• Nombre de cada rol.
• Cardinalidad mínima y máxima de cada rol.

[Link] Agregación (‘parte de”).


La agregación denota una jerarquía de todo/parte, con la capacidad de ir desde el todo hasta
sus partes [Booch 94, p118]. La agregación es esencialmente una asociación fuerte con
más semántica y normalmente transitiva y antisimétrica.
• Así, una agregación es una asociación en la que:
• la cardinalidad máxima para al el rol del lado del todo es 1, significando que una
parte pertenece a un sólo todo.
• La cardinalidad mínima para el rol del lado del todo es 1, significando que la
parte no puede existir sin un todo.
• El efecto de las operaciones sobre el todo usualmente se propaga a las partes.
• El borrado del todo se propaga a las partes constituyéndose un “borrado en
cascada”.
En el diagrama de clase la relación de composición se presenta como una asociación, en la
que el rol del lado del todo se substituye por un símbolo especial.

[Link] Cliente-Servidor (“usa a”).


Un objeto puede aparecer en la interfaz o en la implementación de algún método de un
segundo objeto. La relación entre estos objetos es, entonces, de caracter temporal ya que
ocurre sólo mientras se ejecuta el método. Se dice entonces que el primer objeto usa al
segundo. Si el segundo objeto aparece sólo en la implementación de un método del
primero es porque éste lo crea, lo usa y lo destruye durante la ejecución del método.
En el diagrama de clase la relación de uso se representa por una línea punteada que va de
un método de una clase a la clase del objeto que usa dicho método.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
304
[Link] Herencia (“es un”).
La herencia es el resultado de la aplicación del principio de abstracción a las clases. En
efecto, las propiedades que tienen en común varias clases pueden consignarse en una clase
más general o más “abstracta” que las captura. Las clases que comparten dichas
propiedades pueden considerarse, entonces, como clases que “especializan” a la clase
abstracta, agregándoles lo que las hace diferentes entre sí.
La herencia es, entonces, una relación antisimétrica y transitiva que indica que la clase
especializada “hereda” las propiedades de la clase abstracta. En este sentido la herencia
puede ser vista como un mecanismo para agregar propiedades a varias clases evitando
especificarlas una y otra vez.
La herencia toma, sin embargo, su verdadero sentido cuando se usa para representar la
relación intuitiva “es un”, que es fácil de reconocer en los objetos del área de aplicación del
software. Las instancias de las clases especializadas son, en efecto, también instancias de
la clase general ya que comparten con ella atributos y comportamiento. Entonces, en
cualquier lugar donde se use una instancia de una clase general, se pueden usar instancias
de las clases especializadas.
Además, los métodos de la clase general pueden refinarse o especializarse en las clases
especializadas logrando con ello que los operadores sean “polimórficos”. Los operadores
polimórficos mantienen el perfil declarado en la clase general pero ajustan la tarea que
llevan a cabo a las condiciones de la clase especializada110. Esto permite, por ejemplo,
definir, en la clase general, patrones de procesos que usan en su cuerpo operadores
polimórficos, y que luego, al ser ejecutados, ajustan los patrones de proceso a cada caso
particular usando los operadores de las clases especializadas111.
Un grupo de clases que especializan una clase más general, pueden constituir un conjunto
de categorías de clasificación para los objetos de la clase que especializan. Esta
clasificación puede, además, ser o no ser excluyente en el sentido de que un objeto que
pertenece a una de las especializaciones no puede, o si puede, pertenecer a otra
especialización diferente de dentro de la misma clasificación. Es además posible que
grupos diferentes de clases especializadas constituyan diferentes categorías de clasificación.
En el diagrama de clases se indican las relaciones de herencia entre las clases. Es posible
indicar, además, cuando varias especializaciones de una clase forman un conjunto de
categorías de clasificación para dicha clase, e indicar si dicha clasificación es excluyente o
si no lo es.

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.

[Link] El diagrama de Clases.


Sin entrar en detalles sobre la manera de identificar las clases, sus propiedades y las
relaciones entre las mismas (para lo cual referimos al lector a [Coad 97, pg. 97-150]),
presentamos a continuación una porción de un posible diagrama de clases que recoge las
“abstracciones” fundamentales del Ejemplo al nivel de la fase de análisis.

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( )

15.3 Incompletitud del Diagrama de Clases.


El software debe servir un propósito en el contexto de un problema y para ello durante la
fase de análisis se captura y representa una serie de elementos que provienen de la porción
del mundo donde ocurre dicho problema.
Bajo el paradigma de Orientación por objetos, los elementos del mundo real se proyectan
directamente a Objetos informáticos, de la forma que se muestra en la figura113.

113
Tomada de [Ramirez 99]
Capítulo 15: Complementos Lógicos al Diagrama de Clases
307

15.3.1 Completitud del análisis.


El problema del análisis es básicamente el de capturar todos los elementos del dominio del
problema a ser tenidos en cuenta para que el software sirva su propósito. Durante las fases
posteriores estos elementos se elaboran, se organizan y se suman a los propios de la
plataforma de implementación para obtener el software.
Lo anterior puede ilustrarse gráficamente asociando los modelos de las distintas fases a una
pirámide (ver [martin 94, p 77])114.

Flujo de Información

Modelo de la Empresa

Análisis del Área del Empresa

Diseño del Sistema

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

Debe evitarse, en la medida de lo posible, que la información proveniente del problema


entre a la pirámide a través de las capas inferiores de la pirámide. En este caso diremos que
el análisis quedó incompleto, o “subespecificado”.
La subespecificación del análisis obliga a que los elementos del dominio del problema que
deben ser tenidos en cuenta para el desarrollo del software, se recojan y especifiquen
directamente en las fases de diseño y programación (siendo mas común lo segundo),
generando software ilegible y poco reutilizable.
¿Qué información debe recogerse del problema durante el desarrollo del software?, es, en
consecuencia, una pregunta de importancia primordial para caracterizar la fase del análisis.
15.3.2 El Diagrama de Clases Como un Conjunto de Restricciones.
Como todo modelo, el diagrama de clases puede verse como un conjunto de restricciones
que determina la forma del software, en cuanto a que, las clases del diagrama determinan
las clases de programa y la estructura de la base de datos, y los métodos del diagrama de
clases determinan las funciones de los programas.
Más importante aun es que el diagrama de clases puede verse como un conjunto de
restricciones que se aplica a los datos que representan el área de aplicación. Este diagrama,
en efecto, determina asuntos como los que siguen:
• Cuales tipos de elementos del área de aplicación serán representados por medio
de datos y cuales no lo serán.
• Que datos serán usados para representar los elementos de cada tipo, indicando
cuales datos son opcionales u obligatorios, que valores pueden tomar dichos
datos, cuantos valores pueden asociarse con cada uno, y cuales deben ser sus
valores de defecto al momento de registrarse el elemento en el sistema.
• Cuando dos elementos de un mismo tipo pueden tener o no el mismo valor para
un dato o una combinación dada de datos.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
309
• Cuales tipos de enlaces entre los elementos del área serán registrados y cuales
no lo serán.
• Cuales enlaces deben existir obligatoriamente y cuales no, así como cuantos
enlaces diferentes de cada tipo pueden ser registrado para cada elemento.
15.3.3 Necesidad de Nuevas Restricciones.
A pesar de su expresividad, el diagrama de clases, por si sólo, no es capaz de registrar todas
las restricciones asociadas con los aspectos estructurales del área de aplicación, que son
relevantes al software. Así, de ser éste diagrama el único elemento utilizado para tal
propósito, la fase de análisis quedaría incompleta y el problema subespecificado.
Este hecho ha sido reconocido por la OMG, que ha propuesto completar las
especificaciones del diagrama de clases con especificaciones textuales en un lenguaje semi-
formal, el lenguaje Object Constraint Language (OCL).
Por medio del OCL es posible incorporar en la fase de análisis restricciones adicionales a
las propias del diagrama de clases. Es posible en particular, especificar lo siguiente:
• Las reglas de derivación de los atributos derivados sin necesidad de recurrir a un
lenguaje de programación.
• Restricciones a los valores de los atributos de un objeto que se deriven del valor
de otros atributos del mismo objeto, o de otros objetos con los que se halle
enlazado.
• Especificar el efecto de los eventos sobre el estado de los objetos, sin necesidad
de recurrir al lenguaje de programación.
• Definir la relación que tienen las consultas al software, con los datos descritos
en el diagrama de clases, sin necesidad de proyectar éste último a una
plataforma de implantación específica (Vg. un modelo relacional), para expresar
la consulta en el lenguaje que soporta dicha plataforma (Vg. SQL).
15.3.4 Restricciones Faltantes en el Ejemplo.
Es fácil reconocer elementos faltantes en el diagrama de clases de la Bodega de Wally que
fue presentado como ejemplo en 15.2.3.
En primer lugar, el objetivo de conocer el inventario en todo momento no tiene soporte
directo en el modelo, ya que el número de artículos, que debe calcularse sumando la
cantidad de artículos contenidas en las cajas que contengan dicho artículo, no aparece como
atributo derivado en la clase Articulo.
El objetivo de garantizar, que se despacha sólo lo pedido, todo lo pedido y nada más que lo
pedido, no tiene tampoco soporte en el modelo. En otras palabras, si se construye una
aplicación que permita despachar más elementos que los solicitados, esta estará en
concordancia con las restricciones del modelo, pero no será satisfactoria frente a los
problemas del área de aplicación.115

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.

Cuando un predicado Φ(x) se aplica a todos los elementos de un dominio C, es decir:


∀(xεC) Φ(x)
Usaremos la notación simplificada siguiente119:
Φ(C)

Ejemplo 207
El predicado siguiente:

[Link] = 0
Es una simplificación de.

∀(xεOrdenLineaArticulo) ([Link] = 0)

[Link] Derivaciones en el Ejemplo


Un caso de derivación para el ejemplo es el que se muestra a continuación. Las demás
derivaciones se dejan al lector como ejercicio.

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í:

[Link] <= [Link]

Las cardinalidades de las asociaciones permiten especificar restricciones importantes que


aparecen en el área de aplicación. Ellas, sin embargo, pueden inducir la necesidad de
incluir restricciones menos evidentes.

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.

La aparición de un ciclo de asociaciones en el diagrama de clases, usualmente se asocia con


una sobreespecificación de las asociaciones, en el sentido de que unas pueden derivarse de
las otras, o con la necesidad de una restricción para garantizar que el ciclo de enlaces entre
los correspondientes objetos es cerrado.

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.

15.5 Ejercicios propuestos.


1. Escribir todas las derivaciones y restricciones del ejemplo.
2. Escribir todas las derivaciones y restricciones para los casos de estudio que les sean
presentados.
Capítulo 15: Complementos Lógicos al Diagrama de Clases
315
Bibliografía.
[Abelson 85] Abelson, H. (1985). Structure and Interpretation of Computer Programms,
The MIT Press, [Link]
[Arango 97] Fernando Arango, “Elementos Fundamantales de los Lenguajes del
Computador”, Trabajo de promoción a Profesor Asociado, Departamento de
Sistemas y Administración, Facultad de Minas, Universidad Nacional de Colombia,
Sede Medellín, Diciembre 1997
[Arango 06] Arango Fernando, “Ingeniería del Software: UN Libro Web”, ISBN: 958-
97945-0-5, disponible en XXX.
[Blackburn 01] Blackburn, P. (2001). Learn Prolog Now!, [Link]
[Link]/~kris/prolog-course/html/[Link].
[Booch 96] Grady Booch, “Análisis y Diseño Orientado a Objetos con Aplicaciones 2ª
edición”, Addison-Wesley, 1996
[Bourbaki 72] Nicolas Bourbaki, “Elementos de Historia de las Matemáticas”, Alianza
Universidad S.A., Madrid, 1972.
[Brassard 88] G. Brassard and P. Bratley, “Algorithmics: Theory and Practice”, Prentice –
Hall, Englewood Cliffs, 1988.
[Chang 73] Chang, C. L. (1973). Symbolic Logic and Mechanical Theorem Proving,
Academic Press.
[Clavel 2007] Manuel Clavel, Francisco Durán, Steven Eker, Patrick Lincoln, Narciso
Martí-Oliet, José Meseguer, Carolyn Talcott, “Maude Manual (Version 2.3)”,
January 2007, Revised July 2007 disponible en [Link]
[Coad 97] Coad Peter. “Object Models, strategies, patterns and applications.” USA:
Yourdon Press 1997 [Berryman 07] Ken Berryman, Joel Jones, Junaid Mohiuddin,
“State of The Software Industry 2007: Software2007 Powered by Innovation”,
McKINSEY & COMPANY INC., SAND HILL GROUP, disponible en:
[Link]/grafix/pdf/[Link]
[Dijkstra 68] Edsger Dijkstra, “GoTo Comunications of the Considedred Harmfull”,
Comunications of the ACM, 11(3): 147-148, 1968. disponible en:
[Link]
[Gibbs 94] W. Wayt Gibbs, “Software's Chronic Crisis”, staff writer. Copyright Scientific
American; September 1994;
[Link]
[Grassmann 97]
Grassmann, W. K. (1997). Matemática discreta y lógica, Prentice Hall.
[Gra 2001] Paul Graham, “The roots of LISP”, 2001,
[Link]
Bibliografía
318
[Hanson 2002] Chris Hanson, the MIT Scheme Team and a cast of thousands, “MIT
Scheme Reference Manual”, Massachusetts Institute of Technology, Edition 1.96,
for Scheme Release 7.7.0, 13 March 2002, disponible en
[Link]
[Hoare 61] C A R [Link] 63, Partition; Algorithm 64, Quicksort; Algorithm 65,
Communications of the ACM, 4(7):321-322, Jul 1961.
[Klotz 80] Klotz, I, “The N-Ray Affair”, Scientific American, Mayo 1980.
Laboratory, I. S. (2003). SICStus Prolog User’s Manual, Swedish Institute of Computer
Science, [Link]
[Martin 97] Martin James, Odell Jaimes. Métodos orientados a objetos: conceptos
fundamentales, México: Prentice Hall Hispanoamérica, 1997
[MAUDE 2007] “The Maude System”, Computer Science Laboratory, Departament of
Computer Science, University of Ilinois at Urbana-Champaign,
[Link] página Web.
[McC 79] John McCarthy, “History of Lisp” Artificial Intelligence Laboratory,
Stanford University 12 February 1979, [Link]
[Link]/jmc/history/lisp/[Link]
[Meyer 98] Bertran Meyer, “Construcción de Software Orientado a Objetos, Segunda
Edición”, PRENTICE HALL, Madrid, 1999.
[Sylvan 2007] [Link]
[OMG UML] Object Management Group, Unified Modeling Lanaguge,
[Link]
[Pressman 05] Pressman Roger, “Ingeniería del Software (6ª ed.)”, McGraw-Hill /
Interamericana de México, 2005.
[Ramirez 99] Sergio Ramírez, Fernando Arango, “Modelo de Categorizacióin de Clases
como base de un Metodología para el Desarrollo de Software por Objetos”, borrador de
documento de tesis de maestría del estudiante Sergio Ramírez, presentado para defensa sin
ser aprobado en la Escuela de Sistemas, Facultad de Minas, Universidad Nacional de
Colombia, Sede Medellín, 1999.
[SCHEME 2003] “Scheme”, Massachusetts Institute of Technology,
[Link] página Web mantenida por Chris
Hanson, last update: October 2003.
[Ullman 79] teoría de los lenguajes y la Computación
[Ullman 79] Teoría de la compilación

También podría gustarte