Está en la página 1de 409

Aplicaciones de la Lógica al

Desarrollo del Software.


Volumen I: Lenguajes lógicos y
funcionales 2da edicion

.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


Diciembre de 2015
Datos de catalogación bibliográfica

ARANGO I. F.
Aplicaciones de la Lógica al Desarrollo de Software:
Lenguajes Lógicos y Funcionales.
ISBN:
MATERIA:
Informática 681.3
Formato: Digital

FERNANDO ARANGO ISAZA.


Aplicaciones de la Lógica al Desarrollo de Software:
Lenguajes Lógicos y Funcionales.
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
2015 respecto a la primera edición, por:
FERNANDO ARANGO ISAZA
Profesor Asociado,
Facultad de Minas,
Universidad nacional de Colombia,
Sede Medellín.

ISBN:
TABLA DE CONTENIDO

Contenido
CAPÍTULO 1 ................................................................................................................................................................ 1
INTRODUCCIÓN. .................................................................................................................................... 2
LA MÁQUINA DE ESTADOS. .................................................................................................................. 3
1.2.1 Vista Interna del Computador .................................................................................................... 3
1.2.2 El Proceso Computacional. ........................................................................................................ 5
1.2.3 La Máquina de Estados. ............................................................................................................. 5
LOS PROGRAMAS DEL COMPUTADOR.................................................................................................... 6
LENGUAJES DE PROGRAMACIÓN ........................................................................................................... 8
EVOLUCIÓN DE LOS LENGUAJES DE PROGRAMACIÓN. ........................................................................... 9
1.5.1 Primera Generación: Lenguaje de Máquina. ............................................................................ 9
1.5.2 Segunda Generación: Lenguaje Ensamblador. ....................................................................... 10
1.5.3 Tercera Generación: Lenguajes Procedurales. ....................................................................... 11
1.5.4 Cuarta Generación: SQL y Lenguajes Declarativos. .............................................................. 14
1.5.5 Quinta Generación: Uso de la Lógica. .................................................................................... 15
RESUMEN DEL CAPÍTULO. ................................................................................................................... 16
CAPÍTULO 2 .............................................................................................................................................................. 19
INTRODUCCIÓN ................................................................................................................................... 20
PARADIGMA DE INSTRUCCIONES. ........................................................................................................ 21
PARADIGMA DE FUNCIONES (O PROCESOS). ........................................................................................ 21
2.3.1 Especificación de Funciones. ................................................................................................... 22
2.3.2 Arquitectura funcional. ............................................................................................................. 24
2.3.3 Proceso de desarrollo. .............................................................................................................. 27
PARADIGMA DE ENTIDADES (O ESTRUCTURACIÓN DE DATOS). .......................................................... 28
2.4.1 Estructuración de los datos en archivos. .................................................................................. 28
2.4.2 Problemas con los Archivos y Aparición de los Gestores de Bases de Datos. ......................... 29
2.4.3 Modelo Relacional. ................................................................................................................... 30
2.4.4 La especificación formal bajo el modelo Relacional. ............................................................... 31
2.4.5 Arquitectura Relacional. .......................................................................................................... 32
PARADIGMA DE OBJETOS. ................................................................................................................... 35
2.5.1 Concepto Clásico de Objeto. .................................................................................................... 36
2.5.2 Relaciones entre objetos. .......................................................................................................... 38
2.5.3 Aplicaciones como Sistemas Dinámicos. .................................................................................. 38
2.5.4 Especificación formal bajo el modelo OO. ............................................................................... 39
2.5.5 Arquitectura Objetual. .............................................................................................................. 39
PARADIGMA DE AGENTES. .................................................................................................................. 41
RESUMEN DEL CAPÍTULO. ................................................................................................................... 42
CAPÍTULO 3 .............................................................................................................................................................. 45
INTRODUCCIÓN ................................................................................................................................... 46
LA LÓGICA COMO UNA DISCIPLINA DE RAZONAMIENTO. ................................................................... 46
3.2.1 Lógica en la antigua Grecia. .................................................................................................... 47
3.2.2 La lógica en la matemática moderna. ...................................................................................... 48
LA LÓGICA COMO UNA DISCIPLINA DE LOS LENGUAJES. .................................................................... 49
3.3.1 Sintaxis. .................................................................................................................................... 49
3.3.2 Semántica. ................................................................................................................................ 51
3.3.3 Inferencia. ................................................................................................................................. 52
LÓGICA DE PROPOSICIONES. ............................................................................................................... 53
3.4.1 Proposiciones. .......................................................................................................................... 53
3.4.2 Sintaxis de la lógica proposicional. .......................................................................................... 53
3.4.3 Semántica.en lógica proposicional. .......................................................................................... 58
3.4.4 Inferencia en lógica proposicional. .......................................................................................... 62
3.4.5 Formas Normales. .................................................................................................................... 67
RESUMEN DEL CAPÍTULO. ................................................................................................................... 70
ii Tabla de Contenido.

EJERCICIOS PROPUESTOS. ................................................................................................................... 72


3.6.1 De la lógica en general ............................................................................................................ 72
3.6.2 Lógica de Proposiciones .......................................................................................................... 73
CAPÍTULO 4 .............................................................................................................................................................. 77
INTRODUCCIÓN ................................................................................................................................... 78
SINTAXIS DE LA LÓGICA DE PREDICADOS. ........................................................................................... 78
4.2.1 Alfabeto de símbolos. ................................................................................................................ 79
4.2.2 Criterios Formativos de las fbf en lógica de predicados .......................................................... 81
SEMÁNTICA EN LA LÓGICA DE PREDICADOS. ...................................................................................... 87
4.3.1 Dominio de interpretación........................................................................................................ 87
4.3.2 Significado de las variables. ..................................................................................................... 87
4.3.3 Extensión de los predicados sobre el dominio. ......................................................................... 88
4.3.4 Valor de verdad de las FBF...................................................................................................... 88
4.3.5 semántica de un conjunto de fbfs. ............................................................................................. 90
INFERENCIA EN LÓGICA DE PREDICADOS. ........................................................................................... 90
4.4.1 Criterios de demostración propios de la lógica de predicados. ............................................... 91
4.4.2 Formas Normales en Lógica de Predicados. ............................................................................ 91
TIPOS DE PREDICADOS Y LÓGICAS ASOCIADAS. ................................................................................... 94
RESUMEN DEL CAPÍTULO. ................................................................................................................... 95
EJERCICIOS PROPUESTOS. ................................................................................................................... 95
4.7.1 Verdadero o falso? ................................................................................................................... 95
4.7.2 Árboles sintácticos .................................................................................................................... 97
4.7.3 Variables Libres y Ligadas ....................................................................................................... 98
4.7.4 Cálculo de valores de verdad. .................................................................................................. 98
4.7.5 Varios ....................................................................................................................................... 98
CAPÍTULO 5 ............................................................................................................................................................ 101
INTRODUCCIÓN. ................................................................................................................................ 102
TÉRMINOS. ........................................................................................................................................ 102
5.2.1 Operadores. ............................................................................................................................ 103
5.2.2 Criterios formativos de los términos. ..................................................................................... 104
5.2.3 Términos Complejos ............................................................................................................... 105
5.2.4 Semántica de los Operadores. ................................................................................................ 110
LÓGICA ECUACIONAL ....................................................................................................................... 111
5.3.1 El predicado de igualdad........................................................................................................ 111
5.3.2 Criterios Demostrativos. ........................................................................................................ 112
5.3.3 Teoría en lógica ecuacional. .................................................................................................. 113
SISTEMAS DE REESCRITURA DE TÉRMINOS (SRT). ........................................................................... 116
ALCANCE DE LOS SRTS. ................................................................................................................... 117
ESPECIFICACIÓN EN LÓGICA ECUACIONAL MULTISORT.................................................................... 120
5.6.1 Signatura Multisort................................................................................................................. 120
5.6.2 Términos. ................................................................................................................................ 121
5.6.3 Ecuaciones. ............................................................................................................................. 123
SRT .................................................................................................................................................. 124
5.7.1 Substitución, Particularización y Emparejamiento. ............................................................... 124
5.7.2 Ocurrencias y Reemplazos. .................................................................................................... 126
5.7.3 Relaciones de Reescritura entre términos de la especificación. ............................................. 127
5.7.4 Propiedad de Church-Roser: Confluencia y Terminancia. ................................................... 128
SEMÁNTICA POR CLASES DE EQUIVALENCIA. ................................................................................... 129
RESUMEN DEL CAPÍTULO. ................................................................................................................. 129
EJERCICIOS PROPUESTOS. ............................................................................................................. 131
CAPÍTULO 6 ............................................................................................................................................................ 139
INTRODUCCIÓN. ................................................................................................................................ 140
RESOLUCIÓN. .................................................................................................................................... 140
6.2.1 Resolución en lógica proposicional. ....................................................................................... 140
6.2.2 Resolución en Lógica de Predicados. ..................................................................................... 143
EL LENGUAJE PROLOG VISTO DESDE LA LÓGICA. .......................................................................... 147
Tabla de Contenido. iii
6.3.1 Cláusulas de Horn y programa PROLOG. ............................................................................. 148
6.3.2 Consultas en PROLOG ........................................................................................................... 148
6.3.3 Forma Prenex de un programa PROLOG .............................................................................. 149
SINTAXIS Y NOMENCLATURA DEL PROLOG .................................................................................... 150
6.4.1 Términos. ................................................................................................................................ 150
6.4.2 Predicados. ............................................................................................................................. 151
6.4.3 Programa: Hechos y Reglas. .................................................................................................. 151
6.4.4 Consultas ................................................................................................................................ 152
6.4.5 Ejemplo de PROLOG ............................................................................................................. 153
PROCESAMIENTO POR RESOLUCIÓN SLD. ......................................................................................... 154
6.5.1 Búsqueda de la prueba y árboles de búsqueda ....................................................................... 155
6.5.2 Búsqueda de la prueba y tabla de reescritura de la consulta ................................................. 158
RESUMEN DEL CAPÍTULO. ................................................................................................................. 159
EJERCICIOS PROPUESTOS ................................................................................................................... 161
CAPÍTULO 7 ............................................................................................................................................................ 165
INTRODUCCIÓN ................................................................................................................................. 166
MULTIPLICIDAD DE LENGUAJES........................................................................................................ 167
CONCEPTOS DIFERENCIADORES DE LOS LENGUAJES DE PROGRAMACIÓN. ......................................... 168
IMPLEMENTACIONES E INTERFACES. ................................................................................................. 172
7.4.1 Linea de comandos REPL....................................................................................................... 172
7.4.2 Ciclo Codificación Interpretación Ejecución. ........................................................................ 176
OPERADORES NATIVOS Y EXPRESIONES SIMPLES. ............................................................................. 178
RESUMEN DEL CAPÍTULO. ................................................................................................................. 182
EJERCICIOS PROPUESTOS. ................................................................................................................. 183
CAPÍTULO 8 ............................................................................................................................................................ 185
INTRODUCCIÓN ................................................................................................................................. 186
JUSTIFICACIÓN. ................................................................................................................................. 186
8.2.1 Reuso de código. ..................................................................................................................... 186
8.2.2 Arquitectura de funciones y de objetos. .................................................................................. 187
DEFINICIÓN DE LOS OPERADORES. .................................................................................................... 187
8.3.1 Definición de Operadores en lenguajes Funcionales. ............................................................ 188
8.3.2 Definición de Operadores en lenguajes CLAUSALES. .......................................................... 189
8.3.3 Definición de Operadores en lenguajes PROCEDURALES. .................................................. 190
SELECCIÓN........................................................................................................................................ 191
8.4.1 Selección por emparejamiento de la evocación formal con la evocación real. ...................... 193
8.4.2 Selección por condición de guardia sobre las unidades de especificación. ........................... 194
8.4.3 Operadores e instrucciones de selección. ............................................................................... 195
TIPO DEL OPERADOR Y DE SUS OPERANDOS ...................................................................................... 199
8.5.1 Perfil de los Operadores en SCHEME. .................................................................................. 200
8.5.2 Perfil de los operadores en MAUDE ...................................................................................... 201
8.5.3 Perfil de los Predicados en SWI PROLOG. ........................................................................... 207
MEDIO AMBIENTE Y ASIGNACIÓN...................................................................................................... 208
8.6.1 Medio ambiente en SCHEME. ................................................................................................ 209
8.6.2 Medio ambiente en MAUDE. .................................................................................................. 214
8.6.3 Medio ambiente en PROLOG. ................................................................................................ 215
ESTRATEGIA DE EVALUACIÓN. ......................................................................................................... 215
8.7.1 Estrategia de Evaluación en SCHEME. ................................................................................. 218
8.7.2 Estrategia de evaluación en MAUDE ..................................................................................... 218
8.7.3 Estrategia de evaluación en PROLOG ................................................................................... 219
ESTRUCTURA DE LOS PROGRAMAS. ................................................................................................... 221
8.8.1 Estructura de los programas en SCHEME ............................................................................. 221
8.8.2 Estructura de los programas en MAUDE ............................................................................... 222
RESUMEN DEL CAPÍTULO. ................................................................................................................. 225
EJERCICIOS PROPUESTOS. ............................................................................................................ 227
CAPÍTULO 9 ............................................................................................................................................................ 229
INTRODUCCIÓN. ................................................................................................................................ 230
iv Tabla de Contenido.
EJEMPLOS DE DEFINICIÓN RECURSIVA DE OPERADORES. ................................................................. 231
9.2.1 Sumatoria. .............................................................................................................................. 231
9.2.2 Raíz cuadrada por el método de Newton_Rapson. ................................................................. 233
LA FORMA DEL PROCESO DE CÁLCULO (USO DE MEMORIA). ............................................................. 236
9.3.1 Forma del proceso: acumulador asociativo y no asociativo. ................................................. 240
LA PRECISIÓN DEL PROCESO DE CÁLCULO ........................................................................................ 246
LA EFICIENCIA DEL PROCESO DE CÁLCULO. ...................................................................................... 249
SERIE DE FIBONACCI. ........................................................................................................................ 254
9.6.1 Proceso recursivo. .................................................................................................................. 255
9.6.2 Proceso iterativo..................................................................................................................... 256
UTILIDAD DE LA RECURSIÓN ............................................................................................................. 258
RESUMEN DEL CAPÍTULO. ................................................................................................................. 259
EJERCICIOS PROPUESTOS. .................................................................................................................. 261
CAPÍTULO 10 .......................................................................................................................................................... 267
INTRODUCCIÓN ............................................................................................................................ 268
TIPOS COMPUESTOS ESTRUCTURADOS ......................................................................................... 268
10.2.1 Declaración de tipos compuestos estructurados. ............................................................... 270
10.2.2 Construcción de instancias del tipo compuesto estructurado. ........................................... 272
10.2.3 Selección de componentes de un valor compuesto estructurado. ...................................... 273
10.2.4 Definición de operadores sobre tipos compuestos estructurados. ..................................... 274
10.2.5 Invariantes de Tipo ............................................................................................................ 276
TIPOS COMPUESTOS ITERADOS .................................................................................................... 281
10.3.1 Conexión entre componentes: Pares.................................................................................. 282
10.3.2 Estructura del iterado: conjuntos, listas, árboles y grafos. ............................................... 285
GESTIÓN DE LISTAS...................................................................................................................... 286
10.4.1 Declaración de Listas. ....................................................................................................... 286
10.4.2 Constructores de la lista. ................................................................................................... 287
10.4.3 Recorridos básicos sobre listas. ........................................................................................ 292
10.4.4 Selección sobre listas. ........................................................................................................ 298
10.4.5 Modificadores de la lista ................................................................................................... 306
10.4.6 Transformación de la lista ................................................................................................. 314
GESTIÓN DE ÁRBOLES. ................................................................................................................. 321
10.5.1 Ejemplo: Árbol Binario de Búsqueda. ............................................................................... 321
10.5.2 Construcción del Árbol Binario de Búsqueda. .................................................................. 321
10.5.3 Localización de un Componente en el Árbol Binario de Búsqueda. .................................. 324
10.5.4 Inserción de un Componente en el Árbol Binario de Búsqueda. ....................................... 326
RESUMEN DEL CAPÍTULO. ............................................................................................................ 328
EJERCICIOS PROPUESTOS .............................................................................................................. 330
CAPÍTULO 11 .......................................................................................................................................................... 335
INTRODUCCIÓN ............................................................................................................................ 336
ABSTRACCIÓN DE TIPO Y OPERADOR EN SCHEME. ..................................................................... 336
11.2.1 Abstracción de tipo en SCHEME. ...................................................................................... 337
11.2.2 Abstracción de operadores en SCHEME. .......................................................................... 338
ABSTRACCIÓN DE TEORÍAS EN MAUDE. ..................................................................................... 342
11.3.1 Parametrización de módulos. ............................................................................................ 345
11.3.2 Definición de teorías. ......................................................................................................... 347
11.3.3 Creación de vistas. ............................................................................................................. 349
11.3.4 Creación de instancias de módulos paramétricos. ............................................................ 353
RELACIONES ENTRE TIPOS. .......................................................................................................... 356
11.4.1 “Kinds” y Gestión de Errores. .......................................................................................... 356
11.4.2 Sobrecarga de operadores en subtipos. ............................................................................. 357
11.4.3 Preregularidad .................................................................................................................. 357
11.4.4 Ecuaciones de Membresía y de Membresía Condicional .................................................. 358
11.4.5 Operadores Polimórficos y listas heterogéneas. ............................................................... 361
RESUMEN DEL CAPÍTULO. ............................................................................................................ 362
EJERCICIOS PROPUESTOS .............................................................................................................. 364
Tabla de Contenido. v
CAPÍTULO 12 .......................................................................................................................................................... 367
INTRODUCCIÓN. ........................................................................................................................... 368
CONSULTAS DEDUCTIVAS SOBRE UNA BASE DE DATOS. .............................................................. 368
BÚSQUEDA DE DATOS Y COMBINACIONES DE DATOS. ................................................................... 373
ACERTIJOS ................................................................................................................................... 378
RESUMEN DEL CAPÍTULO. ............................................................................................................ 380
PREFACIO

Este trabajo reúne las experiencias en el contexto de la programación declarativa, que he


venido acopiado en actividades de docencia e investigación durante mi labor académica en
los últimos 20 años.
Aunque el trabajo es fundamentalmente un texto de programación, responde a una idea
generatriz, a saber: “Los diversos lenguajes lógicos y funcionales conforman una familia,
en la que los programas puede caracterizarse de manera abstracta, y con independencia
de la forma que tomen en la sintaxis de un lenguaje particular”.
En consecuencia este no es un texto de programación en un lenguaje particular, sino más
bien un texto de programación en una familia de lenguajes. Para ello, el texto se enfoca
primero en señalar las ideas básicas que fundamentan la concepción de los programas, y
luego, en mostrar la manera como dichas ideas se expresan en los lenguajes particulares.
Un principio básico que se plantea de forma reiterada en el texto, es el de considerar todos
los lenguajes Funcionales y Lógicos como una variante sintáctica de una lógica matemática
de primer orden1. Con ello, todo programa escrito en uno de estos lenguajes, puede
considerarse como una teoría de dicha lógica, y toda ejecución de uno de dichos programas,
puede interpretarse como un proceso de demostración en el marco de la teoría.
El texto tiene, sin embargo, una naturaleza fundamentalmente práctica. Así, aunque parte
de los elementos de una lógica, no se centra en analizar ni demostrar propiedad alguna de
dicha lógica. El texto se centra, más bien, en plantear la manera como se conciben las
teorías asociadas con familias de problemas particulares, y en la manera como dichas
teorías se plasma en una sintaxis particular.
Con ello el autor espera que el lector fije su atención en los aspectos de fondo más bien que
en los aspectos de forma de la programación. El texto muestra, en efecto, que todos los
programas se fundamentan en unos pocos “principios” de fondo, que deben expresarse en
una de las muchas formas sintácticas de los lenguajes particulares. 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 que
concentrándose en estos últimos se apropie rápidamente del mismo.
Para ejemplificar la manera como se plasma la teoría en una sintaxis particular, el texto
presenta más de 250 programas escritos en tres lenguajes específicos, a saber: los lenguajes
SCHEME, MAUDE, y el SWI PROLOG. Los lenguajes SCHEME y MAUDE, se
seleccionaron por representar dos enfoques extremos en el marco de la programación
funcional, así: el SCHEME, muy cercano a los lenguajes de programación procedurales; y
el MAUDE, muy cercano a la expresión matemática de una lógica ecuacional. El lenguaje
PROLOG, se seleccionó por ser el principal representante de los lenguajes lógicos.
El texto se comenzó a escribir desde el año 2004 como una reelaboración del texto
“Elementos Básicos de Los Lenguajes Declarativos” (Fernando Arango Isaza y Daniel
Cabarcas, ISBN: 958-97945-1-3), que presentaba el material de programación desde la

1 Dicha variante sintáctica puede considerarse como “azúcar sintáctica” sobre dicha lógica [Bourbaki 50].
PREFACIO viii
óptica particular de cada uno de los lenguajes considerados. De los capítulos del libro
original, se preservaron algunos apartes relativos a la lógica, junto con las figuras y los
ejemplos.
El trabajo, por otro lado, no puede considerarse un producto terminado. Aparte de las
obligadas revisiones a los ejemplos, nuevos ejercicios y nuevos lenguajes, aún faltan
secciones importantes. Principalmente en lo planeado para los tomos II y III que pretenden
recoger los elementos propios de la especificación, verificación y refinamiento del
software, en el marco del uso de FRAMEWORKs de programación, especializados en
soportar la traducción directa a lenguajes procedurales, de especificaciones declarativas.

Fernando Arango Isaza


INTRODUCCIÓN

El texto es auto contenido y se orienta a ser usado como un medio autónomo de aprendizaje
y consulta2. También ha venido siendo utilizado como texto guía en el curso “Lenguajes
Lógicos y Funcionales”, que ha estado a cargo del autor, en el Departamento de Ciencias de
la Computación y de La Decisión (DCCD) de la Facultad de Minas, Universidad Nacional
de Colombia, Sede Medellín.
A continuación se presenta una descripción básica del contenido y objeto de cada capítulo,
señalando los que son fundamentales, y los que pueden ser obviados en una primera lectura.
Los elementos que conforman el curso referido arriba, junto con los talleres y códigos que
lo soportan, se pueden consultar en la intranet del DCCD 3.
La organización del texto es la siguiente:
 Los capítulos 1 y 2 presentan una visión general de los paradigmas de
desarrollo del software y de los lenguajes de programación: Estos capítulos
le dan sentido al tópico desarrollado en el texto, y, en particular, permiten
reconocer la importancia de la lógica en el marco general del desarrollo del
software. Ellos están dirigidos a lectores con poca información sobre el
tema y pueden ser obviados por lectores más avanzados
 Los capítulos 3 y 4 presentan los elementos de la lógica de proposiciones y
de la lógica de predicados que son relevantes a los desarrollos posteriores.
El capítulo se centra en los aspectos sintácticos y semánticos de las lógicas,
sin profundizar en los temas de consistencia y demostración. Se recomienda
una lectura rápida de repaso a los lectores avanzados
 El capítulo 5 presenta la lógica ecuacional como fundamento de los
lenguajes funcionales. Primero, se enfatizan los criterios formativos para los
términos y las reglas que determinan su estructura sintáctica, luego se
presentan los criterios de demostración asociados con el predicado de
igualdad, y por ultimo, se muestran en detalle los elementos propios de una
demostración por derivación ecuacional. Este capítulo es clave en el
contexto de los lenguajes funcionales y no debe ser obviado por lector
alguno. El capítulo 5 presenta,además, los Sistemas de Reescritura de
Términos (SRT) como una forma de automatizar la demostración en Lógica
Ecuacional, que, si bien no alcanza el potencial demostrativo de dicha
lógica, da cuenta de los lenguajes funcionales. Se plantean los conceptos de
substitución particularización, emparejamiento, y reemplazo; junto con la
relación de reescritura entre términos, y la propiedad de Chrch_Roser
(confluencia y terminancia). El discurso, mas allá del la sección 2, es
abstracto, formal y preciso, y su lectura no es necesaria para abordar los
capítulos que siguen.

2Incluye, en adición a las referencias obligadas, un conjunto importante de referencias WWW (principalmente de
Wikipedia), que sirven como vías de acceso a la literatura que expande los tópicos mencionados.
3 Actualmente en desarrollo, puede verse un prototipo con los contenidos del curso en la pagina del autor:
http://xue.medellin.unal.edu.co/farango/
INTRODUCCIÓN x
 El capítulo 6 introduce los dos lenguajes funcionales que serán usados en el
resto del texto, los lenguajes MIT SCHEME y MAUDE. Luego de
presentar su origen y contexto histórico, se indica la manera de usar los
intérpretes respectivos, y, por último se muestran los cálculos que pueden
efectuarse con los operadores y tipos nativos que ofrecen. La lectura de este
capítulo es indispensable para quienes desean ejecutar los ejemplos y
programar los ejercicios.
 El capítulo 7 presenta los principios y facilidades que ofrecen los lenguajes
para definir nuevos operadores y aplicar mecanismos de selección en los
cálculos. Se asimilan estas facilidades al planteamiento de un conjunto de
aserciones ecuacionales, posiblemente definidas como el consecuente de una
implicación. Se discuten, luego, tópicos relativos a la naturaleza misma de
cada uno de los lenguajes, así: las diferencias entre un lenguaje débilmente
tipado (SCHEME) y un lenguaje fuertemente tipado (MAUDE); los criterios
y modos de controlar la estrategia de evaluación de términos complejos; los
elementos que determinan el medio ambiente de evaluación para los
términos; y por último, los mecanismos de modularización. Este es un
capítulo central en el marco del trabajo.
 El capítulo 8 introduce la recursión como elemento fundamental para definir
operadores matemáticos complejos. Se analiza el efecto de la forma del
programa en la eficiencia del proceso de cálculo. La eficiencia se plantea
tanto en términos del espacio de memoria como del tiempo de proceso. Se
introduce la diferencia entre los procesos estables en memoria (iterativos) y
los procesos inestables en memoria (recursivos) como medida de la
eficiencia en el espacio. Se plantean los conceptos básicos de la teoría del
orden como medio para medir la eficiencia del proceso en el tiempo, y se
ilustra con ejemplos prácticos la manera de evaluar y controlar dicha
eficiencia. Por último, se señala e ilustra con ejemplos, la necesidad de
controlar la propagación de los errores causados por inexactitudes en el
cálculo. Este es un capítulo central en el marco del trabajo.
 El capítulo 9 presenta los principios y facilidades que ofrecen los lenguajes
para definir nuevos tipos de datos, y en particular, tipos para datos
compuestos. Se señalan e ilustran, con múltiples ejemplos, los elementos
necesarios para definir los nuevos tipos, entre ellos la declaración del tipo,
los constructores y selectores, las invariantes de tipo, los operadores sobre
datos compuestos, etc... Este es un capítulo central en el marco del trabajo.
 El capítulo 10 presenta los principios y facilidades que ofrecen los lenguajes
para generalizar los códigos, parametrizándolos por tipo de datos y sus
operaciones asociadas. Se contrasta el enfoque del SCHEME, que se centra
en parametrizar los operadores que se evocan facilitando el paso de
operadores como argumentos de otros operadores; con el enfoque del
MAUDE, que se centra en parametrizar los grupos de tipos o “teorías” que
se usan en otra teoría, facilitando el paso de teorías como argumentos de
otras teorías. Este capitulo es inherentemente complejo y puede obviarse en
una primera lectura del texto.
INTRODUCCIÓN
xi
 El capítulo 11 retoma el tema de la lógica, y presenta los elementos
necesarios para darle soporte a los lenguajes lógicos, en particular al
PROLOG. Presenta, primero, el concepto de equivalencia semántica para
las fórmulas de la lógica, y desarrolla los métodos para la normalización de
fórmulas en la lógica proposicional y de predicados. Presenta, luego, el
teorema de la resolución y el método de demostración por reducción al
absurdo, a través de la introducción de resolventes para fórmulas en lógica
clausal. Ilustra el uso del unificador más general para la introducción
correcta de los resolventes en fórmulas de la lógica de predicados. Se
recomienda una lectura rápida de repaso a los lectores avanzados.
 El capítulo 12 presenta la resolución SLD como una forma de automatizar la
demostración en lógica clausal para el caso de cláusulas de Horn. Presenta
los elementos del lenguaje PROLOG señalando los elementos clausales que
conforman el programa (hechos y reglas) y los elementos clausales que
conforman los objetivos a ser demostrados en el marco del programa.
Ilustra como la resolución SLD permite demostrar los objetivos por medio
de un proceso de reescritura de los mismos, en el cual existen la “falla” y el
“retroceso”. Este capítulo es clave en el contexto de los lenguajes lógicos y
no debe ser obviado por lector alguno.
 El capítulo 13 ilustra las capacidades del PROLOG como un lenguaje
funcional. Muestra la manera de llevar a cabo cálculos aritméticos y de
efectuar operaciones sobre listas, reescribiendo los códigos de los capítulos
correspondientes para los lenguajes funcionales. Este es un capítulo central
en el marco del trabajo.
 El capítulo 14 presenta las capacidades del PROLOG como lenguaje de
búsqueda apoyado en la falla y el retroceso del SLD. Muestra, primero, la
capacidad del lenguaje para hacer consultas “inteligentes” sobre bases de
datos, interpretadas como colecciones de hechos, dando soporte a las “Bases
de Datos Deductivas”. Muestra luego la capacidad del lenguaje para
resolver problemas que implican hallar en un espacio de búsqueda los
valores o tuplas de valores que satisfacen una condición dada. Muestra, por
último ejemplos de solución de “acertijos” planteados como reglas de un
programa PROLOG. Este es un capítulo central en el marco del trabajo.
AGRADECIMIENTOS

A Daniel Cabarcas que trabajó en la elaboración de los módulos de clase para el curso
“Lenguajes Declarativos”, y que dieron lugar al libro anterior. A Daniel Cabarcas, David
Cardona, Willington Vega y Jorge Yukio, que, como monitores del curso, contribuyeron a
la definición de ejemplos, casos de estudio y depuración de los códigos. A los estudiantes
de los cursos “Lenguajes Lógicos y Funcionales” y “Lenguajes Declarativos”, que con sus
anotaciones y críticas han contribuido a depurar los códigos y a mejorar las partes oscuras
del texto.
Capítulo 1
Evolución de los Lenguajes de
Programación
Capítulo 1: Evolución de los Lenguajes de Programación
2
Introducción.
A medida que el uso del computador se extiende a casi todas las actividades del mundo
moderno, se requieren cada vez más y mejores programas de aplicación o “software”.
El desarrollo de software, por su parte, requiere de mano de obra especializada y, por lo
general, el esfuerzo requerido para su desarrollo aumenta rápidamente con su complejidad 4.
Es así como, hoy en día, la construcción del software es el cuello de botella en el uso del
computador y el responsable de la mayor parte de los costos. Esta situación ha sido
denominada “crisis del software” [Gibbs 94].
La necesidad de resolver la crisis del software, junto con la importancia económica que se
le adjudica, ha impulsado el rápido desarrollo de una disciplina asociada con el problema
del desarrollo del software5.
Como todas las disciplinas, la disciplina del desarrollo de software ha evolucionado en muy
diversas direcciones. En [Arango 06] se plantea, sin embargo, que en ella se pueden
reconocer tres ejes fundamentales, a saber:
 El eje de los paradigmas arquitectónicos, que comprende los diferentes
principios y conceptos que soportan la manera de entender y componer una
pieza de software. Los paradigmas determinan los tipos, o “categorías”, de
elementos que se usan para describir, o “modelar”, el software en sus
diferentes fases de desarrollo
 El eje de los lenguajes de programación que comprende los diferentes
principios y conceptos que soportan la estructura y significado de los
elementos usados al especificarle al computador, o “codificar”, una piezas
de software.
 El eje de los paradigmas de gestión que recoge los diferentes modos de
proceder para obtener el software. Estos incluyen tanto los modos de
proceder para la definición, planeamiento y control de las fases y etapas en
un proyecto de desarrollo de software, como los modos de proceder en la
construcción de los modelos y códigos asociados con el mismo.
Los cambios históricos en el eje de los lenguajes de programación es examinada de forma
sumaria en este capítulo. La tesis del capítulo es que la tendencia en el desarrollo de los
lenguajes de programación, apunta a la concepción de lenguajes que describen cada vez
más el área de aplicación del software, en contraste con lenguajes que describen el proceso
computacional asociado con la ejecución del software en el computador. En esta tendencia
se destaca la aparición de lenguajes que se apoyan en elementos de una lógica matemática,
permitiendo elaborar programas que constituyen teorías definidas en la lógicas y que al ser
ejecutados se llevan a cabo procesos de demostración.
La evolución de los paradigmas arquitectónicos es examinada en el capítulo siguiente. En
este capítulo se plantea que los lenguajes que se usan durante las fases de análisis, diseño y

4 En http://www-128.ibm.com/developerworks/rational/library/4700.html se presentan algunas consideraciones sobra la


naturaleza del software y las causas de su complejidad.
5 Usualmente denominada “Ingeniería del Software” [Pressman 05]. Ver http://www.rspa.com/
Capítulo 1: Evolución de los Lenguajes de Programación
3
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 problema6.
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.

La Máquina de Estados.
El “computador” u “ordenador”, es una máquina cuya función básica es la de llevar a cabo
operaciones de almacenamiento, cómputo (y/u ordenamiento), y distribución sobre
elementos de datos.
Desde la óptica de quién usa el computador, la tarea del computador será la de recibir los
datos necesarios para los cómputos, ejecutar los cómputos y devolver (o almacenar y
distribuir) los resultados de la ejecución. Ella es, pues, fundamentalmente una tarea de
transformación de “datos de entrada” en “datos de salida”, o mas simplemente de “datos”
en “resultados”7, que han de ser posteriormente almacenados y distribuidos.

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 los 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 y actuadores 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

6 O al menos a la manera como entendemos el universo del problema.


7Esta 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
computador8.
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 ó ROM9): 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
diferentes10. 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
computadoras11. 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.

8La unidad de almacenamiento mas usada es el “byte”, que almacena ocho dígitos binarios (u ocho “bits” o “pizcas”) que
equivalen O a un número entre 0 y 255, o dos números consecutivos entre 0 y 15
9 Son memorias de acceso directo (o, Random Access Memory), que pueden “escribirse” y “leerse” y Memorias de sólo
lectura (o, Read Only Memory), que sólo pueden leerse.
10En los computadores modernos, sin embargo, se acelera el acceso a ciertos lugares de esta memoria por medio del uso
de memorias auxiliares muy rápidas (memorias “CACHE”) en las que su contenido se duplica.
11Ya que por un lado algunos de los medios de almacenamiento son removibles e intercambiables, y por el otro, las
computadoras tienden a estar interconectadas formando redes de distribución de datos a nivel planetario.
Capítulo 1: Evolución de los Lenguajes de Programación
5
1.2.2 El Proceso Computacional.
Al llevarse a cabo la transformación definida en un programa se lleva a cabo un “proceso
computacional” (al que con frecuencia nos referiremos simplemente como el “proceso”).
Un proceso computacional se caracteriza por estar compuesto de un conjunto de procesos
más elementales que ocurren en una sucesión parcialmente ordenada en el tiempo. Los
procesos que componen a otro proceso se suceden formando uno o varios “hilos de
ejecución” que ocurren en “paralelo”, y que se unen o dividen en ciertos instantes del
tiempo.
Cada uno de los procesos que componen otro proceso, se compone a su vez, de un conjunto
de procesos aun más elementales, que ocurren, también, en una sucesión parcialmente
ordenada en el tiempo. Esta descomposición, puede extenderse hasta niveles donde los
procesos componentes son operaciones muy elementales, que ocurren al nivel de los
circuitos internos del procesador. Para efectos de nuestra discusión, sin embargo, no
consideraremos niveles de descomposición, mas allá del que descompone los procesos, en
un conjunto finito de operaciones denominadas “operaciones elementales del
procesador” u “operaciones de maquina”.
Lo que hace interesante a las operaciones elementales del procesador, es que para cada una
de ellas se ha definido una cadena finita de dígitos binarios (unos y ceros) denominada
“instrucción del procesador” (o “instrucción de máquina”), que la representa de forma
digital. Una secuencia de instrucciones de máquina, puede entonces, representar una
secuencia de operaciones elementales del procesador, que bien podría constituir un hilo de
ejecución en un proceso. El procesador tiene, además, la capacidad de ejecutar el hilo de
proceso representado por una de tales secuencias, siempre y cuando ella se encuentre
almacenada en la memoria de acceso directo (RAM o ROM).
1.2.3 La Máquina de Estados.
Al irse ejecutando las instrucciones de un programa, se producen, tanto ingresos de datos al
computador, como creación interna de nuevos datos a causa de los cálculos. Estos datos
son colocados y recolocados, en los diferentes lugares de almacenamiento a medida que
aparecen y que se manipulan. El proceso correspondiente a cada instrucción de procesador
produce, en efecto, un cambio específico sobre los datos almacenados. Este cambio es lo
que caracteriza a la instrucción, y lo que la distingue de las demás12.
Así, a medida que el proceso avanza, las memorias del computador atraviesan por una serie
de situaciones discretas o "estados", caracterizados por la información almacenada. Al
iniciarse el proceso, las memorias tienen un estado predeterminado, que va cambiando a
medida que nuevos datos entran y se calculan. Al finalizar el proceso, la sucesión de
estados inducida por el programa, debe haber producido, almacenado y posiblemente
“escrito” en algún dispositivo de salida, los resultados del proceso.
Lo interesante de esta visión de los procesos, es que una manera de caracterizarlos, a
cualquier nivel de descomposición, es caracterizando el cambio en el estado de los
dispositivos de almacenamiento que ellos inducen en la máquina. Tal como se verá más

12De no existir este efecto, la instrucción no llevaría a cabo nada. La única excepción son las instrucciones de salida,
cuyo efecto ocurre en el "exterior" de la máquina.
Capítulo 1: Evolución de los Lenguajes de Programación
6
adelante, la definición de los programas (y de sus partes), se fundamentará, ya sea en
definir de forma explícita los procesos elementales que componen a los procesos macro que
determinan, o en definir de forma implícita el cambio global de estado producido por
dichos procesos macro.

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 hilo proceso puede
representarse por una sola secuencia de instrucciones de máquina, y toda secuencia de
instrucciones de máquina determina un hilo 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 denominaremos “lenguaje de máquina”, 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
Capítulo 1: Evolución de los Lenguajes de Programación
7
instrucciones de máquina requiere de conocimientos relativos a la arquitectura específica de
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 al usuario y a los demás programas 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 BIOS13. 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 (“compiladores” e “intérpretes”) :
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

13No implica esto que no pueda utilizar directamente las instrucciones de máquina, sino que utiliza las de la BIOS para
aquellas cosas que ya están programadas allí.
Capítulo 1: Evolución de los Lenguajes de Programación
8
lenguaje muy especializado y lleno de los "tecnicismos", y le que posibilitan el uso de un
lenguaje muy cercano al del contexto del problema.
 Aplicaciones: Implementan las operaciones que el usuario usa para darle solución a sus
problemas. Ellos interactúan con el usuario, de formas que pueden ser muy simples o
muy complejas dependiendo de la manera como se hayan implementado las operaciones.
En esta interacción, el usuario solamente hace referencia a los elementos involucrados
en las operaciones (que son los involucrados en el problema), por lo que las aplicaciones
implementan el nivel de comunicación usuario máquina más especializado y el de más
alto nivel posible.

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

14 En http://www.csci.csusb.edu/dick/samples/languages.html pueden verse una recopilación de lenguajes de


programación, con enlaces web a descripciones más extensas de cada lenguaje. En
http://www.levenez.com/lang/history.html se presenta gráficamente las fechas de aparición, evolución y relaciones entre
un conjunto de lenguajes relevantes.
15En http://en.wikipedia.org/wiki/Category:Programming_language_classification se presentan documentos relacionados
con la clasificación de los lenguajes de programación.
16 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 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”).

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 se agrupan 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 ejecutado17. 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 programa18. 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áquina19 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
dichos registros, y, por último, llevara a la RAM los resultados contenidos en los registros.

aparecen lenguajes que facilitan elaborar elementos ejecutables que se transmiten por la red y se interpretan por los
“navegadores” (JAVA, PEARL, etc..), al usarse el computador para el control de procesos en tiempo real, aparecen
modelos para la gestión y razonamiento sobre el tiempo; al querer que el computador se comporte como un ser humano,
aparecen modelos para representar el razonamiento, los propósitos, las preferencias y la colaboración.
17 Aunque esto es una función del sistema operativo, vale la pena mencionar que, en computadores antiguos (Vg.
IBM1130) era posible definir cada instrucción con un grupo de 16 palancas e ingresarlas a la máquina presionando un
botón.
18 Tiene una memoria que almacena la dirección de la instrucción que está ejecutando.
19Aumente la dirección de la instrucción en ejecución en el tamaño de la “palabra del procesador” que es un valor
constante y el tamaño de todas las instrucciones de máquina.
Capítulo 1: Evolución de los Lenguajes de Programación
10
Nótese que este tipo de instrucciones de máquina son las que generan una secuencia de
estados en la RAM.
De especial importancia, son las instrucciones de máquina que cambian la dirección de la
instrucción siguiente a ser ejecutada por otra diferente. Ellas le permiten al procesador
ejecutar “saltos” sobre las instrucciones del programa, por lo que ellas no necesariamente se
ejecutan siempre en la secuencia en que aparecen.
Los saltos sobre el programa pueden ser, además, optativos según valores previamente
calculados. Estos “saltos bajo condición” le permiten al programa adaptar la secuencia de
ejecución a los datos del caso en ejecución, ampliando así el conjunto de casos para los
cuales el programa opera correctamente.
Por otro lado, los saltos durante la ejecución de un programa, permiten contar con grupos
de instrucciones que “implementan” servicios específicos que pueden ser utilizados desde
diversos programas y lugares en un programa. A estos grupos de instrucciones se les ha
denominado “subprogramas”. Para utilizar o “evocar” un subprograma basta con saltar a su
grupo de instrucciones desde el lugar donde se requiera el servicio, ejecutar las
instrucciones del subprograma, y “retornar” al lugar original con otro salto hacia atrás20.
Los subprogramas pueden hacer o no parte del programa que los usa y estar incluso “micro
codificados” en memorias de alta velocidad dentro del procesador.
1.5.2 Segunda Generación: Lenguaje Ensamblador.
El primer gran salto adelante sobre el lenguaje de máquina, es la aparición de los programas
“ensambladores” que dan lugar a la programación en “lenguaje ensamblador” (ó
“assembler”).
En un lenguaje ensamblador las componentes de las instrucciones de máquina son
substituidas por símbolos mnemotécnicos21.

Una instrucción de máquina en un procesador de 32 bits, para indicar que se copie el contenido de un
registro del procesador en otro, podría usar una secuencia de dígitos binarios, donde los primeros ocho
dígitos indican que la instrucción es una de mover datos, los segundos ocho dígitos indican cual el
registro fuente de la información, los terceros ocho dígitos indican cual es el registro destino de la
información, y los últimos ocho dígitos no tienen significado alguno. Así, en lenguaje ensamblador esta
instrucción podría lucir de la manera siguiente:

10110000100000000100000000000000
La correspondiente instrucción en lenguaje ensamblador podría lucir de la manera siguiente:

mov %r1 %r2

20 Usando la dirección del lugar del programa que llevo a cabo la evocación, que debió haberse almacenado antes del
salto.
21 Ver http://en.wikipedia.org bajo “Assembly Language”.
Capítulo 1: Evolución de los Lenguajes de Programación
11
Algunas de las características más relevantes de un lenguaje ensamblador son las
siguientes:
 Los lugares de almacenamiento que usa el programa (en la RAM o dentro
del procesador) pueden ser referidos por medio de rótulos o “nombres de
variable”. Las variables deben ser definidas en el programa antes de ser
utilizadas, asociándolas con un tamaño de memoria definido22. Es
responsabilidad del traductor y de los programas de apoyo la asignación de
direcciones reales para dichos lugares. Esto le evita al programador tener
que relacionarse directamente con la estructura de direcciones físicas de la
máquina.
 Un símbolo de operaciones no tienen que representar siempre a una
operación específica del lenguaje de máquina. Es posible que este símbolo
represente una operación compleja que debe llevarse a cabo por con varias
instrucciones de máquina. En este caso el ensamblador puede ya sea llevar a
cabo la substitución de la operación referida por las instrucciones de
máquina que le corresponden (que deben haber sido previamente definidas o
“compiladas”), en cuyo caso el operador constituye una “macro”; o incluir
una evocación al subprograma que le corresponde (que debe haber sido
previamente elaborado), en cuyo caso el operador constituye un
subprograma.
A pesar de ser significativo el avance del lenguaje ensamblador en relación con el lenguaje
de máquina, este sigue estando íntimamente asociado al procesador y a las características
propias del computador objetivo. Por ello los programas en lenguaje ensamblador son poco
transportables entre máquinas y entre versiones del sistema operativo de la máquina.
1.5.3 Tercera Generación: Lenguajes Procedurales.
Con la aparición de los lenguajes de tercera generación, la programación de computadoras
deja de ser un oficio propio de los expertos en el “hardware23” o constructores de máquinas,
y se convierte en un oficio independiente. En efecto, la gran mayoría de lenguajes en uso
actualmente son básicamente lenguajes de tercera generación.
Al igual que los lenguajes de la 1ª y 2ª generación, los lenguajes de la 3ª se orientan a
especificar el proceso computacional, visto como la ejecución de una (o varias) secuencias
de instrucciones que modifican el contenido de la memoria.
Ellos ofrecen, sin embargo, mecanismos nuevos y poderosos para definir los cálculos y
controlar el curso del proceso. En los numerales siguientes introduciremos brevemente
dichos mecanismos.
1.5.3.1 Independencia de la Máquina.
Los lenguajes de tercera generación son concebidos con independencia de la máquina. Así,
Para usarlos el programador no necesita conocer la arquitectura de la máquina, en cuanto a
la estructura de las direcciones de memoria, conexión de periféricos, naturaleza de las
instrucciones del procesador etc...

22 Y posiblemente restringido a ciertos tamaños preestablecidos.


23 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 es, por tanto
específico a una familia particular de máquinas.
Por otro lado, la estandarización de los lenguajes de 3ª generación24, 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.
1.5.3.2 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 del á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 (v.g. 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

24 Ver por ejemplo la página web de la ANSI (American National Standars Institute) en http://web.ansi.org.
Capítulo 1: Evolución de los Lenguajes de Programación
13
diferentes conjuntos de instrucciones de máquina requeridos para lleva a cabo cada acción,
y los va incorporando en el programa objeto, a medida que las acciones son referidas en el
programa fuente25.
1.5.3.3 Instrucciones de asignación y de I/O.
El proceso computacional es especificado en los lenguajes de tercera generación, por medio
de secuencias de instrucciones de “asignación” y de “Entrada/Salida” (“I/O”).
Una instrucción de asignación indica la colocación del valor representado por un término
en el lugar de la memoria representado por ya sea por una variable o por una dirección de
memoria (contenida en otra variable).
Una instrucción de Entrada/Salida de datos, indica un intercambio de valores entre los
dispositivos físicos de Entrada/Salida y los lugares de la memoria.
Las instrucciones de asignación y de Entrada/Salida de un programa, se ejecutan en una
secuencia determinada por las instrucciones de “secuenciamiento”. A medida que se
ejecutan dichas instrucciones, la memoria del computador va pasando por una serie de
estados desarrollándose así un proceso computacional.
1.5.3.4 Instrucciones de Secuenciamiento.
Las instrucciones de secuenciamiento determinan la secuencia en que se ejecutan las
instrucciones de asignación y de Entrada/Salida.
Existen en general dos tipos de instrucciones de secuenciamiento, las que ordenan las
instrucciones de forma temporal, y las que indican saltos.
Las instrucciones de asignación y de Entrada/Salida se ordenan, colocándolas una tras otra
separadas por un caracter especial (vg. el caracter “;”, el caracter “.”, o simplemente el
caracter que indica un cambio de línea). El caracter que separa las instrucciones indica
además si ellas se ejecutan en serie o en paralelo. Cuando las instrucciones se ejecutan en
serie, la instrucción que sigue debe esperar a que la anterior termine antes de comenzar a
ejecutarse. Cuando las instrucciones se ejecutan en paralelo, la instrucción que sigue puede
ejecutarse sin esperar a que la anterior haya terminado.
El salto bajo condición del assembler tiene su correspondiente en la mayoría de los
lenguajes de tercera generación (vg. el “if(<condicion>) go to <instruccion>”), e
inicialmente fue la única instrucción de salto disponible.
Las versiones más modernas de los lenguajes de 3ª generación implementan, sin embargo,
las “formas estructuradas de secuenciamiento” (ver Capítulo 2). Estas son básicamente las
siguientes:
 Selección: Indican seleccionar entre dos o más grupos alternativos de
instrucciones con base en el valor de uno o varios términos.
 Repetición: Indican repetir una y otra vez un grupo de instrucciones
terminando la repetición cuando un término toma un cierto valor al ser
calculado.

25 A los traductores de los lenguajes se les denomina “compiladores” o “intérpretes” según el momento en que incorporan
al programa traducido las instrucciones de máquina correspondientes a los elementos del programa.
Capítulo 1: Evolución de los Lenguajes de Programación
14
 Rompimiento: Indican abortar un ciclo de repetición sin ejecutar
completamente el grupo de instrucciones, cuando se cumple una situación
específica en el proceso.
1.5.3.5 Subprogramas y Librerías.
Los lenguajes de tercera generación no sólo acogen el concepto de subprograma, de los
lenguajes de 2ª generación, sino que lo extienden, para estandarizar las formas de
intercambiar datos entre el programa o subprograma que evoca, y el subprograma evocado.
Esta estandarización incluye tanto definir las posibles formas de intercambio de datos (vg.
“por valor” o “por referencia”), como controlar que los datos intercambiados cumplan las
condiciones esperadas por el evocador y el evocado (vg. evitar el intercambio de valores
con tipo incorrecto ya sea rechazando la evocación o transformando los datos
intercambiados).
Una característica importante de los lenguajes de 3ª generación es su facilidad para crear y
utilizar librerías de funciones y tipos definidos, impulsando el intercambio de código entre
programadores y programas. Es así, que al elaborar un programa en un lenguaje de tercera
generación moderno, el programador sólo tiene que escribir un porcentaje reducido del
mismo, estando la mayor parte del programa constituido por código preelaborado.
1.5.4 Cuarta Generación: SQL y Lenguajes Declarativos.
Los lenguajes de 3era generación introdujeron el término o fórmula para indicar o declarar
que el valor resultante de la aplicación de una o varias funciones a datos elementales es
requerido por una instrucción del programa. Lo importante de esta declaración es que el
intérprete es quién se encarga de definir el proceso computacional por medio del cual se
llevan a cabo los cálculos, sin que este proceso esté descrito en el programa. La
importancia de evitar describir este proceso se manifiesta en diversos lenguajes avanzados
de programación.

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.

Los lenguajes que evitan describir de forma explícita el proceso computacional, por medio
de instrucciones de asignación, Entrada/Salida e instrucciones de Secuenciamiento, fueron
denominados como lenguajes de “cuarta generación” [Martin 85]. En estos lenguajes el
programador describe lo QUE desea obtener del programa absteniéndose de definir COMO
debe éste proceder para obtenerlo.

Las hojas de cálculo, tales como LOTUS o EXCEL pueden considerarse como ejemplos de lenguajes de
4ª generación, en cuanto, al definir un programa el usuario usualmente se limita a incluir fórmulas en las
“celdas” y a definir por medio de editores especializados los reportes y las formas de ingreso de datos.
Capítulo 1: Evolución de los Lenguajes de Programación
15
Dado que los gestores de “bases de datos” generalizan las operaciones de organización
acceso y presentación de la información que manipulan, en ellos aparecieron rápidamente
lenguajes especializados, que por limitarse a declarar lo QUE desea obtener, pueden
considerarse de 4ª generación. Entre ellos están lenguajes orientados a describir los datos y
la forma como se organizan y relacionan (o “Data Definition Languages”), lenguajes
especializados en describir consultas sobre los datos (o “Query Languages”) y lenguajes
orientados a describir la forma y contenido de los reportes y pantallas (o “Report
Generation Languages”).

Los lenguajes SQL, RPG, NOMAD y FOCUS que se apoyan en líneas de texto para describir la
estructura y composición de los datos que manipulan. El lenguaje SQL, por su parte, implementa un
modelo de consulta sobre los datos apoyándose en los conceptos de la teoría de conjuntos, el álgebra
relacional y el cálculo de predicados.

Una tendencia actual es la de dotar a los lenguajes de 3ª generación con poderosos medios
ambientes de desarrollo que ofrecen herramientas especializadas para llevar a cabo la
descripción de los diferentes aspectos del programa. En particular es usual que la
definición de interfaces complejos del programa con el usuario, se lleven a cabo por medio
de editores interactivos de menús, formas de dialogo y reportes, que crean una
representación visual del interfaz deseado, dejando al medio ambiente con la
responsabilidad de generar automáticamente el código procedural correspondiente.
El uso de lenguajes especializados para la especificación de los diferentes tipos de
elemento, sin embargo, establece de por sí un fraccionamiento de la especificación
encapsulando las partes en su propio ambiente lingüístico. A la necesidad de usar diversos
lenguajes en la elaboración de una pieza de software se le ha denominado “desacoplo de
impedancia” [Canos 97].
1.5.5 Quinta Generación: Uso de la Lógica.
Si bien en los lenguajes de 4ª generación ya aparece la tendencia a usar fórmulas
matemáticas como medio para describir, de forma declarativa, las tareas que debe ejecutar
el computador. El uso exclusivo de fórmulas, aserciones y ecuaciones y fórmulas bien
formadas de una lógica matemática, como medio para especificar el programa, constituye
un avance significativo, que, aquí incluiremos en la 5ª generación26.
En los lenguajes de 5ª generación se describen las características de los elementos de un
área de aplicación por medio de aserciones (o restricciones) que deben satisfacer. Un
conjunto de datos que describa una configuración particular (o “instancia”) de los
elementos del área de aplicación, puede satisfacer o no las restricciones planteadas.
Cuando la instancia satisface las restricciones planteadas, se denomina un “modelo” de las
restricciones.

26 wikipedia.org/wiki/Fifth-generation_programming_language.
Capítulo 1: Evolución de los Lenguajes de Programación
16
Dado una instancia que es modelo de las restricciones, en un lenguaje de 5ª generación, es
posible resolver automáticamente consultas pertinentes a los elementos de la configuración
particular que describe. La resolución de estas consultas se lleva a cabo, por medio de
procesos automáticos de demostración en el marco de la lógica utilizada.
Una utilidad adicional de estos lenguajes es su capacidad de fundamentar “editores” de
instancias del área de aplicación que garanticen la satisfacción de las restricciones
planteadas.
En los lenguajes “funcionales”27 se describen las propiedades de conjuntos de elementos y
de operadores definidos sobre dichos elementos, por medio de una teoría matemática en
una lógica ecuacional. Un “intérprete” de la teoría así descrita, se usa para llevar a cabo el
cálculo de términos base28 construidos con los operadores definidos.
En los lenguajes “lógicos”29 se describen las propiedades del área de aplicación por medio
de de formas clausales de la lógica de predicados (en particular “cláusulas de Horn”). Si se
cuenta con un “modelo” de dichas fórmulas (los datos que describen el área de aplicación),
es posible someter al intérprete aserciones clausales para que sean verificadas en el modelo.
Estas aserciones pueden contener variables que son valoradas en el proceso de verificación,
convirtiéndose en un medio de consulta.
Es importante anotar que, si bien los programas escritos en lenguajes de 5ª generación, son
teorías en una lógica matemática, no todas las teorías en lógica matemática constituyen
programas. Esto se debe a que los intérpretes de la teoría sólo operan adecuadamente para
teorías que satisfacen ciertas condiciones, en ocasiones bastante restrictivas. Las consultas,
por otra parte, no siempre siguen un proceso eficiente, por lo que los programas no siempre
son “mejores” que los escritos en lenguajes de otras generaciones anteriores.
La tendencia actual es la, primero, de usar estos lenguajes para entender las propiedades del
área de aplicación y crear prototipos de los programas requeridos; y luego, por
“refinamiento” de estos prototipos se obtienen los programas finales requeridos,
posiblemente en lenguajes más eficientes que los de 5ª generación.
El fortalecimiento de los intérpretes de los lenguajes de 5ª generación es, aun, motivo de
estudio e investigación.

Resumen del Capítulo.


La necesidad de contar con más y mejor software, y el costo asociado a su construcción, ha
generado la denominada “crisis del software”. Esta, a su vez, ha impulsado el desarrollo
acelerado de tecnologías de desarrollo en tres ejes fundamentales: los lenguajes, las
arquitecturas y los sistemas de control al desarrollo.
El computador como máquina que ordena datos y efectúa cálculos, es un transformador de
datos en información. La trasformación se lleva a cabo en el computador, por la acción
conjunta de los elementos que lo componen, a saber: los dispositivos de entrada/salida

27 wikipedia.org/wiki/Functional_programming.
28 Expresiones que no tienen variables.
29 wikipedia.org/wiki/Logic_programming
Capítulo 1: Evolución de los Lenguajes de Programación
17
permiten ingresar los datos al computador y entregar la información resultante, los
diferentes tipos de memoria permiten almacenar los datos y resultados con diferentes
tiempos de permanencia, y los dispositivos de procesamiento se encargan de llevar a cabo
las acciones internas de cómputo y ordenamiento.
A medida que se llevan a cabo el ingreso de datos, los cálculos y los ordenamientos, la
información contenida en la memoria va cambiando con el tiempo en lo que se denomina el
“proceso computacional”. Este proceso se compone de procesos más elementales que
ocurren en una sucesión parcialmente ordenada en el tiempo. Estos procesos más
elementales, a su vez, pueden descomponerse en procesos aún más elementales, y estos
últimos en otros más elementales, hasta llegar a un conjunto finito de operaciones atómicas
denominadas “operaciones de máquina”.
El proceso computacional es, sin embargo definido por el usuario por medio de
“programas”. En efecto, las operaciones más elementales se corresponden con cadenas de
dígitos binarios denominadas “instrucciones de máquina”. Una secuencia de dichas
cadenas representa un proceso computacional y constituye, en consecuencia, un programa
(en “lenguaje de máquina”). Para definir la transformación que efectúa el computador,
basta con “cargar” el programa que la representa en la memoria, por lo que para cambiar de
transformación es suficiente con cambiar el programa utilizado.
Por ser las instrucciones de máquina muy elementales, el lenguaje de máquina no es
apropiado para indicarle al computador que lleve a cabo acción alguna, ni mucho menos
para escribir programas de alta complejidad. Para aliviar estos problemas los computadores
ofrecen programas preelaborados que apoyan al usuario, y a los demás programas, en las
tareas de control de la máquina, asignación de recursos, manipulación de archivos,
elaboración y ejecución de programas, control de dispositivos, etc... Entre ellos están los
que constituyen la BIOS encargados de interactuar con los dispositivos físicos; los que
constituyen el sistema operativo (SO) encargados de las tareas de administración de los
recursos y comunicación con los usuarios; los traductores e intérpretes que se encargan de
traducir programas escritos en lenguajes avanzados al lenguaje de máquina; y, por último,
las aplicaciones de usuario final que son las encargadas de llevar a cabo las tareas que
hacen del computador un dispositivo útil.
Los lenguajes en los que se escriben los programas han evolucionado; desde el lenguaje de
máquina mismo (primera generación) a los lenguajes ensambladores (segunda generación),
que usan símbolos mnemotécnicos para referirse tanto a conjuntos de instrucciones de
máquina (previamente “compiladas”) como a los lugares de la memoria; desde los
lenguajes ensambladores a los lenguajes de tercera generación, que usan elementos de la
matemática para indicar los cálculos y movimientos de los datos en memoria junto con
“instrucciones de control” para definir la secuencia en que se efectúan dichos cálculos y
movimientos; desde los lenguajes de tercera generación a los de cuarta generación, que
pretenden definir los programas describiendo el resultado de los procesos que determinan;
y, por último, desde los de cuarta generación a los de quinta generación, que se
fundamentan el la lógica matemática para describir de forma precisa el resultado de los
procesos y la relación de los datos con los resultados.
Capítulo 1: Evolución de los Lenguajes de Programación
18
En el marco de los lenguajes de quinta generación los programas son teorías definidas en
una lógica matemática, y la ejecución de los programas son procesos de demostración en el
marco de dicha lógica
Capítulo 2
Evolución de los paradigmas
arquitectónicos
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
20
Introducción
En el capítulo anterior se argumentó que la tendencia en el desarrollo de los lenguajes de
programación, apunta a la concepción de lenguajes cada vez más capaces de describir el
resultado del proceso computacional, en contraste con lenguajes orientados a describir el
proceso computacional mismo. En el marco de esta tendencia se destacó el uso de la lógica
matemática como lenguaje de programación, donde los programas son teorías definidas en
una lógica y la ejecución de los programas son procesos de demostración en el marco de
dicha lógica.
Por otro lado, en el marco de los paradigmas de gestión modernos no se prescribe iniciar
un proyecto de desarrollo de software llevando a cabo de inmediato la codificación de los
programas. Se prescribe más bien un “pensar antes de actuar”, para darle una razón de ser
junto con una adecuada modularización (o “arquitectura”) al software, en concordancia con
las necesidades y arquitectura del área de aplicación.
Esto usualmente se traduce en que el proceso de desarrollo se divide en las fases siguientes:
modelamiento del área de aplicación o “análisis”, caracterización y estructuración del
software o “diseño”, codificación del software o “construcción”, verificación del software o
“prueba”, y puesta en operación o “implantación”. Este ciclo de fases suele repetirse de
forma reiterada y cíclica en una secuencia de etapas de desarrollo, que generan de forma
progresiva el software30.
Durante cada una de las fases del proyecto se deben elaborar documentos y modelos: Estos
modelos van desde unos que describen fundamentalmente el área de aplicación hasta otros
que describen completamente el software. La tendencia en este proceso es la de usar
lenguajes similares en todas las fases para lograr que la arquitectura del software coincida
con la arquitectura del problema” [Booch 96]. Así, la diferencia fundamental entre los
modelos de las fases de análisis, diseño y programación, es que en las primeras el modelo
no tiene necesariamente que ser ejecutable, o, de serlo, no necesariamente tiene que ser
eficiente.
Para describir de forma adecuada el área de aplicación debe ser posible, además, incorporar
en la especificación del software, los diferentes tipos de conceptos31 con los que se describe
el área de aplicación en lenguaje natural. A estos “tipos” de conceptos los denominaremos
en adelante “categorías conceptuales”32. Los diferentes tipos de construcción que ofrecen
los lenguajes de programación permiten representar entes de las diferentes categorías. Así,
por ejemplo, las variables permiten representar propiedades, los subprogramas permiten

30 Se uso el término “fase” con el objeto de distinguirlo de las “etapas” del proyecto. Estas últimas constituyen una
división temporal del proyecto orientada a obtener paulatinamente resultados tangibles y verificables: En un desarrollo
“en cascada” las etapas coinciden con las fases, y el software se construye sólo al final del desarrollo; en contraste, en un
desarrollo “ágil”, las fases se llevan a cabo de forma repetitiva y cíclica para obtener el software de forma incremental.
31Consideraremos, por ejemplo los conceptos casa, habitación, silla etc.., como miembro de un tipo de concepto, el tipo
de los “objetos físicos”.
32 Las categorías vistas como un sistema de clasificación de las “cosas”, o de las “diferentes formas de ser” de la tradición
aristotélica, nos permite clasificar los conceptos del lenguaje. Por ejemplo las “categories of being” referidas en
http://en.wikipedia.org/wiki/Category_%28philosophy%29 , son las siguientes: objetos físicos, objetos mentales, clases de
objetos, propiedades, relaciones, espacio y tiempo, aserciones, y eventos.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
21
representar procesos y eventos, las estructuras permiten representar relaciones de
composición entre propiedades, las instrucciones de secuenciamiento permiten representar
relaciones de composición entre funciones y eventos etc..
Al conjunto de categorías conceptuales que se pueden usar en un lenguaje lo
denominaremos el “paradigma arquitectónico”, entendido como el modo de comprender y
modularizar el área de aplicación y el software. En este capítulo examinaremos un
conjunto de paradigmas arquitectónicos vistos en el orden de su aparición histórica. La
progresión de estos paradigmas, en opinión del autor, representa un proceso evolutivo que
va incorporando, cada vez, más categorías conceptuales al lenguaje, sin que la aparición de
un nuevo paradigma substituya o elimine al anterior.
Desde el punto de vista de la lógica, la aparición de nuevos paradigmas impulsa la
necesidad de usar lógicas cada vez más complejas, que den cuenta de dichas categorías de
conceptos y de los procesos de razonamiento que involucran los conceptos de estas
categorías.
La clasificación de las lógicas y el estudio de los lenguajes lógicos que de cada tipo de
lógica se derivan, le da la estructura fundamental a este trabajo. Así, el trabajo se divide en
partes. En cada parte se presenta un tipo de lógica para luego estudiar en profundidad los
lenguajes que, de uno u otra forma, implementan o soportan a dicha lógica.

Paradigma de Instrucciones.
Muchos programas fueron concebidos sin tener en cuenta noción alguna de modularización
o arquitectura. Por lo tanto, el nivel básico de granularidad de los modelos lo constituyen
las instrucciones del lenguaje de programación, en particular los de 3era generación (y
hacia atrás).
Los modelos visuales usados en el marco de este paradigma (v.g. diagramas de la “lógica”
de la aplicación o “flow charts” usados en la programación FORTRAN y COBOL
[McCracken 64]), son representaciones más legibles del ensamblaje de instrucciones del
lenguaje que constituye un programa.
En el marco de este paradigma no se utiliza formalismo lógico alguno, por lo que carece de
interés en el marco de éste trabajo.

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

33 Ver http://en.wikipedia.org/wiki/Structured_programming y http://en.wikipedia.org/wiki/Structured_analysis


Capítulo 2: Evolución de los Paradigmas Arquitectónicos
22
2.3.1 Especificación de Funciones.
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 los valores de un conjunto de “datos” (dominio de la función), en valores de un conjunto
de “resultados” (rango de la función). A cada función se le asocia, además, un símbolo que
la representa, y que es usado para referirse al valor del rango que le corresponde a un valor
específico del dominio. A los símbolos que representan las funciones nos referiremos en lo
que sigue como un “operador”, y, tanto a la aplicación de la función a un valor del dominio
como a la especificación misma de la función la denominaremos, una “operación” 34.

Función

Dominio Rango

Para especificar las diferentes operaciones se utilizan lenguajes que van desde el natural,
hasta formalismos lógicos, así:

 Lenguaje natural: Es el punto de partida de toda especificación, y se usa


para verbalizar la transformación que lleva a cabo cada operación. La
contundencia de dicha verbalización garantiza la calidad de cualquier tipo de
especificación.

Serían definiciones adecuadas Serían definiciones


las siguientes: inadecuadas las siguientes:

“calcula el sueldo del “lleva a cabo primero tal


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

34En un sentido estricto, el “operador” es un elemento sintáctico y la “función” es el elemento semántico que representa.
Usaremos sin embargo de forma indistinguida los términos “operación” y “función” en el texto que sigue.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
23
 El seudo código o “mini especificaciones (IBM)”35 describen las
operaciones usando una mezcla de lenguaje natural y lenguaje de
programación. Se centran principalmente en indicar, la forma como se
repiten secuencian y seleccionan en ejecución, los grupos de instrucciones
elementales que componen la operación.

El seudo código para el programa que calcula la raíz podría ser como sigue sería el siguiente:
Lea x,y,dx
Hasta que |x2-y| dx
cambie x por ((y/x)+x)/2

 Especificaciones formales: Dado que la función es un concepto


matemático, es fácil describirla en términos de una lógica matemática de
primer orden. Por medio de la lógica de predicados y, en particular, por
medio de la lógica de la igualdad, se pueden especificar las funciones ya sea
de forma explícita, obteniendo especificaciones ejecutables, o de forma
implícita, que no necesariamente conducen a especificaciones ejecutables.
Son ejemplos de este enfoque la notación VDM36 [Jones 90] y la notación
Z37 [Woodc 96]. Los lenguajes funcionales y lógicos, que constituyen el
tema central del primer tomo de este trabajo, no son otra cosa que
especificaciones ejecutables en lógica de primer orden.

El cálculo del disponible en una cuenta bancaria, se puede describir por medio de la siguiente
especificación formal explícita:

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

Una especificación implícita formal por restricciones de la función raíz cuadrada de un número real (que
no facilita el cálculo de la misma), es la siguiente:

 x  y y=raiz(x)  y2=x


Donde:
“y” es un elemento del rango.
“” representa el conjunto de los números reales.
“x” es un elemento del dominio.

35 Ver:
http://publib.boulder.ibm.com/infocenter/rsysarch/v11/index.jsp?topic=%2Fcom.ibm.sa.process.doc%2Ftopics%2Fc_Min
ispecs_what_how_use.html
36 Ver: http://en.wikipedia.org/wiki/Vienna_Development_Method
37 Ver: http://en.wikipedia.org/wiki/Z_notation
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
24
Una especificación explicita formal de la función raíz cuadrada de un número real (que hace posible el
cálculo de la misma), es la siguiente:

sqrt(y,x,dx) = { if (| x – y * y | < dx ) x else sqrt((x/y+y)/2, x, dx) }

2.3.2 Arquitectura funcional.


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 (v.g. 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” [Dijkstra 70], restringen la
activación de las operaciones componentes a patrones específicos de coordinación para
facilitar la elaboración de las especificaciones.
Para especificar la forma como se asocian las operaciones (de más bajo nivel) para
componer otras operaciones (de más alto nivel), se utilizan principalmente gráficos y
“seudo-código”, [IBM 74], [Yourdon 79] así:
 El diagrama de composición jerárquica de funciones (DHF) 38, especifica la
estructura de composición de las operaciones, indicando que operaciones son
componentes de otras operaciones.

F1 F2

F11 F22

38 Ver: http://www.hit.ac.il/staff/leonidm/information-systems/ch64.html
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
25
 El diagrama de Flujo de Control (DFL)39, describe la forma como se
secuencian, repiten, y seleccionan para ejecución, las diferentes operaciones
componentes, al llevarse a cabo la ejecución de la operación que componen.

P
P1
?

P11 P12

P2

P21

 Los diagramas de Flujo de Datos (DFD), describen la manera como se


intercambian los datos entre las componentes de una operación, indicando
que operaciones reciben como datos los valores que otras operaciones
producen como resultados. Son ejemplos de ellos el diagrama el diagrama
IBM IPO40, los diagramas con la notación de Yourdon/DeMarco 41, y los
diagramas con “swim lines” 42

Una transacción que crea una nueva reserva presupuestal para un gasto, puede especificarse en un
diagrama IPO de de la forma siguiente:

39 O “flow chart”. Ver: http://en.wikipedia.org/wiki/Flowchart y http://www.rff.com/structured_flowchart.htm


40 Ver: http://www.hit.ac.il/staff/leonidm/information-systems/ch64.html y http://en.wikipedia.org/wiki/IPO_Model
41 Ver: http://www.philblock.info/hitkb/c/creating_data_flow_diagrams_yourdon.html y
http://en.wikipedia.org/wiki/Data_flow_diagram
42 O “functional bands” y “BPMN Notations” Ver http://www.bpmn.org/
http://www.agilemodeling.com/style/activityDiagram.htm , http://en.wikipedia.org/wiki/Swimlane y
http://en.wikipedia.org/wiki/Business_Process_Model_and_Notation
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
26

El proceso de compra en un almacén puede especificarse en un DFD bajo la notación de Yourdon de la


forma siguiente:

Ítems solicitados
Cliente Buscar
ítems en
almacén
Datos Ítems
Cobro Despachados Cliente

Remisión

Factura
Registro
Cobro pago
Entrega Salida almacén
Ítems
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
27

El proceso de gestión de los trabajos de grado de los estudiantes de ingeniería en la Universidad, puede
describirse por medio de un DFD con “swimlines”, así:

Solución propuesta

Necesidad de Consulta la Oferta d


conocer demanda demanda de propuestas
GI

Evaluación
de propuestas propuestas de TDG Base de 9 del DTDG
Propuestas

7
Base de Copia Informe
Realiza pre-registros Consulta Formulario Modifica Plan Realiza Evalúa al Entrega copia
Desea conseguir Elabora Plan Elabora el Sustenta el Final del TDG
pre-registro propuestas del Plan 3 registro del Director del del Informe
E

propuesta de TDG inicial de TDG inicial TDG TDG


de TDG disponibles de TDG TDG TDG Final
5
a 1 2 4

No 1 Aprueba el Plan
Publica la 2 Formulario
Estudia y El Plan de TDG de TDG y Presenta
CAPC

Al inicio de cada programación diligenciado


evalúa el Plan cumple con Si diligencia el reporte sobre e
semestre de eventos requerimientos
de TDG formulario en sus d los TDG Reporte
del TDG
apartes b de TDG
a 6

Diligencia el Formulario
formulario de archivado Nombra el Jurado
DE

TDG
nombramiento del Calificador del
Director del TDG
3 finalizado
TDG
b

Informe Final
DTDG

Establece un del TDG


Elabora el
Horario de
TDG
Asesoría

No 7
Estudia y Evalúa la Diligencia el Acta Especial de
No 5
evalúa el El TDG amerita sustentación Acta Especial Calificaciones
JTDG

c Si El Jurado
Informe final sustentación pública del de
Calificador
del TDG TDG Calificaciones Si
aprueba el
Informe Final 4 TDG
evaluado

No

Estudia, aprueba y
e Estudia el Es necesario
CF

modifica acuerdos y
reporte de tomar medidas Si FIN
6 correctivas
reglamentos
TDG
relacionados con TDG

CPC: COORDINADOR DE PROGRAMA CURRICULAR; CAPC: COMITÈ ASESOR DE PROGRAMA CURRICULAR; DE: DIRECTOR DE LA ESCUELA; E: ESTUDIANTE; DTDG: DIRECTOR DEL TDG; JTDG: JURADO CALIFICADOR DEL TDG; GI: GRUPOS DE INVESTIGACIÓN; CF: CONSEJO DE
FACULTAD

FIGURA 3.1 DIAGRAMA DE PROCESOS DE LA SOLUCIÓN PROPUESTA

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
28
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, bajo la óptica del concepto de
“archivo”. El archivo es esencialmente un periférico con el que el programa intercambia
datos43. 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 el
periférico.
2.4.1 Estructuración de los datos en archivos.
El enfoque inicial de los 3GL para los datos, dio lugar a una arquitectura de datos que los
agrupa en archivos separados, compuestos por listas de registros; que a su vez, son
conjuntos de valores organizados en forma de árboles disjuntos.
En este contexto, un registro es un dato compuesto por una serie ítems de dato compuestos
más elementales, y estos en otros más elementales, y así sucesivamente, hasta terminar en
valores “escalares” asociados a un tamaño y representación específica. Para leer o escribir
un registro, un programa debía partir del conocimiento de su estructura y composición.
Este conocimiento se incluía como parte del programa en la forma de una instrucción
“declarativa”. Ejemplos de estas instrucciones son las declaraciones de registros del
lenguaje COBOL, y las instrucciones struct del lenguaje C.

La descripción del registro que contiene la información de un gasto en un programa COBOL, se muestra
a continuación:

01 REG-D.
05 CLAVE-D.
10 SOLICITUD-D.
15 LETRAS-SOL-D PIC X(2).
15 DIGITOS-SOL-D PIC X(5).
10 ARTICULO-D PIC 9(10).
05 IMPUTACION-D.
10 TIPO-REC-D PIC 9.
10 SUBP-D PIC 99.
10 RUBRO-D PIC 99.
10 ORD-D PIC 99.
10 PROG-D PIC 99.
05 DEPCIA-D PIC X(5).
05 IND-SOLICITUD-CUMPLIDA-D PIC XX.
88 SOLICITUD-CUMPLIDA-D VALUE "SI".
05 IND-ESTADO-D PIC XX.

43 El uso de este concepto implica que el programa usará operadores de intercambio de información de o hacia los
dispositivos externos al mismo (operadores de “lectura” y “escritura”). Una alternativa a esta opción es la de localizar
datos del programa en varios “medios ambientes” con diferentes grados de permanencia con respecto a la ejecución del
programa (v.g. la variable SESSION del lenguaje PHP contiene valores disponibles durante las diversas ejecuciones del
programa que componen una “sesión”).
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
29
88 CUMPLIDO-D VALUE "CU".
88 CANCELACION-RESERVA-D VALUE "CR".
88 ACTIVO-D VALUE "AC".
05 DESCRIPCION-D PIC X(60).
05 ELEMENTO-D.
10 TIPO-MOV-D PIC 99.
10 DOCUMENTO-D.
15 LETRAS-DOC-D PIC XX.
15 DIGITOS-DOC-D PIC X(5).
10 FECHA-CAUSACION-D.
15 ANO-D PIC 99.
15 MES-D PIC 99.
15 DIA-D PIC 99.
10 FECHA-ASIENTO-D PIC 9(6).
10 VALOR-D PIC 9(12)

2.4.2 Problemas con los Archivos y Aparición de los Gestores de Bases de


Datos.
Este manejo de la información almacenada en archivos mostró rápidamente serios defectos.
Algunos de estos están ligados a que cuando varios programas requieren los mismos datos,
deben ya sea duplicarlos en archivos diferentes o compartir los mismos archivos, generando
problemas como los que se indican a continuación:

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 se impone a la usada por los programas, 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 puede entonces generar una
“anomalía de inserción”, pues el sistema puede almacenar valores distintos
para el mismo ítem de información.
 Un dato almacenado puede ser el resultado de un cálculo basado en otros
datos también almacenados, sin que necesariamente sea actualizado (o
recalculado) cuando los otros lo son.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
30
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 para los
mismos (quedando dependientes de la visión de cada programador).
 No es posible tener sistemas que permitan acceso a los datos por múltiples
usuarios.
 Es difícil llevar a cabo consultas no planeadas ya que estas deben ser parte
de los programas existentes.

A la solución de estos problemas se orientaron los programas encargados de los datos o


“gestores de bases de datos44” que aplican de forma uniforme diversas estrategias para
solucionarlos, entre ellas las siguientes:
 Separar la parte de los programas que llevan a cabo los cálculos y el
despliegue de los resultados, de la parte que especifica y controla los datos
almacenados.
 Crear un único programa “gestor de la base de datos” (o DBMS45)
especializado en dichas tareas, que incorpore la descripción de los datos
almacenados, garantice el mantenimiento de las relaciones de dependencia
entre los mismos e implemente los procesos de consulta, actualización y
control de acceso.
 Proveer un mecanismo de comunicación entre el DBMS y los demás
programas por medio de un interfaz lingüístico, que les permita a los
programas acceder a los datos, sin que ello implique estar restringidos por la
forma como son almacenados, manipulados y nombrados en el DBMS.
2.4.3 Modelo Relacional.
La creación de programas especializados en el manejo de los datos impulsaron también un
cambio en la manera como los usuarios de los archivos (programas y usuarios que los
consultan), perciben la organización de los datos que usan, o sea la arquitectura de los
datos.
La arquitectura de datos más utilizada actualmente, se apoya en el paradigma relacional,
que es 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 46 [Codd 70] [Codd 82]47,
así:
 En esta arquitectura, los datos, o “atributos”, se agrupan en relaciones
matemáticas compuestas por conjuntos de tuplas de valores elementales,
eliminando los registros en forma de árbol.

44 Ver: http://en.wikipedia.org/wiki/Database
45 Por “Data Base Management System”
46 Ver: http://en.wikipedia.org/wiki/Relational_database_management_system y
http://en.wikipedia.org/wiki/Relational_database
47 Ver: http://en.wikipedia.org/wiki/E._F._Codd
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
31
 Las dependencias entre los datos se representan con base en los “atributos
clave” de las tuplas. Así, el valor de la clave es único para cada tupla en una
relación, identificándola de forma unívoca, y, al incluirlo en otra tupla como
“clave foránea” permite representar relaciones de dependencia funcional
entre las tuplas.
 La normalización organiza la información entre las tuplas garantizando que
se representen adecuadamente las relaciones de dependencia funcional entre
los datos, se eliminen las redundancias y se eviten las “anomalías” por el
mantenimiento de datos.
 El álgebra relacional permite implementar un lenguaje de consulta de tipo
algebraico que, básicamente, constituye una aplicación a las bases de datos
de los operadores de la teoría de conjuntos
 El cálculo relacional, es una aplicación del cálculo de predicados al
problema de las consultas sobre las bases de datos. El lenguaje de 4ª
generación “Strucured Query Language” o SQL48 le da soporte tanto al
álgebra relacional como al cálculo relacional.
2.4.4 La especificación formal bajo el modelo Relacional.
En el marco del modelo relacional, las especificaciones de la estructura de los datos
almacenados en archivos, y de las posibles consultas a los mismos, no son otra cosa que
aplicaciones a la informática de los conceptos matemáticos de Tupla, Relación, y
Subconjunto, de la teoría clásica de Conjuntos.
Los conceptos de actualización, o cambio en la información contenida en los archivos,
requiere, sin embargo, de formalismos más elaborados. Entre ellos vale la pena destacar las
nociones de “medio ambiente” para la valoración de las expresiones con variables y el uso
de PRE y POST condiciones para dar cuenta de los cambios en el medio ambiente
inducidos por las instrucciones del programa [Hoare 69]49, [Baber 02]. Las PRE y POST
condiciones están, en efecto, incorporadas en las notaciones VDM [Jones 90] y Z [Woodc
96] referidas antes para el caso de las funciones50.

 Pre y Post condiciones: Describen el efecto de la operación especificando


las condiciones que prevalecen antes y después de su ejecución .

La especificación formal de una transacción que indica que se aumente la reserva disponible para un
gasto (v.g. una compra), puede expresarse de la forma siguiente:

Sea:
G = { xGasto / x.codigo = AumentoReserva.codigo }
Antes de la transacción:
G = { JGasto }

48 Ver: http://en.wikipedia.org/wiki/SQL
49 Ver: http://en.wikipedia.org/wiki/Hoare_logic
50Cabe, sin embargo, anotar que cada notación usa su propia terminología. Así, para VDM las funciones que generan un
cambio de estado son denominadas “operaciones”; en cambio para Z estas son un tipo especial de “schema”.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
32
J.Reserva = R
| J.CentroCosto |=1
AumentoReserva.Valor + R  (J.CentroCosto).Disponible
Después de la transacción:
J.Reserva = AumentoReserva + R

Donde:
“AumentoReserva” es la transacción cuyos datos se entran por pantalla.
“J” Identifica el gasto al que hace referencia la transacción, que debe existir para poder
aumentarle su valor.
“CentroCosto” es la cuenta sobre la que se hace el gasto que debe existir y tener dinero
disponible par el nuevo valor de la reserva.

2.4.5 Arquitectura Relacional.


La identificación de las entidades del área del problema, de sus propiedades y de sus
relaciones de dependencia orientan la arquitectura de los datos, así:
 Las tuplas se relacionan a las entidades concretas del área de aplicación y las
relaciones a los tipos de entidades (tuplas con el mismo número y tipo de
atributos).
 Los datos que distinguen una entidad concreta de otra, se incluyen en la
tupla que describe sus propiedades señalándolos como la clave de la tupla.
 Entre las entidades existen diversos tipos de conexiones que son
representadas incluyendo la clave de una tupla, como clave foránea en otras
tuplas diferentes.
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.

La especificación de la arquitectura de los datos, bajo el modelo relacional, para una aplicación de
gestión de las compras en una entidad pública, el marco del programa Microsoft Access ®, se muestra
a continuación:
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
33

 El modelo Entidad/Relación51 [Chen 76], que representa las entidades


lógicas del área de aplicación junto con sus relaciones. La derivación del
modelo relacional desde el modelo E/R es prácticamente automática.

La especificación de la arquitectura de los datos, bajo el modelo Entidad/Relación, para el caso del
ejemplo anterior se ilustra a continuación:

51 Ver: http://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model y http://en.wikipedia.org/wiki/Peter_Chen


Capítulo 2: Evolución de los Paradigmas Arquitectónicos
34
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
35

Paradigma de Objetos.
La descomposición progresiva de funciones del paradigma de funciones no se orienta “per
se” a la identificación de componentes funcionales genéricos reutilizables que puedan ser
utilizados como partes al construir las diferentes funciones de una aplicación. Esto genera
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
36
una tendencia a reprogramar una y otra vez los mismos procesos, generando no solo una
perdida de esfuerzo al elaborar el software, sino también un enorme costo al modificarlo.
La utilización de las “entidades” del paradigma de datos en diferentes programas, permite
vislumbrar la frecuente ocurrencia de procesos básicos asociados a los atributos de dichas
entidades. Esto sugiere la conveniencia de asociar y almacenar la especificación de estos
procesos como parte de la especificación de dichas entidades, para hacerlos más
reutilizables.
La asociación datos/procesos aparece primero en el contexto de los lenguajes de
programación procedural, como la capacidad de definir tipos de datos adicionales a los
ofrecidos por el lenguaje (v.g. el lenguaje ADA).
2.5.1 Concepto Clásico de Objeto.
A la capacidad de definir nuevos tipos se le incorpora la de redefinir los tipos nuevos o los
existentes por medio de la relación de “herencia”, para impulsar el reuso de
especificaciones ya existentes.
A estos tipos susceptibles de herencia se les denominó, entonces, como “clases” y a las
instancias de ellos se les denominó “objetos” en el lenguaje SIMULA 6752 [Ole-Johan 70].
Modernamente se considera la aproximación Orientada Por Objetos como una disciplina
para el desarrollo de Software53, que se aplica de forma uniforme en todas las etapas y fases
del desarrollo [Booch 96]54, [Jacobson 92]55, [Rumbaugh 91]56, [Jacobson 98], [Meyer
98]57.
De [Meyer 98] se pueden extraer los elementos siguientes como condiciones para que una
disciplina de desarrollo pueda ser denominada “orientada por objetos”:
El concepto de Objeto como hilo conductor del método: Todo el proceso de desarrollo
debe fundamentarse en el concepto de objeto como hilo conductor del proceso, sin que
haya un cambio de aproximación cuando se pasar de una etapa o fase a otra diferente.
 Los objetos tienen “estado” caracterizado por el valor de una serie de
“atributos” que pueden observarse a través del interfaz.
 El estado de los objetos puede ser observado y modificado por medio de una
serie de operaciones denominadas “métodos” que constituyen el “interfaz”
del objeto.
 Los objetos son creados y destruidos por métodos especializados a tal fin.

52Ver: https://en.wikipedia.org/wiki/Object-oriented_programming y https://en.wikipedia.org/wiki/Simula#cite_note-


CommonBase-1
53 Ver: http://www.omg.org/ y http://en.wikipedia.org/wiki/Unified_Modeling_Language y
http://www.ipipan.gda.pl/~marek/objects/TOA/OOMethod/mcr.html
54 Ver: http://en.wikipedia.org/wiki/Grady_Booch
55 Ver: http://en.wikipedia.org/wiki/Ivar_Jacobson
56 Ver: http://en.wikipedia.org/wiki/James_Rumbaugh
57 Ver: http://en.wikipedia.org/wiki/Bertrand_Meyer
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
37
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.
 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 la
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, los métodos y atributos que pertenecen a la parte pública y a la parte
privada de las instancias.
 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 (v.g.
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.
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
38
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í:
Relaciones de asociación y agregación [Rumbaugh 91], 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 [Booch 96], 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.
La especificación de una clase con sus atributos puede considerarse como la especificación
de un Tipo Abstracto de Datos en el sentido algebraico, siendo todas las posibles instancias
de dicha clase los elementos que conforman el tipo. Para dar cuenta de las nociones de
creación, cambio y desaparición de los objetos, los cambios de estado de los objetos pueden
verse como trayectorias sobre el tipo abstracto58, que deben estar identificadas por un valor
único asociado al objeto (v.g. un atributo constante) [Ram 93].
Los métodos, pueden dividirse en aquellos que consultan el estado del objeto,
constituyéndose en observaciones, y aquellos que modifican el estado, constituyéndose en
“eventos” [Pastor 95]. Una secuencia de eventos sobre los objetos define, entonces, una
secuencia de estados de los mismos que se denomina “vida del objeto”.
El conjunto de objetos que conforman una aplicación no es, en consecuencia, otra cosa que
un “sistema dinámico” compuesto por múltiples objetos, que se van creando modificando y
destruyendo, como consecuencia de las interacciones que ocurren entre ellos y entre ellos y
su entorno [Pastor 95], [Vangheluwe 02]. . La activación de las observaciones y eventos
ocurre como consecuencia del envío o la recepción de los “mensajes” que constituyen las
interacciones entre los objetos y entre ellos y su entorno [Pastor 95].

58 Secuencia de “puntos” del tipo rotuladas por el tiempo.


Capítulo 2: Evolución de los Paradigmas Arquitectónicos
39
En este contexto, la especificación del sistema, no sólo debe incluir las clases, con sus
atributos, métodos, relaciones (de asociación uso y herencia), y restricciones de acceso,
sino también una serie de restricciones que limiten tanto el domino de los tipos, como la
aplicabilidad de los eventos y las posibles secuencias de estados por las que puede o debe
transitar un objeto.
2.5.4 Especificación formal bajo el modelo OO.
Los métodos sobre los objetos pueden asimilarse a funciones, así:
 Las observaciones, tienen dentro de su dominio al tipo asociado con la clase
del objeto (mas el tipo de los demás argumentos), y como rango el tipo del
valor observado.
 Los eventos, tienen dentro de su dominio asociado con la clase del objeto
(mas el tipo de los demás argumentos), y como rango el tipo asociado con la
clase del objeto 59.
En consecuencia ellos pueden especificarse con los mismos formalismos referidos antes;
solo que ahora, las especificaciones deben quedar “encapsuladas” en las clases. Este es el
enfoque usado por las notaciones VDM++ [Fitzgerald 04], Object Z, y OCL.
La especificación de las vidas posibles de un objeto, que incluye la especificación de
secuencias de eventos permitidas y obligatorias, requiere de elementos tomados de la teoría
de los lenguajes [Hopcroft 01], y lógicas más avanzadas, como la lógica “dinámica”60
[Harel 2000], y la lógica “deóntica”61 [Lennart 94]. Una aplicación de algunos de estos
elementos aparece en la notación formal para especificaciones OO, OASIS, [Letelier 00].
2.5.5 Arquitectura Objetual.
De la discusión anterior puede inducirse que la especificación del software, concebido bajo
el paradigma objetual, es esencialmente compleja y conlleva un conjunto sustancial de
elementos de diversos tipos. Una manera de reducir la complejidad de la especificación, es
la de presentar de forma separada los diferentes “aspectos” de dicha especificación. En la
notación UML de la OMG62 [OMG UML], se propone una forma de elaborar la
especificación, separándola con base en un conjunto de aspectos interrelacionados que se
plantean de forma independiente, por medio de diversos tipos de modelos expresados por
medio de textos y diagramas.
A continuación se presentan e ilustran tres de dichos aspectos. El lector interesado puede
remitirse a [Booch 05].

 El modelo de clases que presenta las clases con sus atributos y métodos,
junto con sus relaciones de herencia, asociación y uso.

59Es decir, proyectan un punto del dominio de la clase (el estado del objeto antes del evento) a otro punto del mismo
dominio (el estado en que queda el objeto luego del evento).
60 Ver: http://en.wikipedia.org/wiki/Dynamic_logic
61 Ver: http://en.wikipedia.org/wiki/Deontic_logic
62 Ver: http://www.uml.org/
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
40

A continuación se presenta el modelo de clases de una aplicación orientada a la gestión de los trabajos
de grado.

Cronograma
-*Tema
-Semanas

ACTA DE CALIFICACION 0...n


SUSTENTACION
-Fecha de Elaboracion MODIFICACION TDG
-Observaciones 1 -Fecha 0,1 0...n
-Hora -Titulo
-Concepto Informe -Objetivos, Metas y Alcances
-Concepto Sustentacion 0,1 -Aula list
-Concepto Final +Realizar_Citacion() +Modificar_Plan()
+Elaborar_Acta_Calificacion() 1 1 1
SERVICIOS
TRABAJO DE GRADO
-*Codigo
-*Codigo Reserva
-Fecha
-Fecha de Reserva
-Dependencia
-/Posicion en Cola
1 -Tipo de Servicio ACTA CONCEPTO FINAL
-Fecha de Cancelacion
ACTA ASESORIA
-Tipo de Cancelacion -Fecha de Elaboracion
-*Nro PLAN
1 -Semestre de Iniciacion
0...2 -Objetivos
-Fecha de Elaboracion 1...n -Semestre de Terminacion -Difinicion
-Fecha de Actualizacion 0...n -Antecedentes y Justificacion -Cronograma
-Temas Tratados -Metodologia -Observaciones
-Metas -Estimado -Pre-Requisitos 1
-Completitud -Fuente -Co-Requisitos
-Calidad 1...n -Colaboradores 1 PLAN
0,1 -Concepto Final
+Elaborar_Acta() -Tipo de Colaboracion +Asignar_Concepto_Final()
+Actualizar_Acta() -Plan de Temas
-Bibliografia
1...n -Fecha de Presentacion 0...n RESERVA
-Estado PROPUESTA
1...n -/Titulo Actual 1
FRANJA DISPONIBLE 1 RESERVA PROPUESTA
-/Objetivos Actuales
-*Fecha 0...n -*Nro
JURADO 1

JURADO 2

+Reservar_Propuesta()
-*Franja Horaria 1...n 1 +Registrar_Plan() -Título
+Establecer_Disponibilidad() +Cancelar_Reserva() -Tipologia
-Nro de Personas PROPUESTA
-Objetivos
DIRECTOR 0,1 0,1 0,1 0...n PLAN2
-Recursos Economicos 0...n
DOCENTE -Recursos Fisicos
-/Nro de Reservas
-Categoria
0,1 ESTUDIANTE 2 -/Plan Vigente
-Oficina
-Extension +Publicar_Propuesta()
1 ESTUDIANTE 1
+Nombrar_Director_TDG()
+Nombrar_Jurado_TDG()
INSCRIPCION
1...n 1
1...n
Director Propuesto 0,1 -*Nro 0...n /Solicitudes
-Fecha de Inscripcion
-Condiciones

AUTOR
1 +Registrar_Pre-Registro()
PERSONA
EVENTO PROGRAMA CURRICULAR 0...n 1 1
-*Documento de Identidad
-*Codigo -Nombre -*Nombre GRUPO DE INVESTIGACION
-Actividad -Login -Facultad -*Nombre
-Responsable -Password
-Fecha
1...n 1
+Publicar_Programacion() 1,2
AREA DE INTERES
-*Linea de Profundizacion
1...n 1...n
1...n

 El modelo de transición de estados que representa las posibles secuencias


de eventos aplicables a los objetos de una clase, como una “máquina de
estados finitos” de la teoría de lenguajes formales [Hopcroft 01].

A continuación se presenta el modelo de transición de estados para la clase “Trabajo de Grado”.


Capítulo 2: Evolución de los Paradigmas Arquitectónicos
41
TDG: Transición de Estados

Grupo de
Investigación.Publicar_Propuesta Reservar_Propuesta(Documento de
(Titulo, Tipología, Nro de Personas, Identidad, Código Reserva, Nro)
Objetivos, Recursos Económicos, PUBLICADA RESERVADA
Recursos Físicos, Nombre)
Entry: Registrar Fecha Entry: Registrar fecha de reserva
Publicación Do: Calcular fecha de vencimiento
Inicio [Nro_Reservas =1 ] Calcular posición en la cola
/ Estudiante.Cancelar_Reserva(Codigo
Reserva)

[Nro_Reserva >1 ] [Posición en Cola =1] /Registrar_Plan


/ Cancelar_Reserva(Codigo Reserva) (Codigo Reserva, Id1, Id2, Semestre de
Reservar_Propuesta(Documento Iniciación,Semestre de Terminación, Título,
de Identidad, Código Reserva, Nro) Objetivos, Antecedentes, Metodología, Estimado,
Acta Concepto Fuente, Colaboradores, Tipo de Colaboración,
Final.Asignar_Concepto_Final().Concepto = Dependencia1, Tipo de Servicio1, Fecha1,
“RECHAZADO” Dependencia2, Tipo de Servicio2, Fecha2, Plan
Acta Concepto
[Propuesta.Nro_Reservas = 0 ] de Temas, Tema1, Semana1, ………….,TemaN,
Final.Asignar_Concepto_Final().Concepto =
SemanaN, Bibliografía)
“RECHAZADO”
[Propuesta.Nro_Reservas >0 ]

Acta Concepto
Acta de Calificación.Elaborar_Acta_Calificación Final.Asignar_Concepto_Final().Concepto =
EN ELABORACIÓN EN ESTUDIO
().Concepto Final = “APROBADO” “APROBADO”
Entry: Asignar Estado Entry: Asignar Estado

TERMINADO Modificar_Plan (Codigo


Reserva, Titulo, Objetivos)
Entry: Asignar Estado

FIN

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

A continuación se presenta el modelo de casos de uso, que representa la interacción de los usuarios
interesados en la evaluación de un trabajo de grado:

Evaluación
del TDG
Nombrar Jurado
Calificador

Ingresar
Disponibilidad Horaria

Director
Escuela

Consultar Citación Realizar Citación

Jurado Estudiante
Calificador

Elaborar Acta Consultar


Especial Acta/Acta Especial

Paradigma de Agentes.
Sin entrar en detalles sobre este paradigma, que pertenece más al campo de la Inteligencia
Artificial, podemos señalar que adiciona al paradigma de los objetos conceptos propios de
las organizaciones humanas de cooperación, tales como, conocimiento, objetivos o
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
42
intenciones, coordinación de tareas con otros agentes, capacidad de viajar, etc. [Nwana
96]63.

Resumen del Capítulo.


La correcta gestión de los proyectos de software, prescribe que su elaboración se divida en
las fases de “análisis”, “diseño”, “codificación”, “prueba”, e “implantación”, que se llevan
a cabo ya sea en un solo ciclo, siguiendo un modelo de desarrollo en “cascada”, o de forma
reiterada, siguiendo un modelo de desarrollo “ágil”.
A medida que se suceden las fases del desarrollo, se pasa de describir (o “modelar”) los
elementos del área de la aplicación del software, a modelar los elementos del software
mismo. Se considera, además, conveniente usar un lenguaje de especificación similar en
las diferentes fases del desarrollo. Para ello es necesario que los lenguajes usados en las
diferentes fases incorporen los mismos conceptos, y, en particular, que los conceptos con
los que se describe el área de aplicación se usen también para describir el software.
Al conjunto de categorías conceptuales en los que se apoya un lenguaje de desarrollo lo
denominaremos el “paradigma arquitectónico”, entendido como el modo de comprender y
modularizar la descripción del área de aplicación y del software. La evolución de los
paradigmas arquitectónicos, es, en consecuencia, un tópico fundamental en el marco de la
Ingeniería del Software. Desde el punto de vista del uso de la lógica, la aparición de
nuevos paradigmas de desarrollo 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 ellos involucran.
En el “paradigma de instrucciones”, los conceptos del lenguaje de desarrollo son extraídos
del lenguaje con el que se describe el software, sin que se incorporen criterios claros de
modularización.
El “paradigma de funciones”, se centra en describir los procesos (o “funciones”) del área de
aplicación y del software, vistos como transformadores de datos en resultados. Las
funciones de describen en lenguajes que van desde el lenguaje natural, hasta lógicas de
primer orden que describen de forma precisa la relación de los datos con los resultados. La
arquitectura de las funciones se centra en plantear la forma como las funciones se
descomponen en funciones más elementales que se suceden en el tiempo, e intercambian
información. Para describir esta arquitectura se usan diversos tipos de diagramas entre los
que se destacan los diagramas de descomposición jerárquica, los diagramas de flujo de
control, y los diagramas de flujo de datos.
El “paradigma de entidades”, se centra en la estructuración de los datos que se almacenan
en archivos, para ser usados en las múltiples ejecuciones de los programas por diversos
programas. Dicha estructuración evoluciona de secuencias de “registros” constituidos por
agrupaciones de valores en forma de árboles, y que se definen y manipulan en los
programas siguiendo el modelo de descomposición progresiva del paradigma de funciones;
hasta llegar a conjuntos de tuplas de valores que forman “tablas” interrelacionadas, y que se
definen y manipulan en gestores de “Bases de Datos” siguiendo el modelo de las

63 Ver: http://en.wikipedia.org/wiki/Software_agent
Capítulo 2: Evolución de los Paradigmas Arquitectónicos
43
“relaciones matemáticas” de la teoría de conjuntos (modelo “relacional”). El modelo
relacional se asocia, además, con una serie de restricciones de “normalización” que elimina
problemas existentes en el modelo anterior, tales como la duplicación inconsistente de los
datos del área de aplicación, y la desaparición inadvertida de datos dependientes de otros
datos, por operaciones que manipulan los otros. Para describir la arquitectura de los datos
bajo el modelo relacional, se usa, principalmente el “modelo Entidad-Relación” que asocia
las tuplas de valores en las tablas con las “entidades” del área de aplicación; y para
describir la relación de los datos con los programas se usan lenguajes algebraicos como el
lenguaje SQL.
El “paradigma de objetos”, asocia los datos que describen las entidades del área de
aplicación con los elementos de programa que los manipulan, creando el concepto de
“objeto”. La especificación de las propiedades comunes a grupos de objetos similares
constituyen las “clases”, que se incorporan ya sea en los programas o en los gestores de
“Bases de Objetos”. El paradigma clásico objetual incorpora ,además, una serie de
conceptos y elementos de especificación orientados a ampliar la capacidad de modelo para
describir los elementos del área de aplicación, que, incluidos en las clases, maximizan la
reutilización de las especificaciones; entre ellos, la “herencia” que evita duplicar la
especificaciones en clases similares, el “encapsulamiento” que estandariza la utilización de
los objetos en los programas que los manipulan, el “polimorfismo” que permite la escritura
de código genérico reutilizable en varias clases relacionadas, y las relaciones de
“asociación”, “agregación”, y “uso”, que estandarizan los tipos de interacción entre los
objetos. Una evolución del paradigma clásico, ve los objetos como “procesos dinámicos
comunicados” que ocurren en el dominio de tipos abstractos de datos determinados por las
clases; incorporando la capacidad de especificar algunas de las características de dichos
procesos, entre ellas las condiciones en que se inician y terminan los procesos, los pasos
posibles y obligatorios dentro de los procesos, y las comunicaciones posibles en las
diferentes etapas de los procesos. Dada la complejidad del modelo, la arquitectura objetual
se especifica por medio de una gran diversidad de modelos, que describen diferentes
aspectos de la misma. La notación UML, por ejemplo, incorpora una gran variedad de
primitivas y diagramas, entre los que se destacan, el diagrama de “Clases”, el diagrama de
“Transición de Estados”, el diagrama de “Interacciones”, y el diagrama de “Casos de Uso”.
El “paradigma de agentes”, adiciona al paradigma de los objetos conceptos propios de las
organizaciones humanas de cooperación, tales como, conocimiento, objetivos o
intenciones, coordinación de tareas con otros agentes, capacidad de viajar, etc.
Capítulo 3
Lógica Proposicional
Capítulo 3: Lógica Proposicional
46
Introducción
En el primer capítulo del trabajo se adujo que los lenguajes de programación han
evolucionado de lenguajes orientados a describir en detalle el proceso computacional que
ocurre al ejecutar un programa, a lenguajes orientados a describir las características
relevantes del área de aplicación.
En el marco de esa evolución se destacó el uso de la lógica matemática como lenguaje de
programación. La lógica permite describir las características del área de aplicación con
base en aserciones o restricciones, facilita la construcción de instancias que modelan el área
de aplicación satisfaciendo las restricciones, y, por último, permite obtener respuestas a
consultas relativas a dichas instancias con base en demostraciones soportadas en la lógica.
En el segundo capítulo se mostró que para representar adecuadamente áreas de aplicación
cada vez más complejas, los lenguajes que dan soporte al desarrollo han venido
incorporando tipos de conceptos o “categorías conceptuales” cada vez más cercanas a las
del lenguaje natural. A un conjunto específico de dichas categorías se le denominó
“paradigma arquitectónico”. El software elaborado con base en uno de dichos paradigmas
posee una forma de modularización estrechamente ligada al paradigma, por lo que éste
constituye la base de todo método de desarrollo.
Para que sea posible usar la lógica matemática como un lenguaje de especificación en el
marco de un método moderno de desarrollo es, entonces, necesario que ella pueda dar
cuenta de los conceptos involucrados en los diferentes paradigmas arquitectónicos. A este
efecto se señalaron, en el capítulo anterior, diferentes lógicas como medio para dar soporte
a los diferentes paradigmas.
El enfoque de este trabajo es el de presentar sólo los aspectos de la lógica que soportan
cada uno de los temas de software que se traten. En consecuencia, los aspectos de la lógica
se van presentando de forma paulatina, antecediendo los temas de software, y sin presentar
temas de lógica que sean irrelevantes a los mismos.
El presente tomo, se centra en presentar la manera como la lógica da soporte a los lenguajes
de programación: Se deja para tomos a ser escritos posteriormente, la discusión relativa a
los lenguajes de análisis y diseño.
En este capítulo se introducen, en particular, los conceptos básicos de la lógica de
proposiciones y de la lógica de predicados, centrándose en los aspectos de sintaxis y
semántica. Estos dos aspectos son básicos y necesarios para todas las presentaciones de
lógica que se hacen posteriormente.

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.
Capítulo 3: Lógica Proposicional
47
El criterio de autoridad es, sin embargo, insuficiente e incluso regresivo en el mundo de la
ciencia moderna64. En efecto, el científico moderno debe esperar, no sólo poder comprobar
por sí mismo los hechos referidos, sino también, poder convencerse por sí mismo de las
aserciones que se derivan de los hechos. Es en este sentido que la ciencia moderna exige
tanto la reproducibilidad de los experimentos65, 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 Organon66, 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 razonamiento67 que denomina
“silogismo”.
Un silogismo consta de dos premisas y una conclusión, todas consistentes en frases de una
de las formas68: Todo S es P, Todo S no es P, Algún S es P y 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.

64 Al menos de la ciencia “Occidental”.


65Un caso ilustrativo de la importancia de la reproducibilidad de los experimentos, es el del descubrimiento de los rayos
N (ver http://en.wikipedia.org/wiki/N_ray, [Klotz 80]), que a pesar del interés que suscitaron y de las publicaciones
asociadas, resultaron ser un fenómeno inexistente.
66Nombre dado al conjunto de obras sobre Lógica escritas por Aristóteles de Estágira y recopiladas por Andrónico de
Rodas (ver http://es.wikipedia.org/wiki/Organon)
67 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.
68 Ver http://en.wikipedia.org/wiki/Syllogism
Capítulo 3: Lógica Proposicional
48
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 Leibniz, 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], Leibniz “...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, Leibniz pretende soportar la lógica en un lenguaje
simbólico69. A este respecto refiere [Bourbaki 72, pg. 20] que Leibniz “ 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...” Leibniz 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, George 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 conjuntos70. 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
conjunto71, las variables “proposicionales” que representan proposiciones indeterminadas
dentro de otras proposiciones, y otros elementos de notación que se usan hoy en día (v.g. el
símbolo para pertenece y para subconjunto).

69 Se refiere en que por ejemplo, “ hace notar que puede remplazarse la proposición
El algebra boleana puede ser vista como una álgebra de los números enteros “modulo 2” con las operaciones suma y
70

multiplicación de enteros.
71 que ya se vislumbraban en las proposiciones aristotélicas.
Capítulo 3: Lógica Proposicional
49
La Lógica Como una Disciplina de los Lenguajes.
Para sistematizar el razonamiento, la lógica debe apoyarse en una notación simbólica que
permita representar las palabras y frases con las que se especifican los problemas y se
caracterizan los elementos de sus posibles campos de aplicación. En otras palabras toda
lógica debe ser un lenguaje simbólico.
El lenguaje de la lógica debe ser, además, preciso y carente de ambigüedades. Por ello la
lógica debe imponer reglas a la formación e interpretación de sus palabras y frases, que
eliminen las ambigüedades e imprecisiones de la mayoría de los lenguajes simbólicos, y en
particular, las que ocurren en los lenguajes naturales
La teoría de los lenguajes formales [Hopcroft 01], ofrece las condiciones que permiten
lograr dicha precisión y capacidad de interpretación. Desde la óptica de los lenguajes
formales, una lógica no es más que un conjunto de ensamblajes de símbolos que satisface
una serie de condiciones previamente definidas. Estas condiciones son básicamente las
siguientes:
 La existencia de una gramática o “sintaxis” precisa para el lenguaje, que
permita distinguir los ensamblajes (o frases del lenguaje) que son correctos o
“bien formados” de los que no lo son.
 La existencia de una semántica que permita darle un significado a las frases
bien formadas del lenguaje, para poder, a partir de ellas, tomar las decisiones
que sean de interés.
 La existencia de unas reglas de inferencia que permitan obtener (deducir)
frases nuevas con significado conocido (conclusiones), a partir de otras
frases cuyo significado es ya previamente conocido (premisas).
Si adicionalmente se pretende utilizar la lógica como un lenguaje de programación, es
necesario que sus palabras y frases puedan interpretarse de forma automática en el
computador, y que sus reglas de inferencia puedan dirigirse de forma automática a la
obtención de las conclusiones deseadas. Para ello las reglas de inferencia deben determinar
procesos computacionales de demostración, mucho más reglados que los que determinan la
mayoría de las lógicas existentes. En otras palabras no todas las lógicas pueden ser usadas
como un lenguaje de programación.
3.3.1 Sintaxis.
La sintaxis de un lenguaje está constituida por un conjunto de elementos que regulan la
conformación y estructura de sus frases. A una frase que satisface la sintaxis del lenguaje
lógico se le ha denominado “formula bien formada” (en lo que sigue “fbf”).
En primer lugar, el “alfabeto de símbolos” define las unidades básicas de especificación
que pueden ser utilizadas para formar las frases. Estas unidades pueden ser letras, símbolos
especiales, palabras, o figuras que son consideradas elementales e indivisibles. La
especificación del lenguaje debe definir el alfabeto de símbolos, y el intérprete debe señalar
como errado el uso de cualquier símbolo que no pertenezca al alfabeto.
La forma más simple de definir el alfabeto es la de listar los símbolos que lo componen.
Sin embargo cuando los símbolos son ensamblajes básicos de letras u otros componentes
más elementales, el alfabeto de símbolos puede ser infinito. En este caso se deben proveer
reglas que permitan establecer si un ensamblaje constituye o no un símbolo valido del
Capítulo 3: Lógica Proposicional
50
alfabeto. Estas reglas usualmente se dan en la forma de “expresiones regulares” o
“máquinas de estado finitas”, que son básicamente patrones para definir dichos
ensamblajes72.
En segundo lugar, los “criterios formativos” (en lo que sigue “CF”), en general,
determinan las formas válidas de ensamblar frases bien formadas del lenguaje para formar
otras frases bien formadas más complejas. Para ello los CF señalan que símbolos del
alfabeto se usan para unir las frases y que símbolos constituyen las frases bien formadas
más elementales. Los CF permiten entonces, descomponer, de forma progresiva, una frase
del lenguaje en componentes o “subfrases” que pueden a su vez estar compuestas por otras
componentes y así sucesivamente hasta llegar a símbolos del alfabeto. Esta descomposición
puede representarse en forma de árbol, formando lo que denominaremos el “árbol
sintáctico” de la frase.

Las expresiones de la aritmética como “3+4” son fbf de la lógica de predicados que usan los símbolos
del alfabeto (números y operadores), y los ensamblan de formas predefinidas (p.e. el operador “+” tiene
el perfil “<operando> + <operando>”).
El árbol sintáctico de esta fbf es el siguiente:

3 4

La expresión “ama(juan,luisa)ama(luisa,juan)” es también una fbf de la lógica de predicados tal como


se verá en lo que sigue.
El árbol sintáctico de esta fbf es el siguiente:

ama ama
a
juan luisa luisa juan
3

El carácter recursivo de los criterios formativos, induce a que el conjunto de las fbfs de una
lógica sea, en general, infinito.
Los criterios formativos pueden especificarse por medio de notaciones formales como la de
una gramática en notación de Backus Naur (BNF)73. Bajo ciertas condiciones las

72 Estos patrones definen un lenguaje de símbolos que es usado dentro del lenguaje. Para mayor información sobre estos
elementos el lector debe referirse a [Hopcroft 01]
73 Ver: http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form
Capítulo 3: Lógica Proposicional
51
especificaciones BNF permiten la generación automática de los programas requeridos para
establecer el árbol sintáctico asociado con una frase, y señalar posibles violaciones a los
criterios formativos. A estos programas se les denomina “parsers”74.
En lo que resta del trabajo iremos presentando la gramática de los lenguajes estudiados, por
medio de especificaciones simplificadas en la forma de patrones que, de forma conjunta,
constituyen una especificación de la gramática por medio de expresiones regulares y
gramáticas BNF.
3.3.2 Semántica.
Para asociar un significado a las fbfs de una lógica, es necesario contar con uno o varios
“dominios semánticos” asociados con la lógica, que contendrán los significados. El
significado de cada fbf (que en lo que sigue denominaremos ya sea como su “valor
semántico”, o como el elemento que ella “denota”), es un elemento o una estructura de
elementos en los dominios semánticos. La precisión de un lenguaje lógico estriba, además,
en que cada fbf tenga un significado único.
La manera obtener el significado de una fbf depende de las características propias de cada
lógica y será objeto de estudio en cada caso. Sin embargo, en principio, el significado de
una fbf debe estar completamente determinado por el significado de sus fbfs componentes y
por la manera en que ellas se ensamblan. Para ello es necesario que los símbolos que
componen las frases tengan de antemano un significado dado.

La fbf “3+4” tendrá su valor semántico en el dominio de los números enteros y dicho valor puede ser
establecido o “calculado” con base en el valor semántico de sus símbolos, es decir los enteros que
representan los símbolos “3” y “4” y la función representada por el símbolo “+”.:
La fbf “ama(juan,luisa)ama(luisa,juan)” tendrá su valor semántico en el dominio {verdadero, falso} y
su valor sólo puede ser establecido luego de conocer el valor semántico de “juan” y “luisa” (las personas
a las que refieren dichos nombres), el valor semántico del predicado “ama” (una relación entre personas
que ha sido denominada “ama”), y el valor semántico del conector “” (una función con codominio
{verdadero, falso})

Los símbolos de “constantes” del alfabeto (es decir aquellos cuyo significado no cambia en
el contexto de un cálculo o una demostración ver 4.2.1.2.2), pueden ser símbolos lógicos, o
símbolos no lógicos. El significado de los primeros es inmutable, propio de la lógica, e
independiente del contexto donde se aplique. El significado de los segundos, por su lado,
depende del contexto al que se aplican las fbfs, y debe establecerse como una proyección
arbitraria, de dichos símbolos a elementos o a estructuras de elementos de los dominios
semánticos de la lógica. En lo que sigue, a los dominios semánticos a los que se proyectan
los símbolos no lógicos de la lógica los denominaremos como “dominios de
interpretación” (de los símbolos no lógicos). A la función que proyecta los símbolos no
lógicos la denominaremos una “interpretación” (de los símbolos no lógicos sobre el
dominio semántico). La interpretación de los símbolos no lógicos es, en general, arbitraria

74 El lector interesado debe consultar “Compilers: Principles, Techniques, and Tools” [Ullman 86]
Capítulo 3: Lógica Proposicional
52
por lo que pueden existir múltiples interpretaciones para el conjunto de símbolos no lógicos
de las fbfs.

Si bien los símbolos de la fbf “3+4” tendrán un valor semántico fijo e inmutable (por acuerdo universal),
y por tanto una sola interpretación, los símbolos de la fbf “ama(juan,luisa)” tendrán un valor semántico
que puede ser diferente en diferentes grupos de personas (su contexto), y por tanto múltiples posibles
interpretaciones.

El proceso por el cual se obtiene el valor semántico de una fbf a partir del valor semántico
de los símbolos del alfabeto, lo denominaremos “calculo del valor de la fbf”. Es sin
embargo importante notar que como se verá más adelante, este cálculo no siempre se puede
llevar a cabo por razones prácticas.

En el marco de la lógica de predicados, el cálculo del valor semántico de una fórmula con variables (ver
3.5.2) cuantificadas sobre conjuntos infinitos es prácticamente imposible.

Dado que el término “semántica” del lenguaje natural se entiende de muy diversas formas,
en lo que sigue usaremos el término “valor” para referirnos al valor semántico de una fbf
en general, y la frase “valor de verdad” para referirnos al valor semántico de una fbf cuyo
dominio semántico es el conjunto {verdadero, falso}. El término genérico “semántica”,
por su parte, lo reservaremos para referirnos al conjunto de interpretaciones en las que una
(o varias) fbf de dicho tipo tiene(n) como su valor de verdad a “verdadero”.

La fbf “ama(juan,luisa)ama(luisa,juan)” tendrán como su semántica a el conjunto de grupos humanos


donde los individuos referidos por “juan” y “luisa” se aman mutuamente.

3.3.3 Inferencia.
Si bien el valor semántico de una fbf debería poder calcularse a partir del valor semántico
de sus símbolos, este significado no siempre se puede obtener por razones prácticas (ver
4.3).
Para resolver este problema se asocian una serie de “criterios demostrativos” con el
lenguaje lógico. Los criterios demostrativos permiten obtener o “inferir” nuevas fbfs, con
un valor semántico dado, a partir de otras fbfs cuyo valor semántico es conocido
previamente. Desde el punto de vista lingüístico, los criterios demostrativos no son otra
cosa que reglas de transformación que obtienen nuevas fbfs útiles, a partir de otras fbfs
previamente definidas.

De saberse previamente que el valor de verdad, bajo una cierta interpretación, es “verdadero” para las
fórmulas “ama(juan,luisa)” y “XYama(X,Y)ama(Y,X)”, se puede deducir que el valor de verdad de
Capítulo 3: Lógica Proposicional
53
la fbf “ama(luisa,juan)” también es “verdadero” bajo dicha interpretación.

Para poder usar los criterios demostrativos es necesario contar con fbfs “semilla” cuyo
valor de verdad sea conocido posiblemente por observación sobre lo que ocurre en el
dominio de interpretación. En los casos en que este valor de verdad no puede ser
(completamente) observado, el valor de verdad de las fórmulas semilla debe ser asumido
constituyéndose estas fórmulas en los “axiomas de la teoría”.

En la teoría de los números enteros la validez de la ley conmutativa de la suma (X(X+Y= Y+X )
puede ser observada sólo para un conjunto finito de pares de enteros, por lo debe ser asumida
constituyéndose en un axioma a partir del cual se demuestran otras verdades o “teoremas”.

En general, distinguiremos dos tipos de reglas de derivación, que denominaremos


“criterios transformativos” y “criterios demostrativos”. Los criterios transformativos,
permiten transformar una fbf a otra que siempre tiene, en todas las interpretaciones, el
mismo valor semántico que la original, por lo que se dice que es “semánticamente
equivalente”. Los criterios demostrativos permiten obtener la semántica de ciertas fbf, a
partir de otras fbfs de semántica conocida, sin que necesariamente las primeras sean
semánticamente equivalentes a las segundas, por lo que se dice que las primeras son
“consecuencia lógica” de las segundas

Lógica de Proposiciones.
En la lógica de proposiciones el dominio semántico es el conjunto {0, 1} que puede ser
entendido como los valores de verdad “verdadero” y “falso”75 (en lo que sigue
denominaremos a este dominio como “BOOL”). En otras palabras, la lógica de
proposiciones es la lógica de la verdad o falsedad de los planteamientos que puedan
formarse en un lenguaje cualquiera76, en particular en el lenguaje natural.
3.4.1 Proposiciones.
Una proposición es una afirmación que es “o bien” verdadera “o bien” falsa.
3.4.2 Sintaxis de la lógica proposicional.
En la lógica básica de proposiciones, la sintaxis es extremadamente simple ya que los
símbolos son las letras y unos pocos conectores.
3.4.2.1 Alfabeto
Las proposiciones más obvias, son precisamente las constantes “verdadero” y “falso”, que
denotaremos con las letras V y F respectivamente.
Las “variables proposicionales” serán letras en minúscula con o sin subíndice: {a, a1, a2, a3,
… , b, b1, b2, b3, … p, p1, p2, p3, … , q, q1, q2, q3, …}. Ellas serán usadas para representan las

75 A los que nos referiremos de diversas formas: {ciero, falso},{true, false},{verdadero,falso},{V,F},{T,F} etc..
76 http://en.wikipedia.org/wiki/Propositional_logic
Capítulo 3: Lógica Proposicional
54
proposiciones relativas al área de aplicación que son consideradas elementales en cuanto no
se dividen en proposiciones más simples. A estas proposiciones elementales nos
referiremos en lo que sigue como los “átomos” o “primitivas” de la teoría. Las variables
proposicionales constituyen los símbolos no lógicos (constantes) de la lógica
proposicional77. Vale la pena anotar que la selección de cuales frases del lenguaje
constituyen proposiciones “primitivas” es arbitraria; en consecuencia, la única condición
que en este trabajo se le impone al conjunto de proposiciones primitiva, es que el valor de
verdad dado a una o a varias de las primitivas (en una interpretación) no determine o afecte
el valor de verdad que pueda dársele a otra u otras de la primitivas; es decir, que las
primitivas sean independientes.
Las letras mayúsculas con o sin subíndices, { A, A1, A2, A3, … , B, B1, B2, B3, … P, P1, P2, P3, … , Q, Q1,
Q2, Q3}, junto con los símbolos del alfabeto griego, serán usados para referirnos a las
proposiciones que pueden ser subfórmulas en otra fbf planteada de forma genérica (como
una plantilla). Estos símbolos constituyen meta-variables que refiere a una cualquiera de
las fbfs de un conjunto determinado, sean ellas atómicas o no, (usualmente, todas las
proposiciones posibles). Las metavariables serán usadas para discutir o plantear las
propiedades compartidas por las fbfs que resulten de substituir las meta-variables por una
proposición cualquiera de las del conjunto al que se asocian.
Los símbolos lógicos de la lógica proposicional son los conectores lógicos que se utilizan
para formar proposiciones complejas con base en proposiciones más simples. Los
conectores lógicos que usaremos en lo que sigue son los siguientes78:
  : denominado “no” (que ocasionalmente representaremos como NOT)
  : denominado “o” (que ocasionalmente representaremos como OR)
 , denominado “y” (que ocasionalmente representaremos como AND).
  (), denominado “implica” (que ocasionalmente representaremos como
==> o como <==).
  , denominado “doble implicación” (que ocasionalmente representaremos
como <==>).

3.4.2.2 Criterios Formativos


Los conectores permiten ensamblar las variables proposicionales para formar proposiciones
complejas. Por ejemplo si p, q y r son variables proposicionales, p(((p(¬r))(r((¬p)q)))
es una proposición compleja.
No todos los ensamblajes de conectores y variables son, sin embargo, correctos (no
constituyen una proposición). Para que un ensamblaje sea correcto o “formula bien
formada” (fbf) debe cumplir con los criterios formativos, así:
En lenguaje natural:
 Una variable o constante proposicional es una fbf.

77Al igual que para todos los símbolos no lógicos, el valor de las “variables proposicionales” es constante durante los
procesos de cálculo e inferencia que se lleven a cabo en el marco de una interpretación, pero varía con la interpretación.
78 Es posible escoger como básico al conjunto {, } o al conjunto {, } y considerar los otros como derivados (ver
p.e. )
Capítulo 3: Lógica Proposicional
55
 Si F es una fbf entonces también lo es F
 Si F y G son fbf entonces también lo son FG, FG, FG (GF), y FG
 Ningún otro ensamblaje es una fbf
En gramática BNF:
φ ::= p | (φ) | (φ  φ) | (φ  φ) | (φ  φ) | (φ  φ) | (φ  φ)
Dónde:

p Es una variable proposicional (o “átomo”).


φ Es una fbf.
3.4.2.3 Árbol sintáctico
La naturaleza recursiva de los criterios formativos (en cuanto ensamblan fbfs para generar
nuevas fbfs), hace posible que un conector ensamble no sólo variables proposicionales sino
también proposiciones complejas. En consecuencia, una proposición compleja puede tener
múltiples conectores y múltiples variables proposicionales.
Para poder establecer el valor de verdad de una proposición con varias variables y
conectores, es necesario saber cuáles son las proposiciones (simples o complejas) que
ensambla cada conector. Es decir, se debe conocer cuál 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 subfórmula de la fórmula global (o de otra
subfórmula) es representada por una rama (u hoja) del árbol.
 Cada conector de la fbf corresponde a un nodo en el árbol.
 Cada aparición de una variable proposicional en la fbf corresponde a una
hoja en el á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.

La fbf siguiente:

p((p¬r)r(¬pq))
Puede representarse por medio del árbol sintáctico siguiente:

p



r 
Capítulo 3: Lógica Proposicional
56

3.4.2.4 Uso de paréntesis.


Para clarificar las subfórmulas 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. Si cada subfórmula se escribe entre paréntesis se dice que la fórmula está
“completamente parentetizada”

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

3.4.2.5 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, ya sea como una lista que se recorre de izquierda a derecha o por medio de un
número de orden entero, 1…N, que indica cual operador actúa de primero, de segundo etc...
El de mayor precedencia (el de más a la izquierda, o el de menor número) actúa primero
uniendo las fbf que tiene a su lado formando una fbfs más compleja que, a su vez, puede ser
unida por otro conector de menor precedencia.
Es usual el siguiente orden de precedencia:
, , , , 
En los ejemplos y talleres, sin embargo, nos tomamos la libertad de usar otros órdenes de
precedencia diferentes al usual.

Nótese que el árbol sintáctico de las dos fbfs que siguen es el mismo, bajo el orden de precedencia
anterior:

p(((p(¬r))(r((¬p)q))))

p((p¬r)r(¬pq))
Capítulo 3: Lógica Proposicional
57
Es importante anotar que, a pesar de existir un orden de precedencia que permite usar
menos paréntesis, no es posible dejar de usarlos completamente en una fbf.

El lector debe notar que, a pesar de existir un orden de precedencia que permitió usar menos paréntesis,
no es posible eliminarlos completamente en una fbf sin cambiar su árbol sintáctico. En efecto, el árbol
sintáctico de la fbf anterior sin paréntesis:
p p¬rr¬pq
Es el que se muestra a continuación:

 

p p ¬ q

r
r ¬
p

El árbol sintáctico cambia también al cambiar el orden de precedencia. Así, la fbf anterior bajo el orden
de precedencia {, , , , }, sería el que se muestra a continuación:





p p

q
¬ ¬
r
p
r

3.4.2.6 Uso del sentido de asociatividad entre los operadores.


Cuando aparece una secuencia de conectores iguales (de igual precedencia) se debe definir
un “sentido de asociatividad” que será “->” (izquierda a derecha) o “<-“ (derecha a
izquierda). En nuestro trabajo, el sentido de asociatividad (la dirección a la que apunta la
flecha) indica el orden en que se los conectores se van asociando con las fbfs que los
rodean79.
Por defecto asumiremos que los conectores diádicos (,, , ) asocian de izquierda a
derecha (->), y el conector monádico () al tener el operando a su izquierda, debe asociarse

79 En [Aho 2007], el sentido de asociatividad se define sin definir una secuencia de aplicación de los operadores,
indicando que : “We say that the operator + associates to the left, because an operand with plus signs on both sides of it
belongs to the operator to its left.”.
Capítulo 3: Lógica Proposicional
58
de derecha a izquierda (<-) ya que el primero a ser aplicado en una secuencia de conectores
contiguos que preceden una fbf (v.g. P), debe ser el más cercano a la fbf.

El árbol sintáctico de la fbf siguiente:

pp¬rr¬pq
Es el que se muestra a continuación:



 ¬ q
p p 
r
r ¬
p

3.4.3 Semántica.en lógica proposicional.


El significado de una fbf en lógica de proposiciones es su valor de verdad, es decir, un valor
en el dominio BOOL {falso, verdadero} ({falso=0, verdadero=1}), que en lo que sigue
representaremos como {F, V} . 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.80
De lo referido en 3.3.2, acerca de la semántica, se desprende que el valor de verdad de una
fbf debe satisfacer los dos criterios siguientes:
 Debe ser único o de lo contrario se tendría un lenguaje ambiguo.
 Debe estar determinado de forma rigurosa por el valor de verdad de las fbf
más elementales que la componen.
3.4.3.1 Interpretación.
Ya que las variables proposicionales que hacen referencia a las primitivas son fbfs
indivisibles, su valor de verdad no puede calcularse a partir del valor de verdad de sus
componentes. Ellas constituyen los símbolos no lógicos del lenguaje y su valor de verdad
debe ser definido por las características del contexto en el que se aplique la lógica.
Definición:
A las constantes y las variables proposicionales en minúscula se les denomina
“proposiciones atómicas” o “átomos” ya que son proposiciones que no pueden
descomponerse en otras proposiciones más elementales.
Definición:

80 En este sentido la lógica proposicional es “bivalente”. Es posible, sin embargo, concebir lógicas “trivalentes” o
“multivalentes” y lógicas en que se le da un “grado de certeza” al valor de verdad asociado con una fbf. (ver
http://es.wikipedia.org/wiki/L%C3%B3gica_difusa)
Capítulo 3: Lógica Proposicional
59
Una “interpretación” , en lógica proposicional es una asignación de valor de
verdad para los símbolos no lógicos (variables proposicionales) que aparecen en las
fbfs de una especificación.
: {a, a1, a2, a3, … , b, b1, b2, b3, … p, p1, p2, p3, … , q, q1, q2, q3, …} {F,V}
Si la especificación tiene n variables, para ella existen 2n posibles interpretaciones.
3.4.3.2 Semántica de los conectores.
Como se vio arriba, una fórmula compleja, es una fórmula que ensambla fórmulas menos
complejas por medio de los conectores. El valor de verdad de una fbf compleja será
establecido con base en el valor de verdad de las fbfs que ensambla y del valor semántico
de los conectores.
Los conectores son los símbolos lógicos de la lógica de proposiciones y representan
operadores en el álgebra de Bool. En particular el conector ”” representa al operador “+”
(suma), el conector “” representa al operador “*” (producto), y el conector “” representa
el operador “-“ (cambio de signo).
En el álgebra de Bool, estos conectores están asociados a funciones de dominios y
codominios basados en el dominio BOOL, que pueden representarse de forma explícita por
medio de tablas. En particular los operadores “, , ” representan las funciones siguientes:

P Q P PQ PQ
V V F V V

V F F V F

F V V V F

F F V F F

Los conectores “, ”, por su parte, representan a operadores compuestos, definidos a
partir de los anteriores, así:
 FG Es una forma de escribir (F)G
 FG Es una forma de escribir ((F)G)((G)F)
En consecuencia los conectores “, ” representan las funciones siguientes:

P Q PQ PQ
V V V V

V F F F

F V V F

F F V V
Capítulo 3: Lógica Proposicional
60
Los conectores “” y “” están también relacionados ya que  puede definirse en función
del “” y del “”, así:
 FG Es una forma de escribir (FG)
En consecuencia el conjunto mínimo de conectores81 es uno cualquiera de los conjuntos {,
}, {, }, o {, }
Nótese que las tablas refieren a fbfs cualquiera como argumentos de los conectores
(representadas por variables en mayúscula) y no sólo a átomos, en cuanto lo que interesa es
el valor de verdad de la fbf (primitiva o no) a la que se aplique el conector.
Así, la semántica de los conectores (en el sentido expuesto en 3.3.2 ) puede resumirse de la
forma siguiente:
 : verdadero cuando su argumento es falso.
  : verdadedro sólo si sus dos argumentos son verdaderos.
  : verdadero cuando alguno de sus argumentos es verdadero.
  : verdadero excepto cuando siendo el primer argumento (antecedente) verdadero,
su segundo argumento (consecuente) es falso.
  : verdadero cuando los dos argumentos tienen el mismo valor de verdad.

3.4.3.3 Semántica de las fbfs complejas.


El valor de verdad de una fórmula compleja puede calcularse a partir del valor de verdad de
sus subfórmulas constituyentes aplicando la semántica de los conectores. Esto se debe
hacer de forma progresiva desde los átomos hasta cubrir las más complejas y
eventualmente toda la fbf. Este proceso equivale a hallar el valor de verdad para cada
conector del árbol sintáctico.
El valor de verdad de un fbf cualquiera es en consecuencia dependiente de la interpretación
(). Así, si 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) tiene como valor a V 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.3.4 Tipos de fórmulas en relación con las interpretaciones.

81Que además son “funcionalmente completos” ver:


https://en.wikipedia.org/wiki/Functional_completeness#Minimal_functionally_complete_operator_sets
Capítulo 3: Lógica Proposicional
61
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
Definición:
Una fbf F es contingente si para algunas interpretaciones es verdadera, y para otras
es falsa.

Son satisfacibles y contingentes las fbf: p, p  q

Definición:
Una fbf F es insatisfacible o una contradicción si NO existe una interpretación 
que sea modelo de F
Definición:
Una fbf F es válida o una tautología si toda posible interpretación  es modelo de F
Note que si F es válida, entonces F es insatisfacible, y que una fbf F es contingente si no
es válida ni contradictoria

Son insatisfacibles las fbf siguientes:

P P, (P  Q)  (P  Q)


Son válidas las fbf siguientes:

Ley de la separación: P  (P  Q)  Q
Modus tollendo tollen: Q  (P  Q)  P
Modus tollendo ponens: P  (P  Q)  Q
Ley de la simplificación: P  Q  P
Ley del silog. hipotético: (P  Q)  (Q  R)  (P  R)
Ley de la exportación: (P  Q  R)  (P  (Q  R))
Ley de la importación: (P  (Q  R))  (P  Q  R)
Ley del absurdo: (P  Q)  Q  P
Ley de la adición: P  P  Q

Vale la pena justificar el uso de meta-variables en el ejemplo anterior. Para ello basta con
notar que para definir si una fbf es válida o insatisfacible, es suficiente con considerar su
valor de verdad para todas las posibles combinaciones de valores de verdad de sus
variables, AÚN CUANDO ESTAS NO REPRESENTEN ÁTOMOS. Esto es debido a que
el cálculo del valor de verdad de la fbf para cualquiera de las posibles combinaciones de
valores de verdad de sus átomos pasa por hallar el valor de verdad para alguna de las
Capítulo 3: Lógica Proposicional
62
posibles combinaciones de valores de verdad de sus variables (que en todos los casos ya ha
quedado analizada).
En este sentido, las fbfs que son tautologías o contradicciones pueden considerarse como
plantillas que representan familias de fbfs que son tautologías o contradicciones. Estas
familias están compuestas por las fbfs resultantes de substituir cada variable por una fbf
cualquiera, primitiva o no (substituyendo por la misma fbf todas las ocurrencias de cada
variable particular).
Para el caso en que al considerar el valor de verdad de una fbf para todas las posibles
combinaciones de valores de verdad de sus variables, dicha fórmula resulte ser contingente,
ella NO puede ser considerada como una plantilla que represente a una familia de fbfs que
también son contingentes, considerando sus variables como meta-variables. En efecto
siempre queda la posibilidad de que algunas de las combinaciones de valor de verdad de
dichas meta-variables, no ocurran en todas las posibles combinaciones de valores de verdad
de los átomos de las que ellas pudieran estar compuestas (y tal vez dichas combinaciones
fueron las que determinaron que la fbf original fuera contingente).
3.4.3.5 Semántica de un conjunto de fbfs.
Tal como se definió 3.3.2 la semántica de una fbf que tiene como dominio semántico a
BOOL, es el conjunto de interpretaciones de sus símbolos no lógicos, para los que el valor de
verdad de la fórmula es “verdadero”. En el marco de la lógica proposicional, esto se
traduce al conjunto de asignaciones de valores de verdad para los átomos (o variables
proposicionales) de la fbf para las que ella calcula a “verdadero”.

La semántica de una tautología es el todo el conjunto de posibles asignaciones de valores de verdad para
sus átomos.
La semántica de una contradicción es el conjunto vacío.

Cuando una fbf hace parte de un conjunto de fbfs, podemos extender el concepto de
semántica de la fbf al conjunto de interpretaciones de los símbolos no lógicos de todas las
fórmulas que aparecen en el conjunto, para las que el valor de verdad de la fbf es
“verdadero”.
Así, en lo que sigue consideraremos que la semántica de un conjunto de fbfs, es la
intersección de las semánticas extendidas de cada una de las fórmulas que contiene. Esta
semántica no es otra que la semántica de la fbf resultante de conectar todas las del conjunto
con el conector “”.
3.4.4 Inferencia en lógica proposicional.
Si bien el valor de verdad de una fbf en lógica proposicional debe poder obtenerse del
significado de sus átomos, este valor se hace cada vez más difícil de obtener a medida que
crece el número de átomos.
Los criterios demostrativos de la lógica de proposiciones permiten, por su parte, inferir el
significado de fbfs sin necesidad de recurrir al significado de sus átomos. Igualmente
Capítulo 3: Lógica Proposicional
63
dichos criterios permiten “razonar” para inferir nuevas fbfs verdaderas a partir de otras
previamente dadas como tal.

3.4.4.1 Consecuencia lógica y Equivalencia Semántica.


Tal como se refiere en [Chang 73], 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”. Simbólicamante: F1,F2, ...,Fn |= G .
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.
Simbólicamante: P  Q .
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 previas de “tautología” y “contradicción”, podemos
asimilar los conceptos de consecuencia lógica y de equivalencia semántica a las
afirmaciones siguientes82:
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 consecuencia 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:

82No nos ocuparemos aquí del caso en que F1F2...Fn es una contradicción, ya que de considerarse suficiente que
(F1F2...Fn) G sea una tautolgía para que G sea consecuncia lógica de las Fi, si F1F2...Fn es una contradicción
entonces cualquier fbf seria su consecuencia lógica.
Capítulo 3: Lógica Proposicional
64
Una teoría en una lógica es una secuencia de fbfs, F1,F2, ...,Fn, tales que para cada
fórmula Fi, esta es ya sea un axioma de la teoría o una consecuencia lógica de F1,F2,
...,Fi-1.
En una teoría existen, en efecto, un conjunto de fórmulas que son dadas por verdaderas,
denominadas axiomas, y un conjunto de fórmulas que son consecuencia lógica de los
axiomas, que son llamadas teoremas.
3.4.4.2 Tablas de verdad
Una forma de probar que (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 el valor de verdad de la fórmula para 2n interpretaciones, siendo n el número de
variables que involucra.
Cuando n es pequeña, el cálculo de los valores de verdad de la fórmula para todas las
interpretaciones, puede organizarse fácilmente en una tabla de verdad. El proceso, a
grandes rasgos consiste en construir una tabla donde se colocan, en el encabezamiento de
cada columna, de izquierda a derecha, una a una, desde la más simple hacia la más
compleja, las sub-fórmulas que conforman la fórmula (quedando las variables de la fórmula
en las primeras columnas). Luego se llena cada fila iniciando las casillas que corresponden
a las variables con una de las posibles asignaciones de valores de verdad a dichas variables;
y más luego se llenan las casillas que siguen, de izquierda a derecha, con base en la
semántica del conector principal de la fórmula de la columna.
Cuándo las variables de la tabla representan los átomos, ella tiene entonces una fila por
cada una de las interpretaciones más uno (2n+1), y una columna por cada una de las
subfórmulas de la fórmula (una columna para cada variable y una columna para cada
conector). Para más detalles y ejemplos ver sección 1.3.5 de [Grassmann 97].

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

Esta proposición posee una propiedad interesante que vale la pena resaltar. Todas las asignaciones
posibles de sus variables, hacen a la proposición verdadera. Luego, P((P¬R)R(¬PQ)) es una
tautología. La importancia de las tautologías radica en su uso a la hora de definir las reglas de inferencia
Capítulo 3: Lógica Proposicional
65
del razonamiento lógico.
En la siguiente tabla de verdad se puede verificar que Q es consecuencia lógica de las proposiciones P y
PQ. Veamos:

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(PQ) es verdadero (solo la
primera fila), también lo es Q, que es la consecuencia. Nótese que Q es verdadera para otras
interpretaciones, pero esto es permitido por la definición.
Para ilustrar lo anterior, probaremos mediante una tabla de verdad, que R es consecuencia lógica de las
proposiciones 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 pruebas 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R)P, con la columna bajo R, notando que siempre que la primera es verdadera también lo
es la segunda. Como segunda prueba basta computar la séptima columna que muestra que
((PQ)(QR)P)R es una tautología, lo que corresponde a la segunda definición. Finalmente la
última columna muestra que (PQ)(QR)P¬R es una contradicción.
Nótese que tal como se explicó en el Ejemplo 31¡Error! No se encuentra el origen de la referencia.
una fbf que es verdadera [o falsa] para todas las combinaciones de valor de verdad de sus variables, sean
estas átomos o no, es una tautología [o contradicción].

3.4.4.3 Criterios demostrativos


Las tablas de verdad parecen un buen método para llevar a cabo las pruebas, 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.
Capítulo 3: Lógica Proposicional
66
Una alternativa a las tablas de verdad es la de usar “reglas de inferencia” o “criterios de
demostración”. Una regla de inferencia es un patrón que permite establecer si entre unas
formulas específicas existe una relación de “demostración” o “inferencia”. Aplicando las
reglas de inferencia podemos establecer si una fbf G puede demostrarse o inferirse a partir
de otras fbfs F1,F2, ...,Fn. Simbólicamante: F1,F2, ...,Fn |- G .
Una regla de inferencia toma, en general, la forma siguiente:

Premisa1
Premisa2
..
Premisan
_______________________________________
Conclusión
Dónde:
Premisai: Son fbfs en las que las variables, denominadas “meta variables
proposicionales”, representan otras fbfs.
Conclusión Es una fbf en las que en las que las meta variables proposicionales,
representan las mismas fbfs que representaban en las premisas.
Definición:
Un conjunto de reglas de inferencia se denomina un “sistema deductivo”

3.4.4.3.1 Completitud y consistencia.


Un sistema deductivo es “completo”, si todo lo que es consecuencia lógica puede
demostrarse como tal, usando los criterios deductivos planteados.
Si todo lo que se demuestra usando los criterios deductivos planteados es realmente
consecuencia lógica, se dice que el sistema deductivo es “consistente”.
Así, un sistema deductivo es completo y consistente si se cumple que:
(F1,F2, ...,Fn |- G)  (F1,F2, ...,Fn |= G)
En [Hut 2004] se presenta un conjunto de criterios deductivos para la lógica de
proposiciones denominado “deducción natural” y se demuestra que es completo y
consistente.
3.4.4.3.2 Tautologías como Reglas de Inferencia.
Nótese que las tautologías presentadas antes permiten plantear tautologías más complejas
substituyendo las letras Q, P, R etc... por fórmulas más complejas.
Cualquier substitución de las letras de una tautología básica, por otras fbfs (simples o
complejas) sigue siendo una tautología.
En efecto, si se descubre que existe una tautología que tenga la forma (F1F2F3F4...Fn)
 G y que existe una substitución de las letras que en ella ocurren (por fbfs), tal que las Fi
Capítulo 3: Lógica Proposicional
67
de la tautología (las partes separadas por “”) coinciden con algunas de las fbf de una
teoría, entonces la fbf correspondiente a la substitución de las letras de la G en la tautología
es consecuencia lógica de la teoría.
Así las tautologías se pueden usarse para derivar reglas de inferencia.

La tautología

P(PQ)Q
Tiene la forma (F1F2)G , con F1=P, F2=(PQ) y G=Q.
Por tanto una secuencia {F1, F2, F3, F4, ..., Fn} donde Fk tenga la forma FjG para j<k, tiene como un
posible teorema a G.
En este caso la regla de inferencia correspondiente a esta tautología es la siguiente:

A
AB
_______________________________________
B
Dónde: A y B : Son meta variables proposicionales que, representan otras fbfs.
Considere:

el niño cae
si el niño cae entonces el niño llora
_______________________________________
el niño llora

3.4.5 Formas Normales.


Las Formulas Bien Formadas de la lógica de predicados pueden ser de enorme complejidad
debido a la recursividad propia de los criterios formativos. Simplificar las fórmulas será
entonces, útil no sólo desde el punto de vista de la lectura de las aserciones, sino también
desde la posibilidad de automatizar las demostraciones.
En esta sección se presenta, bajo el concepto de “equivalencia semántica” la posibilidad de
transformar las fbf, a formas estandarizadas, que se han denominado “formas normales”.
3.4.5.1 Substitución de Subfórmulas.
Es fácil ver que si una subfórmula de una fórmula se substituye por otra semánticamente
equivalente, la fórmula resultante será también equivalente a la original. Esto es obvio si se
tiene en cuenta que en las tablas de verdad de ambas fórmulas las columnas asociadas con
las dos subfórmulas tendrán los mismos valores de verdad (para todas las interpretaciones),
y en consecuencia tendrán el mismo efecto sobre la fórmula a la que pertenecen.
Será, entonces, de gran utilidad una provisión adecuada de fórmulas equivalentes que
puedan ser usadas a la hora de transformar una fórmula cualquiera en otra 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
Capítulo 3: Lógica Proposicional
68
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)  (PQ)(PR) (b)P(QR)  (PQ)(PR) Leyes distributivas
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 Leyes del medio excluido y de
(a)P¬P  V (b)P¬P  F
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.
3.4.5.2 Forma Normal Conjuntiva (forma “clausal”).
La “forma normal conjuntiva” (FNC) reduce las fbfs a la conjunción de “cláusulas”, por lo
que diremos que las fbfs están en “forma clausal”. La importancia de esta forma normal es
debida a que bajo ciertas condiciones, una fbf en forma clausal constituye una
Capítulo 3: Lógica Proposicional
69
especificación lógica para la que es posible automatizar la demostración que establece, que
otra cláusula sometida como “consulta”, es consecuencia lógica de la especificación.
Definición:
Llamaremos “literal” a un átomo o la negación de un átomo.
Definición:
Llamaremos “cláusula” a una fbf compuesta por literales conectados por “” (una
“disyunción” de literales).
Definición:
Diremos que una fbf está en “forma normal conjuntiva” (o FNC) si y sólo si es
una serie de cláusulas conectadas por “” (una “conjunción de cláusulas).

Por ejemplo, si p, q, y r son átomos, todas las siguientes fórmulas están en forma normal conjuntiva:

(p¬qr)(¬pq)(¬rp)
Teniendo en cuanta que un átomo cualquiera p (ó q) es semánticamente equivalente a pF (ó qF) , p (ó
q) es considerado por si mismo una fbf en forma clausal y por tanto las siguientes fbf también lo son:

pq
F

Para toda fórmula en lógica proposicional, existe una fórmula en FNC, semánticamente
equivalente a ella. Esto no lo probaremos, pero exhibiremos un proceso que permite
encontrar una FNC equivalente para cualquier fórmula.
Basta seguir los siguientes pasos aplicando las equivalencias presentadas en la sección
anterior:
1. Eliminar todas las “” y “”, utilizando las equivalencias 1 y 2.
2. Si la fórmula resultante contiene cualquier subexpresión compuesta negada,
eliminar la negación utilizando las equivalencias 9 y 10.
3. Entonces, se utiliza sucesivamente la equivalencia 5(a) para llevar las “” hacia
adentro y las “” hacia afuera hasta encontrar una FNC.

Veamos un par de ejemplos para ilustrar el proceso de normalización:

¬((QP)¬R)
= ¬((¬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
Capítulo 3: Lógica Proposicional
70
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í:
Definición:
Diremos que una fbf está en “forma normal disyuntiva” (o FND) si y sólo si es
una disyunción de conjunciones de literales.
Siguiendo un proceso similar al presentado arriba, para cualquier fórmula se puede
encontrar una fórmula en forma normal disyuntiva equivalente. Queda en manos del lector,
intentarlo.

Resumen del Capítulo.


La lógica es una disciplina que busca la manera de entender y sistematizar el razonamiento,
que es una capacidad básica del ser humano. El objeto de la lógica NO es el de garantizar
la corrección o “veracidad” de las conclusiones que por su medio se obtengan.
Aristóteles pretende sistematizar el proceso de razonamiento mediante el “silogismo”. En
el silogismo el razonamiento se asocia a la “forma” de las frases en lugar de asociarse con
su significado, dando lugar al concepto de “lógica formal”. El padre de la lógica moderna
es George Boole quién inventó el “álgebra Boleana” fundamentándola en las operaciones
que pueden realizarse sobre el conjunto {0, 1}. El álgebra boleana expresa en un marco
algebraico las proposiciones aristotélicas y da cuenta tanto del razonamiento del silogismo
clásico como de otras formas modernas de razonar.
Desde la óptica de los lenguajes formales, una lógica no es otra cosa que un lenguaje con
un conjunto de propiedades útiles, a saber:
 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” (fbf) de las que no lo son. La sintaxis define el “alfabeto de
símbolos” que es el conjunto de símbolos que pueden ser utilizados para
formar las frases, y los “criterios formativos” que determinan las formas
válidas de ensamblar los símbolos para formar las fbf. Los criterios
formativos descomponen, de forma progresiva, una fbf en otras fbf más
elementales que, a su vez, se descomponen sucesivamente hasta llegar a los
símbolos del alfabeto. La estructura de composición de una fbf puede
representarse por medio de un “árbol sintáctico”.
Capítulo 3: Lógica Proposicional
71
 La existencia de una semántica precisa, que le da a cada fbf un significado
(o valor semántico) preciso, proyectándolas a un y sólo un elemento del
“dominio semántico”. El significado de una fbf está determinado por el
significado de sus componentes y la manera en que se ensamblan. Los
componentes más elementales pueden ser símbolos “lógicos” cuyo
significado es fijado por la lógica, y los símbolos “no lógicos” cuyo
significado depende de su “interpretación” en el contexto al que hacen
referencia las fbf de la lógica. La obtención del significado de una fbf con
base en el significado de sus componentes lo denominaremos “cálculo” (de
dicho significado). El valor semántico de una fbf que toma su significado en
el dominio {verdadero, falso} (el dominio “BOOL”), lo denominaremos
“valor de verdad” de la fbf. Al conjunto de interpretaciones para las que
una fbf toma como su valor de verdad el “verdadero” lo denominaremos
“semántica” de la fbf.
 La existencia de unas reglas de inferencia, que permitan obtener (o
“deducir”) fbfs nuevas con un significado específico (o “conclusiones”), a
partir de fbfs de significado previamente conocido o asumido (o
“premisas”). Las reglas de inferencia son ya sea “criterios
transformativos”, que permiten transformar una fbf a otra que tiene el
mismo significado, en todas las interpretaciones, por lo que se dice que son
“semánticamente equivalentes”; o ya sea los “criterios demostrativos”, que
permiten obtener nuevas fbf con un significado deseado, a partir de otras
fbfs de significado previamente conocido o asumido (sin que necesariamente
las primeras sean semánticamente equivalentes a las segundas).
En la lógica de proposiciones, el alfabeto de símbolos lo constituyen, las constantes {F,V} o
{falso, verdadero}, las letras del alfabeto con o sin subíndices. { a, a1, a2, … , b, b1, b2, … p,
p1, p2, … , q, q1, q2, …}, y los conectores diádicos { , , , } y el conector monádico {}.
Los ensamblajes se forman uniendo dos fbf con los conectores diádicos de notación infija,
y aplicando como prefijo a una fbf el conector monádico. Las constantes y las letras
constituyen las fbf más elementales o “átomos” del lenguaje. En una fbf con múltiples
conectores y átomos, se usa el árbol sintáctico para indicar cuales son las dos subfórmulas
unidas por cada conector diádico y la subfórmula a la que se aplica el conector monádico.
Para determinar el árbol sintáctico de una fbf escrita como una secuencia de símbolos del
alfabeto y conectores, se deben tener en cuenta tanto los paréntesis como un “orden de
precedencia” y “sentido de asociatividad” en la aplicación de los conectores.
En la lógica de proposiciones, el valor semántico de una fbf se da en el dominio {F,V} o
{falso, verdadero}. Los símbolos lógicos de la lógica de proposiciones son las constantes y
los conectores. El significado de las constantes corresponde con su igual en el dominio
semántico. El significado de los conectores básicos {, , } corresponde a la semántica
del cambio de signo, la adición y la multiplicación en la aritmética de los enteros base dos.
La semántica de los conectores derivados {, }, es definida con base en los conectores
básicos. Los símbolos no lógicos de la lógica de proposiciones son las letras y su
significado es definido por su interpretación (en el contexto). La interpretación le da un
valor de verdad a cada una de las letras que aparecen en las fbf. El número de posibles
interpretaciones de n letras es 2n. El significado de una fbf compuesta puede calcularse
Capítulo 3: Lógica Proposicional
72
para una interpretación dada de sus letras, aplicando las funciones que definen la semántica
de los conectores.
Las fbfs pueden clasificarse con base en el valor de verdad que tiene para todas las posibles
interpretaciones de sus átomos en: “tautologías” que siempre son ciertas, “contradicciones”,
que siempre son falsas, y “contingentes”, que a veces con ciertas y a veces con falsas.
En la lógica de proposiciones, las reglas de inferencias están asociadas con las tautologías y
las contradicciones, en particular:
 Si dos fbfs G y F son semánticamente equivalentes se cumple que G  F es
una tautología.
 Se dice que una fbf G es consecuencia lógica de un conjunto de fórmulas F1,
F2, F3,…,Fn si para todas las interpretaciones en que las Fi son ciertas G
también lo es. En este caso se cumple que la fórmula F1F2F3…FnG es
una tautología y la fórmula F1F2F3…FnG es una contradicción.

Ejercicios Propuestos.
3.6.1 De la lógica en general
( ) La lógica es el estudio del pensamiento humano.
( ) La lógica es una sola y es la ciencia de la verdad.
( ) La lógica es un lenguaje.
( ) La lógica tiene que ver con todos los tipos de lenguaje utilizado por los humanos.
( ) La lógica es el estudio de los todos los lenguajes compuestos por ensamblajes de
símbolos.
(…) Todo lenguaje de programación es una lógica.
(…) La lógica puede ser usada como lenguaje de programación.
(…) Toda lógica es un lenguaje de programación.
( ) Una lógica determina cuales ensamblajes de símbolos son correctos en el lenguaje
simbólico que le compete.
( ) La sintaxis de la lógica limita los símbolos que pueden ser usados en un lenguaje
lógico.
( ) El alfabeto de una lógica es en general limitado y el conjunto de fbfs es en general
ilimitado.
( ) El alfabeto de una lógica es en general ilimitado y el conjunto de fbfs es en general
limitado.
( ) Todo ensamblaje de símbolos del alfabeto propio de una lógica es una fbf en dicha
lógica.
( ) La sintaxis de la lógica la componen el alfabeto de símbolos y los criterios formativos
para sus posibles ensamblajes.
( ) La lógica determina un significado para todos los posibles ensamblajes de símbolos.
Capítulo 3: Lógica Proposicional
73
( ) Una fórmula atómica de una lógica es una formula cuyas partes constituyentes no son
formulas bien formadas.
( ) Los símbolos del alfabeto son los átomos de la lógica.
( ) El significado de una fbf depende del significado de los átomos que contiene.
( ) El significado de un ensamblaje correcto puede variar y por tanto es siempre
ambiguo.
( ) Todo ensamblaje de los símbolos de una lógica, que satisfaga los criterios formativos
de la lógica, es un átomo de dicha lógica.

3.6.2 Lógica de Proposiciones

3.6.2.1 ¿Verdadero o falso?.


(...) La única manera de conectar los átomos de la lógica de proposiciones es por medio
de los conectores lógicos.
(...) Ninguna fbf en lógica proposicional, puede tener dos conectores lógicos
consecutivos.
(...) En una fbf compuesta siempre es necesario usar paréntesis para saber a qué
subfórmulas se aplica(n) los conectores.
(...) Definir un orden de prioridad entre los conectores lógicos permite siempre establecer
cuáles son las subfórmulas a las que se aplica(n) los conectores, sin necesidad del uso
de paréntesis.
(...) Los paréntesis no son requeridos en ningún caso debido a la existencia de un orden de
precedencia entre los conectores.
(...) El orden de precedencia entre los operadores no es estrictamente necesario ya que es
posible indicar por medio de paréntesis a cuales subfórmulas se aplica cada conector.
( ) Defina el operador o exclusivo con base en los operadores básicos (“” y “”).
( ) La semántica de una formulas en lógica proposicional es el valor de verdad que les
corresponde.
( ) En lógica proposicional, el valor de verdad de los átomos se halla a partir del valor de
verdad asignado a un conjunto de fórmulas bien formadas.
( ) En lógica proposicional, el valor de un conjunto finito de fórmulas bien formadas, no
puede hallarse debido a que no es posible definir el valor de verdad de todos los
átomos.
( ) Una interpretación para los átomos de un conjunto de fbfs en lógica proposicional no
puede ser “modelo” de dicho conjunto si alguna de las fbfs del conjunto tiene valor de
verdad “falso” para otras asignaciones diferentes.
( ) Una interpretación de los átomos de una teoría en lógica proposicional no puede ser
“modelo” de una formula bien formada de la teoría cuando esta es insatisfacible.
Capítulo 3: Lógica Proposicional
74
( ) Una interpretación de los átomos de una teoría en lógica proposicional es “modelo”
de una formula bien formada de la teoría si esta tiene valor de verdad “verdadero”,
sólo para esa asignación.
( ) Una interpretación de los átomos de una teoría en lógica proposicional es “modelo”
de una formula bien formada de la teoría cuando esta es satisfacible.
( ) Toda fórmula “válida” es “satisfacible”.
( ) Toda fórmula “satisfacible” es “válida”.
( ) Toda fórmula no “insatisfacible” es “contingente”.
( ) Toda fórmula no “valida” es “contingente”.
( ) Toda fórmula no “valida” es “insatisfacible”.
( ) Una fórmula no puede ser consecuencia lógica de un conjunto de fórmulas cuando no
pertenece al conjunto.
( ) Una fórmula no puede ser “consecuencia lógica” de un conjunto de fórmulas si
pertenece a dicho conjunto.
( ) Cuando una fórmula pertenece a un conjunto de fórmulas siempre es consecuencia
lógica de dicho conjunto.
( ) Si existe una interpretación que no es modelo de un conjunto de fórmulas F pero si es
modelo de una fórmula G, entonces G no puede ser consecuencia lógica de F.
( ) Si un conjunto de fórmulas no tiene modelo, no puede tener tampoco consecuencias
lógicas.
( ) Si un conjunto de fórmulas es insatisfacible, la negación de una cualquiera de sus
fórmulas es consecuencia lógica de las demás.
( ) De una tautología sólo puede ser consecuencia lógica otra tautología.
( ) De un conjunto de fórmulas contradictoria, es consecuencia lógica cualquier otra
fórmula.
( ) Si H es consecuencia lógica de G y G es consecuencia lógica de F, entonces H es
consecuencia lógica de F.
( ) Si la formula G es consecuencia lógica de un conjunto de fórmulas F, entonces la
fórmula GF es una tautología.
Equivalencia semántica
( ) Si la formula F y la fórmula G son semánticamente equivalentes, entonces GF es
una tautología.
( ) Dos tautologías son siempre semánticamente equivalentes.
( ) Dos contradicciones no pueden ser semánticamente equivalentes.
( ) Si una formula G es consecuencia lógica de otra fórmula F, la fórmula G es
semánticamente equivalente a la fórmula F.
Capítulo 3: Lógica Proposicional
75
( ) Si H es semánticamente equivalente a de G y G es semánticamente equivalente a F,
entonces H es semánticamente equivalente a F.

3.6.2.2 Fórmulas bien y mal formadas


Señale cuales están bien formadas y cuáles no, indicando la razón.

P  Q  R
P  Q  R
P  Q    R

P  Q  P  R   Q
P  Q  P  R  Q
P  Q  P  R   Q

3.6.2.3 Árbol Sintáctico


Elabore el árbol sintáctico de las fórmulas siguientes, para los dos órdenes de precedencia
siguientes:
, , , , 
, , , , 
Usando primero como sentido de asociatividad -> y luego <-

PQ R
PQ R

PQPRQ
(P  Q)  (P  R  Q)

PQRPQRS
(P  (Q  R))   ((P  Q)  (R  S))

3.6.2.4 Prueba.
1- Pruebe que las fbfs del ejemplo Ejemplo 36 son tautologías, y luego expréselas como
reglas de inferencia.
2- Pruebe que las parejas de fbf presentadas en el numeral 3.4.5.1 son equivalencias
semánticas utilizando tablas de verdad:

3.6.2.5 Varios
1. Desarrollar la tabla de verdad para cada una de las definiciones de los conectivos lógicos.
(¬P, PQ, PQ, PQ y PQ).
2. Determinar cuáles de las siguientes son proposiciones atómicas y cuáles no. Si no lo son,
descomponerlas en sus átomos constituyentes y rescribirlas como fórmulas de la lógica
proposicional.
a) “Si la humedad es alta y la temperatura es alta, entonces uno no se siente
confortable.”
Capítulo 3: Lógica Proposicional
76
b) “Todos los artistas están locos.”
c) “Héctor es rico y José es rico y también feliz.”
d) “La colombina que se comió Camila era roja o naranjada.”
e) “Si Héctor está en la piscina, entonces José debe de estar también en la piscina.”
3. Dibujar el árbol sintáctico para la siguiente declaración de la lógica de proposiciones: “Si
no me equivoco, Héctor estaba en la piscina y José no, de manera que Sandra dijo
mentiras.”
4. Construir la tabla de verdad y el árbol sintáctico para las siguientes fórmulas:
(¬P(QP)P)(¬P(P¬Q))
(¬PQ)(P¬Q¬R)(¬P¬R)R
5. 7. Explicar por qué las primeras dos definiciones de consecuencia lógica son
equivalentes. (Pista: la razón yace sobre la definición de implicación y de tautología).
a) Qué relación existe entre las fórmulas (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.”
Capítulo 4
Lógica de Predicados
Capítulo 4: Lógica de Predicados.
78
Introducción
Para utilizar proposiciones que se refieran a propiedades de conjuntos (o individuos) y
poder derivar de ellas proposiciones que se refieran a individuos (o conjuntos), se requieren
elementos adicionales a los de la lógica proposicional.
Por ejemplo: de las proposiciones: “Todos los hombres son mortales” y “Juan es hombre”
se debe poder derivar que “Juan es mortal”; y, de las proposiciones “Juan es mortal”; y
“Juan es hombre”, se debe poder derivar que “Algún hombre es mortal”.
Para clarificar que las proposiciones más elementales (o atómicas), son afirmaciones (o
predicas) sobre individuos (u objetos), en la lógica de predicados se distingue lo que se
predica (v.g. un verbo) de los objetos sobre los que se predica (v.g. un sustantivo). De esta
manera el cálculo de predicados permite representar en una lógica las afirmaciones más
comunes del lenguaje natural, así:
 Uso de sintagmas verbales como medio para afirmar.
 Uso de sintagmas nominales para referirse a los objetos del mundo sobre los que
recae la afirmación.

Sintaxis de la lógica de predicados.


La lógica de predicados extiende la lógica de proposiciones introduciendo nuevas
construcciones sintácticas, así:
 Los “términos”, que se usan para hacer referencia a objetos del dominio de
interpretación. Los términos pueden ser simples “constantes” o “variables”, o
términos compuestos formados aplicándole “operadores” a otros téminos más
elementales.
 Los “predicados” que representan afirmaciones elementales (o “predicas”) que
recaen sobre los objetos del dominio de interpretación. Las proposiciones
elementales en lógica de predicados son, entonces, ensamblajes de términos por
medio de los predicados. Estos ensamablajes remplazan a las letras de la lógica
proposicional constituyéndose en las proposiciones atómicas. Los predicados y los
términos se ensamblan de la forma que lo indica la “plantilla” (o “perfil”) de cada
predicado. (v.g. “4 > 3”, se apoya en la plantilla, “_>_” y en los términos “4” y “3”
para modela la afirmación “cuatro es mayor que tres”; y “ama(juan,novia_de(jorge))”
se apoya en la plantilla “ama(_,_)” y en los términos “juan” y “novia_de(jorge)” para
modela la afirmación “Juan ama a la novia de Jorge”).
 Las fórmulas “cuantificadas” permiten usar los predicados para hacer afirmaciones
sobre conjuntos de objetos. Para ello los “cuantificadores” (con variables) se
aplican a fbf que involucran predicados y variables (v.g. para decir que: “Juan ama
todas las cosas” la lógica de predicados se apoya en el cuantificador “”, la variable
“X”, y la proposición atómica “ama(juan,X)”, así: “X ama(juan,X)” ).
Capítulo 4: Lógica de Predicados
79
4.2.1 Alfabeto de símbolos.
Para construir la lógica de predicados que usaremos en este trabajo, adicionaremos al
alfabeto de la lógica de proposiciones, los elementos que se describen a continuación.
4.2.1.1 Sorts o símbolos de tipo.
Son rótulos o palabras que han sido declaradas como símbolos de “sort” y representan
conjuntos de individuos o “tipos”. Nótese que un conjunto de individuos es siempre
subconjunto del conjunto universal U.
En lo que sigue los símbolos de sort serán usados, para restringir el tipo de los objetos
sobre los que hacen afirmaciones las proposiciones cuantificadas, restringir los objetos que
pueden ser representados por una variable o constante, y restringir los objetos a los que se
les puede aplicar una operación. De no declarase el sort de dichos objetos se asume que su
tipo es el conjunto universal.
A una lógica, proposición, variable u operación que no esté restringida por el tipo de los
objetos a los que refiere, la calificaremos como “atipada”, en caso contrario será “tipada”.

Son símbolos de sort los siguientes:

Persona, Int, Hombre

4.2.1.2 Términos.
Son construcciones sintácticas que representan los objetos sobre los que recae el predicado.
En una lógica tipada cada término debe estar asociado a un tipo.
Se fundamentan en los símbolos siguientes.
4.2.1.2.1 Constantes
Símbolos que permiten hacer referencia a objetos particulares. Las constantes en una
lógica tipada deben estar asociadas con su tipo (ya sea de forma explícita por medio de una
“declaración” o de forma implícita con base en la forma de la constante. En lo que sigue
usaremos símbolos especiales o rótulos que empiezan con minúscula para las constantes.

Son constantes las siguientes:


juan, maria, 35
Que de ser necesario serán declaradas asociándolas a un sort:
juan, maria : Persona
35 está implictamente asociada con el tipo Int

4.2.1.2.2 Variables.
Capítulo 4: Lógica de Predicados.
80
Símbolos que permiten predicar sobre objetos indeterminados de un tipo para hacer
afirmaciones sobre el tipo. Las variables en una lógica tipada deben ser declaradas y
asociadas con su sort
Usaremos letras mayúsculas para representar las variables, y en algunos casos se indicará
de forma explícita su tipo.

Son variables las siguientes:

X, Y, Z:Int

4.2.1.2.3 Términos compuestos.


Son ensamblajes de símbolos de funciones u “operadores” con variables y constantes. El
capítulo siguiente trata de forma detallada este tipo de término.

Son términos compuestos los siguientes:

X+Y
novio_de(maria)

4.2.1.3 Predicados y plantillas.


Definen lo que se dice, modelando las afirmaciones elementales del lenguaje natural.
4.2.1.3.1 Predicados.
Son uno o varios símbolos (palabras y signos de puntuación) que corresponden a una
afirmación o “predica”83. Usaremos, palabras en minúscula con predicados concretos,
letras en minúscula para referirnos a predicados indeterminados, y símbolos especiales para
predicados destacados.

Son predicados los siguientes:

ama, persona, p, q, =, _ ama a _ de forma _

4.2.1.3.2 Plantillas de los predicados y operadores .


Para cada predicado, su “plantilla” determina la forma como se ensamblan el predicado (u
operador 5.2.1) y los términos para formar una proposicion atómica.
La plantilla de un predicado aporta los siguientes elementos de información:

83En lo que sigue sólo indicaremos de forma explícita que una construcción constituyue un predicado cuando ello no es
obvio del contexto en que se usa.
Capítulo 4: Lógica de Predicados
81
 El número de términos que se deben ensamblar con el predicado para definir una
proposición atómica (“aridad” del predicado), que en nuestro caso será constante
para cada predicado84.
 Sort asociado a cada uno de los términos ensamblados por el predicado.
 Símbolos que forman el predicado y la manera como se disponen en relación a los
términos que ensambla.
 Lugar y posición (número de orden) que le corresponde a cada uno de los términos
en el marco de la disposición de los símbolos que forman el predicado.
Distinguiremos dos tipos de plantilla:
 Las “plantillas estándar”, que disponen en sucesión los elementos, colocando
primero un símbolo para el predicado, y luego entre “( )” una lista con los términos,
separados por “,”.dándole a cada término una posición fija en la lista.
 Las “plantillas infijas” que distribuyen los símbolos del predicado y los términos de
forma arbitraria, fijando para cada término un lugar en el ensamblaje (que en lo que
sigue señalaremos con el símbolo “_”). La plantilla infija determina un número de
orden para cada uno de los lugares (por ejemplo contándolos de izquierda a
derecha), para darle a cada término una posición fija en el ensamblaje.

ama(_,_) : Persona, Persona


Es una plantilla estándar tipada de aridad 2 para el predicado ama:
_=_
Es una plantilla infija de aridad 2 para los predicados de igualdad
p(_,_) : Int, Int
Es una plantilla tipada estándar de aridad 2 para un predicado indeterminado

Los espacios disponibles se consideran ordenados (cada uno corresponde a un entero entre
{1..aridad}). El significado de la proposición está asociado a dichas posiciones, por lo que
dos proposiciones de un predicado sobre los mismos objetos en posiciones diferentes de la
plantilla pueden tener, en general, diferente significado.
4.2.1.4 Cuantificadores.
Son los símbolos “ ” denominado “para todo”, y el símbolo “ ” denominado “existe”.
Se usan en combinación de una o varias variables y una fbf para formar una fbf
“cuantificada”
4.2.2 Criterios Formativos de las fbf en lógica de predicados
Un predicado se ensambla con uno o varios términos de la forma que lo indica su plantilla
para formar una una proposición atómica. Las fbfs, en general, se ensamblan con los
conectores de la lógica de proposiciones de la misma forma que en dicha lógica. Cualquier

84 En general una plantilla con las mismas palabras y distinta aridad puede considerarse asociada con un predicado
diferente.
Capítulo 4: Lógica de Predicados.
82
fbfs pueden además ensamblarse con cuantificadores y variables para formar fbfs
“cuantificadas”.
4.2.2.1 Criterios formativos de una proposición atómica en lógica de predicados.
Una plantilla de predicado en el que cada uno de los símbolos de espacio disponible ha sido
substituido (o “instanciado”) por un término del tipo correspondiente al espacio, es una fbf
atómica en lógica de predicados.
A los términos que instancian los espacios de una plantilla de predicado se les denomina
“argumentos” del predicado.
Restricciones:
 En una lógica de predicados tipada, los argumentos deben ser del tipo previamente
asociado al espacio que ocupan en la plantilla.
 El orden en que aparecen los argumentos en los espacios de la plantilla es
importante:
Así dos proposiciones definidos sobre la misma plantilla y con los mismos
argumentos son proposiciones diferentes si el orden de los argumentos es diferente.

ama(juan, maria) tiene un significado diferente a ama(maria, juan) .


X>4 difiere de 4>X

4.2.2.2 Uso de cuantificadores.


Un cuantificador seguido de una variable y una fbf a la que se aplica, es una fbf
cuantificada. La variable del cuantificador puede estar asociada a un tipo, así:
 <variable>[:<tipo>] <fbf>
 <variable>[:<tipo>] <fbf>

Son fbf las fórmulas siguientes:

X ama(X,maria)
Y:Hombre ama(Y,juan)
X:Int ((X+1)>X)

Nótese que una fbf cuantificada es una fbf, y por lo tanto puede ser también cuantificada.

Son fbf las fórmulas siguientes:

Y X ama(X,Y)
Capítulo 4: Lógica de Predicados
83
X Y:Hombre ama(Y,X)
X Y ama(X,Y)
X:Int Y:Int (X+Y)=(Y+X)

Para simplificar la escritura de las fbf donde se repite el mismo cuantificador sobre
variables del mismo tipo, escribiremos la lista de las variables luego de una sola ocurrencia
del cuantificador.

La última fbf del ejemplo anterior puede escribirse de la forma siguiente:

X,Y:Int (X+Y)=(Y+X)

4.2.2.2.1 Alcance de un cuantificador.


La fbf a la que se aplica el cuantificador (con su variable) se denomina el “alcance del
cuantificador”.
Para determinar el alcance de un cuantificador es necesario tener en cuenta su orden de
prioridad en relación con la de los conectores que ocurren en la fórmula. Por defecto
usaremos en lo que sigue el siguiente orden de prioridad.
, , , , , ( / )
Donde los operadores “” y “” , tienen la misma prioridad y, por tener la fbf a la que se
aplican a la derecha, se asocian en sentido derecha-izquierda (<-), aplicándose primero el
cuantificador más a la derecha.

En las fbfs siguientes:


X (ama(X,maria)  ama(Y,juan))
X ama(X,maria)  ama(Y,juan)
El alcance del X es ama(X,maria)  ama(Y,juan).
En las fbfs siguientes:
X ama(X,maria)  Y ama(Y,juan)
X ama(X,maria)  (Y ama(Y,juan))
X (ama(X,maria)  Y ama(Y,juan))
El alcance del X es: ama(X,maria)  Y ama(Y,juan).
En la fbf siguiente:
(X ama(X,maria))  Y ama(Y,juan)
El alcance del X es restringido por los paréntesis a: ama(X,maria).
En las fbfs siguientes:
X (Y ama(X,maria)  ama(Y,juan))
X Y ama(X,maria)  ama(Y,juan)
El alcance del cuantificador X es: Y ama(X,maria)  ama(Y,juan), y.el alcance del cuantificador Y
Capítulo 4: Lógica de Predicados.
84
es: ama(X,maria)  ama(Y,juan).
El árbol sintáctico de estas dos fbfs es el que se muestra a continuación:

X

Y

ama ama
a
X maria Y juan
a3

Aunque los ciantificadores deben tener igual prioridad y deben tener un sentido de
asociatividad de derecha a izquierda, nótese que el orden de prioridad con respecto a los
conectores es arbitraria, por lo que podríamos darles otra precedencia a la presentada arriba
(cambiando el significado de la fbf)85.

85En [Hut 04], los cuantificadores tienen la mayor precedencia (junto con la negación). En este trabajo, sin embargo,
adoptaremos la estrategia de las notaciones VDM [Jones 90] y Z [Woodc 96], donde al tener los cuantificadores la mínima
precedencia, se minimiza el uso de paréntesis.
Capítulo 4: Lógica de Predicados
85

Bajo el orden de precedenciasiguiente:


( / ), , , , , 
X (Y ama(X,maria)  ama(Y,juan))
X Y ama(X,maria)  ama(Y,juan)
El alcance del cuantificador X en la primera fbf es inducido por los paréntesis a: Y ama(X,maria) 
ama(Y,juan), mientras que en la segunda fbf es sólo: Y ama(X,maria).
El alcance del cuantificador Y, por su parte, es en ambas fbfs la poposición: ama(X,maria).
El árbol sintáctico de la segunda fbfs es el que se muestra a continuación:

X
ama

Y Y juan

ama

X maria

4.2.2.2.2 Ocurrencias de variable.


Una ocurrencia de la variable es la aparición de dicha variable en un lugar distinto a la que
precede el cuantificador.

En:
X (ama(X,Y)  ama(Y,juan))
Hay dos ocurrencias de Y y una de X

4.2.2.2.3 Ligaduras de las variables al cuantificador.


Capítulo 4: Lógica de Predicados.
86
Una variable que ocurre dentro del alcance de un cuantificador aplicado sobre dicha
variable (y sólo uno) es una “variable ligada” a dicho cuantificador.

En la fbf siguiente, la ocurrencia de X es ligada al X


X (ama(X,Y)  ama(Y,juan))

Una variable que ocurre dentro del alcance de dos o más cuantificadores aplicados sobre la
misma variable queda ligada al cuantificador (sobre la variable) mas cercano en el árbol
sintáctico.

En la fbf siguiente, la primera ocurrencia de Y está ligada al Y mientras que la segunda está ligada al
Y.
Y (Y ama(X,Y))  ama(Y,juan)

Una variable que no es ligada es una “variable libre”


Si una fbf tiene variables libres, se denomina “fbf abierta”. Si una fbf no es abierta se
denomina “fbf cerrada”.

En la fbf siguiente, las dos ocurrencias de Y son libres y la ocurrencia de X es ligada al X.
X (ama(X,Y)  ama(Y,juan))
La primera de las fbf siguientes es abierta, mientras que la segunda es cerrada:
X (ama(X,Y)  ama(Y,juan))
Y X (ama(X,Y)  ama(Y,juan))

En lo que sigue usaremos letras en mayúscula para representar fbfs indeterminadas,


atomicas o compuestas, colocando entre paréntesis las variables líbres de dichas fórmulas.

En la fbf siguiente, P(X,Y) refiere a una fbf indeterminada en la que X y Y son variables libres.
X P(X,Y)
Nótese que el cuantificador X liga todas las ocurrencias libres de la variable X en P(X,Y), quedando la
variable Y libre.

4.2.2.3 Uso de conectores


Tal como en el cálculo de preposiciones, pueden formarse fbfs más complejas utilizando
los conectores del cálculo de proposiciones con cualquier fbf ya sea cuantificada o no
cuantificada.
Capítulo 4: Lógica de Predicados
87

Considere las fbfs siguientes:

(X ama(X,maria))  (Y ama(Y,juan))


Y (X (ama(X,maria))  ama(Y,juan))

Semántica en la lógica de Predicados.


El valor de verdad de una fbf en lógica de predicados, depende del valor de verdad de los
átomos sin variables que puedan ser fomados con los predicados que involucra. A estos
átomos los denominaremos en lo que sigue como “átomos base”. Así, el valor de verdad de
las fbfs se calcula con base en el valor de verdad de los “átomos base”.
Sin embargo, para definir el valor de verdad de los átomos base es, ahora, necesario darle
un significado a las constantes, las variables, y a los predicados, como símbolos no lógicos;
y una semántica a los cuantificadores como símbolos lógicos.
Así, la interpretación de un conjunto de fbfs en lógica de predicados esta definida con base
en los conjuntos de objetos sobre los que ellas predican, y por la manera como sobre dichos
conjuntos se define el significado de las constantes, variables y predicados.
La semántica de los cuantificadores, por su parte, es independiente de la interpretación y
propia de la lógica de predicados como tal.
4.3.1 Dominio de interpretación.
Es el conjunto de todos los objetos sobre los que recaen (o predican) los predicados. Este
conjunto lo denominaremos el “conjunto universal” (U)
Por cada símbolo de sort, debe existir un conjunto que es subconjunto U.

Son símbolos de sort los siguiente:


HumanosU, IntU

Cada símbolo de constante se asocia a un (y solo a un) objeto del dominio de


interpretación. Este objeto constituye entonces el valor semántico de la constante. Si la
constante es declarada como perteneciente a un tipo, el objeto que se le asocia debe
pertenecer al conjunto asociado con su tipo.
4.3.2 Significado de las variables.
Cada símbolo de variable se asocia a un objeto indeterminado del dominio de interpretación
en el conjunto asociado al tipo de la variable (U si la variable no es tipada). Este conjunto
constituye entonces el valor semántico de la variable.
Nótese que si a una variable libre se le asocia un objeto específico del dominio
correspondiente a su tipo por medio de un predicado de “asignación”, ella adquiere el
mismo rol que la constante asignada, por lo que en lo que sigue no considrearemos este
Capítulo 4: Lógica de Predicados.
88
caso. La asignación de valores a variables, en la lógica de predicados, es útil sólo como un
medio para facilitar la edición de la fbfs donde ocurre (ya que al cambiar la asignación se
cambia la fbf a otra similar, que difiere sólo en las ocurrencias de la constante asignada a la
variable).
El significado de una fbf con variables es discutido más adelante en el capítulo.
4.3.3 Extensión de los predicados sobre el dominio.
El valor de verdad de los átomos base, depende de la naturaleza del conjunto de objetos
sobre los que predican. En ese sentido un predicado base puede ser verdadero para un
dominio de interpretación y falso para otro (v.g. ama(juan,maria), puede ser verdadero en un
grupo de personas y falso en otro).
Al valor de verdad de todos los átomos base para un predicado específico, lo
denominaremos la “extensión” de dicho predicado. Es claro que al cambiarse el dominio
de interpretación (o el significado de las constantes en el dominio) puede cambiar la
extensión de los predicados.
Una forma simple de expresar las extensiones de los predicados de una lógica, es la de
presentar el conjunto de los átomos base que son ciertos para cada predicado, y asumir que
los no presentados son falsos (que es lo mismo que tener una Base de Datos o de “hechos”).
Este conjunto es, además, isomorfo con el conjunto de tuplas formadas con los términos de
los átomos bases ciertos en la extensión, tomándo dichos términos en la posición que les
señala la plantilla del predicado. Este conjunto es de hecho un subconjunto del producto
cartesiano de los tipos correspondientes a los términos, o sea una “relación” en el sentido
matemático. Así, en lo que sigue usaremos el término “extensión de un predicado”, para
referirnos tanto a el conjunto de los átomos base del predicado que tienen como valor de
verdad el verdadero, como a la relación a la que este conjunto correspone.
El valor semántico de un predicado, es entonces su extensión en el marco de una
interpretación dada.
4.3.4 Valor de verdad de las FBF.
4.3.4.1 Átomos base.
Dado por la extensión de los predicados.
4.3.4.2 Fórmulas Cuantificadas.
El valor de verdad de la fórmula X P(X), es el valor de verdad de la fbf resultante de
conectar con “” todas las substituciones posibles de las variables libres de P ligadas con X
El valor de verdad de la fórmula X P(X), es el valor de verdad de la fbf resultante de
conectar con “” todas las substituciones posibles de las variables libres de P ligadas con X.

Las fórmulas (atipadas) siguientes:


X Y ama(X,Y) (Toda persona ama a alguien)
X Y ama(Y,X) (Toda persona es amada por alguien)
X Y ama(X,Y) (Existe alguien que ama a todos)
X Y ama(Y,X) (Existe alguien que es amado por todos)
Capítulo 4: Lógica de Predicados
89
En un dominio de interpretación compuesto por tres personas: {c,l,j}, Equivaldrían a las siguientes
formulas sin cuantificadores:

X Y ama(X,Y) 
(ama(c,c)  ama(c,l)  ama(c,j) ) 
(ama(l,c)  ama(l,l)  ama(l,j) ) 
(ama(j,c)  ama(j,l)  ama(j,j) )

X Y ama(Y,X) 
(ama(c,c)  ama(l,c)  ama(j,c) ) 
(ama(c,l)  ama(l,l)  ama(j,l) ) 
(ama(c,j)  ama(l,j)  ama(j,j) )

X Y ama(X,Y) 
(ama(c,c)  ama(c,l)  ama(c,j) ) 
(ama(l,c)  ama(l,l)  ama(l,j) ) 
(ama(j,c)  ama(j,l)  ama(j,j) )

X Y ama(Y,X) 
(ama(c,c)  ama(l,c)  ama(j,c) ) 
(ama(c,l)  ama(l,l)  ama(j,l) ) 
(ama(c,j)  ama(l,j)  ama(j,j) )
Y las interpretaciones siguientes, representadas de forma tabulada (en la que la persona correspondiente
a una fila ama a la persona correspondiente a la columna), serían modelo de las formulas:

La interpretación:
c l j
c x
l x
j x
Es modelo de X Y ama(X,Y)
La interpretación:
c l j
c x
l x
j x
Es modelo de X Y ama(Y,X)
La interpretación:
c l j
c x x x
l
j
Es modelo de X Y ama(X,Y)
La interpretación:
c l j
c x
l x
j x
Capítulo 4: Lógica de Predicados.
90
Es modelo de X Y ama(Y,X)

Nótese que si la variable se asocia explícitamente con un tipo, sólo se tienen en cuenta los
objetos del tipo. En caso contrario se asume que la variable recorre todos los objetos de U.
Una consecuencia de la semántica de una fbf con cuantificadores, es que si ellos cuantifican
una variable definida sobre un conjunto infinito (con un número infinito de objetos como es
el caso de los números enteros), el valor de verdad de la fbf no se puede calcular. En estos
casos el valor de verdad de las fórmulas sólo puede establecerse por demostración.
Todo lo que se puede expresar en una lógica tipada, se puede expresar también en una
lógica atipada usando “predicados de tipo”. Estos predicados se aplican a un sólo
argumento y son ciertos cuando el término es del tipo referido en el predicado. Así, una
lógica tipada se puede considerar como un caso particular de una lógica atipada o una
forma de “azúcar sintáctico” [Bourbaki 50], sobre la lógica atipada (que es la lógica
clásica).
Así, las fórmulas siguientes son semánticamente equivalentes ():
X:T P(X)  X (XTP(X))
X:T P(X)  X (XTP(X))
Por la semántica del “”, si T es el conjunto vacío { }, entonces se cumple para cualquier
P(X) que:

(X:{ } P(X))  V

Por la semántica del “” si T es el conjunto vacío { }, entonces se cumple para cualquier
P(X) que:

(X:{ } P(X))  F

4.3.4.3 fbfs complejas


Se debe calcular con base en las tablas de verdad de los conectores y los criterios para
calcular el valor de verdad de las fórmulas cuantificadas.
4.3.5 semántica de un conjunto de fbfs.
Tal como se definió en 3.3.2, con el término “semántica” en este trabajo hacemos
referencia al conjunto de interpretaciones para las que un conjunto de fbfs con significado
en el conjunto {verdadero, falso}, tienen como valor el “verdadero”.
Cabe entonces resaltar que para la lógica de predicados esto se traduce al conjunto de
posibles dominos de interpretación, proyecciones de las constantes a elementos del
dominio, y proyecciones de los predicados a relaciones en los dominios, para los que las
fbfs toman como valor el “verdadero”.

Inferencia en lógica de Predicados.


Si bien el valor de verdad de una fbf en lógica proposicional se debería poder calcular a
partir del significado de sus átomos. Este cálculo es cada vez más difícil de efectuar a
medida que crece el número de átomos. Además, para el caso de las fbf cuantificadas, el
número de átomos crece sustancialmente, ya que ellos están ligados a las extensiones de los
Capítulo 4: Lógica de Predicados
91
predicados. Para dominios de interpretación con un número infinito de elementos (v.g. el
conjunto de los números enteros), el número de átomos en una fórmula cuantificada puede
ser, en efecto, infinito.
En caso de no poderse calcular el valor de verdad de las fbfs con base en el valor de verdad
de los átomos, el único recurso que queda es el de la demostración. Como la demostración
parte de fbf dadas por ciertas es usual que para dominios de interpretación con infinito
número de elementos, se deba partir de fbfs cuantificadas que son consideradas ciertas sin
verificación ni demostración alguna. A estas fbfs se les denomina “axiomas”, y a partir de
ellos por demostración se construyen las “teorías” (ver 3.4.4.1 ).
4.4.1 Criterios de demostración propios de la lógica de predicados.
Los criterios demostrativos de la lógica de predicados se centran en la particularización de
las fórmulas cuantificadas y en la gneralización de las fórmulas con constantes. En
particular:
1. 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, y siempre y cuando estas
variables no coincidan con variables libres en la fbf original.

X Y ...P(X,Y,..)
––––––––––––––––––––––––––––
V1 V2 ...P(fx(V1, V2..,a,b..), fy(V1, V2..,a,b..),...)
Donde V1, V2,.. son variables, a,b.. son constantes y fx(V1, V2..,a,b..), fy(V1, V2..,a,b..)... son
términos del sort de X, Y, etc..
2. Prueba de existencia: De una fbf que tenga un cuantificador universal asociado a una
variable, se deduce la fórmula que remplaza el cuantificador universal por el existencial
para dicha variable. De una fbf donde ocurra un término base se deriva una fbf que
cuantifica existencialmente a una variable que substituye el término base (en cualquiera
de sus ocurrencias).
3. Generalización de predicados a una variable: De una fbf en la que no aparece una cierta
variable como variable libre. Puede deducirse la fbf que tiene un cuantificador sobre la
variable y como alcance del cuantificador a la fórmula.

Cláramente el criterio de particularización se cumple tanto para variables tipadas universalmente o no, y
siempre y cuando la particularización ocurra a un valor del tipo de la variable.
Se deja al lector la tarea de interpretar los otros dos criterios para el caso de variables no tipadas
universalmente, sino sobre un tipo TU .

4.4.2 Formas Normales en Lógica de Predicados.


La definición de equivalencia lógica dada en la sección 3.4.4.1, también es válida para la
lógica de predicados.
Capítulo 4: Lógica de Predicados.
92
Además, como las equivalencias presentadas en la sección 3.4.5.1 provienen de la
definición de los conectores, dichas equivalencias también son válidas en la lógica de
predicados. Lo que hace falta es generalizar algunas equivalencias de la lógica de
proposiciones para asociarlas al uso de cuantificadores. Para una explicación más detallada
de los contenidos de esta sección el lector debe referirse a [Chang 73]
4.4.2.1 Equivalencia Semántica.
Si representamos a una fbf A que contiene entre sus variables libres a X, como A[X] (en
lugar de escribir A(..,X,..) ), y a una fbf B que no contiene entre sus variables libres a X;
entonces tenemos las siguientes equivalencias, donde “Q” representa “” o “”
indiferentemente:

Equivalencia Nombre común

11 (a) (Q X A[X])B  Q X A[X]B


11 (b) (Q X A[X])B  Q X 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  una interpretación arbitraria en un dominio D. Hay que considerar 2 casos:
1. Si ¬(X A[X])es verdadera en : entonces X A[X] es falsa en . 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 : entonces X A[X] es verdadero en . 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[X] y C[X] 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 se debe evitar
usar las equivalencias siguientes:
Capítulo 4: Lógica de Predicados
93
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 aparezca libre en C.

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


4.4.2.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”.
Una fbf se dice estar en una forma normal Prenex si y solo si la fbf tiene la forma:
Q1X1...QnXn M
Donde cada Qi, i=1,...,n, es ya sea un “” o un “”, y M es una fbf que no contiene ningún
cuantificador.

Por ejemplo, éstas son algunas fbfs en forma normal Prenex:

X H(X)M(X)
X Y p(f(X,Y),f(Y,X))
x y z P(x,y)R(z)

Para llevar cualquier fbf a una forma normal prenex equivalente se deben llevar a cabo los
siguientes pasos:
1. Eliminar todas las “” y “”, utilizando las equivalencias 1 y 2 de la lógica
proposicional (ver 3.4.5.1).
Capítulo 4: Lógica de Predicados.
94
2. Si la fórmula resultante contiene cualquier subfórmula compuesta negada, llevar las
negaciones hasta los átomos de la subfórmula, utilizando las equivalencias 9, 10 de
la lógica proposicional y , 12(a) y 12(b), de la lógica de predicados hasta que solo
aparezcan átomos negados.
3. Llevar los cuantificadores hacia afuera utilizando las equivalencias 11 y 13, y si es
necesario renombrando variables para evitar la introducción de ligaduras entre
variables ligadas a cuantificadores diferentes (ver 0 ).

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 ¬P(X,Z)¬P(Y,Z)(U Q(X,Y,U)) Por eq. 11(a)
= 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 3.4.5.2.

Tipos de predicados y lógicas asociadas.


Si bien las nociones generales de predicado y de Fórmula Bien Formada, son suficientes
para llevar a cabo cualquier tipo de afirmación sobre los elementos de un á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 del capítulo 5.
 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
Capítulo 4: Lógica de Predicados
95
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 del capítulo 6.
 En la Lógica Dinámica 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 asociados será (en versiones futuras del trabajo) el objeto de
estudio del Tomo II.

Resumen del Capítulo.


En la lógica de predicados, las letras del alfabeto con o sin subíndices se substituyen por los
“predicados” como proposiciones atómicas de la lógica. Un predicado afirma la existencia
de una propiedad o relación entre objetos de un dominio de interpretación (que puede ser
infinito). Los objetos del dominio se representan por medio de “términos”, entre los
términos se encuentran las “constantes” y las “variables” de la lógica. Los términos pueden
estar asociados a “tipos” que son subconjuntos del dominio de interpretación. Un
predicado se obtiene a partir de un ensamblaje que denominamos “plantilla de predicado”,
substituyendo por términos los lugares señalados con “_” en la plantilla La plantilla puede
restringir el tipo del término que puede sustituir cada uno de los lugares señalados. Una fbf
en lógica de predicados constituye el “alcance del cuantificador” si está precedida por un
símbolo de cuantificador {, } seguido, (o “aplicado”), de (a) una variable. Si una
variable aparece en el alcance de un cuantificador aplicado a la misma variable, decimos
que esta “ligada”, en caso contrario es “libre”.
Las fórmulas cuantificadas permiten afirmar la existencia de una propiedad para el conjunto
de los objetos del tipo de la variable del cuantificador. En efecto, una fórmula cuantificada
es equivalente a la fórmula no cuantificada resultante de unir todas instancias86 del alcance,
con un “” para el caso del “”, y con un “” para el caso del “”.
La lógica de predicados adiciona dos criterios de demostración importantes; la ley de
Particularización que indica que de ser cierta una fbf cuantificada universalmente, también
es cierta la fbf para cualquier instancia de la variable cuantificada; y la Prueba de existencia
que indica que de ser cierta una fbf con una constante, también lo es la fbf con una variable
cuantificada existencialmente que substituya una o varias de las o las ocurrencias de la
constante.

Ejercicios Propuestos.
4.7.1 Verdadero o falso?
( ) La lógica de predicados permite deducir afirmaciones sobre miembros de un conjunto a
partir de afirmaciones sobre el conjunto.

86 Entendiendo como instancia la substitución de las ocurrencias de una variable por un elemento de su dominio.
Capítulo 4: Lógica de Predicados.
96
( ) Las proposiciones de la lógica de predicados, son afirmaciones sobre objetos o
conjuntos de objetos.
( ) En la lógica de predicados se distingue lo que se predica de los objetos sobre los que
se predica.
( ) La lógica de predicados permite representar de forma simplificada las afirmaciones del
lenguaje natural.
( ) La lógica de predicados no adiciona nuevos elementos al alfabeto de símbolos.
( ) Los operadores y operandos no hacen parte de la lógica de predicados ya que sólo se
usan en lógicas más avanzadas (v.g. aritmética, álgebra y cálculo).
( ) En la lógica de predicados lo que se predica se representa en la “plantilla” del
predicado y los objetos sobre los que se predica se representan por medio de
términos.
( ) Los términos de la lógica de predicados no tienen relación con los tipos, ya que estos
sólo existen en los lenguajes de programación.
( ) Las plantillas de los predicados no tienen relación con los tipos de los términos.
( ) El orden de los términos dentro de la plantilla del predicado es insustancial en la
lógica de predicados clásica.
( ) Los predicados de la lógica de predicados son átomos que pueden tener como partes
constitutiva a otros átomos de la lógica proposicional.
( ) Las proposiciones atómicas de la lógica de predicados, son plantillas de predicado
instanciadas sobre objetos específicos o indeterminados.
( ) Los cuantificadores sirven para calcular el valor de verdad de los términos.
( ) Los cuantificadores permiten llevar a cabo afirmaciones sobre conjuntos de objetos.
( ) El alcance de un cuantificador sólo puede ser un predicado.
( ) En la lógica de predicados no se usan conectores.
( ) El alcance de un cuantificador puede cambiar según su orden de prioridad relativo a
los conectores.
( ) En una fbf cuantificada, la aparición de una variable en cualquier parte de la fbf, se
denomina “una ocurrencia” de la variable.
( ) Una formula bien formada en lógica de predicados, no puede tener variables libres si
la fbf está dentro del alcance de un cuantificador.
( ) Una ocurrencia de una variable está ligada a un cuantificador cuando este cuantifica
la variable y la ocurrencia está en el alcance del cuantificador.
( ) Una ocurrencia de una variable que está en el alcance de un cuantificador sobre la
misma variable puede declararse como libre en la definición de la variable.
( ) Todas las ocurrencias de una variable en una fbf están ligadas a un mismo
cuantificador o todas son libres.
Capítulo 4: Lógica de Predicados
97
( ) Todas las ocurrencias de una variable en una fbf están ligadas a un mismo
cuantificador.
( ) Si en una formula bien formada hay una ocurrencia de una variable dentro del alcance
de un cuantificador ligado a la variable y otra ocurrencia por fuera del alcance de
dicho cuantificador, entonces ambas ocurrencias corresponderán, a la misma variable
luego de que esta sea asignada.
( ) Los átomos base de la lógica proposicional son plantillas de predicados instanciadas
con variables.
( ) La extensión de los predicados determina el valor de verdad de los átomos base de la
lógica de predicados.
( ) Las tablas de una Base de Datos pueden asimilarse a la extensión de los predicados de
una lógica de predicados.
( ) En cálculo de predicados no tiene sentido hablar de una Interpretación ya que no hay
variables proposicionales.
( ) La interpretación para un conjunto de fbfs en una lógica de predicados es el conjunto
de extensiones de los predicados que involucran.
( ) Sólo puede haber una interpretación para un conjunto de fbfs en una lógica de
predicados.
( ) Para conocer el valor de verdad de una fbf en lógica de predicados es necesario usar
siempre criterios de demostración.
( ) El valor de verdad de una fbf cuantificada con varios cuantificadores consecutivos, no
depende de la posición relativa de dichos cuantificadores.
( ) El valor de verdad de una fbf con un solo cuantificador sobre una variable y una sola
variable no cambia si se cambia la variable por otra diferente.
( ) El valor de verdad de una fbf con varios cuantificadores sobre varias variables no
cambia si se cambia la variable de uno de los cuantificadores y todas sus ocurrencias
en el alcance por otra variable diferente.
( ) Las variables ligadas deben ser asignadas a un valor del dominio antes de calcular la
semántica de una fórmula en lógica de predicados.
( ) Para calcular el valor de verdad de una fbf con variables libres, ellas deben ser
asignadas a una constante específica, además de definir la interpretación.
(...) Si hay dos cuantificadores seguidos sobre la misma variable, no es posible hallar la
semántica de la fbf.
4.7.2 Árboles sintácticos
Elabore el árbol sintáctico de las fórmulas siguientes, para los dos órdenes de precedencia
siguientes:
, , , , ,( / )
( / / ), , , , 
X ama(X,maria)  ama(Y,juan)
Capítulo 4: Lógica de Predicados.
98
(X ama(X,maria))  Y ama(Y,juan)
X ((Y ama(X,maria))  ama(Y,juan))
X Y ama(X,maria)  ama(Y,juan)
4.7.3 Variables Libres y Ligadas
Indique por medio de líneas las ligaduras entre las variables de las fórmulas y las del
cuantificador, dejando sin conectar las variables libres.
Haga el ejercicio para los dos órdenes de precedencia siguientes:
, , , , ,( / )
( / / ), , , , 
X ama(X,maria)  ama(Y,juan)
(X ama(X,Y))  Y ama(Y,juan)
X ((Y ama(X,Y))  ama(Y,juan))
X Y ama(X,maria)  ama(Y,X)
4.7.4 Cálculo de valores de verdad.
Asuma que la interpretación de los predicados p(X,Y) y q(X,Y) (donde X, Y toman sus valores
en el dominio {a, b}) está dada por las tablas siguientes (donde cada línea corresponde a un
átomo que toma el valor de verdadero y se asume que lo átomos no representados toman el
valor de falso):
q(x,y):
X Y
a a
b b

p(x,y):
X Y
a b
b a

Halle el valor de verdad de las fórmulas siguientes desarrollando los cuantificadores con
base en los átomos asociados a cada predicado:
X Y (p(X,Y)  q(Y,X))
4.7.5 Varios
5. Suponga que q(X) y r(X) representan “X es un número racional y “X es un número real”
respectivamente. Simbolizar las siguientes declaraciones de la lógica de primero orden:
a) “Todo número racional es un número real.”
Capítulo 4: Lógica de Predicados
99
b) “Algunos número reales son números racionales.”
c) “No todo número real es un número racional.”
6. Para la siguiente interpretación sobre el dominio D={1,2}:
Asignación para las constantes a y b.
a b
1 2

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

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)
Capítulo 5
Lógica Ecuacional
Capítulo 5: Lógica Ecuacional
102
Introducción.
En el Capítulo 4 se presentaron los conceptos básicos de la lógica de predicados. Allí se
definió una proposición atómica como un predicado que afirmaba algo sobre objetos
específicos o indeterminados del 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 “término” 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 los “operadores”. 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 clásica.

Términos.
Como ejemplo informal del uso de predicados en lenguaje natural se presentó en el
capítulo anterior, la frase “Juan ama a María”, señalando que contiene una afirmación
(“ama a”) que recae sobre dos entidades específicas (“Juan” y “María”) de un dominio de
interpretación específico (v.g. “los estudiantes del curso de Lenguajes Declarativos que se
dicta en el salón M3-206”).
En el lenguaje formal presentado, el predicado anterior se tradujo a la fbf
“ama_a(juan,maría)” que separa de forma más precisa la afirmación, de las entidades sobre
las que ella recae. La afirmación en si misma está representada por el símbolo de predicado
“ama_a”, y las entidades sobre las que ella recae están representadas por los símbolos
constantes “juan” y “maría”.
Como ejemplo de la expresividad del lenguaje formal se ilustró el uso de los
cuantificadores y variables para hacer afirmaciones sobre todos los miembros del dominio
de interpretación. Así la afirmación “Todos aman a María” se tradujo a la fbf “X
ama_a(X,maría)”, que se apoya en el símbolo variable “X” para referirse a todos los
miembros del dominio de interpretación.
Si bien las constantes y las variables le permiten a la lógica referirse a las entidades del
dominio de interpretación, ellas no permiten referirse a las relaciones existentes entre
dichas entidades, ni referirse a una entidad que tiene una cierta relación con otras. En
efecto, si entre los estudiantes del curso de Lenguajes Declarativo ocurren relaciones de
Capítulo 5: Lógica Ecuacional
103
noviazgo, habrá seguramente afirmaciones interesantes sobre el novio de alguien o sobre el
carácter general de los noviazgos. Las constantes y las variables son insuficientes para
representar estas afirmaciones en la lógica de predicados y razonar sobre ellas.
Para solucionar este problema la lógica de predicados ofrece una construcción sintáctica
que denominaremos “término”. El término incorpora las constantes y las variables como
términos elementales y permite su agrupación en términos más complejos usando
“operadores”.
5.2.1 Operadores.
Los operadores son símbolos de la lógica de predicados que se utilizan para construir
términos complejos a partir de términos más simples y, en últimas, a partir de las constantes
y variables. A cualquiera de los términos agrupados por un operador en un término, se le
denomina “operando”.
Al igual que ocurre con los predicados, cada operador se asocia a un “perfil” o “notación”.
El perfil de un operador es una plantilla (ver 4.2.1.3.2) que indica lo siguiente:
 El sort87 del término resultante de la aplicación del operador.
 El número y sort de los operandos.
 La posición relativa de cada uno de los operandos con respecto al operador.
En lo que sigue será relevante distinguir entre las plantillas con notación
“prefija” de las plantillas con de notación “infija” (ver 4.2.1.3.2).
 Los signos de puntuación que deben aparecer en un término construido con
base en el operador.
 Un número de orden de cada operando en la aplicación del operador, que
será usado como medio para hacer referencia a dicho operando (Usualmente
este número de orden no se hace explícito y se asume que corresponde al
orden de aparición del operando recorriendo el término de izquierda a
derecha). Ocasionalmente la plantilla asigna un nombre a cada uno de los
operandos a ser usado como referencia en lugar del número de orden.
Definición:
aridad - Número de términos que agrupa el operador.
Una plantilla de operador puede indicar una aridad variable para términos de un mismo
sort.

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

87 En lo que sigue usaremos la palabra “sort” para referirnos a un tipo.


Capítulo 5: Lógica Ecuacional
104
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 lector confunda los operadores con los predicados, ya que ambos tienen una forma sintáctica similar
(lucen parecidos pero son distintos). Un predicado puede considerarse un operador que al evocarlo representa un
valor de tipo boleano.
Como ejemplo introduciremos el predicado “novios”, aplicable a dos estudiantes, para afirmar que entre ellos existe
una relación de noviazgo.
Nótese que la introducción de un símbolo de la lógica, exige declarar el tipo de construcción a la que corresponde,
por tanto se sabe si un símbolo es de función (o de predicado) porque fue declarado como tal.

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.

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:

(+ _ _ _ ...)

5.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:
En palabras:
 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 debe está en concordancia con la
plantilla del operador, si los términos que agrupa corresponden en número y sort 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. En lo que sigue a los términos a los que se aplica el operador
Capítulo 5: Lógica Ecuacional
105
de un término los denominaremos “subtérminos”. Las constantes y los términos
compuestos que sólo tienen constantes los denominaremos términos "base".
Nótese que la aplicación de un operador es recursiva, permitiendo que los subté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.

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

Son instancias del operador “+” con notación infija las siguientes:

1+1
2+1+3

Son instancias del operador “+” del lenguaje LISP las siguientes.

(+ 1 1)
(+ 1 (+ 2 3) 7 (+ 4 5))

5.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.
5.2.3.1 Á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
corresponderá a la raíz y sus operandos a cada una de las ramas principales. Si uno de estos
Capítulo 5: Lógica Ecuacional
106
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.
Definición:
Á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.

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

1 -

4 7

5.2.3.2 Uso de Paréntesis.


Cuando se usan operadores con notación infija, se pueden encerrar entre paréntesis cada
uno de los subtérminos de un término para hacer explícito el árbol sintáctico. Un término
se considera “completamente parentetizado” si todo término 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 los operandos de dicho operador se
haga sin ninguna ambigüedad, facilitando la elaboración del árbol sintáctico.

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 a todo el término.

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

El uso exclusivo de la notación estandar para las funciones, también evita las
ambiguedades.
Capítulo 5: Lógica Ecuacional
107

El uso exclusivo de la notación “estandar” para las funciones, también evita la aparición de
ambiguedades, ejemplo:

+( +(2, 3), +(4, 5))


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

El término 2 * 4 + 7 podría estar asociado a cualquiera de los dos árboles sintácticos que se muestran a
continuación, haciendo ambiguo su significado (en un caso evaluaría a 22 y en el otro a 13).

* +

2 + * 7

4 7 2 4

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.
 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 respecto a su
operando determina su sentido de asociatividad, ya que este debe ir siempre
en la dirección que va del operando al operador.
Capítulo 5: Lógica Ecuacional
108
 Precedencia entre asociatividades: Es un orden impuesto a los elementos
del conjunto donde toma valor la asociatividad. Es decir se le puede dar 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 los 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 con la misma
precedencia e igual sentido de asociatividad, los operadores se aplican en el orden indicado
por el sentido de asociatividad (de la cola hacia la flecha).
En un término (o subtérmino) donde aparecen en secuencia varios operadores con la misma
precedencia y diferente sentido de asociatividad, se aplican primero los operadores con el
sentido de asociatividad de mayor precedencia.

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

Sería interpretado en correspondencia con la secuencia de términos equivalentes que se muestra en la


tabla siguiente.

Termino equivalente interpretación


Capítulo 5: Lógica Ecuacional
109
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) ^5) * 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.

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

Es importante distinguir entre la “precedencia” y el “sentido de asociatividad” de un


operador y los conceptos de “operador asociativo” y “operador conmutativo”. Así,
mientras que los primeros son conceptos sintácticos que determinan la forma de los árboles
sintácticos, los segundos son conceptos semánticos que indican que dos términos con
árboles sintácticos diferentes tienen el mismo valor semántico.

Los términos (2 + 4) + 7 y 2 + (4 + 7) tienen los dos árboles sintácticos diferentes que se muestan en la
figura. Pero por ser el operador “+” un “operador asociativo”, se conoce que los dos árboles calculan al
mismo valor.
Capítulo 5: Lógica Ecuacional
110
+ +

2 + + 7

4 7 2 4

5.2.4 Semántica 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.
 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.

El ejemplo siguiente lleva a cabo una afirmación sobre el estudiante que tiene la relación de noviazgo
con una estudiante específica de la clase.

es-alto(novio|a-de(maría))
PRECAUCIÓN:
Note que el símbolo “es-alto” es un símbolo de predicado mientras que el símbolo “novio|a-de” es un símbolo de
función. Para ello es necesario que en la definición del alfabeto de símbolos de la lógica hayan sido declarados
como tal.

Los operadores deben ser entonces interpretados por medio de funciones que existen en el
dominio de interpretación. En este sentido los operadores constituyen un medio para
referirse a la función dándole un nombre o “signatura”.88
La asociación de los operadores con las funciones, permite usar los términos para describir
las propiedades de las funciones que ocurren en el dominio de interpretación.

La aserción siguiente lleva a cabo una afirmación sobre la relación de noviazgo (que los novios se

88Es usual que no distingamos entre el nombre de una cosa y la cosa misma, así creemos erróneamente que “sen(x)”es
una función, cuando en realidad es un nombre asignado a una función (un apareamiento entre los reales) que ya existía.
Capítulo 5: Lógica Ecuacional
111
aman) usando la igualdad (vista como referencia al mismo elemento del dominio) el operador que la
representa.

x ( ama_a(x,novio|de(x))  ama_a(novio|de(x),x) )

Lógica Ecuacional
La lógica ecuacional es una rama de la lógica de predicados que se ocupa del razonamiento
sobre las propiedades de las funciones. Para ello se apoya en el predicado de igualdad
aplicado a términos definidos sobre los operadores asociados con dichas funciones.
La lógica ecuacional da soporte formal al razonamiento en matemáticas que se ocupa de las
propiedades de las funciones definidas sobre las entidades matemáticas (v.g. los números).
A continuación se presenta primero el predicado de igualdad, luego los criterios de
demostración que se le asocian, y por último el carácter de las teorías construidas con base
en dicho predicado.
5.3.1 El predicado de igualdad.
El predicado de igualdad permite afirmar que dos términos que pueden ser sintácticamente
diferentes, son iguales o “dan lo mismo” desde el punto de vista semántico. Una razón
obvia para que dos términos puedan considerarse iguales es que su valor semántico sea el
mismo objeto en el dominio de interpretación.
En este punto el lector debe notar que es posible definir una relación de igualdad entre los
objetos del dominio de interpretación, bajo una condición distinta a ser el mismo objeto.
Para ello basta que dos miembros del dominio de interpretación “den lo mismo” desde el
punto de vista de un propósito o criterio específico. Esto permite plantear diversos criterios
de igualdad sobre un mismo dominio con, condiciones de igualdad específicas para cada
criterio. Así, los criterios de igualdad puede verse como una categoría (o dimensión de
clasificación) para los elementos del dominio, en el que las condiciones que determinen la
“igualdad” determinan que elementos pertenecen a cada una de las clases de equivalencia
de la categoría.

Los estudiantes de la clase pueden dividirse en grupos de estudiantes “igualmente estudiosos”, donde un
estudiante de un grupo pueda ser considerado tan estudioso como otro estudiante del mismo grupo, pero
más o menos estudioso que un estudiante de un grupo diferente. Así, en la categoría de clasificación
“estudioso” existen tantas clases de equivalencia como niveles de “estudioso” puedan ser reconocidos.

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.
 Sustituibilidad: Un elemento puede ser substituido por otro igual a él en el
marco del criterio en el que fue definida la igualdad.
Capítulo 5: Lógica Ecuacional
112

Si un estudiante desea llevar a cabo un trabajo académico con un compañero “igualmente estudiosos” a
sí mismo, le debe ser indiferente cualquiera de los miembros de la clase de equivalencia a la que
pertenece.

En lo que sigue, sin embargo, el significado del concepto de igualdad es una relación entre
los términos, más bien que una elación entre los objetos del dominio de interpretación,
entendiendo que la igualdad de dos términos (sintácticamante diferentes) equivale a que
tengan igual valor semántico (calculen al mismo objeto).
El predicado de igualdad es fundamental para describir las propiedades de las funciones
que ocurren en el dominio de interpretación.

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

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)

5.3.2 Criterios Demostrativos.


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:
4. Ley reflexiva: Un término es igual a sí mismo.
t=t
5. Ley simétrica: Si un término es igual a otro, el otro es igual al primero.
t1 = t2
––––––
t2 = t1
6. 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
Capítulo 5: Lógica Ecuacional
113
7. Ley de sustitutividad de funciones:
t1 = t1’, t2=t2’, t3=t3’, ...
––––––––––––––––––––––––––––
f(t1, t2, t3, ...) = f(t1’, t2’, t3’, ...)
8. 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..
5.3.3 Teoría en lógica ecuacional.
Tal como se explicó en el capítulo anterior, una teoría es una secuencia de aserciones, que
comienza con unas aserciones básicas denominadas axiomas, y continúa con unas
aserciones derivadas denominadas teoremas; donde cada teorema, tienen la propiedad de
ser consecuencia lógica de las aserciones que le preceden.
En lógica ecuacional las aserciones de una teoría, son planteamientos de igualdad entre
términos formados con los operadores declarados en la teoría. Si la teoría es interpretada
sobre un dominio que satisface los axiomas, estos simplemente expresan las propiedades de
las funciones que representan los operadores involucrados en los axiomas. Además, si los
criterios de demostración son correctos, los teoremas expresan propiedades de dichas
funciones que son “descubiertas” a través de los procesos de demostración.
En dominios de interpretación con un número infinito de objetos, como los números
enteros, fraccionarios, reales, y demás tipos de valores matemáticos, los axiomas con
cuantificadores no pueden ser calculados (y “comprobados”), por lo que deben ser
asumidos como ciertos. Por las mismas razones, la correctitud de los teoremas que
involucren cuantificadores, solo puede comprobarse para un número limitado de casos, así,
sólo es posible demostrar la incorrectitud de un teorema (y por tanto la de los axiomas, o la
de los criterios de demostración que lo soportan), si se halla un caso particula para el que el
teorema no se cumple.
En lo que sigue ilustraremos los elementos de una teoría en lógica Ecuacional, para luego
plantear un proceso de demostración que puede ser automatizado y que le da soporte a los
lenguajes de programación denominados “funcionales”.
5.3.3.1 Elementos de una teoría en Lógica Ecuacional
Una teoría en lógica Ecuacional Multisort está, en consecuencia, constituidas por los
elementos que se describen a continuación.
5.3.3.1.1 Declaración de sorts.
Nombres de los sorts sobre los que se definen los términos de la teoría.
Capítulo 5: Lógica Ecuacional
114
En una teoría que para razonar sobre la aritmética de los enteros se incluirá la siguiente definición de
sort.
sort: Int

5.3.3.1.2 Perfil de los operadores.


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

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.

5.3.3.1.3 Axiomas.
Aserciones básicas sobre las funciones asociadas a los operadores, que son asumidas como
“ciertas” en el marco de la teoría.

En la teoría asociada a los enteros se incluirán los axiomas siguientes.


Axiomas:

(1) X:Int (X + 0 = X)
(2) X:Int (0 + X = X)
(3) X:Int (-X + X = 0)
(4) Y:Int Y:Int Z:Int (X + (Y + Z) = (X + Y) + Z)

Para simplificar la especificación de los axiomas, en lo que sigue se omitirán los cuantificadores
universales, y se introducirá el tipo de las variables cuantificadas por medio de una “declaración de
variable” que antecede a los axiomas que la usan. Así la especificación que sigue es equivalente a la
anterior.

Axiomas:

(0) X, Y, Z : Int
(1) X+0=X
(2) 0+X=X
(3) -X + X = 0
(4) X + (Y + Z) = (X + Y) + Z
Capítulo 5: Lógica Ecuacional
115
5.3.3.2 Demostración en Lógica Ecuacional.
La incorporación de teoremas en la lógica ecuacional se lleva a cabo por un proceso de
demostración apoyado en una “derivación”.
Definición:
Derivación – Una derivación es una secuencia de términos t0,t1,t2,...,tn que
pueden demostrarse iguales por razonamiento ecuacional.
Para simplificar la especificación de la igualdad entre todos los términos de una derivación
usaremos la notación siguiente:
NOTACION:
La expresión t0=t1, representa la aserción siguiente:
v0 v1 v2... vm (t0=t1)
La expresión t0=t1=t2=...=tn, representa las aserciones siguientes:
v0 v1 v2... vm (t0=t1)
v0 v1 v2... vm (t1=t2)
...
v0 v1 v2... vm (tk-1=tk) para todo 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) ).
Capítulo 5: Lógica Ecuacional
116
Nótese que por la ley de sustitutividad de funciones, el nuevo término actual, resultante de
la reescritura, es semánticamente igual al anterior.

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.

Sistemas de Reescritura de Términos (SRT).


En el Capítulo 5 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
Capítulo 5: Lógica Ecuacional
117
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 [Terese 2003] [Klint 07]. 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.

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,
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.
Capítulo 5: Lógica Ecuacional
118
 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.

En la demostración presentada en el Ejemplo 27 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 que desechen las que resulten inadecuadas. El segundo, es el de
simplificar la teoría utilizada para evitar que aparezcan caminos de derivación inadecuados.
El primer enfoque da lugar a la aparición de líneas de investigación dentro del campo
general de la inteligencia artificial, mientras que el segundo enfoque da lugar a la aparición
de los lenguajes funcionales.
Para evitar caminos de derivación inadecuados los lenguajes funcionales optan, primero,
por utilizar sólo axiomas en la derivación (desechando los teoremas) y, segundo, por
utilizarlos en un único sentido (v.g. de izquierda a derecha). Así, en cada paso de la
derivación, se selecciona una terceta axioma/particularización/subtérmino teniendo
solamente en cuenta que el lado izquierdo del axioma escogido, ya particularizado, sea
idéntico al subtérmino seleccionado. El uso exclusivo de axiomas agiliza la selección de la
Capítulo 5: Lógica Ecuacional
119
terceta, y el uso de los axiomas en un solo sentido evita procesos cíclicos triviales inducidos
por la selección repetitiva del mismo axioma.
Los lenguajes funcionales no ofrecen, sin embargo, otros mecanismos automáticos para
evitar que ocurran ciclos no triviales ni caminos de derivación que lleven a resultados no
deseados. Ellos dejan esta responsabilidad al matemático (o programador) quién debe
simplificar por sí mismo la teoría (o programa) para que estos problemas no aparezcan. Es,
entonces, necesario que las ecuaciones (o axiomas) que determinan los procesos de
derivación en un programa de un lenguaje funcional, tengan “buenas” propiedades. Dos
propiedades esenciales, que se les conoce como las asunciones de Church-Rosser89 [Church
36], [Toyama 90], son la terminancia y la confluencia. La terminancia implica que todos
los posibles caminos de derivación llegan eventualmente a un término final del cual ya no
es posible continuar, y la confluencia implica que el término final de todos los caminos es
el mismo.
Lo que sí ofrecen los lenguajes funcionales, es la capacidad de hallar de forma automática
la terceta axioma/particularización/término, a ser aplicada en cada paso del proceso de
derivación. De existir más de una terceta aplicable en el proceso, ellos optan por aplicar
reglas propias de selección, y/o por permitir que se lleven a cabo de forma paralela las
reescrituras definida por las varias tercetas. Esta última alternativa permite que en los
lenguajes funcionales los procesos de ejecución en paralelo se definan de forma natural, sin
necesidad de adicionar elementos sintácticos especializados a tal fin90.
Una consecuencia importante del enfoque asumido por los lenguajes funcionales, es que
ellos no pueden dar cuenta de toda la gama de demostraciones posibles en una teoría.
Como se verá en los capítulos siguientes, esta limitación, si bien es importante, no limita la
potencia expresiva de los lenguajes funcionales en relación con la de los lenguajes
procedurales. Es fácil ilustrar, en efecto, que todo proceso que puede especificarse en un
lenguaje procedural, puede también especificarse, de forma indirecta, como una derivación
en el marco de una teoría descrita en un lenguaje funcional.
Así, si bien los programas escritos en el marco de los lenguajes funcionales son
esencialmente teorías en una lógica ecuacional, y las ejecuciones de dichos programas
pueden asimilarse a demostraciones en las teorías. Su razón de ser, sin embargo, no es la
de demostrar teoremas que den luz sobre las propiedades de los objetos de la teoría, sino la
de llevar a cabo cálculos que obtengan (o construyan) objetos específicos de la teoría
(necesarios a un fin práctico concreto). En consecuencia, las derivaciones de las que se
ocupa el resto del texto, partirán en general de términos base complejos (la “fórmula” a ser
“calculada”) y terminarán en unos términos base más simples (el resultado del cálculo). No
serán tema de este trabajo, las derivaciones que parten de términos con variables (como la
del Ejemplo 27).

La utilidad de estas demostraciones en la vida cuotidiana es obvia, si consideramos que una compra de

89 Es decir, se asume que el programador da cuenta de ellas ver


http://en.wikipedia.org/wiki/Church%E2%80%93Rosser_theorem
90 Por ejemplo el uso de la instrucción “fork” del lenguaje C.
Capítulo 5: Lógica Ecuacional
120
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.

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.6.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 disjuntas91) indexadas por los elementos del
conjunto S*S92, 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.
Definición:
Constante –Una constante es un operador cuyo rango es <,s>, donde  representa
la cadena vacía en S*.

La especificación presentada en el capítulo anterior para razonar sobre la aritmética de los enteros se
apoya en la signatura definida por los elementos siguientes:

S es el conjunto { int }

91 Si un símbolo de operación aparece en dos familias distintas, se dice que es un operador “sobrecargado”.
92 S* es el conjunto potencia de S
Capítulo 5: Lógica Ecuacional
121
 es el conjunto { - , +, 0, s }

La relación entre los conjuntos S y  , definida en la signatura, asigna a cada símbolo de operación al
menos un elemento del conjunto S*S, que define su rango, aridad y coaridad, así:

Operador Rango Aridad Coaridad


- < int , int > int int
+ < int int , int > int int
int
0 <  , int >  int
S < int , int > int int

5.6.2 Términos.
Con la Signatura de una Especificación Multisort y un conjunto de variables, se pueden
definir los términos de la especificación. Los términos son, entonces, relativos a las
variables, de tal manera que a cada conjunto de variables le corresponde un conjunto de
términos específico.
Las variables de la signatura se definen con base en un conjunto (finito) X de símbolos de
variable x1,x2,x3,....,xn y una relación denominada tipo entre X y S. Los símbolos de
variable son distintos a los símbolos de la signatura (S,). La relación entre X y S clasifica
las variables asociándolas a un símbolo de sort, así:
X = {Xs | 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.

Un conjunto de términos se representará por expresiones de la forma t:s.


Capítulo 5: Lógica Ecuacional
122
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 5), para representar la aplicación de los símbolos de operación (u
operadores) a los términos que agrega (sus operandos). En lo que sigue, sin
embargo, se asume que la aplicación de los símbolos de operación a los términos
se lleva a cabo con base a la plantilla asociada al operador (si esta plantilla existe).
Para una caracterización informal de la plantilla de un operador el lector debe
referirse a la sección 5.2.1.

Es posible definir un conjunto de términos asociado a la signatura presentada en el Ejemplo 30, con base
en un conjunto de variables, así:

Sea X es el conjunto { X, Y, Z }

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: Lógica Ecuacional
123
5.6.3 Ecuaciones.
Con los términos de la especificación se puede definir el conjunto de ecuaciones de la
especificación E. E está compuesto por un conjunto finito de ecuaciones e1,e2,e3,....,em. Las
ecuaciones pueden ser ya sea ecuaciones simples o ecuaciones condicionales.
Las ecuaciones simples son construcciones de la forma:
l=r
Donde l y r son términos en ,s(X), para algún sort (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.

La especificación multisort de los dos ejemplos anteriores puede completarse con el conjunto de
ecuaciones E siguiente:

x+0=x
0+x=x
-x + x = 0
x + (y + z) = (x + y) + z
Que corresponden a los axiomas presentados en el Ejemplo 26.

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: Lógica Ecuacional
124
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.

SRT
Tal como se indicó en el capítulo anterior, es posible usar razonamiento ecuacional para
establecer si dos términos de una especificación multisort son equivalentes, sin recurrir a su
significado en el domino de interpretación.
En el marco de los lenguajes funcionales, este razonamiento es automatizado usando las
ecuaciones de la especificación como reglas dirigidas de reescritura, que se aplican de
forma automática al término actual de la derivación para generar el término siguiente.
Desde este punto de vista, una especificación multisort no es más que un conjunto de reglas
de reescritura aplicables a los términos de la especificación, a la que se le denomina
Sistema de Reescritura de Términos o SRT [Terese 2003] [Klint 07].
La aplicación de una ecuación (de una especificación multisort) como una regla de
reescritura sobre un término (de la especificación) para derivar otro término
semánticamente equivalente (en el marco de la especificación), esta definida y regulada por
los conceptos de substitución particularización, emparejamiento, y reemplazo.
En esta sección se presentan formalmente estos conceptos en el marco de la teoría de
lenguajes.
Cuando un término se demuestra semánticamente equivalente a otro derivándolo de él por
aplicación de las ecuaciones de la especificación como reglas dirigidas de reescritura, entre
ellos existe una relación dirigida que se denomina relación de reescritura. Nótese que el
uso de las ecuaciones como reglas dirigidas de reescritura impone limitaciones al
razonamiento93 y, por lo tanto, no todo par de términos que puedan demostrarse
semánticamente equivalente por razonamiento ecuacional, están relacionados por la
relación de reescritura.
En esta sección se define formalmente la relación de reescritura.
Al ser automática la aplicación de las reglas de reescritura el camino que toma una
derivación estará restringido por las reglas que son aplicables en cada paso de la reescritura.
Una forma de evitar que aparezcan caminos indeseables es obligar al SRT a satisfacer las
condiciones de terminancia y confluencia, que imponen condiciones a las reglas aplicables
en cada paso de la derivación.
En esta sección se definen formalmente estos conceptos.
5.7.1 Substitución, Particularización y Emparejamiento.
Una substitución  es una función que mapea un conjunto de variables
X={x1:s ,x2:s ,x3:s ,...,xn:sxn} a un conjunto de términos T={t1:st1,t2:st2,t3:st3,...,tm:stm}, y
x1 x2 x3

que cumple con la condición siguiente:

93 Sólo pueden usarse las ecuaciones (o axiomas) en un sentido.


Capítulo 5: Lógica Ecuacional
125
(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.

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

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

Sea t el término M(x,A(x,y)).


Capítulo 5: Lógica Ecuacional
126
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),z),A(0,z)))

5.7.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).
Definición:
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.

El árbol que se muestra a continuación representa el término M(x, S(A(y, 0))). En cada nodo del árbol se
indica el camino correspondiente.
Capítulo 5: Lógica Ecuacional
127

M

1 2
x S

2.1
A

2.1.1 2.1.2
y 0
Las siguientes referencias denotan los términos indicado:
t| denota el subtérmino M(x, S(A(y, 0)))
t|1 denota el subtérmino x;
t|2 denota el subtérmino S(A(y,0));
t|2.1 denota el subtérmino A(y,0);
t|2.1.1 denota el subtérmino y;
t|2.1.2 denota el subtérmino 0.

Sean t:st y r:sr términos tales que st=sr y sea u una ocurrencia de t. Llamaremos reemplazo,
y escribiremos t[ur], al término que resulta de reemplazar en t, el subtérmino t|u por r.

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.7.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 r, (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ón94.

94Demostración que se deberá hacer por reescritura, convirtiéndose esta condición en la de que exista entre los términos
de las ecuaciones una relación ui*E vi
Capítulo 5: Lógica Ecuacional
128
La relación entre t y t’ es denotada como 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.

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
5.3.3.2).
En una derivación automatizada el término que sigue a otro es seleccionado por el SRT
entre aquellos que mantiene con él una relación de reescritura. Esto implica que la
automatización de la derivación de un SRT es correcta en el sentido de que toda derivación
en el SRT es una demostración en lógica ecuacional.
De haber más de un candidato a término siguiente, el SRT escoge uno cualquiera de los
candidatos. Las propiedades de confluencia y terminancia hacen que cualquier escogencia
sea adecuada.
5.7.4 Propiedad de Church-Roser: Confluencia y Terminancia.
Un conjunto de ecuaciones E es terminante si no hay secuencias infinitas de reescritura. El
conjunto E es confluente, si el resultado final de reescribir un término es único, en el
sentido de que si existen t*E t1 y t*E t2 con t1  t2 entonces existe un término t’ tal que
t1*E t’ y t2*E t’. Si un conjunto de ecuaciones E es terminante y confluente se dice
que es canónico o que cumple la condición de Church-Rosser [Toyama 90].
Si un conjunto de ecuaciones E es Church-Rosser, todo término t podrá reescribirse hasta
un término final 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).

Con el mismo alfabeto de los ejemplos anteriores, definamos un SRT (, R) con R={M(x,y) M(y,x),}.
Este SRT no es terminante, pues M(x1,y1) se puede reescribir a M(y1,x1), y este a su vez se puede
reescribir a M(x1,y1), y así infinitamente, sin encontrar un término canónico.
Capítulo 5: Lógica Ecuacional
129

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.

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 base de la
signatura y establecer si se satisface, o no, una ecuación cualquiera entre términos base 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 un álgebra
inicial de la especificación.

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.
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.
Capítulo 5: Lógica Ecuacional
130
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 sustituibilidad.
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.
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 asumciones de Church-Rosser.
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
Capítulo 5: Lógica Ecuacional
131
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 mapea 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.

Ejercicios propuestos.
VERDADERO O FALSO?
( ) Los operadores son símbolos de la lógica de predicados que se utilizan para construir
términos complejos a partir de proposiciones simples.
( ) Los términos complejos se usan para agrupar términos más simples usando predicados.
( ) Los predicados pueden considerarse como operadores cuyo sort de término resultante
es de tipo Boleano.
Capítulo 5: Lógica Ecuacional
132
( ) Los términos atómicos son las constantes y las variables.
( ) Existen operadores con número variable de operandos.
( ) La aplicación de un operador a un conjunto de operandos no requiere satisfacer las
restricciones aridad y tipo propias de los predicados.
( ) Los criterios formativos de los términos limitan el conjunto de términos que usan un
operador a ser un conjunto finito.
( ) La naturaleza recursiva de los criterios formativos de los términos permite, en general,
que estos sean de tamaño indeterminado.
( ) La naturaleza recursiva de los criterios formativos de los términos permite, en general,
que el conjunto de ellos sea infinito.
( ) El árbol sintáctico de un término permite visualizar cuales son los operandos de cada
operador.
( ) Los criterios de prioridad y sentido de asociatividad de los operadores sólo son útiles
cuando se presentan operadores con notación infija.
( ) El uso de paréntesis en los términos es suficiente para determinar el árbol sintáctico
aún con operandos de notación infija.
( ) Los criterios de prioridad y sentido de asociatividad de los operadores sólo son
suficientes para representar cualquier término, sin necesidad de usar paréntesis.
( ) La semántica de los operadores son las funciones que representan en el dominio de
interpretación.
( ) La lógica Ecuacional posibilita plantear y razonar acerca de las propiedades de las
funciones que ocurren en el dominio de interpretación.
( ) El predicado de igualdad permite afirmar que dos términos que sintácticamente
diferentes, son iguales desde el punto de vista semántico (v.g. representan el mismo objeto
del dominio de interpretación).
( ) Los criterios demostrativos de la lógica Ecuacional, son la base para definir las reglas
de transformación de las ecuaciones del álgebra.
( ) Si en una fbf de lógica de predicados cuantificada universalmente se particulariza una
variable a un término que tiene otras variables diferentes, estas debe cuantificarse
existencialmente.
( ) Una teoría es una secuencia de términos que pueden demostrarse iguales por
razonamiento Ecuacional.
( ) Una derivación es una secuencia de proposiciones ecuacionales donde cada una es un
axioma o es demostrada como cierta a partir de las proposiciones que la preceden.

Á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.
Capítulo 5: Lógica Ecuacional
133
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.

ÁRBOL SINTÁCTICO EN NOTACIÓN INFIJA CON PRECEDENCIA Y


ASOCIATIVIDAD ENTRE OPERADORES.

a) Dado el conjunto de operadores, orden de precedencia y sentido de asociatividad que se


muestra en la tabla (0 precede a1, 1 precede a 2, 2 precede a 3 etc..).

Operador Descripción Precedencia Asociatividad


_ Trunca al entero anterior 0 izquierda->derecha
-_ Cambia el signo 0 derecha<-izquierda
_ ^_ Eleva a una potencia 1 izquierda->derecha
_*_ Multiplica 2 izquierda->derecha
_/_ Divide 2 derecha<-izquierda
_+_ Suma 3 izquierda->derecha
_-_ Resta 3 Derecha<-izquierda

Teniendo en cuenta que izquierda->derecha precede a derecha->izquierda


Elabore el árbol sintáctico de la siguientes expresiones:

d^ c^ b
d^ c^ b
d^ c^ b
- - d
b + c + d + d
b - c - d - d
b - c + d + d
c - -b + c + d – d + c - b
c - -b*c/d + d^ c+ b

b) Varie el ejemplo anterior cambiando la precedencia o sentido de asociatividad de los


operadores binarios.

c) Considre el siguiente término y el árbol sintáctico correspondiente. Seleccione el orden


de precedencia y sentido de asociatividad utilizados.
2−2 * 2^2+2+2^2 * 2−2
a) Operador Precedencia Asociatividad
Operador Precedencia Asociatividad _+_ 1 <-
_+_ 1 <- _−_ 2 <-
_^_ 2 -> _^_ 3 <-
_−_ 3 -> ___ 4 <-
_*_ 4 <-

b)
Operador Precedencia Asociatividad
_+_ 1 ->
_^_ 2 <-
_−_ 3 <-
_*_ 4 ->!

c)
Operador Precedencia Asociatividad
_+_ 1 ->
_−_ 2 ->
_^_ 3 ->
_*_ 4 ->

d)
DEFINA EL ÁRBOL SINTÁCTICO PARA LA SIGUIENTE DEFINICIÓN DE UNA
VARIABLE EN EL LENGUAJE C.
int *a()[];

DERIVACIÓN EN LÓGICA ECUACIONAL


Sea: ={F, G, H, I} el conjunto de operadores de una teoría, con 2, 2, 1 y 0 argumentos
respectivamente, y {x,y,z} un conjunto de variables.
Dado el siguiente conjunto de axiomas ecuacionales
1- G(x,x)= H(x)
2- F(H(I),x)=x
3- H(H(I))=I
Describa usando la tabla adjunta 6 pasos consecutivos de un proceso de derivación
ecuacional partiendo del término mostrado:
Columna 1: El subtérmino a ser substituido subrayándolo en el término.
Columna 2: El numero de la ecuación seleccionada.
Columna 3: La particularización de las variables efectuada sobre la ecuación para
emparejar uno de sus lados con el subtérmino subrayado .
Columna 4: El término que substituye al subtérmino subrayado.
Columna 1: El término resultante de la substitución.

Termino regla Sustitución Particularización


F(F(H(I),G(I,H(G(I,I)))),H(G(H(I),G(I,I)))) ?/X

TEORIAS
Un campo, se define en matemáticas como un conjunto F dotado de 2 operaciones, la
operación “+” y la operación “*”, que cumplen 7 axiomas conocidos como los axiomas de
campo.
Una teoría en lógica ecuacional para campos es la siguiente:
Capítulo 6: Sistema de Reescritura de Términos (SRT)
136
Conjuntos:
F
Operadores:
_+_ : F F -> F
-_ : F -> F
0 : -> F
_*_ : F F -> F
_-1 : F -> F
1 : -> F
Variables (para todo):
a, b, c : F
Axiomas:
(1) (a+b)+c = a+(b+c)
(2) a+b = b+a
(3) a+0 = a
(4) a+(-a) = 0
(5) (a*b)*c = a*(b*c)
(6) a*b = b*a
(7) a*1 = a
(8) a*a-1 = 1
(9) a*(b+c) = a*b + a*c
Basados en esta teoría, podemos demostrar por ejemplo que a*0=0, construyendo la
siguiente derivación:
a*0 = (a*0)+0 = (a*0)+(a+(-a)) = ((a*0)+a)+(-a) = ((a*0)+(a*1))+(-a) = a*(0+1)+(-a) =
a*(1+0)+(-a) = a*1+(-a) = a+(-a) = 0
a. Seguir la demostración presentada, detallando la sustitución hecha en cada paso.
b. De manera análoga a la presentada en el ejemplo de la sección 5.3.3.2, demostrar
que --a=a.
c. Demostrar que a*(-b) = -(a*b). Se puede hacer uso de los resultados obtenidos
arriba. Aunque existen varias maneras de demostrarlo, una posible demostración
comienza por utilizar los axiomas (3), (4) y (9), el resto lo dejamos al lector.
Análogamente se puede demostrar que (-a)*b = -(a*b).
d. Demostrar que (-a)*(-a) = a*a.
e. Demostrar que (-1)*a = -a.

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


2. Sea ={F, G, H, I} el conjunto de operadores de un alfabeto, con 3, 2, 1 y 0 argumentos
respectivamente.
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[1.3.2.1
G(I,I)]; t[1 x];
Capítulo 6: Sistema de Reescritura de Términos (SRT)
137
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
Resolución y demostración
automática de teoremas
Capítulo 6: Resolución y demostración automática de teoremas
140
Introducción.
En el Capítulo 5 luego de introducir 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 probarse iguales
(semánticamente) por razonamiento ecuacional. La automatización de este proceso de
demostración, dio lugar a los SRTs (Sistemas de Reescritura de Téminos), que son el
fundamanto de los lenguajes de programación “funcionales”
En este capítulo introduciremos un método de demostración, aplicable a las fbf en FNC que
estén compuestas por cláusulas de “Horn”. El método se apoya en la introducción de
“resolventes” en la fbfs clausal. Luego de presentar las bases del método de demostración,
presentaremos un modo de automatizarlo que ha sido denominado, “resolución SLD” (por
sus siglas en Inglés: “Selective Linear Definite clause resolution”). La resolución SLD es el
fundamento de los lenguajes de programación “lógicos”.
El teorema de la “resolución” sirve, en efecto, para fundamentar un método que permite
probar que una fbf G es consecuencia lógica de las fbfs F1,F2, ...,Fn, siempre y cuando
todas las fbfs involucradas sean “clausulas de Horn”. Este método se apoya en que probar
que la fbf F1F2...Fn¬ G es una contradicción.
La automatización de este proceso da lugar a una familia de lenguajes cuyo principal
representante es el lenguaje PROLOG.

Resolución.
La resolución sirve para fundamentar un método para demostrar que una fórmula en FNC
(Forma normal Conjuntiva ver 3.4.5.2) de la forma F1F2...Fn¬ G es insatisfacible,
probando con ello que G es consecuencia lógica de las fbfs F1,F2, ...,Fn (ver 3.4.4.1 ).
6.2.1 Resolución en lógica proposicional.
Empezaremos por presentar el método de resolución para la lógica de proposiciones, donde
se comprenderá la esencia del problema.
6.2.1.1 Notación
Ahora, para facilitar la presentación del método, introduciremos algo de notación. Una
fórmula en FNC es de la forma:
(L1,1  L1,2 … L1, i)  (L2,1  L2,2 … L2, j)  ……  (Ln,1  Ln,2 … Ln,m)
Donde los Li,j son literales (un átomo o su negación); será representada en lo que sigue de la
manera siguiente:
{L1,1 , L1,2 ,…, L1, i} , {L2,1 , L2,2,…, L2, j} , …… , {Ln,1 , Ln,2 ,…, Ln,m}
Donde los Li,j son los literales, {Lk,1 , Lk,2 ,…, Lk,j}, son las cláusulas, las “,” que separan los
literales de las cláusulas tepresentan el operador “”, {…} , {…} , …… , {…} es la conjunción
de cláusulas, y las “,” que separan las cláusulas representan el operador “”.
6.2.1.2 Resolventes
Definición:
Capítulo 6: Resolución y demostración automática de teoremas
141
Sea C1, C2 y R cláusulas. A R se le llama resolvente de C1 y C2 si hay un literal
lC1, tal que ¬lC2 y R tiene la forma (C1 – {l})  (C2 – {¬l}) y es consecuencia lógica
de C1 y C2.
Donde: ‘–’ es la operación diferencia para conjuntos y ‘’ la operación unión de conjuntos.
De manera que R es el conjunto formado por los literales de C1 sin l y los literales
de C2 sin ¬l.

Los resolventes de las siguientes parejas de cláusulas:

{q,¬r,p} , {r,¬p}
Son, una cualquiera de las cláusulas siguientes:

{q, p, ¬p}
{q, ¬r, r}
Donde el lector debe notar que la misma pareja de cláusulas puede tener varios resolventes diferentes.
Es importante tener en cuenta que en el caso de la primera pareja que pueden resolverse tanto por Q
como por R, debe escogerse uno de los dos siendo un error eliminar simultáneamente a ambos. Así:
NO ES RESOLVENTE DE LA PRIMERA CLAUSULA LA SIGUIENTE:

{q}
Esto puede verse más claramente si se considera que por la tabla de verdad asociada al conector , los
dos resolventes de la primera pareja son equivalentes al valor lógico V (verdadero), y este no es
equivalente a q.

Nótese que el resolvente de dos cláusulas es consecuencia lógica de dichas cláusulas.

Se deja al lector la tarea de comprobar que los resolventes propuestos en el ejemplo anterior son
consecuencia lógica de las cláusulas que lo originan.

Igualmenter se deja al lector la tarea de comprobar si los resolventes “candidatos” que se presentan
mas abajo son realmente consecuencia lógica de las parejas de cláusulas asociadas:

{q,¬r,r} , {r,¬p} con resolvente: {q, r, ¬p}


{q,¬r,r} , {¬p} con resolvente: {q, ¬p}
{q,¬r, ¬r} , {r,¬p} con resolvente: {q, ¬r, ¬p}

El resolvente de las cláusulas {¬r} y {r} es el valor lógico F (falso). Esto puede verse
claramente si se tiene en cuenta que la fbf {¬r},{r} es semánticamente equivalente a
{¬r,F},{r,F} , cuyo resolvente es obviamente F. En lo que sigue el resolvente de dos
clausulas con un literal y su negación (F) lo denominarémos la cláusula “vacía”.
El siguiente teorema servirá para mostrar que al agregar a un conjunto de cláusulas el
resolvente de dos de sus cláusulas, no se afecta el valor de verdad de la forma clausal. En
lo que resta de ésta sección aparecerán algunas demostraciones, que consideramos
Capítulo 6: Resolución y demostración automática de teoremas
142
importantes, no solo por el esfuerzo de precisar argumentos, sino para evidenciar la
efectividad del método que presentaremos más adelante.
TEOREMA 6-1: Sea G={C1,C2} una fórmula en FNC, representada como conjunto de
cláusulas. Sea R un resolvente de las cláusulas C1,C2. Entonces G’={C1,C2,R} es
equivalente a G.
Demostración: Para probarlo, empezaremos por suponer una interpretación que hace verdadero a G’ y
probaremos que también G es verdadero bajo tal interpretación. Luego supondremos una interpretación que
hace verdadero a G y probaremos que también G’ es verdadero bajo tal interpretación.
1. Sea I una interpretación que hace verdadero a G’. Como G’ es una conjunción de Cláusulas, I hace
verdadera todas las cláusulas de G’. En Particular hace verdadero a C1 y a C2. Por tanto G={C1,C2}
es también verdadero.
2. Sea I una interpretación que hace verdadero a G. De nuevo C1 y C2 son verdaderas bajo I, y tan solo
falta probar que R es verdadero bajo I para probar que G’ es verdadero bajo I. Sabemos que R tiene
la forma (C1–{L})(C2–{¬L}), con 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.
Habiendo hecho evidente el caso más simple del Teorema, no es difícil ver el caso más
general: Si G es una fórmula en FNC, representada como conjunto de cláusulas, y R es un
resolvente de dos de sus cláusulas, G es equivalente a G{R}.
6.2.1.3 Demostración por introducción de resolventes.
La idea del método de demostración basado en resolventes, es tomar el conjunto de
cláusulas (que representa la fórmula en FNC) y agregar sucesivamente resolventes, hasta
que se evidencie una contradicción. La contradicción se evidencia, cuando se introduce
como resolvente la cláusula vacía (F), ya que ésta proviene de un par de cláusulas de la
forma {L},{¬L}. La presencia de una pareja de cláusulas de ésta forma hace insatisfacible o
contradictoria la FNC que representa pues L¬L=F y como el conjunto de cláusulas
representa la conjunción de ellas, la presencia de F allí hace falsa toda la expresión para
cualquier interpretación..
Aunque el método tenga sentido, no es evidente que siempre que se enfrente a una FNC
contradictoria se llegue a la introducción de F probando la contradicción, así que lo que
sigue es demostrar éste hecho que se conoce como completitud del método.
6.2.1.4 El teorema de Resolución

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:


Capítulo 6: Resolución y demostración automática de teoremas
143
Res0(G) =G
Resn+1(G) = Res(Resn(G)) (para n0)

Y finalmente:
Res*(G) =  Res (G)
n0
n

Por ejemplo, sea:

G = {{¬A,¬B,¬D},{¬C,A},{C},{B},{¬G,D},{G}}
Entonces
Res0(G) = G
Para obtener Res1(G), debemos resolver todas las parejas de cláusulas de G y agregar los resolventes a
G. Entonces

Res1(G) = G{{¬B,¬D,¬C},{¬A,¬D},{¬A,¬B,¬G},{A},{D}}
Para obtener Res2(G), debemos resolver todas las parejas de cláusulas de Res1(G). Sin embargo,
Res1(G) contiene las cláusulas de G que ya hemos resuelto, así que solo falta resolver las cláusulas de G
con las nuevas cláusulas de Res1(G) y las nuevas de Res1(G) entre sí. Entonces

Res2(G)=Res1(G){{¬B,¬D},{¬A,¬B},{¬C,¬D},{¬B,¬G,¬C},{¬A,¬G},{¬B,¬C},{¬D},{¬A}, {¬B,¬G}}
Luego Res3(G), se obtiene de manera análoga:

Res3(G)=Res2(G){{¬G,¬C},{¬C},{¬G},{¬B}, false }.
Al intentar encontrar Res4(G) de forma análoga, nos encontramos con que no se pueden encontrar más
resolventes, de manera que:

Res4(G) = Res3(G).
Finalmente, se puede concluir que

Res3(G) = Res4(G) = ... = Res*(G).


Nótese que FRes3(G). De manera que Res3(G) es una contradicción, y según el Teorema 11-1 Res3(G)
es equivalente a G, de manera que también G es contradictoria.

TEOREMA 6-2: Si un conjunto de cláusulas G es contradictorio, entonces FRes*(G).


Demostración: Dejamos al lector interesado la taréa de consultar otras fuentes, por ejemplo [Chang 73].

6.2.2 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.
6.2.2.1 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í
Capítulo 6: Resolución y demostración automática de teoremas
144
como lo hicimos al presentar la resolución en la lógica proposicional, transformaremos
nuestro problema a probar que (F1F2...Fn¬G) es contradictoria, para el caso en que los
átomos involucrados en las fórmulas son predicados, con variables.
Además supondremos que (F1F2...Fn¬G) está en forma normal Prenex, con matriz en
FNC y 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 xr  P1,1 x1 ,, xr     P1,n1 x1 ,, xr     Pk ,1 x1 ,, xr     Pk ,nk x1 ,, xr 

Es una contradicción.
En adelante omitiremos los cuantificadores y simplemente supondremos que cualquier
variable que aparezca está cuantificada universalmente. Además utilizaremos la notación
de cláusulas y conjuntos de cláusulas introducida en la subsección 140 , de manera que f
aparecerá así:
{{P1,1(X1,…,Xr), , P1,n1(X1,…,Xr)}, , {Pk,1(X1,…,Xr), , Pk,nk(X1,…,Xr)}}
Donde, sin pérdida de generalidad asumiremos que cada claúsula estará en forma prenex
con sus cuantificadores universales a la derecha de la cláusula.
6.2.2.2 Unificación
En la subsección 140 vimos que la clave para la aplicación del principio de resolución es
encontrar un literal dentro de una cláusula que sea complementario a un literal en otra
cláusula. Para cláusulas sin variables, esto es bastante simple. Sin embargo, para cláusulas
que contienen variables, es un poco más complicado. Por ejemplo consideremos las
cláusulas:

C1: {P(X),Q(X)}
C2: {¬P(f(Y)),R(Y)}
A primera vista, no hay ningún literal en C1 que sea complementario a uno en C2. Sin
embargo teniendo en cuenta que las variables de ambas cláusulas están cuantificadas
universalmente, es posible entender que C1 y C2 representan un conjunto de cláusulas sin
variables con todas las posibles particularizaciones de sus variables.
Podemos, entonces, sustituir X por f(a) en C1 y a Y por a en C2, para obtener:

C1’: {P(f(a)),Q(f(a))}
C2’: {¬P(f(a)),R(a)}
Que son instancias de C1 y C2 respectivamente, es decir, son cláusulas que se encontrarían
al expandir todo el cuantificador universal que implícitamente cuantifica ésta cláusula. Al
hacer este reemplazo aparecen P(f(a)) y ¬P(f(a)) que son complementarios entre si. Luego de
C1’ y C2’ podemos obtener el resolvente

C3’: {Q(f(a)),R(a)}
Capítulo 6: Resolución y demostración automática de teoremas
145
Es claro que existe una infinidad de formas diferentes de instanciar C1 y C2 para encontrar
un resolvente. Sin embargo podríamos obtener no uno sino un conjunto de resolventes si
particularizamos a Y por f(Y) en C1, para obtener el conjunto de cláusulas contenido en C1

C1*: {P(f(Y)),Q(f(Y))}
De nuevo, C1* es una instancia de C1, aunque una “más general” que C1’. En esta ocasión
P(f(X)) en C1* es complementario a ¬P(f(Y)) en C2. Luego, podemos obtener el siguiente
resolvente de C1* y C2

C3: {Q(f(Y)),R(Y)}
Que expresa el conjunto (infinito) de resolventes correspondientes a las posibles
particularizaciones de la variable Y . C3 tiene, además, la característica muy especial de
que cualquier otro resolvente que resulte de instancias de C1 y C2 es a su vez una instancia
de C3. A esta sustitución se le llama la “más general”.
Aunque éstos conceptos se pueden formalizar, y el método para encontrar el unificador más
general se puede estructurar para ser aplicado por un computador, preferimos en éste caso
dejar la idea intuitiva y presentar ejemplos, para que el lector adquiera experiencia
haciéndolo.
Para plasmar la idea del unificador “mas general” necesitamos un mínimo de nomenclatura
que nos permita expresar las ideas.
 Las sustituciones las representaremos como conjuntos de la forma: ={t1/V1,...,tn/Vn}.
Donde V1,...,Vn son variables distintas entre si y t1,...,tn son términos. Lo que
representa la sustitución de Vi por ti, para cada i=1,..,n.
 Dada una expresión E y una sustitución ={t1/V1,...,tn/Vn}, E es la expresión que se
obtiene a partir de E, sustituyendo Vi por ti para cada i=1,...,n.
 Dados dos literales L1 y L2, se le llama unificador de L1 y L2 a una sustitución de
variables que hace iguales a L1 y L2. Es decir,  es un unificador para L1 y L2 sii
L1=L2.

 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 si y solo si para todo unificador  de L1 y L2, existe una sustitución
 tal que (L1)=L1.

Algunos ejemplos, para que el lector coincida con la opinión de que la idea intuitiva es bastante simple.
Dados los literales

L1 = P(a,X,f(g(Y))) y L2 = P(Z,f(Z),f(U)):
 {a/Z, f(a)/X, g(a)/U, a/Y} es un unificador para L1 y L2.
 {a/Z, f(a)/X, g(Y)/U} es el unificador más general para L1 y L2.
 La primera sustitución (a/Z) es bastante obvia, pues P(a,.. y P(Z,.. solo pueden ser iguales si Z
Capítulo 6: Resolución y demostración automática de teoremas
146
se reemplaza por a.
 Con la primera establecida, nótese que se obtiene L2 = P(a,f(a),f(U)). De manera que la segunda
sustitución (f(a)/X) nuevamente es obvia, pues P(a,X,... y P(a,f(a),.. solo pueden ser iguales si
se sustituye X por f(a).
 La tercera sustitución sin embargo nos da un poco más de flexibilidad, pues en éste punto
tenemos: L1 = P(a,f(a),f(g(Y))) y L2 = P(a,f(a),f(U)), de manera que lo que hace falta es que
f(U) sea igual a f(g(Y)). Debe ser claro que la forma más general de conseguirlo es
reemplazando U por g(Y).
Dados los literales

L1 = Q(f(a),g(X)) y L2 = Q(Y,Y):
 No existe unificador para L1 y L2.
 Procediendo como en el ejemplo anterior, es claro que Y debe reemplazarse por f(a), con lo que
obtenemos L2 = Q(f(a),f(a)). Luego es imposible encontrar una sustitución de variables que
haga iguales a g(X) con f(a).
Dados los literales

L1 = P(f(a,X),y,f(g(Y),a)) y L2 = P(Z,f(Z,U),f(W,a)):
 {a/X, f(a,a)/Z, f(f(a,a),U)/Y, g(f(f(a,a),U))/W} es un unificador para L1 y L2.
 {f(a,X)/Z, f(f(a,X),U)/Y, g(f(f(a,X),U))/W} es el unificador más general para L1 y L2.
 La manera más general de hacer iguales a Z con f(a,X) es haciendo tal sustitución.
 Sin embargo al hacer ésta sustitución obtenemos L2 = P(f(a,X),f(f(a,X),U),f(W,a)), de modo que
la siguiente sustitución debe sustituir y con f(f(a,X),U).
 El resto del proceso lo dejamos para que el lector lo analice.

Algo que notar es que en los ejemplos presentados, no hay variables comunes entre L1 y L2,
lo que hace más fácil el trabajo pues cada elemento de la sustitución no influye sino a uno
de los dos literales. Sin embargo esto no tendría que ser así. Se debe tener especial cuidado
cuando la particularización de las variables de una cláusula involucra un término con
variables que ya existen en la otra, ya que las variables de igual nombre no necesariamente
deben ser las mismas en el resolvente; considere, por ejemplo, las cláusulas siguientes.

C1: {P(X),Q(X,Y)}
C2: {¬P(f(Y)),R(Y)}
Si particularizamos a x por f(Y) en C1, para obtener el conjunto de cláusulas contenido en C1

C1*: {P(f(Y)),Q(f(Y),Y)}
Habremos introducido una ligadura incorrecta entre las dos variables de Q, que antes
referían objetos diferentes.
Afortunadamente, en lo que sigue podremos independizar las variables entre una cláusula y
otra, incluso cuando sea necesario cambiar el nombre de algunas variables para que esto se
vea explícitamente.
6.2.2.3 Resolventes
Capítulo 6: Resolución y demostración automática de teoremas
147
La manera de generar resolventes y el método para encontrar contradicciones a partir de
cláusulas en lógica de primer orden son idénticos a los mostrados en la sección 142 para la
lógica proposicional, excepto por la unificación.
Sean C1 y C2 dos cláusulas sin variables en común. Sean 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.

Sean:

C1={P(X),Q(X)} y C2={¬P(a),R(X)}
Nótese que C1 y C2 comparten la variable X, de manera que lo primero es renombrar X en C2 digamos
por Z (simplemente cualquier otra variable que no aparezca en C1), con lo que obtenemos

C1={P(X),Q(X)} y C2={¬P(a),R(Z)}.
Luego, escogiendo L1=P(X)C1 y L2=¬P(a)C2, el lector puede verificar que  ={a/X}, es el mgu de
L1 y ¬L2. Entonces:

(C1 – L1)  (C2 – L2)


= ({P(a),Q(a)} – {P(a)})  ({¬P(a),R(Z)} – {¬P(a)})
= {Q(a)}  {R(Z)}
= {Q(a),R(Z)}
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 F, agregando resolventes.

El Lenguaje PROLOG visto desde la Lógica.


Los lenguajes lógicos automatizan el proceso de demostración por reducción al absurdo
basandose la resolución.
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.
Capítulo 6: Resolución y demostración automática de teoremas
148
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.
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 programa
propiamente dicho y la fbf G constituye una “consulta” a dicho programa, a la que en
adelante nos referiremos también como “objetivo de la prueba”, o símplemente “objetivo”.
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.
6.3.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.
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(...)  B1,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(...) 
...

6.3.2 Consultas en PROLOG


Las consultas G en PROLOG son fbfs de la forma siguiente:
G = X1X2X3... Xn ( G1(...)G2(...)G3(...)..)
Capítulo 6: Resolución y demostración automática de teoremas
149
Donde:
X1,X2,X3.... ,Xn son variables ligadas a los cuantificadores.
son predicados atómicos en los que pueden aparecer variables
G1(...),G2(...),G3(...), ...
cuantificadas, a estos predicados nos referirmeos en adelante como las “metas” de la
consulta (o “metas” del objetivo).
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 = (X1X2X3.... Xm ( 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.
6.3.3 Forma Prenex de un programa PROLOG
Es importante hacer notar que por estar, en (F1F2...Fn¬G), cada cláusula cuantificada de
forma independiente, 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.
Así, a pesar de que es usual que en un programa aparezcan las mismas variables en distintas
cláusulas, al momento de llevarse a cabo una unificación e introducir un resolvente (ver
6.2.2.2 ), el intérprete lleva a cabo los cambios de nombres de variables que sean necesarios
para garantizar que las cláusulas que participan en la resolución tengan nombres de
variables diferentes. Con ello se evita que al obtener el resolverte se introduzcan ligaduras
innecesarias, obligando a variables que originalmente representaban objetos diferentes, a
referirse al mismo objeto luego de la resolución.

Sean:

C1={P(X),Q(X)} y C2={¬P(Y),R(X,Y)}
Nótese que a pesar de que C1 y C2 comparten la variable X, esta variable esta cuantificada
(universalmente) de forma independiente en cada cláusula representando a objetos diferentes. La
cláusula C2 , por su parte, posee dos variables diferentes que por tener cada una su propio cuantificador,
refieren a objetos diferentes.
Si al unificar las dos ocurrencias del predicado P( ) se efectúa la substitución X/Y en C2, sin haber
cambiado previamente los nombres de las variables, se obtiene como resolvente la claúsula siguiente:

R={ Q(X), R(X,X) }.


En las que todas las variables están ligadas al mismo cuantificador obligando, de forma equivocada, a
Capítulo 6: Resolución y demostración automática de teoremas
150
que el segundo argumento de del predicado R( ) se refiera ahora al mismo objeto que refieren el primer
argumento de R(.) y el (único) argumento de Q( ),
Si, por otro lado, al unificar las dos ocurrencias del predicado P( ) se efectúa la substitución X/Y en C2
luego de efectuar el cambio de variable Z/X, se obtiene como resolverte la claúsula siguiente:

R={ Q(X), R(X,Z) }.


En la que el segundo argumento de del predicado R( ) mantiene su independencia de los otros
argumentos de la cláusula resolvente.

Este cambio de variables, aplicado a todas las cláusulas del programa (evitando variables
repetidas entre las cláusulas) es equivalente a utilizar las equivalencias expuestas en la
sección 4.4.2.2, para lograr que todos los cuantificadores que aparecen en la expresión
(F1F2...Fn¬G) se lleven hacia la izquierda, dejando a la derecha una expresión sin
cuantificadores y en forma normal conjuntiva. En el 4.4.2.2 se muestra un caso de este
proceso de transformación que es fácil generalizar.

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.
6.4.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.
6.4.1.1 Á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.
6.4.1.2 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.
Capítulo 6: Resolución y demostración automática de teoremas
151
6.4.1.3 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
aparecer dos veces en una sentencia se interpreta como variables diferentes, lo que no
ocurre con la variable Tio por ejemplo.
6.4.1.4 Términos Complejos
Un término complejo consta de una serie de operadores que se aplican a una serie de
operandos, con la estructura y significado expuestos en el capítulo dedicado a la lógica
ecuacional. Para el PROLOG, sin embargo un término complejo es una estructura
fundamentalmente sintáctica (un árbol sintáctico compuesto por “functores” y argumentos),
por lo que ofrece la capacidad de cálculo sólo para los términos aritméticos.
6.4.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.

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

6.4.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 6: Resolución y demostración automática de teoremas
152
 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 6.3.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(...) .
...

6.4.3.1 Hechos
Las cláusulas del programa que sólo tienen un literal positivo (lo implicado) se denominan
“hechos”. En otras palabras los hechos tienen un sólo predicado que puede tener o no
variables.

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

6.4.3.2 Reglas
Las cláusulas del programa que tienen, además del literal positivo (lo implicado), tiene
literales negativos (los que implican) se denominan “reglas”.
El literal implicado se denomina la “cabeza” de la regla y la lista de literales que implican
se denomina el “cuerpo” de la regla.

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

6.4.4 Consultas
Las consultas se escriben en PROLOG como una secuencia de predicados, separados con
“,” que termina con el símbolo “.” y es precedido por el símbolo “:-“, o por “?-“95 .

95 En SWI PROLOG la consulta se escribe directamente luego de ?-


Capítulo 6: Resolución y demostración automática de teoremas
153

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


6.4.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: “true”. Y obviamente frente a la consulta siguiente:

:- hermana(ana,jose)

El intérprete de PROLOG respondería: “false”.


Otra consulta podrías ser la siguiente:

:- hermana(ana,X) .
A la que el intérprete de PROLOG respondería: X = juan.
Finalmente a la consulta siguiente:

:- tia(ana,S) .
El intérprete respondería: yes. S = esteban.
Capítulo 6: Resolución y demostración automática de teoremas
154
Procesamiento por Resolución SLD.
El intérprete de PROLOG, utiliza el principio de resolución para probar que el programa
con la adición de la consulta negada es una fbf contradictoria. Sin embargo, gracias a lo
restringido de las cláusulas a las que se enfrenta el intérprete (por ser de Horn), veremos
que para aplicar el principio de resolución no es necesario computar todos los posibles
resolventes del conjunto, sino tan solo unos pocos, muy específicos, que llevan rápidamente
hacia la contradicción.
Lo primero que hay que notar es que la base de conocimientos no debe ser contradictoria en
si misma. De allí que la contradicción debe provenir de la introducción de la consulta
negada. Por ello es tan solo es necesario computar resolventes que provengan de la
consulta.
Recordemos además, que la consulta es una cláusula que tan solo contiene literales
negados, mientras que un hecho es una cláusula que sólo tiene un literal positivo, y una
regla es una cláusula que tiene un sólo literal positivo (la cabeza) seguido de una serie de
literales negados. De esta manera, es posible restringir la obtención de resolventes a
aquellos que se derivan de unificaciones entre un literal de la consulta, y el literal de un
hecho o la cabeza de una regla contenidos en el programa.
El proceso de resolución SLD (“Selective Linear Definite clause resolution96”) consiste, en
efecto, en buscar siempre para el primer literal de la consulta, al que en adelante
denominaremos “meta”, un hecho o la cabeza de una regla del programa que unifiquen con
dicha meta. Ésta búsqueda se hace en orden de arriba hacia abajo a través de las
cláusulas del programa, en lo que se conoce como resolución lineal.
Tras hallar el primer hecho o regla que unifique con la meta de la consulta, se obtiene el
resolvente, y se usa como la nueva consulta substituyendo a la anterior en el proceso
posterior de introducción de resolventes. Nótese que cuando la unificación de la meta se
efectúa con un hecho, la nueva consulta se obtiene de suprimir dicha meta en la consulta
anterior, luego de efectuadas las substituciones de las variables pertinentes a la unificación
en los demás literales de la consulta; en este caso diremos que la meta ha sido satisfecha o
alcanzada. Por otro lado, cuando la unificación de la meta de la consulta se efectúa con la
cabeza de una regla, la nueva consulta se obtiene de sustituir la meta en la consulta
anterior por la cola de la regla, luego de efectuadas las substituciones de las variables
pertinentes a la unificación en los demás literales de la consulta y en la cola de la regla; en
este caso diremos que la meta ha sido substituida por un conjunto de metas que la implican.
El proceso, en consecuencia, parte de una consulta que por supresión e introducción de
metas y substitución de sus variables, se va transformando de forma paulatina hasta llegar a
un éxito o a un fracaso. Se llega a un éxito cuando la supresión de metas conduce a la
consulta vacía, implicando que se ha demostrado la contradicción del programa aumentado
con la consulta negada. Se llega un fracaso cuando no se encuentra en el programa hecho o
cabeza de regla alguna que unifique con la meta de la consulta actual del proceso,
implicando que aún no se ha demostrado la contradicción del programa aumentado.

96http://en.wikipedia.org/wiki/SLD_resolution
Capítulo 6: Resolución y demostración automática de teoremas
155
Al producirse un fracaso el proceso de resolución SLD trata de efectuar un retroceso. El
retroceso consiste en desechar el objetivo que produjo el fracaso, y devolverse al objetivo
anterior, para tratar de resolverlo con un hecho o regla del programa distinto al que se
utilizó para llegar al objetivo fracasado. Para encontrar este nuevo hecho o regla,
simplemente se retoma la búsqueda del hecho o la regla que unifique con la meta del
objetivo sin tener en cuenta, o retractándose, del que se había utilizado anteriormente. De
no hallarse tal nuevo hecho o regla, se procede de nuevo a desechar el objetivo anterior,
tomando el que le antecedía (el anterior del anterior) para tratar de resolverlo con un hecho
o regla diferente a la usada antes. El proceso de desechar objetivos y devolverse al anterior,
se repite una y otra vez hasta que se llegue a un éxito o hasta que se agoten las
posibilidades de moverse hacia atrás en la secuencia de objetivos. De agotarse la secuencia
de objetivos, se considera entonces que el objetivo inicial no era consecuencia lógica del
programa. Esta consideración se conoce como negación de la consulta bajo fracaso.
En los subnumerales que siguen se ilustra el proceso de resolución SLD por medio de un
ejemplo sencillo, y se presentan dos formas de representarlo, una en forma de árbol que
enfatiza los caminos de falla y retroceso, y otra en forma de tabla que enfatiza el proceso
como uno de reescritura de la consulta. Esta última representación, por ser más práctica,
será la que usaremos para ilustrar el proceso asociado a los códigos que se escriben en los
capítulos siguientes.
6.5.1 Búsqueda de la prueba y árboles de búsqueda
Supongamos la siguiente programa PROLOG:
f(a) .
f(b) .

g(a) .
g(b) .

h(b) .

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


Inicialmente supongamos que se le plantea la consulta:
:- g(b).
El intérprete inmediatamente encuentra, buscando de arriba abajo en el programa, una
unificación para la única meta con el cuarto hecho logrando un éxito y respondiendo “yes”
a la consulta.
Ahora supongamos que planteamos la consulta:
:- k(Y) .
Probablemente el lector ya haya deducido que existe una única respuesta a esta consulta
que es Y=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(Y). En este caso solo hay una posibilidad,
que es la cabeza de la única regla (k(Y) :- f(Y), g(Y), h(Y) .).
Capítulo 6: Resolución y demostración automática de teoremas
156
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 las
variables de la consulta no son las misma que las de la regla. Entonces tenemos la consulta
k(Y) y la regla k(_01) :- f(_01), g(_01), h(_01), con lo que, hecha la unificación bajo la
substitución _01/Y y obtenido el resolvente, obtenemos una nueva consulta
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 Y que tuviera la propiedad k”; Luego él, encontró una regla que
decía que “un Y tiene la propiedad k si también tiene las propiedades f, g y h”. De manera
que ahora el intérprete buscará un Y, con estas últimas tres propiedades.
Las deducciones suelen ilustrarse mediante árboles como el de la Figura 6.1, donde aparece
la consulta o inicial y la transición a la siguiente consulta mediante una línea rotulada con el
reemplazo realizado, sobre las variables de la consulta y regla.

k(Y)
Y = _01
f(_01), g(_01), h(_01)
Figura 6.1. Primer paso en la deducción para la consulta K(Y).

Para satisfacer ésta nueva consulta, el intérprete busca de nuevo un hecho o la cabeza de
una regla que unifique con la meta de la nueva consulta. En éste caso encuentra que
mediante el reemplazo de _01 por a, se puede unificar la meta f(_01), con f(a), para obtener
una nueva consulta, g(a),h(a). Luego, sin necesidad de ningún reemplazo, la primera meta
de la consulta se puede unificar con el hecho g(a), eliminándola para obtener la nueva
consulta h(a). La representación hasta éste punto se puede observar en la Figura 6.2.
k(Y)
Y = _01
f(_01), g(_01), h(_01)
_01 = a
g(a), h(a)

h(a)
Figura 6.2. Paso intermedio en la deducción para la consulta K(Y).

Pero no hay manera de satisfacer la meta de esta última consulta. De manera que el
intérprete reconoce haber cometido un error y verifica si en algún punto existía la
Capítulo 6: Resolución y demostración automática de teoremas
157
posibilidad de unificar con la meta de manera diferente. Esto lo hace regresando un paso en
el camino mostrado en la Figura 6.2, y buscando a través del resto de la base de
conocimientos. En éste caso no existía otra posibilidad para unificar g(a). Así que sube un
peldaño más y allí si encuentra otra posibilidad. Mediante el reemplazo de _01 por b, se
puede unificar la meta f(_01) con f(b), para obtener una nueva consulta g(b),h(b). Este
proceso se conoce como “backtracking” o retroceso.
Luego, y sin necesidad de ningún reemplazo la meta de la consulta se puede unificar con el
hecho g(b), eliminándola para obtener la nueva consulta h(b). Y finalmente la única meta de
esta consulta unificar con el hecho h(b), para obtener la consulta vacía (o la cláusula vacía
en términos del principio de resolución). En la Figura 6.3, se ilustra el proceso completo.
k(Y)
Y = _01
f(_01), g(_01), h(_01)
_01 = a _01 = b
g(a), h(a) g(b), h(b)

h(a) h(b)

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

A la representación de la Figura 6.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 las consultas
a satisfacer en un momento determinado de la búsqueda, mientras que las líneas muestran
el reemplazo necesario para encontrar una nueva consulta. Las hojas (es decir, los nodos
terminales), en caso de estar vacíos muestran un posible camino de solución, mientras que
si no están vacíos, 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, al contestar “yes Y=b” como respuesta a la consulta .
Hay ocasiones en que existe más de una solución para una consulta planteada, como es el
caso 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 “;” para terminar la consulta. En nuestro caso por ejemplo,
para la consulta:
Capítulo 6: Resolución y demostración automática de teoremas
158
:- f(X);
Se obtendría como respuesta;
X=a;
X=b;
no
Que podemos representar con le árbol de la figura siguiente:

f(X)
X = a X = b

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

6.5.2 Búsqueda de la prueba y tabla de reescritura de la consulta


Si bién la representación del proceso de resolución SLD por medio del árbol de
“búsqueda”, es útil para mostrar los puntos de falla y retroceso, no sólo tiene el
inconveniente de ser engorroso de elaborar sino que tampoco enfatiza que la resolución de
la consulta no es otra cosa que un proceso de reescritura que transforma el objetivo inicial
de la contradicción (la consulta) hasta llegar al “falso” (la contradicción misma).
La secuencia de transformaciones que sufre el objetivo, puede puede ilustrarse por medio
de una tabla que muestra su evolución para una consulta específica sobre el programa. Para
ello reproduciremos primero el programa PROLOG del ejemplo anterior, pero numerando
cada una de las cláusulas, así:
1) f(a) .
2) f(b) .
3) g(a) .
4) g(b) .
5) h(b) .
6) k(Y) :- f(Y), g(Y), h(Y) .
El proceso para la consulta :- k(Y), ilustrado antes en forma de árbol, es descrito
tabularmente a continuación. En la tabla las filas ilustran el resultado del proceso para cada
uno de los pasos de la resolución97. En la primera columna de cada fila se muestra el
número de orden del paso de reescritura, en la segunda columna se indica el número de
orden de la cláusula del programa usada para obtener el resolvente (del objetivo de la línea
anterior con dicha cláusula), en la tercera columna se muestra la substitución de variables
necesaria para la unificación de la primera meta del objetivo anterior (que es un átomo
negado) con la cabeza de la cláusula (que es un átomo positivo), y, en la última columna se
muestra el resolvente del paso que es el nuevo objetivo del proceso.

Paso regla substitución objetivo


0 k(Y)

97 En los ejemplos de los capítulos siguientes, con frecuencia usaremos una línea para ilustrar más de un paso.
Capítulo 6: Resolución y demostración automática de teoremas
159
1 6 Y/Y´ f(Y), g(Y), h(Y)
2 1 a/Y g(a), h(a)
3 3 h(a)
4 ¿ Falla ya que no hay unificación posible

2 2 b/Y g(b), h(b)


3 4 h(b)
4 5 F

Para completar la descripción del proceso, vale la pena anotar que en el paso 1 se usó la
cláusula 6 del programa, pero luego de cambiarle la varible Y por Y´para evitar la colisión
de variables entre dicha cláusula y el objetivo; además, luego de la falla en el paso 4,
aparece de nuevo el paso 2 significando que hubo un retroceso para cambiar la claúsula del
programa con la que se había efectuado la resolución; esto es, al fallar el proceso con la
cláusula 1) del programa se reintenta con la 2).

Resumen del Capítulo.


Bajo el concepto de “equivalencia semántica” es posible transformar las fbf a formas
estandarizadas, denominadas “formas normales”. La “forma normal conjuntiva” (FNC)
reduce las fbfs a la conjunción de “cláusulas”, por lo que diremos que las fbfs están en
“forma clausal”. Si la fórmula (F1F2...Fn¬G) está en forma clausal, es posible demostrar
que es contradictoria, por introducción de resolventes, probando que G es consecuencia
lógica de F1,F2, ...,Fn.
Dos fbfs G y H se dicen equivalentes semánticamente, si y solo si, los valores de verdad de
G y H son los mismos bajo cualquier interpretación de sus átomos, en este caso la fbf GH
es una tautología.
Para transformar una fbf en otra equivalente, basta con sustituir una de sus subfórmulas por
otra equivalente. Así, la transformación de una fbf será apoyada por equivalencias básicas,
que denominamos “leyes”.
Una formula A se dice estar en forma normal conjuntiva (FNC) si y solo si, tiene la forma
P1P2...Pn donde cada P1,P2,...,Pn es una disyunción de literales, entendiendo por
“literal” a un átomo o a su negación. A una disyunción de literales la denominaremos
“cláusula”, y diremos que una fbf en FNC está en forma “clausal”. Para transformar una fbf
en su equivalente en FNC, basta seguir los siguientes pasos aplicando las leyes: Eliminar
todas las  y , utilizando sus definiciones; y, si la fórmula contiene subfórmulas
compuestas negadas se deben simplificar con las leyes de Morgan y doble negación;
utilizar luego sucesivamente la ley distributiva para llevar las  hacia adentro y las  hacia
afuera hasta encontrar una FNC.
Para las fbfs en lógica de predicados, es posible también modificar la posición de los
cuantificadores manteniendo la equivalencia semántica. Las leyes usadas para dichas
transformaciones deben, sin embargo, ser demostradas por razonamiento lógico. Por medio
de esta ley es posible transformar cualquier fbf en otra que tenga la forma “prenex”. Una
fbf en forma “prenex” tiene todos sus cuantificadores en secuencia a la izquierda de la
fórmula. Una fbf en forma “normal conjuntiva prenex” tendrá todos los cuantificadores a
la izquierda y la fbf multicuantificada estará en FNC.
Capítulo 6: Resolución y demostración automática de teoremas
160
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 es la cláusula cuyos literales son el conjunto R = (C1 – {L})  (C2 – {¬L}), es
decir, los de C1 y C2 sin L y ¬L . Nótese que R es consecuencia lógica de C1 y C2.
Es fácil demostrar que si una fbf está en forma clausal, la fbf resultante de agregarle como
una cláusula más el resolvente de dos de sus cláusulas, es semánticamente equivalente a la
original. Además se puede probar que si a una fbf contradictoria en forma clausal, se le
introducen todos los posibles resolventes de sus cláusulas y de sus cláusulas con los
resolventes introducidos, uno de los resolventes será necesariamente al valor lógico “falso”
(F)
Para el caso en que los átomos de los literales son predicados con variables, asumiremos
que cada claúsula está en forma prenex y que sus variables están cuantificadas
universalmente (y de forma separada en cada cláusula). En este caso podemos considerar
que las cláusulas cuantificadas representan un conjunto (posiblemente infinito) de cláusulas
sin variables. Es entonces posible que una cierta particularización de las variables de dos
cláusulas diferentes, generen dos conjuntos con el mismo número de cláusulas sin variables
en los que todas las cláusulas de uno puedan unificarse con una claúsula correspondiente
del otro; en este caso el conjunto de resolventes se puede representar con una cláusula
cuantificada. La substitución de las variables de las cláusulas que permite llevar a cabo las
resoluciones se denomina el “unificador”, y el unificador que obtiene todas las resoluciones
posibles se denomina el unificador “más general”.Los lenguajes lógicos automatizan el
proceso de demostración por reducción al absurdo basado en la resolución. Ellos
simplifican 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.
Un programa PROLOG es una fbf en forma clausal, en el que todas las cláusulas son
cláusulas de “Horn”. En una cláusula de Horn hay cero o un literal positivo, con o sin
literales negados. Estas cláusulas pueden tomar, entonces, una de las formas siguientes
(siendo A y Bi predicados atómicos):
 Hechos: X1X2X3.... Xn (A(...))
 Reglas: X1X2X3.... Xn (A(...) B1(...)  B2(...)  B3(...) ..)
El programa permite dar respuesta a consultas, probando que de él se puede deducir una fbf
de la forma siguiente (dando como respuesta el valor de las variables):
 Consultas: G = X1X2X3... Xn ( G1(...)G2(...)G3(...)..)
Para probar que la consulta es consecuencia lógica del programa, ella se niega, se adjunta al
programa y se trata de probar que el todo es insatisfacible. Nótese que al negar la consulta
esta toma la forma de una claúsula de Horn de la forma siguiente:
 Consultas: X1X2X3...( G1G2G3..)
Para facilitar la escritura del programa los elementos del programa toman la forma
siguiente:
 Hechos: A(...) .
 Reglas: A(...) :- B1(...) , B2(...) , B3(...) ,..
 Consultas: :- G1 , G2 , G3..
Capítulo 6: Resolución y demostración automática de teoremas
161
Dado que el programa más la consulta negada es una fbf en forma clausal, la demostración
de insatisfacibilidad se lleva a cabo por introducción de resolventes.
Sin embargo, gracias a que las cláusulas son de Horn, para probar la insatisfacibilidad del
programa más la consulta, es suficiente con introducir unos pocos resolventes del conjunto
de los posibles llegando rápidamente a la contradicción. Si se tiene en cuenta que el
programa no debe ser contradictorio en si mismo, la contradicción debe provenir de la
introducción de la consulta negada. Por ello solo es necesario computar resolventes que
provengan de la consulta. Además como la consulta solo contiene literales negados, es
posible restringir la obtención de resolventes a aquellos que se derivan de unificaciones
entre un literal de la consulta, y el literal de un hecho o la cabeza de una regla contenidos en
el programa.
El proceso de resolución SLD consiste, en efecto, en buscar siempre para el primer literal
de la consulta, al que en adelante denominaremos “meta”, un hecho o la cabeza de una
regla del programa que unifiquen con dicha meta. Ésta búsqueda se hace en orden de
arriba hacia abajo a través de las cláusulas del programa.
Tras hallar el primer hecho o regla que unifique con la meta de la consulta, se obtiene el
resolvente, y se usa como la nueva consulta substituyendo a la anterior en el proceso de
introducción de resolventes. El proceso, en consecuencia, parte de una consulta, que por
supresión e introducción de metas y substitución de sus variables, se va transformando de
forma paulatina hasta llegar a un éxito o a un fracaso. Se llega a un éxito cuando la
supresión de metas conduce a la falsedad (consulta vacía), implicando que se ha
demostrado la contradicción del programa aumentado con la consulta negada. Se llega un
fracaso cuando no se encuentra en el programa hecho o cabeza de regla alguna que unifique
con la meta de la consulta actual del proceso, implicando que aún no se ha demostrado la
contradicción.
Al producirse un fracaso el proceso trata de efectuar un retroceso. El retroceso consiste en
desechar el objetivo que produjo el fracaso, y devolverse al objetivo anterior, para tratar de
resolverlo con un hecho o regla del programa distinto al que se utilizó para llegar al
objetivo fracasado retractándose, del que se había utilizado anteriormente. De no hallarse
tal nuevo hecho o regla, se procede de nuevo a desechar el objetivo más anterior, tomando
el que le antecedía (el anterior del anterior) para tratar de resolverlo con un hecho o regla
diferente. Este proceso se repite una y otra vez hasta que se llegue a un éxito o hasta que se
agoten las posibilidades de moverse hacia atrás en la secuencia de objetivos. De agotarse la
secuencia se considera entonces que el objetivo inicial no era consecuencia lógica del
programa. Esta consideración se conoce como negación de la consulta bajo fracaso.

Ejercicios propuestos
1. Demuestre cada una de las equivalencias de la tabla de equivalencias de la sección
4.4.2.1 utilizando tablas de verdad.
2. Para las siguientes fórmulas encuentre una fórmula en forma normal conjuntiva
equivalente a ella; Y luego una forma normal disyuntiva.
(¬PQ)R
P((QR)S)
(¬P(QP)P)(¬P(P¬Q))
Capítulo 6: Resolución y demostración automática de teoremas
162
¬(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.)
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}}

2. Considerar la siguiente base de conocimientos:


Capítulo 6: Resolución y demostración automática de teoremas
163
a(a1,1).
a(A,2).
a(a3,N).

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

###  |= F=≠≈≤≥ F V


Capítulo 7
Elementos Básicos de Los
Lenguajes de Programación
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
166
Introducción
En los capítulos 5 y 6 se presentaron los SRTs y la resolución SLD, como medios para
automatizar la demostración de teoremas que hacen posible el uso de logicas como
lenguajes de programación.
En el capítulo 5 se mostró que en un SRT, los axiomas de la teoría son utilizados como
reglas de reescritura que se aplican de forma automática a una consulta --constituida por un
término base--, transformándola hasta llegar a la respuesta deseada.
En el capítulo 6 se mostró que en un programa PROLOG, las cláusulas de la teoría son
utilizadas como reglas de reescritura que se aplican de forma automática a una consulta --
constituida por una claúsula con variables--, transformándola hasta el valor lógico F
(“falso”) y dando como respuesta las particularizaciones de las variables realizadas en el
proceso.
El objeto general de la discusión que sigue en el texto, es el de resaltar las similitudes y
diferencias entre los programas escritos en un conjunto de lenguajes de programación que
de alguna manera incorporan elementos de un SRT o de la resolución SLD. A estos
lenguajes los denominaremos de forma sumaria como “lenguajes derivados de la lógica”98.
En el texto, por medio de un conjunto seleccionado de ejemplos, mostramos que los
patrones de programación usados en los programas son escencialmente los mismos en todos
estos lenguajes. Por lo anterior consideramos que si un lector conoce los patrones de
programación más utilizados y relevantes, le bastará sólo con “acomodarse” a la sintaxis de
un lenguaje para desempeñarse exitosamante en cualquiera de ellos.
Bajo la óptica de los conceptos asociados con los lenguajes lógicos, analizaremos también,
algunos de los lenguajes “procedurales” más representativos. El análisis estará orientado a
mostrar la forma en que estos lenguajes dan cuenta de los patrones de programación
estudiados para los lenguajes lógicos. Con ello estará capacitado para aplicar en los
lenguajes procedurales las habilidades adquiridas en el estudio de los lenguajes derivados
de la lógica.
Al objeto de comparar los diferentes lenguajes, en este capítulo presentamos una serie de
conceptos que hemos denominado “criterios diferenciadores”, con los que caracterizaremos
los diferentes lenguajes. Bajo la óptica de estos conceptos clasificaremos los lenguajes que
se tienen en cuenta en los capítulos siguientes para, sin proceder a un juicio de valor, sentar
las bases a la selección del lenguaje más adecuado en una situación de
programaciónespecífica.
En este capítulo se señalamos varios lenguajes de programación que considerarmos
relevantes y representativos de una forma particular de implementar los criterios
diferenciadores. Estos lenguajes serán usados a lo largo del trabajo para implementar los
ejemplos. Aunque nuestra selección de lenguajes es relativamente arbitraria, como se
explica en la sección correspondiente, está influenciada tanto por el grado de aceptación del

98 En opinión del autor, desde la apración del FORTRAN, los lenguajes de programación incorporan elementos de la
lógica, en particular todos utilizan términos (o “formulas”) para referirse a los valores manipulados. El centro del texto es
entonces la incorporación de los axiomas de igualdad y las cláusulas como patrón de codificación.
Capítulo 7: Elementos básicos de los Lenguajes de Programación
167
lenguaje en la comunidad de desarrolladores como por la posición del lenguaje en el
contexto histórico del desarrollo de los conceptos.
Vale la pena advertir en este punto que el texto no es un tratado del enfoque lingüístico
“funcional” o “lógico” como tal. En particular, no pretende cubrir las ideas fundamentales
que soportan dichos enfoques, ni las que determinan los avances en este contexto. Es así
que para los lenguajes usados en el texto, se puede afirmar que lo referido no cubre todas
las propiedades del lenguaje ni enfatiza su pureza frente a los conceptos teóricos de la
aproximación a la que se asocie.

Multiplicidad de Lenguajes
¿Cuál es el mejor lenguaje?: Es “la pregunta del siglo”.
Para responderla, el primer problema es la gran cantidad de lenguajes existentes. Como
una muestra, Bill Kinnersley relaciona en su página web 2500 lenguajes, que categoriza
alrededor de 18+ “paradigmas” [Kinnersley 1991-now]; allí Kinnersley destaca como los más
“influyentes” a un conjunto de lenguajes vistos en orden histórico [Kinnersley 1991-now /A
Chronology of Influential Languages]. Wikipedia, por su parte, presenta en su “List of
programming languages” una cantidad igualmente extensa de lenguajes bajo un intento de
clasificación múltiple compuesto de mas de 50+ categorías.
Por otro lado, el grado de interés sobre los distintos lenguajes (su “popularidad”), es
estimado en el “indice TIOBE” a partir de factores tales como las consultas en la WEB, el
número de cursos asociados con cada lenguaje y otros adicionales [TIOBE ¿-now]. Según
este índice, la popularidad de los lenguajes no parece fluctuar mucho con los años; así, en el
índice se refiere entre otras cosas que “Si comparamos los 10 primeros del índice actual
con los 10 primeros del índice de hace 10 años, veremos que contienen exactamente los
mismos lenguajes de programación”99.
Los lenguajes, además, no son un fenómeno aislado; de hecho, las ideas útiles que aparecen
en un lenguaje rápidamante se reproducen en sus “sucesores” creándose relaciones de
influencia entre los lenguajes, o “familias”, que son significativas al momento de
caracterizarlos. Una propuesta de la forma en que los lenguajes históricamente se
relacionan entre sí, aparece en en el grafo de Levenez [Levenez ¿-now].
A partir de estas tres fuentes, y del conocimiento previo del autor sobre la naturaleza de
varios de los lenguaje, hemos señalado como lenguajes de interés para este trabajo, a un
subconjunto de lenguajes que además de ser considerados “populares” ó “relevantes”,
pueden verse como representativos de tres familias lingúisticas, así:
 De la gran famila de los lenguajes denominados “procedurales”, que derivan del
FORTRAN y el ALGOL, señalamos al lenguaje clásico C, su versión objetuale
C++, y sus descendientes, los lenguajes de “script”, C#, JAVA, PHP y PYTON.
 De la familia de los lenguajes denominados “funcionales” que derivan del LISP y el
ML, señalamos el SCHEME, influenciado por el ALGOL y por el LISP, el
HASKEL influenciado por el ML y por el MIRANDA, el OCAML derivado directo
del ML, el SCALA y el F# como la contraparte funcional del JAVA y del C#, y

99 Traducción libre del autor.


Capítulo 7: Elementos básicos de los Lenguajes de Programación.
168
finalmente al lenguaje MAUDE, como un lenguaje de investigación que incorpora
característica novedosas dentro de su familia.
 De la familia de los lenguajes denominados PROLOG, señalamos el SWI
PROLOG, por su alta difusión y su accesibilidad en los compiladores “on line ”.

Conceptos diferenciadores de los lenguajes de


programación.
Para dar una idea más precisa del carácter de los diferentes lenguajes, y, en particular, de
sus similitudes y diferencias, presentamos a continuación un conjunto de conceptos que al
tomar forma en un lenguaje constituyen, en nuestro parecer, los “criterios diferenciadores”
que determinan el carácter fundamental de un lenguaje particular. Con ello, en este
volumen, sin proponer una teoría general de los lenguajes de programación, nos
proponemos capacitar al lector tanto para reconocer rápidamente la naturaleza general del
lenguaje que confronta en un proyecto de desarrollo, como para intuir la forma de aplicar
en dicho lenguaje los patrones de programación que se presentan en el resto del trabajo.
Los criterios diferenciadores que proponemos son los siguientes:
 Tipos de valores y operadores nativos: Los valores y términos que manipulan los
programas, en todos los lenguajes señalados, se forman con base en valores y
operadores (o predicados100) elementales pertenecientes a los diversos tipos
“nativos” del lenguaje. Así, los tipos de valor, los valores de cada tipo, y el
comportamiento de los operadores nativos (en cuanto a modelo de evaluación,
precedencia, asociatividad, precisión etc..), determinan en gran medida el carácter
de un lenguaje y su rango de utilización.
 Naturaleza de los Operadores Definidos: Todos los lenguajes señalados permiten
la definción de nuevos operadores con base en los nativos. Ellos, sin embargo,
difieren en la naturaleza de los operadores definidos. En este contexto son criterios
diferenciadores los siguientes:
o Plantilla: La mayoría de los lenguajes procedurales permiten sólo la definción
de operadores con notación “prefija”, mientras que algunos de los lenguajes
funcionales permiten definir operadores con notación “infija” (ver 4.2.1.3.2).
o Precedencia y asociatividad: Si un lenguaje posibilita la definición de
operadores con notación infija, debe proveer también mecanismos para
establecer su orden de precedencia y, de ser el caso, su sentido de asociatividad
(ver 5.2.3.3). En el lenguaje MAUDE es posible incluso, declarar que un
operador diádico tiene propiedades semánticas (v.g. es “asociativo” o
“conmutativo”), posibilitando la construcción de intérpretes que transformen el
árbol sintáctico de una evocación a otro árbol equivalente que convenga mejor a
la valoración de un término específico101.
 Definición de los Operadores: La definición de un operador especifica la relación
que debe existir entre el valor de una evocación, con los valores de los operandos de
la evocación. La manera de plantear dicha relación difiere, sin embargo, en

100 Los predicados serán entendidos como operadores que tienen como codominio a BOOL.
101 Sin que el intérprete utilizado en el trabajo tenga realmente esta propiedad.
Capítulo 7: Elementos básicos de los Lenguajes de Programación
169
diversos aspectos entre las familias de lenguajes. Entre los diversos aspectos
consideramos diferenciadores los siguientes:
o Definición del proceso computacional: Como es de esperarse la la manera de
definir el proceso que da lugar al cálculo del valor de una evocación, está
determinada por la forma del proceso, así:
 Intrucciones de asignacion, control, declaración y E/S: En la familia de
los lenguajes procedurales la especificación del proceso se apoya en
secuencias de “instrucciones de asignación”, “instrucciones de control”, e
instrucciones de “entrada/salida”, que determinan procesos de cambio o
“reescritura” del medio ambiente que define el significado de los rótulos
del programa.
 SRT: En la familia de los lenguajes funcionales, la especificación del
proceso se fundamenta en un Sistemas de Reescritura de Terminos ó SRT,
que determina la manera como evolucionan los términos sometidos al
programa para su cálculo.
 FNC: En la familia de los lenguajes lógicos, la especificación del proceso
se fundamenta en una fbf en forma normal conjuntiva (FNC) compuesta
por cláusulas de Horn (ver 6.3.1), que determinan la manera como
evoluciona los objetivos sometidos al programa para su verificación.
o Unidades de especificación: El número de unidades de especificación con las
que se define el valor de una evocación, es también un criterio diferenciador.
En la gran mayoría de los lenguajes procedurales se usa una única unidad de
especificación que define el proceso para todas las posibles evocaciones del
operador. En ellos, para distinguir entre los diferentes casos de cálculo, se usan
las instrucciones de control del proceso. En la mayoría de los lenguajes lógicos
y funcionales se permiten múltiples unidades de especificación, cada una
definiendo el proceso para un grupo limitado de evocaciones. En ellos, para
distinguir entre los diferentes casos de cálculo, se tiende a usar unidades de
especificación diferentes
o Argumentos y variables: Una unidad de especificación define el valor de las
evocaciones para todas las posibles particularizaciones de las variables de una
evocación ficticia o “formal” del operador. En los lenguajes que utilizan una
sola unidad de especificación le corresponde, a cada argumento de la evocación
formal, una variable diferente del medio ambiente propio. La variable que le
corresponde a un argumento, toma el valor de dicho argumento al inicio del
proceso en las evocaciones reales del operador. En los lenguajes que utilizan
varias unidades de especificación, los argumentos de la evocación formal
pueden ser términos complejos cuyos subtérminos pueden ser constantes o
variables del medio ambiente propio (no necesariamente diferentes). Estas
variables toman al inicio del proceso el valor que tienen los subtérmino de igual
posición en el árbol sintáctico del argumento que les corresponde en las
evocación reales.
 Evaluación de los Operadores: El cálculo del valor para una evocación de un
operador, es un proceso cuyas características y efectos son particulares a cada
familia de lenguajes. En este contexto son criterios diferenciadores los siguientes:
o Estados del Proceso: El cálculo del valor de una evocación se lleva a cabo por
medio de un proceso “computacional”, que no es otra cosa que una secuencia
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
170
de cambios del contenido (o “estado”) de la memoria del computador asociada
con el proceso (ver 1.2). Sin embargo, lo que constituye el estado del proceso es
diferente para las diferentes familias de lenguajes, así:
 Medio ambiente: Para la familia de los lenguajes procedurales, un estado
es un “medio ambiente” o “espacio de nombres” formado por asociaciones
de rótulos (comúnmente denominados “variables”), valores, operadores y
tipos manipulados en el programa (ver XX).
 Terminos: Para la familia de los lenguajes funcionales un estado es un
conjunto de términos formados por operadores, variables y valores del
programa (ver 5.3.3.2).
 Cláusula: Para la familia de los lenguajes lógicos un estado es uno o varios
objetivos o “cláusulas de Horn”, formados por predicados, operadores,
variables y valores del programa (ver 6.5 ).
o Estrategia de cálculo de argumentos: Para calcular el valor de una evocación,
es necesario calcular o “evaluar” el valor de los operandos (u argumentos
reales) de la evocación. Cada lenguaje determina la estrategia de evaluación
que debe usarse al calcular el valor de los operandos, así: la evaluación “en
orden aplicativo” (o “avariciosa”) determina calcular primero los operandos
antes de aplicarlos en la definición del operador; mientras que, las evaluación
en “orden normal” (o “prerezosa”) determina aplicar los operandos en la
definición del operador sin calcularlos, llevando a cabo dichos cálculos sólo
para y cuando los valores de los operandos sean requeridos para obtener el
valor de la evocación. En el lenguaje MAUDE es incluso posible defnir cuales
operandos se calculan antes y cuales operandos se calculan después de ser
aplicados en la definición de un operador.
o Medio ambiente: Para calcular el valor de una evocación, es necesario conocer
el valor de las variables que ocurren al interior de la definición del operador. El
valor de estas variables constituye el “medio ambiente de evaluación propio”
del operador. En la mayoría de los lenguajes funcionales y lógicos, este medio
ambiente está totalmente determinado por el valor de los argumentos de la
evocación. En este caso, diremos que el lenguaje define operadores
“autonomos”. En la mayoría de los lenguajes procedurales existe, sin
embargo, la posibilidad de que los valores de algunas variables del medio
ambiente propio estén determinados por el medio ambiente en el que se evocó
el operador, por el medio ambiente en el que se definío, e incluso por el medio
ambiente de las evocaciones anteriores del mismo operador. En este caso
diremos que el lenguaje define operadores “dependientes”.
o Efectos: La evaluación de una evocación puede cambiar el estado del proceso,
de varias formas posibles, así: (1) substituyendo la evocación por el valor
calculado; (2) asignando valores a variables no asignadas en el medio ambiente
propio; (3) asignando valores a variables no asignadas en el medio ambiente de
la evocación; (4) reasignando valores a variables previamente asignadas en los
medios ambientes, propio, de la evocación, o de la definición. En todos los
lenguajes ocurre como efecto principal el (1). En un lenguaje funcional
usualmente ocurre máximo hasta el (2). En un lenguaje lógico ocurre máximo
hasta el (3). Y, en un lenguaje procedural pueden ocurrir todos los efectos
referidos. Si el efecto de una evocación no incluye el (4), diremos que el
Capítulo 7: Elementos básicos de los Lenguajes de Programación
171
lenguaje tiene “transparencia referencial”, en caso contrario diremos que
permite que las evaluaciones tengan “efectos colaterales”.
 Control de tipo en los términos: En los programas de todos los lenguajes señalados
se escriben términos complejos que evocan tanto los operadores nativos del
lenguaje como los definidos en el programa. Sin embargo, los diferentes lenguajes
ejercen diferente grado de control a la correspondencia entre el tipo de los
operandos pre-supuestos para un operador, y el tipo de los operandos realmente
usados en las evocaciones de dicho operador. En efecto, un lenguaje puede señalar
o no señalar”, -- y hacerlo con diferentes grado de severidad--, como incorrectas o
“mal formadas” las evocaciones en las que no se garantiza dicha correspondencia.
Así, los diferentes lenguajes van desde los “fuértemente tipados” en los que
cualquiér evocación riesgoza es rechazada durante la escritura del programa, para
garantizar que los cálculos se lleven a cabo correctamente en ejecución; hasta los
“débilmente tipados” en los que no se rechaza evocacion alguna, dejando al
interprete el problema de corregir las inconsistencias en ejecución (o de asumir
terminaciones anormales del proceso en caso no ser posible la corrección).
 Parametrización: La definción de un operador define el valor resultante para todas
las evocaciones que particularicen los argumentos a un valor de su tipo. Para
lograrlo remplaza los operandos a ser usados en los términos de la definción por
variables que “paramerizan” el cálculo que dichos términos representan. La
mayoría de los lenguajes permiten parametrizar los cálculos, remplazando por
variables no sólo los operandos a los que se aplican los operadores sino también los
operadores que se aplican a los operandos. Así, al evocarse un operador deben
particularizarse a un operador las variables que representen operadores dentro de la
definción. Hay lenguajes en que es posible parametrizar los cálculos no sólo en
cuanto a los operandos y operadores que participan en el proceso, sino también en
cuanto a los tipos de los operandos involucrados. En el lenguaje MAUDE la
parametrización se extiende hasta los módulos incluidos dentro de otros módulos en
la estructura del programa. El grado y tipo de parametrización que permite un
lenguaje es, en consecuencia, un importante criterio diferenciador.
 Metaprogramación: Si bién los códigos que definen los operadores (y el programa)
son escencialmente estáticos, es posible que al ejecutarse ellos den como resultado
otros códigos ejecutables. Si en un lenguaje es posible escribir programas que
escriban y ejecuten parte del mismo (u otro) programa, diremos que el lenguaje
permite la “metaprogramación”. La capacidad, forma y alcance de la de
metaprogramción es un importante criterio diferenciador de los lenguajes.
En el curso las presentaciones se harán bajo los lenguajes SCHEME, MAUDE, PROLOG,
PHP, el objeto de ilustrar el uso de los enfoques funcional y lógico, así como sus
diferencias con respecto al enfoque procedural.
Se solicitará como ejercicios adicionales, la codificación de los ejemplos en otros lenguajes
(Haskel, Erlang, Scala, ML, etc..)
Usaremos compiladores en línea hasta donde sea posible http://www.compileonline.com,
https://ideone.com/
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
172
Implementaciones e interfaces.
Tanto para poder escribir los programas como para ejecutarlos, es necesario contar con un
programa que reciba las instrucciones del programa, verifique su correctitud y lo convierta
a un programa ejecutable compilándolo o interpretándolo. Estos programas, que
denominamos indistintamente como las “implementaciones” o “interpretes” del lenguaje
(ver 1.3), deben poseer mecanismos efectivos de comunicación con el programador, y ser
capaces de dotar al programa ejecutable con mecanismos efectivos de comunicación con el
usuario. A estos mecanismos los denominaremos las “interfaces” del interprete, y del
programa.
En lo que sigue caracterizaremos brevemente dichas interfaces para presentar luego las que
ofrecen las implementaciones utilizadas en este trabajo
7.4.1 Linea de comandos REPL.
Los lenguajes funcionales están usualmente orientados a definir programas que llevan a
cabo el cálculo de términos (o “expresiones”, o “fórmulas”). En efecto, dichos programas
son básicamente artefactos que le ofrecen al usuario la posibilidad de someter al
computador términos a ser calculados.

Al someter al programa el término:


3*32+5*20
Se obtiene como resultado su valor semántico:
196102

A efectos de poder someter los términos a ser calculados y de poder recibir la respuesta del
cálculo, el usuario debe contar con una interfaz de entrada y salida de datos. Esta interfaz
puede consistir en una línea de comandos o en una forma de diálogo, en la que el usuario
escribe uno o varios términos base y recibe como respuesta el o los valores calculados.
La línea de comandos está usualmente señalada por un símbolo específico del sistema (o
“prompt”), luego del cual el usuario escribe una orden a la que el sistema responde con un
ciclo de: “lectura del comando - evaluación del comando - escritura del resultado de la
evaluación – y vuelta al símbolo del sistema”. A este ciclo se le denominó en el lenguaje
LISP como ciclo REPL, por el acrónimo de “Read-Eval-Print Loop”.

La mayoría de los lenguajes funcionales seleccionados en el trabajo ofrecen implementaciones con una
línea de comandos. En particular, un término base escrito en la línea de comandos es inmediatamente
calculado así:
MIT SCHEME103

102
Que no es otra cosa que una notación simplificada para el término (1*100+9*10+6*1).
Capítulo 7: Elementos básicos de los Lenguajes de Programación
173
Para iniciar la línea de comandos o retornar a ella desde un estado de error, se debe usar la combinación
CTRL+G .
1 ]=> (+ (* 3 32) (* 5 20))
;Value: 196
HASKEL104
Prelude> 3*32+5*20
196
SCALA105
scala> 3*32+5*20
res0: Int = 196
MAUDE106
En la implementación de MAUDE, el comando rew (o “rewrite”) ordena la reescritura del témino que
le sigue, con base en los axiomas del módulo “actual”.
Maude> rew 3 * 32 + 5 * 20 .
rewrite in CONVERSION : 3 * 32 + 5 * 20 .
rewrites: 3 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result NzNat: 196
Maude>

En lugar de una línea de comandos, el REPL puede consistir en una forma de diálogo
compleja, con la que el usuario interactúa para plantear uno varios términos a ser
calculados (permitiendo que unos términos se calculen con base los valores de los otros107)
para obtener diversos resultados.

:
En la “hoja de cálculo” siguiente se han dispuesto un conjunto de campos rotulados para que el usuario
ingrese los términos a ser calculados y obtenga los resultados de los cálculos.

A B C
1
2 B1-A1 B1-A2 B2*A2
Dos elementos son destacables en la manera como se disponen los campos en la hoja, así:
 Cada término se coloca en el campo en el que se desea obtener el resultado de su cálculo.
 En los términos se pueden sustituir tanto valores base como subtérminos por rótulos asociados a
otros campos que contendrán estos valores base y subtérminos. Esta estrategia facilita no sólo
la escritura de términos complejos, sino también la rápida modificación de los términos a ser
calculados modificando sus valores base y/o sus subtérminos.

103
Para el lenguaje SCHEME se usó la implementación MIT SCHEME, ofrecida por el Instituto Tecnológico de
Massachussets (MIT) bajo licencia Publica General GNU [SCHEME].
104 Medio ambiente interactivo GHCi de la implementación GHC (“Glasgow Haskell Compiler”) [HASKELL].
105 Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_40) [SCALA].
106 Maude 2.6 built: Mar 31 2011 23:36:02 [MAUDE].
107 Evitando dependencias circulares entre los téminos.
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
174
En el ejemplo, cuando que el usuario ingresa los datos mostrados en la primera fila, se remplazan los
términos de la segunda con los resultados de los cálculos respectivos.

A B C
1 1 2 3
2 1 1 1

En este contexto los elementos del programa pueden introducirse como un elemento más
del ciclo REPL entremezclándolos con los términos a ser calculados.

Un operador puede ser definido “en línea” introduciendo las ecuaciones del SRT correspondientes:
MIT SCHEME
1 ]=> (define (cua X) (* X X))
;Value: cua
1 ]=> (cua 4)
;Value: 16
HASKEL
Prelude> let cua x = x * x
Prelude> cua 4
16
SCALA
scala> def cua(X:Int):Int = X*X
cua: (X: Int)Int
scala> cua(4)
res0: Int = 16
MAUDE
Maude> fmod CUA is
> protecting INT .
> op cua : Int -> Int .
> var I : Int .
> eq cua(I) = I * I .
> endfm
Advisory: redefining module CUA.
Maude> rew cua(4) .
rewrite in CUA : cua(4)
rewrites: 2 in 16280360
result NzNat: 16

Es importante recordar aquí que bajo el modelo de ejecución por reescritura de términos
propio de los SRT (ver 5.4), propio de los lenguajes funcionales, el orden en que se
introducen las ecuaciones del programa no determina el orden en que se llevan a cabo los
pasos del proceso.

La definición de un operador puede usar otro operador definido posteriormente:


Capítulo 7: Elementos básicos de los Lenguajes de Programación
175
MIT SCHEME
1 ]=> (define (sumcua X Y) (+ (cua X) (cua Y)))
;Value: cua
1 ]=> (define (cua X) (* X X))
;Value: cua
1 ]=> (sumcua 4 3)
;Value: 25

Los REPL usualmente ofrecen también la posibilidad de tomar la definición de los


operadores de un archivo de texto previamente creado.

La definición de un operador puede tomarse de un archivo de texto, siempre y cuando esté almacenado
en el directorio de trabajo.
MIT SCHEME
1 ]=> (load “cua.scm”)
;Unable to find file “cua.scm” because: File does not exist.
;To continue …
;…
2 error>;;;use <ctrl+G>
;Quit!
1 ]=> (pwd)
;Value: 13 #[pathname “c:\\...\\mit scheme”]
1 ]=> (cd “c:\\ejscheme”)
;Value: 14 #[pathname “c:\\ejscheme”]
1 ]=> (load “cua.scm”)
;Loading “cua.scm” -- done
;Value: cua
1 ]=> (cua 4)
;Value: 16

HASKEL
Prelude> :cd C:\\ejhaskell
Prelude> :! dir

Directory of C:\ejhaskell

Prelude> :load cua.hs
[1 of 1] Compiling Main ( cua.hs, interpreted )
Ok, modules loaded: Main.
*Main> cua 4
16

SCALA
scala> :load C:\\ejscala\\cua.txt
Loading C:\ejscala\cua.txt...
cua: (X: Int)Int
scala> cua(4)
res0: Int = 16
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
176
MAUDE
Maude> pwd
/cygdrive/c/Program Files (x86)/MaudeFW
Maude> cd /cygdrive/c/ejmaude
Maude> load cua.maude
Maude> rew cua(4) .
rewrite in CUA : cua(4) .
rewrites: 2 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result NzNat: 16

7.4.2 Ciclo Codificación Interpretación Ejecución.


La implementaciones de los lenguajes procedurales puede consistir ya sea de programas
simples que toman el código de un archivo de texto para verificarlo ensamblarlo y
ejecutarlo; o ya sea, de medios ambientes complejos108 que incorpora facilidades tales
como editores especializados para elaborar el código y las interfaces, modulos de
ensamablaje y ejecución que permitan ejecuciones controladas, gestores de proyectos que
manipulan versiones, y chequeo del código por ejecución automática de conjuntos de casos
de prueba.
En cualquiera de estos contextos el programa se debe primero escribir completamente como
un texto, que luego se somete al intérprete para su traducción y posterior ejecución.
Además los lenguajes procedurales están usualmente orientados a definir programas que al
ejecutarse toman los datos desde un dispositivo de entrada de datos, llevan a cabo los
cálculos, y envían los resultados a un dispositivo de salida.
Por simplicidad en este trabajo las pruebas de los códigos escritos en estos lenguajes se
apoyaron en implementaciones WEB o “en línea”, tales como la referida en [ideone].

En la implementación en línea referida en [ideone], es simple escribir un programa que indica primero
leer datos del archivo de texto stdin, para luego escribir los resultados en el archivo de texto stdout, así:
Archivo stdin
Juan
Archivo stdout
Hola Juan
A continuación se muestra dicho programa en algunos lenguajes procedurales:
C
#include <stdio.h>

int main(void) {
char nm[20];
scanf("%s", nm);
printf("%s %s", "Hola", nm);
return 0;

108 O IDE por “Integrated Development Environment”


Capítulo 7: Elementos básicos de los Lenguajes de Programación
177
}

JAVA
import java.io.*;
class Hola
{
public static void main (String[] args) throws
java.lang.Exception
{
BufferedReader r = new BufferedReader (new
InputStreamReader (System.in));
String s = new String("Hola ");
s =s.concat(r.readLine());
System.out.println(s);
}
}

PYTHON
nm = raw_input()
print "Hola " + nm

PHP
<?php
$input = trim(fgets(STDIN));
fwrite(STDOUT, "Hola $input\n");
?>

Es importante notar que en los lenguajes procedurales, el orden de las instrucciones del
programa, junto con las instruccions de conrol, determinan el orden en que se llevan a cabo
los pasos del proceso. De esta manera, para leer en el orden correcto los datos de un
archivo de entrada basta con colocar, en el mismo orden, las instrucciones de lectura en el
programa.
Ls lenguajes funcionales, sin embargo, deben proveer mecanismos de ordenamiento de los
pasos del proceso para lleva a cabo, por ejemplo, la lectura de los datos en el orden
correcto.

La lectura de una serie de datos desde stdin para llevar a cabo un calculo y escribir el resultado en
stdout:
Archivo stdin
Juan
24
Archivo stdout
La edad de Juan es 24
Requiere de instruccines que lleven a cabo las lecturas en el orden correcto, así:
MIT SCHEME
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
178
(begin (define Nm (read))(display "la edad de ")
(write Nm)(display " es ")
(define Ed (read))
(write Ed)
)
HASKEL
import System.IO

main = do
nm <- readLn :: IO [Char] – requiere strings en “xx” o getLine.
ed <- readLn :: IO Int
putStr ("la edad de " ++ nm ++ " es " ++ (show ed))
SCALA
import scala.io.StdIn.{readLine,readInt}
object Main extends App {
val nm = readLine()
val ed = readLine().toInt
printf("la edad de " + nm + " es " + ed)
}

Operadores nativos y Expresiones simples.


Todos los lenguajes considerados en este trabajo, ofrecen un conjunto de tipos de valores y
operadores nativos implementados, que el programador puede utilizar en sus programas.
Aunque el conjunto de tipos de valores y operadores nativos es una característica propia y
diferenciadora de cada lenguaje, todos los considerados 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
el lenguaje, y lo que es más importante los resultados de las operaciones no son tampoco
las mismas.
No siendo posible analizar a fondo los tipos nativos y operadores de cada lenguaje, se
remite al lector a la documentación particular de cada uno de ellos. En el ejemplo siguiente
presentamos, sin embargo, algunas de las características de la aritmética que consideramos
de particular interés.

Algunas características importantes de los operadores aritméticos que implementa un lenguaje, pueden
descubrirse fácilmente con ejemplos sencillos como el que se muestra a continuación109.
C
Codigo
#include <stdio.h>

int main(void) {
printf("%s%i \n", "1+1(%i)=", 1+1);

109 Para las ejecuciones se usaron las implementaciones en línea de la página web referida en [ideone].
Capítulo 7: Elementos básicos de los Lenguajes de Programación
179
printf("%s%f \n", "1+1(%f)=", 1+1);

printf("%s%i \n", "1+1.0(%i)=", 1+1.0);


printf("%s%f \n", "1+1.0(%f)=", 1+1.0);

printf("%s%i \n", "4/2(%i)=", 4/2);


printf("%s%f \n", "4/2.0(%f)=", 4/2.0);
printf("%s%i \n", "4/3(%i)=", 4/3);
printf("%s%f \n", "4/3.0(%f)=", 4/3.0);
printf("%s%i \n", "4/3+2(%i)=", 4/3+2);
printf("%s%f \n", "4/3+2.0(%f)=", 4/3+2.0);
printf("%s%i \n", "4/3+3/5(%i)=", 4/3+3/5);
printf("%s%f \n", "4/3.0+3/5.0(%f)=", 4/3.0+3/5.0);

printf("%s%f \n", "1.0e1+0.001-1.0e1(%f)=", 1.0e1+0.001-1.0e1);


printf("%s%f \n", "1.0e13+0.001-1.0e13(%f)=", 1.0e13+0.001-1.0e13);
printf("%s%f \n", "1.0e15+0.001-1.0e15(%f)=", 1.0e15+0.001-1.0e15);
printf("%s%f \n", "1.0e17+0.001-1.0e17(%f)=", 1.0e17+0.001-1.0e17);

return 0;
}
Salida comentada
La escritura de un valor entero con formato equivocado puede generar resultados inesperados en lugar de
un mensaje de error110.
1+1(%i)=2
1+1(%f)=-
80589945585618742857856832637749543328912847380906773773586705717352014841
76638265689135570516912376420923811154711064155433560393093297457331668876
57675915854327924207637688957348577500845761677301122642136188331523314394
401447520268135324599670866761407739228848128.000000

Un operando flotante induce transformar el operando entero a flotante (por los que se debe ser
especialmente cuidadoso con el formato de escritura).
1+1.0(%i)=0
1+1.0(%f)=2.000000

La división entera da como resultado un entero truncando los decimales, esto ocurre en cualquier rama
del árbol sintáctico en la que los operandos son enteros. El resultado (truncado) puede ser
posteriormente transformado a flotante al ser operando de otro operador. Note que el formato
corresponde al tipo del valor resultante.
4/2(%i)=2
4/2.0(%f)=2.000000
4/3(%i)=1
4/3.0(%f)=1.333333
4/3+2(%i)=3
4/3+2.0(%f)=3.000000
4/3+3/5(%i)=1
4/3.0+3/5.0(%f)=1.933333

La precisión del valor flotante da lugar a resultados errados cuando se combinan valores de magnitudes
muy diferentes.
1.0e1+0.001-1.0e1(%f)=0.001000
1.0e13+0.001-1.0e13(%f)=0.001000
1.0e15+0.001-1.0e15(%f)=0.000977
1.0e17+0.001-1.0e17(%f)=0.000000

110 Es considerado error de sintaxis en algunas implementaciones.


Capítulo 7: Elementos básicos de los Lenguajes de Programación.
180
SCHEME
Codigo111
(begin
(display '(+ 1 1)) (display "=") (display (+ 1 1))
(display '(+ 1 1.0)) (display "=") (display (+ 1 1.0))

(display '(/ 4 2)) (display "=") (display (/ 4 2))


(display '(/ 4 2.0)) (display "=") (display (/ 4 2.0))
(display '(/ 4 3)) (display "=") (display (/ 4 3))
(display '(/ 4 3.0)) (display "=") (display (/ 4 3.0))
(display '(+ 2 (/ 4 3))) (display "=") (display (+ 2 (/ 4 3)))
(display '(+ 2.0 (/ 4 3))) (display "=") (display (+ 2.0 (/ 4 3)))
(display '(+ (/ 3 5) (/ 4 3))) (display "=") (display (+ (/ 3 5) (/ 4 3)))
(display '(+ (/ 3 5) (/ 4 3) 0.0)) (display "=") (display (+ (/ 3 5) (/ 4 3) 1.0))

(display '(+ 1.0e01 0.001 (- 1.0e01))) (display "=")


(display (+ 1.0e01 0.001 (- 1.0e01)))
(display '(+ 1.0e05 0.001 (- 1.0e05))) (display "=")
(display (+ 1.0e05 0.001 (- 1.0e05)))
(display '(+ 1.0e010 0.001 (- 1.0e10))) (display "=")
(display (+ 1.0e10 0.001 (- 1.0e10)))
(display '(+ 1.0e13 0.001 (- 1.0e13))) (display "=")
(display (+ 1.0e13 0.001 (- 1.0e13)))
(display '(+ 1.0e14 0.001 (- 1.0e14))) (display "=")
(display (+ 1.0e14 0.001 (- 1.0e14)))
)

Salida comentada
Un operando flotante forza transformar el otro operando a flotante (en este caso el intérprete se ocupa de
escribir el valor corréctamente según el tipo112).
(+ 1 1)=2
(+ 1 1.0)=2.0
(/ 4 2)=2
(/ 4 2.0)=2.0

La aparición del tipo fraccionario evita el truncamiento en la división entera. Sólo la ocurrencia de un
operando flotante obliga la conversión del fraccionario a flotante.

(/ 4 3)=4/3
(
/ 4 3.0)=1.3333333333333333
(+ 2 (/ 4 3))=10/3
(+ 2.0 (/ 4 3))=3.3333333333333335
(+ (/ 3 5) (/ 4 3))=29/15
(+ (/ 3 5) (/ 4 3) 0.0)=2.933333333333333

La precisión del valor flotante da lugar a resultados errados cuando se combinan valores de magnitudes
muy diferentes.
(+ 10.0 0.001 (- 10.0))=9.999999999994458e-4
(+ 100000.0 0.001 (- 100000.0))=0.0010000000038417056
(+ 1.0e10 0.001 (- 1.0e10))=9.9945068359375e-4
(+ 1.0e13 0.001 (- 1.0e13))=0.001953125
(+ 1.0e14 0.001 (- 1.0e14))=0.0

111 No se muestran los cambios de línea por simplicidad.


112 Se deja al lector analizar el efecto de usar elementos de formateo.
Capítulo 7: Elementos básicos de los Lenguajes de Programación
181
PROLOG
Codigo113
main:-
write('1+1= '), S is 1+1, writeln(S),
write('1+1.0= '), S1 is 1+1.0, writeln(S1),
write('4/2= '), S2 is 4/2, write(S2),
write('4/2.0= '), S3 is 4/2.0, writeln(S3),
write('4/3= '), S4 is 4/3, write(S4),
write('4/3.0= '), S5 is 4/3.0, writeln(S5),
write('4/3+1.0= '), S6 is 4/3+1.0, write(S6),
write('4/3.0+1.0= '), S7 is 4/3.0+1.0, writeln(S7),
write('4/3+3/5= '), S8 is 4/3+3/5, writeln(S8),
write('1+"1"= '), S9 is 1+"1", writeln(S9),
write('1.0e15+0.001-1.0e15= '), S10 is 1.0e15+0.001-1.0e15, writeln(S10),
halt.

:- main.

Salida comentada
Un operando flotante forza transformar el otro operando a flotante.
1+1= 2
1+1.0= 2.0

A pesar de contar con el tipo fracionario114, al efectuar la división se transforman los operandos enteros a
flotante cuando hay operandos en flotante o cuando podría haber truncamiento.
4/2= 2
4/2.0= 2.0
4/3= 1.3333333333333333
4/3.0= 1.3333333333333333
4/3+1.0= 2.333333333333333
4/3.0+1.0= 2.333333333333333
4/3+3/5= 1.9333333333333331

La operación aritmética con un operando tipo “string” genera un resultado inesperado sin que se reporte
error algunao.
1+"1"= 50

La precisión del valor flotante continúa dando resultados errados cuando se combinan valores de
magnitudes muy diferentes.
1.0e15+0.001-1.0e15= 0.0

PHP
Codigo
<?php
echo " 1+1= " . (1+1) . " | printf" . sprintf(" 1+1(d)= %d 1+1(f)= %f\n", 1+1, 1+1);
echo " 1+1.0= " . (1+1.1) . " | printf" . sprintf(" 1+1.0(d)= %d 1+1.0(f)= %f\n",
1+1.0 , 1+1.0);
printf(" 1+1.0(f)= %f \n", 1+1.0);
printf(" 1+'1'(f)= %f \n", 1+'1');

echo "4/2=" . (4/2) . " 4/2.0=" . (4/2.0) . " 4/'2.0'=" . (4/'2.0') . "\n";
echo "4/3+3/5=" . (4/3+3/5) . " 4/3.0+3/5.0=" . (4/3.0+3/5.0) . " 4/'3.0'+'3/5'="
. (4/'3.0'+'3/5') . "\n";
echo "1e1+0.001-1e1=" . (1e1+0.001-1e1) .
" 1e13+0.001-1e13=" . (1e13+0.001-1e13) .
" 1e14+0.001-1e14=" . (1e14+0.001-1e14) . "\n";

113 No se muestran los cambios de línea por simplicidad.


114 Bajo una representación diferente a la de dividir un entero por otro.
Capítulo 7: Elementos básicos de los Lenguajes de Programación.
182
?>

Salida comentada
La operación mantiene la precisión de un cálculo en flotante, independientemente de los tipos de los
operandos. Es incluso posible operar con un entero representado en un tipo “string”. El formato, por su
parte, no genera una salida con valores errrados, ya que sólo tiene efecto sobre el formato de la salida y el
número de decimales.
1+1= 2
printf 1+1(d)= 2 1+1(f)= 2.000000
1+1.0= 2.1
printf 1+1.0(d)= 2 1+1.0(f)= 2.000000
1+1.0(f)= 2.000000
1+'1'(f)= 2.000000
4/2=2
4/2.0=2
4/'2.0'=2
4/3+3/5=1.9333333333333
4/3.0+3/5.0=1.9333333333333

Aunque es posible operar con números representados por “string”, no se opera corréctamente con una
expresión representada de tal forma.
4/'3.0'+'3/5'=4.3333333333333

La precisión del valor flotante continúa dando resultados errados cuando se combinan valores de
magnitudes muy diferentes.
1e1+0.001-1e1=0.00099999999999945
1e13+0.001-1e13=0.001953125
1e14+0.001-1e14=0

Resumen del Capítulo.


La multiplicidad de lenguajes de programación impone un dificultad seria a los
desarrolladores, cuando requieren desempeñarse adecuádamante un lenguaje que
desconocen. Esta dificultad se traduce tanto en costos muy altos cuando es necesario un
cambio de lenguaje para programas existentes, como en una tendencia en persistir en el uso
de lenguajes con problemas ya superados.
Sin embargo, dado que los lenguajes nuevos incorporan las facilidades exitosas de los
viejos, es posible identificar “familias” de lenguajes, para facilitar su análisis y
comprensión. Por otro lado las familias de lenguajes se pueden caracterizar con base en
una serie de criterios caracterizan por una serie de “criterios diferenciadores”, que son los
que determinan el carácter de un lenguaje particular. En este trabajo se propone un conjunto
de criterios diferenciadores que serán usados en el resto del trabajo, para establecer las
similitudes y las diferencias entre los lenguajes considerados, a saber:
 Tipos de valores y operadores nativos.
 Naturaleza de los Operadores Definidos:
o Plantilla.
o Precedencia y asociatividad.
 Evaluación de los Operadores:
o Estrategia.
o Medio ambiente.
o Estados del Proceso:
Capítulo 7: Elementos básicos de los Lenguajes de Programación
183
 Medio ambiente.
 Terminos.
 Cláusula.
o Efectos.
 Definición de los Operadores:
o Modelo del proceso:
 Intrucciones de asignacion, control, declaración y E/S.
 SRT.
 FNC.
o Unidades de especificación.
o Argumentos y variables.
 Control de tipo en los términos.
 Parametrización.
 Metaprogramación.
Como lenguajes de interés para este trabajo, se señalaron varios lenguajes que además de
ser considerados “populares” ó “relevantes”, pueden verse como representativos de tres
familias lingúisticas, así:
 De los lenguajes denominados “procedurales”, señalamos C, su versión objetual
C++, y sus descendientes, los lenguajes de “script”, C#, JAVA, PHP y PYTON.
 De los lenguajes denominados “funcionales” señalamos el SCHEME, el HASKEL,
el OCAML el SCALA y el F#. Finalmente, el lenguaje MAUDE, como un lenguaje
de investigación que incorpora característica nuevas dentro de su familia.
 De la familia de los lenguajes denominados PROLOG, señalamos el SWI
PROLOG, por su alta difusión y su accesibilidad en los compiladores “on line ”.
Para alagunos de los lenguajes señalados, se presentaron las caracteríastica relevantes de las
implementaciones que serán base de los ejemplos posteriores en el trabajo. Se presentaron
primero implementaciones que ofrecen una línea de comando REPL (“Read-Eval-Print
Loop”), y luego implementaciones que se apoyan en lectura de datos desde archivos de
“entrada” y escritura de resultados en archivos de “salida”.
Finalmente se hizo énfasis en la necesidad de conocer a fondo las características de los
tipos y operadores “natovos” de cada lenguaje, que constituyen una caracerística básica de
cada lenguaje; y, no siendo posible analizar a fondo los tipos nativos y operadores de todos
ellos, se mostró que con ejemplos sencillos es posible identificar algunas de las
características de la aritmética que consideramos de particular interés.

Ejercicios Propuestos.
1- Someta a los intérpretes de HASKELL, SCALA, PYTHON y MAUDE los
términos usados en los ejemplos del numeral 7.5 .
Capítulo 8
Operadores Definidos
Capítulo 8: Operadores Definidos
186
Introducción
En el capítulo anterior se presentaron los lenguajes funcionales y procedurales que serán
analizados en este trabajo. Se indicó, además, que sus respectivas implementaciones
ofrecían una serie de operadores nativos que pueden ser usados por el usuario para llevar a
cabo cálculos.
Si dichas implementaciones ofrecieran sólo estos operadores, no tendrían más poder que la
de una calculadora de bolsillo. La potencia real de los lenguajes surge, en efecto, de las
facilidades que tienen para proponer nuevos operadores y nuevos tipos de dato con sus
operadores asociados.
En éste capítulo se presentan los elementos básicos disponibles por los diferentes lenguajes
para definir nuevos operadores. En principio, nos ocuparemos del planteamiento de
operadores definidos sencillos en el marco de los tipos de datos nativos al lenguaje.
Dejaremos para capítulos posteriores el problema del planteamiento de nuevos tipos de dato
y de sus operaciones asociadas.
Con base en el planteamiento de estos operadores sencillos, sin embargo, se discutirán
asuntos importantes como la definición del operador, el operador de selección, el manejo de
los tipos, el medio ambiente de evaluación, las estrategias de evaluación y la estructuración
de los programas.
Aunque la discusión se extiende a las características particulares de cada lenguaje sólo en
los ejemplos, se presenta como factor unificador una caracterización abstracta de los
diferentes enfoques como conceptos diferenciadores entre los lenguajes (ver 7.3).

Justificación.
La capacidad de definir nuevos operadores o “funciones”115 es un problema central en
todos los lenguajes de programación. Vale, entonces, la pena recordar las razones básicas
que justifican su inclusión en prácticamente todos los lenguajes.
8.2.1 Reuso de código.
En casi todo proceso de cómputo es normal que un mismo cálculo se lleve a cabo varias
veces y sobre diferentes datos. Esto obligaría, en principio, a repetir la fórmula o pieza de
código que define el cálculo cada vez que se requiera, posiblemente cambiando los datos
sobre los que se aplica.
La solución obvia a este problema es la de definir una sola vez el cálculo y asociarlo a un
nuevo operador. Así, cuando se requiera indicar que el cálculo se debe llevar a cabo sobre
datos específicos, simplemente se escribe (o se “evoca”) el operador colocándole las
referencias a los datos específicos como sus operandos.

115 La distinción entre operadores y funciones se asocia en los lenguajes procedurales a elementos meramente sintácticos.
En particular suelen ser denominados como “operadores” sólo los operadores de notación infija que son prefijados por el
lenguaje; y, suelen ser denominados como “funciones”, los operadores con notación prefija que pueden ser introducidos
libremente por el programador. En este documento el término “operador” se asocia al rasgo sintáctico usado en los
programas (sea este prefijo o infijo), y el término “función” se asocia a la función matemática que representa el operador,
o sea a su semántica.
Capítulo 8: Operadores Definidos
187
8.2.2 Arquitectura de funciones y de objetos.
Con el crecimiento del uso del computador, en situaciones cada vez mas variadas, los
programas se han hecho cada vez más grandes y complejos. El aumento del tamaño y
complejidad del software va asociado con un aumento en los costos de producción y en las
dificultades para mantener su calidad.
Estos problemas asociados con la complejidad del software, impulsaron desde los años 60,
una serie de esfuerzos para regular el entonces “arte” de escribir software, y acercarlo al
nivel de una disciplina de ingeniería.
Los primeros esfuerzos se orientaron a examinar el software mismo, y a moldearlo a formas
más inteligibles para darle solución al problema de la complejidad. Se planteó entonces,
que la complejidad del software se asociaba principalmente al hecho de estar compuesto
por múltiples pequeños elementos con múltiples relaciones entre sí, haciéndolo difícil de
comprender por los seres humanos [Miller 1956].
Se propuso entonces como solución, la aplicación de los principios de la modularidad y de
la descomposición progresiva, así:
 Bajo el paradigma de funciones (ver 2.3), una pieza de software debe ser considerada
como un agregado de un pequeño conjunto de funciones encargadas de transformar los
datos, que recibían del usuario o de otras funciones, en resultados a ser enviados hacia el
usuario o hacia otras funciones del programa. Una función debe, además, ser
considerada también como como un agregado de funciones más pequeñas, y asi
sucesivamante, hasta llegar a las instrucciones básicas del lenguaje. El programador
debe entonces reflejar el diseño en el programa escribiendo las funciones (u operadores)
correspondientes a las componentes de la descomposición progresiva.
 Bajo el paradigma de objetos (ver 2.5), una pieza de software debe ser considerada como
el agregado de objetos, que interactúan entre sí por medio de mensajes. Como respuesta
a un mensaje, un objeto lleva a cabo un proceso definido como un operador definido en
el medio ambiente de los atributos del objeto (ver 8.6). El programador debe entonces
reflejar el diseño en el programa consignando en las “clases” las características comunes
a grupos de objetos, codificando los operadores con los que estos responden a los
diferentes mensajes que reciben.
En consecuencia, si en un lenguaje no es posible proponer nuevos operadores no es posible,
tampoco, escribir con él programas que reflejen la arquitectura de sus funciones u objetos.

Definición de los operadores.


La definición de un operador debe especificar tanto la forma como se evoca el operador
como la forma como se obtiene el valor de una evocación. Para lo segundo debemos
primero comprender el modelo del proceso ofrecido por el lenguaje, puesto que este
determina la manera como el intérprete procede para obtener el valor de la evocación;
luego, en el marco de este modelo debemos utilizar las unidades de especificación del
lenguaje para indicar los cálculos que se deben realizar para obtener el valor; y por último
debemos definir la relación entre los argumentos de la evocación y las variables que
intervienen en los cálculos.
Capítulo 8: Operadores Definidos
188
8.3.1 Definición de Operadores en lenguajes Funcionales.
En la familia de los lenguajes funcionales, la especificación del proceso se fundamenta en
un Sistema de Reescritura de Terminos ó SRT (ver 5.4). Un SRT es un conjunto de
axiomas ecuacionales dirigidos, posiblemete asociados a una condición que los implica.
Los axiomas son utilizados por el intérprete para modificar los términos que constituyen el
estado del proceso, reescribiéndolos a otros semánticamente equivalentes (ver 5.3.3.2 ). El
proceso parte de uno o varios términos de entrada a ser calculados, que el intérprete va
reescribiendo, aplicando un axioma en cada paso hasta converirlos en los resultados. La
selección del axioma a ser usado en cada paso de reescritura es guiada, tanto por los
operadores evocados en los términos como por las condiciones que implican los axiomas
En su forma más simple la definición de un operador es precisamente la de escribir
unidades de especificación que pueda asimilarse a un axioma de igualdad. Es por esto que
todos los lenguajes analizados ofrecen una construcción que puede entenderse como una
ecuación de la forma:

t1 = t2
Donde:
 El símbolo = es substituido por algún símbolo del lenguaje.
 El término t1 tiene como operador principal el operador definido y sus
operandos son patrones de emparejamiento que serán denominados en lo que
sigue “argumentos formales de la especificación”.
 El término t2 es término que remplaza la evocación y utiliza las variables
que aparecen en los argumentos formales.

MIT SCHEME
En el lenguaje SCHEME la capacidad de definir operadores se asocia a la capacidad de “definir” el
significado de los rótulos116 a ser usados en el programa [Abelson 1985 1.1.2]. Así la “definición”
siguiente le da valor al rótulo pi:
(define pi 3.141542)

La definición de rótulos se extiende de una manera natural a rótulos con argumentos llamados
“procedures” (procedimientos), que se constituyen en los operadores definidos en el programa. Para ello
el SCHEME ofrece una construcción de la forma siguiente:
(define (t1) (t2))

Donde: Los argumentos formales son todos nombres de variable. Los paréntesis son obligatorios. Los
términos son listas de elementos separados por espacio, donde el primer elemento es el operador y los
siguientes los operandos.
Son definiciones de operador las siguientes:
(define (cua X) (* X X))

116Estos rótulos son denominados “variables”. En este trabajo, sin embargo, preferimos usar el término “variable” en el
sentido de la lógica.
Capítulo 8: Operadores Definidos
189
(define (sum_cua X Y) (+ (cua X) (cua Y))

HASKELL
El lenguaje HASKELL ofrece las mismas facilidades del SCHEME con base en construcciones de la
forma siguiente:
t1 = t2

Donde: Los argumentos formales pueden ser nombres de variable, constantes o términos compuestos.
Las variables deben estar en minúscula. Cada definción debe iniciarse en una línea de texto diferente.
Los términos son listas de elementos separados por espacio donde el primer elemento es el operador y
los siguientes son los operandos. Un término compuesto que aparece como operando debe encerrarse
entre paréntesis.
Son definiciones de operador las siguientes:
cua x = x*x
sum_cua x y = (cua x)+(cua y)

SCALA
Para definir funciones, el lenguaje SCALA ofrece construcciones de la forma siguiente:
def t1 : [tr] = t2
def t1 : [tr] = {tn … t3 t2}

Donde : Los argumentos formales tienen la forma <v>:<ty> siendo v un nombre de variable y ty su tipo;
tr es opcional y representa el tipo del resultado de la evocación. Cada definción debe iniciarse en una
línea de texto diferente. tn … t3 t2 es una lista de términos ocupando cada un o una línes diferente,
siendo el último el que determina el valor de la evocación. Los términos usan notación estándar.
Son definiciones de operador las siguientes:
def cua(X:Int):Int = X*X
def sum_cua(X:Int,Y:Int):Int = cua(X)+cua(Y)

MAUDE
El lenguaje MAUDE ofrece una construcción de la forma siguiente, para definir los operadores:
eq t1 = t2 .

Donde: Los argumentos formales pueden ser nombres de variable, constantes o términos compuestos. El
prefijo eq señala la ecuación que define el operador. Lós términos usan notación estándar por defecto.
Es además necesario declarar el perfil del operador inidicando el tipo de los argumentos y el tipo del
resultado. Todas las variables deben, además, estár declaradas con su tipo, así:
op cua : Float -> Float .
op sumcua : Float Float -> Float .
vars X Y : Float .
eq cua(X) = X * X .
eq sumcua(X, Y) = cua(X) + cua(Y) .

Donde: Los prefijos op, vars, identifican las declaraciones del perfil del operador y de las variables.
Nótese que todas las construcciones se deben terminar con un punto.

8.3.2 Definición de Operadores en lenguajes CLAUSALES.


En la familia de los lenguajes clausales, la especificación del proceso se fundamenta en una
fbf en forma normal conjuntiva (FNC) compuesta por cláusulas de Horn (ver 6.3.1). El
estado inicial del proceso es una cláusula con todos los literales negados que niega el
“objetivo” a ser demostrado. El proceso de resolución SLD utiliza las cláusulas del
programa para reescribir un estado a otro estado (o cláusula) de la que el primero es
Capítulo 8: Operadores Definidos
190
consecuencia lógica. En cada paso de reescritura el nuevo estado es un resolvente del
estado anterior con una cláusula del programa (ver 6.5). El proceso continúa hasta llegar a
la cláusula elemental F (false) o fallar en el intento. De no fallar los resultados del proceso
son los valores asignados a las variables del objetivo inicial durante el proceso de
reescritura.
En su forma más simple la definición de un operador consiste en una única unidad de
especificación que puede asimilarse a una cláusula de Horn con un solo literal positivo, así:

h  t1, t2, , tn
Donde:
 El símbolo  es substituido por algún símbolo del lenguaje.
 El literal h es un predicado que representa al operador y tiene como
argumentos los operandos del operador junto con una variable que
representa el reultado del cálculo.

Los operadores en PROLOG deben definirse como predicados en los que tanto los operandos como el
resultado del cálculo se asocian con sus argumentos. La definición siguiente implementa un predicado
para elevar al cuadrado un número dado:
cua(X,Y) :- Y is X*X .
sum_cua(X,Y,Z) :- cua(X,X2), cua(Y,Y2), Z is X+Y .

Que al ser usado en una consulta, entrega como resultado el valor de la variable que se coloque en el
lugar de la variable calculada en el predicado is, así:
2 ?- cua(10,W).
W = 100.

Sintaxis: Los predicado se escriben en notación estándar. Las variables deben comenzar con letras en
mayúscula. El es substituido por “:-“ . Cada cláusula del programa debe terminar en punto.
Nótese que puesto que en la resolución SLD las metas de la consulta (los predicados t1, t2, …) se
verifican de izquierda a derecha, el orden de las metas es significativa al momento de obtener el
resultado correcto. Así, La definición siguiente del predicado anterior, genera un mensaje de error, al
tratar de llevar a cabo la suma sin haber calculado antes los sumandos:
sumcua(X,Y,S) :- S is X2+Y2, cua(X,X2), cua(Y,Y2) .

1 ?- sumcua(2,3,Z).
ERROR: is/2: Arguments are not sufficiently instantiated

8.3.3 Definición de Operadores en lenguajes PROCEDURALES.


En la familia de los lenguajes procedurales, la especificación del proceso se fundamenta en
xxxx

C
PYTHON
SCALA
Capítulo 8: Operadores Definidos
191
PHP

Selección.
Es, usual que el término a ser calculado para obtener el valor de una evocación de un
operador, dependa del valor de los operandos de la evocación. Este es, por ejemplo, el caso
de operadores que representan funciones discontinuas que componen otras funciones
indexándolas por regiones del dominio.

Considere la función valor absoluto de un número real |x| . Esta función compone las funciones f(x) = x
y f(x) = -x , indexándolas según si el valor del argumento es mayor o menor que cero.

Para hacer posible la definición de este tipo de operadores, los lenguajes de programación
ofrecen mecanismos de selección. Los mecanismos de selección se orientan a escoger entre
las varias disponibles, una definición particular para el operador. Así, para el caso de los
lenguajes procedurales, la selección escoge entre varios grupos de instrucciones candidatos
el que va a ser ejecutado para calcular una evocación del operador; para el caso de los
lenguajes funcionales la selección escoge entre varios términos candidatos el que
remplazará una evocación del operador; y, en los lenguajes clausales la selección escoge
entre varios objetivos candidatos el que remplazará una evocación del operador.
Además, en todos los casos la escogencia ha de fundamentarse en los valores de los
operandos o argumentos de la evocación, y ocasionalemente en los valores asociados a los
rótulos del medio ambiente en que fúe definido el operador; tótulos que, en adelante,
consideraremos como argumentos adicionales (aunque implícitos) de la evocación. En lo
que sigue nos referiremos a los valores de los operandos o argumentos de la evocación
como “argumentos reales”; y nos referiremos, de forma sumaria, al grupo de instrucciones,
o al término, o al objetivo que será usados para calcular el valor de una evocación, como el
“caso de reescritura”.
Planteados en estos términos, los mecanismos de selección planten un conjunto (finito) de
posibles casos de reescritura, y luego asocian un conjunto específico de evocaciones a cada
uno de estos casos. Puesto que cada una de las posibles evocaciones corresponde a una
combinación específica de valores para los argumentos reales, los mecanismos de selección
reálmente asocian los casos de reescritura con un subconjunto del conjunto de todas las
posibles combinación de valores para los argumentos reales.
En términos más precisos, podemos decir que los mecanismos de selección definen una
partición sobre el producto cartesiano de los tipos de los argumentos reales, y rotulan cada
miembro de dicha partición con un elemento (usualmente distinto) del conjunto de posibles
casos de reescritura.
Una forma de definir una partición sobre el producto cartesiano de los tipos de los
argumentos, es la de imponer a cada uno de sus miembros (es decir a una combinación
específica de valores para los argumentos reales), una condición de pertenencia a cada
miembro de la partición.
Capítulo 8: Operadores Definidos
192
Para definir esta condición de pertenencia, los diferentes lenguajes analizados usan en
sucesión dos posibles mecanismos, el primero es el emparejamiento o “pattern matching”, y
el segundo es una proposición explícita o condición de “guardia”.
El emparejamiento se apoya en plantear para cada caso de reescritura, una evocación
“formal” del operador cuyos operandos pueden ser términos complejos con constantes y
variables, y en verificar, frente a una evocación “real” del operador, si existe una
substitución de las variables de la evocación formal que haga dicha evocación idéntica a la
real (en términos sintácticos). La condición de guardia, por su parte, se apoya en plantear
para cada caso de reescritura, una proposición formada con las variables de la evocación
formal, y en verificar que las evocaciones que satisfacen el emparejamiento satisfagan
también la proposición.
En términos abstráctos, al definir un operador O de aridad n, para el que existen m casos
posibles de reescritura (para m≥1), todos los lenguajes le permiten al programador plantean
un conjunto de parejas {casos de reescritura - condiciones de pertenencia para los
argumentos reales}, que pueden asimilarse con las m expresiones siguientes que en lo que
sigue denominaremos como “expresiones de selección de un caso” o simplemente
“selección de un caso”:

V11 V12 ….O1( af11(V11, V12, ...), af12(V11, V12, ...), ...)  {C1}  G1(V11, V12, ...)
V21 V22 ….O2( af21(V21, V22, ...), af22(V21, V22, ...), ...)  {C2}  G2(V21, V22, ...)

Vm1 Vm2 ….Om( afm1(Vm1, Vm2, ...), afm2(Vm1, Vm2, ...), ...)  {Cm}  Gm(Vm1, Vm2, ...)

Donde:
 Ok(…..) es la evocación del operador para el caso de orden k{1..m}.
 afkj(….) para j{1..n} son los argumentos “formales” de la evocación para el
caso de orden k. Los argumentos formales pueden ser una varible, una
constante o un término complejo con varios operadores constantes y
variables.
 Vki spara i{1..nvk}, son las variables que ocurren en los argumentos formales
de la evocación para el caso de orden k.
 Ck es el caso de reescritura de orden k que, en general, depende de las
variables que ocurren en los argumentos formales asociados al mismo caso.
 Gk(...) es la condición de guardia del caso de reescritura de orden k que, en
general, depende de las variables que ocurren en los argumentos formales
asociados al mismo caso.
Para hallar el caso de reescritura correspondiente a una evocación real, basta con hallar la
expresión en la que la evocación real empareja con la evocación formal de la expresión y en
la que, además, se satisface la condición de guarda, para luego, usar como caso de
reescritura el planteado en dicha expresión.
XXXXXcuando hay varias que cumplen???XXX y cuando hay evocaciones no
contempladas???
Si bien la selección puede entenderse en todos los lenguajes con base en las expresiones de
selección arriba presentadas, ella toma diversas formas entre los diferentes lenguajes e
Capítulo 8: Operadores Definidos
193
incluso diversas formas dentro de un mismo lenguaje. Así, en las secciones que siguen
presentamos las formas más usadas para definir las expresiones de selección que a pesar de
consistir básicamente en formas sintácticas simplificadas117, son de gran importancia al
momento de sospesar la simplicidad y potencia de un lenguaje.
8.4.1 Selección por emparejamiento de la evocación formal con la evocación
real.
En varios de los lenguajes analizados, se usa el emparejamiento como un medio para
seleccionar el caso de reescritura. Para ello el lenguaje permite plantear varias unidades de
especificación asociando cada una de ellas a un conjunto (diferente) de casos de reescritura.
Esta estrategia corresponde a condensar en una sóla unidad de especificación, las
expresiones de selección del caso que comparten los mismos argumentos formales.
Aun cuando la mayor utilidad del emparejamiento ocurre frente a valores compuestos,
donde permite minimizar el uso de “selectores” (ver ..), es particulamente útil cuando un
argumento real debe corresponder con un valor entre un conjunto finito de opciones.

Considere un operador que dado un valor numérico calcula calcula ya sea el área de un circulo cuyo
radio es dicho valor, o ya sea el área de un triángulo equilátero inscrito en dicho círculo, y que distingue
el cálculo requerido con un string con la palabra “circulo” o con la palabra “triangulo”.
HASKELL
area x "circulo" = pi*x*x
area x "triangulo" =

PROLOG
area(X,"circulo",R) :- R is pi*X*X
area(X,"triangulo",R) :- R is pi*X*X

MAUDE
eq area(X,"circulo") = pi*X*X
eq area(X,"triangulo") =

Si se evoca un operador con operandos reales que no emparejan con alguna de las opciones
de emparejamiento propuestas, el cálculo no puede llevarse a cabo y el proceso falla
generando alguna forma de mensaje de error. En por tanto deseable incluir casos de
emparejamiento que cubran todas las evocaciones posibles, para darle un tratamiento
adecuado a los casos excepcionales.
Es importante notar que de ser una variable el argumento formal, cualquier argumento real
cuyo tipo sea compatible con el tipo de la variable empareja transfiriendo su valor a la
variable. En algunos lenguajes existe un símbolo especial (v.g. “_”), que representa una
variable muda, y que puede usarse cuando sólo es necesario garantizar el emparejamiento.

117 “Azucar sintáctico” [Bourbaki 50]


Capítulo 8: Operadores Definidos
194
Es posible adicionar casos de emparejamiento que capturen los no considedrados. En los lenguajes
siguientes se puede usar ya sea una variable explícita o una variable muda, así.
HASKELL
area x "circulo" = pi*x*x
area x "triangulo" =
-- para capturar los casos no considerados en area use una de las dos
area x OOP = error "argumento” ++ OOP ++ "equivocado al evocar area”
area _ OOP = error "argumento equivocado al evocar area”

PROLOG
area(X,"circulo",R) :- R is pi*X*X
area(X,"triangulo",R) :- R is pi*X*X
% para capturar los casos no considerados en area use una de las dos
area(X,OOP,R) :- string_concat("argumento”, OOP, S1),
string_concat(S1," equivocado al evocar area”, S2,
write(S2), fail .
area(X,_,R) :- write("argumento equivocado al evocar area”),fail .

Nótese que en los lenguajes del ejemplo anterior la ambigüedad que introduce la nueva
unidad de especificación se elimina debido a que las unidades existentes son consideradas
en el orden en que aparecen en el código, así, al colocar de última la unidad que empareja
con todas la evocaciones posibles, se garantiza que no entra en conflicto con las anteriores.
De no estár garantizado el orden en que se consideran dichas unidades, no es posible
garantizar que se cubren todas las evocaciones posibles.

En los lenguajes siguientes el intérprete detecta las ambigüedades que puedan presentarse entre las
unidades de especificación de un operador.
MAUDE
op area(X,"circulo") = pi*X*X .
op area(X,"triangulo") =
op area(X,OOP) = error ….

8.4.2 Selección por condición de guardia sobre las unidades de


especificación.
Hay lenguajes en los que junto con el emparejamiento, como medio para seleccionar entre
varias unidades de especificación, pueden usarse condiciones de guardia aplicadas a la
unidad. Así, para acceder a los casos de reescritura de una unidad de especificación, se
debe primero emparejar con la evocación formal del operador, y luego, usando los valores
de las variables particularizadas, satisfacer la condición de guardia de la unidad.

En las especificaciones que siguen el cálculo del valor absoluto de un número, se apoya en seleccionar
entre varias unidades de especificación, usando emparejamiento y condiciones de guardia.
PROLOG
abs(0.0,0.0) .
abs(X,R) :- X>0.0, R is X .
Capítulo 8: Operadores Definidos
195
abs(X,R) :- R is -X .

MAUDE
eq abs(0.0) = 0.0 .
ceq abs(X) = X if(X>0.0) .
ceq abs(X) = -X if(X<0.0) .

Elector debe notar que en el caso del PROLOG no es necesaria la condición en la última unidad de
especificación debido a que las reglas son examinadas de arriba abajo por el intérprete. Por no ser este
el caso en el lenguaje MAUDE, requiere que se incluya la condición de guardia en todas las unidades.
Xxxxx casos de no cumplir xxxx

8.4.3 Operadores e instrucciones de selección.


La forma más simple de implementar la selección, y sin duda la mas usada, es la de definir
operadores o instrucciones de selección que puedan usarse como parte de los términos o de
las instrucciones del lenguaje. Ellos permiten incluir varios casos de reescritura en una
misma unidad de especificación, distinguiéndolos sólo por medio de condiciones de
guardia. Para los lenguajes que sólo permiten escribir una sola unidad de especificación,
los operadores y las instrucciones de selección son el único medio que ofrecen para escoger
entre los diversos casos de reescritura.
El operador de selección más sencillo, denominado “if funcional”, selecciona entre dos
términos con base en un solo predicado, donde el predicado mismo es la condición de
guarda del primer término y su negación es la condición de guardia del segundo. Si además
el caso de reescritura es en si mismo un término, este puede contener otros operadores de
selección como operandos, posibilitando que una cantidad indeterminada de casos en un
solo término.

En las especificaciones que siguen el cálculo del valor absoluto de un número, se apoya en seleccionar
entre varios términos con base en condiciones de guardia, utilizando el “if funcional” (o el denominado
“operador ternario” en lenguajes procedurales).
Siendo obvio el rol de las componenetes de la especificación, se omiten explicaciones adicionales.
MIT SCHEME
(define (abs x)
(if (< x 0) (- x) x )
)
)

HASKELL
abs x = if (x>=0) then x else 0-x

SCALA
def abs(x: Int) = if (x >= 0) x else -x

MAUDE
var X : Float .
op op abs : Float -> Float .
eq abs(X) = if (X > 0.0) then X else (- X) fi .
Capítulo 8: Operadores Definidos
196
C
float abs(float X) { return X>0.0 ? X : -X }

JAVA
// colocar como un metodo de una clase
public float abs(float X) { return X>0.0 ? X : -X ;}

PHP
function abs($X) { return $X>0.0 ? $X : -$X }

PYTHON
def abs(X) :
return X if X>0.0 else -X

El operador de selección se extiende fácilmente a un operador con varias condiciones de


guarada que seleccionen entre igual número de términos. Para representar como un
condición de guardia la opción de no cumplir con las condiciones de guardia representadas
explícitmante (como predicados), es usual que exista una una palabra reservada en el
lenguaje, que en lo que sigue que denominaremos guardia “de defecto”,.

En las especificaciones que siguen el cálculo, se apoya en seleccionar entre varios términos con base en
varias condiciones de guardia. Para evaluar una evocación de un operador de selección el intérprete de
evalúa las condiciones de guardia p1 … pm, asociadas a los términos e1 … em, de forma sucesiva hasta
hallar la primera que evalue a verdadero. De hallarse que la condición de orden k es la primera que
evalúa a verdadero, se usa como caso de reescritura el término ek asociado a dicho predicado. Nótese
que la última condición de guardia es la guardia de defecto.
MIT SCHEME
El lenguaje SCHEME ofrece las “formas especiales” if y cond como mecanismos de selección118. La
forma general del y del cond es las siguientes:
(if <p1> <e1> <a>)

(cond
(<p1> <e1>)
(<p2> <e2>)

(<pm-1> <em-1>)
[(else < em>)])
)

Donde: La palabra reservada else es la guardia de defecto siendo opcional usarla. De no satisfacerse
ninguna de las condiciones el resultado se considera “indeterminado”.
Con ellas se puede definir una función que calcule el valor absoluto, así:
(define (abs x)
(cond ((> x 0) x)
((= x 0) 0)
(else (- x))
)
)

HASKELL

118
No se considran operadores debido a la rigida estrategia de evaluación de los operandos de los operadors en
SCHEME.
Capítulo 8: Operadores Definidos
197
Haskell ofrece la construcción if como un operador de selección y una sintaxis para definir operadores
que llevan a cabo selección por medio de guardias, que tiene la forma siguiente:
<id> af1 … afm
| <p1> = e1
| <p2> = e2
...
| <pm-1> = em-1
[| otherwise = em]

Donde: id es el nombre del nuevo operador, afj para j{1..m} son los argumentos formales del operador,
pi es una condición de guradia y ei es su término asociado. La palabra reservada otherwise es la guardia
de defecto siendo opcional usarla. De no satisfacerse ninguna de las condiciones la ejecución fracasa.
Con ellas se pueden definir funciones que calculen el valor absoluto, así:
abs x
| x>0.0 = x
| x<0 = 0.0-x
| otherwise = 0.0

SCALA
En Scala la construccion match implementa un mecanismos de selección que utiliza tanto condiciones
de guardia como emparejamiento. La forma general de las construcciones match es la siguiente:
<v> match {
case <af01> [(| <afj1>)*] [if <p1>] => <e1>
case <af02> [(| <afj2>)*] [if <p2>] => <e2>
….
case <af0n> [(| <afjn>)*] [if <pn>] => <en>
}

Donde: v es un valor, afij para j{1..n} y para i{0..mj} , son los argumentos formales del operador
match, que pueden ser, entre otras cosas, un nombre de variable (con o sin tipo), el símbolo “_” que
representa un nombre de variable no especificado, una constante, o un término complejo. pi es una
proposición y ei es un termino, para i{1..n}..
Para evaluar una evocación de un operador de selección match SCALA procede de forma sucesiva a
considerar las construcciones case hasta hallar la primera en la que uno de sus af empareja con el valor v
y el predicado p evalúe a verdadero. Nótese que el emparejamiento le da valor a las variables de af que
son locales al case (son diferentes de otras con el mismo nombre en la función), y pueden ser usadas en
las p y e del case. De hallarse que la construcción case de orden k cumple con las dos condiciones
referidas, el término a ser calculado para obtener el valor de la evocación es el término asociado a dicha
construcción, es decir ek. De no hallarse una construcción que satisfaga las dos condiciones se genera un
mensaje de error.
Con ellas se puede definir una función que calcule el valor absoluto, así:
def abs1(x: Int) = if (x >= 0) x else -x

def abs2(x: Int) = x match {


case y if (y>0) => x
case y if (y<0) => -x
case y [if y==0] => 0
}

La instrucción de selección más sencilla, denominada “if procedural”, selecciona entre dos
grupos de instrucciones con base en un solo predicado, donde el predicado mismo es la
condición de guarda del primer grupo y su negación es la condición de guardia del segundo
grupo. Además, cualquiera de los dos casos de reescritura (que en este caso son grupos de
Capítulo 8: Operadores Definidos
198
instrucciones), puede contiene otras instrucciones de selección posibilitando que una
cantidad indeterminada de caminos de ejecución en una solo instrucción de selección.

En las especificaciones que siguen el cálculo del valor absoluto de un número, se apoya en seleccionar
entre instrucciones con base en condiciones de guardia, utilizando el “if procedural”.
Siendo obvio el rol de las componenetes de la especificación, se omiten explicaciones adicionales.
C
float abs(float X) { float Y; if(X>0.0) Y=X; else Y=–X; return Y;}

JAVA
// colocar como un metodo de una clase
public float abs(float X) { float Y; if(X>0.0) Y=X; else Y=–X; return Y;}}

PHP
function abs($X) { if(X>0.0) $Y=$X; else $Y=–$X; return $Y;}

PYTHON
def abs(X):
if X>0.0:
Y=X
else:
Y=-X
return Y

La instrucción de selección “if procedural”, también se extiende fácilmente a una


instrucción con varias condiciones de guardia que seleccionan entre igual número de grupos
de instrucciones. Al igual que antes, para representar como un condición de guardia la
opción de no cumplir con las condiciones de guardia representadas explícitamente, es usual
que exista una una palabra reservada para representar la guardia “de defecto”,.

La construcción switch aparece en varios de los lenguajes procedurales considerados, y permite que el
proceso se apoye en una selección entre varios posibles caminos de ejecución , así:
switch (e) {
case g1: [s1 [break;]]
case g1: [s2 [break;]]

case gm: [sm [break;]]
[default: sm+1 [break;]]
}

Donde: e es un término; gj para j{1..m} son términos que definen las condiciones de guardia; sj para
j{1..m} son grupos opcionales de instrucciones; break es una instrucción opcional de control de
secuencia; y default es la condición de guardia por defecto.
Al ejecutarse la instrucción switch, el valor del término e se evalúa (una sola vez) y se compara, de
forma sucesiva, con los valores de los términos gk hasta hallar uno cuyo valor sea igual. De hallarse que
el término de orden k es el primero cuyo valor es igual al de e, el proceso se reanuda con el primer
grupo de instrucciones s situado adelante de ck, y prosigue hasta que se terminen los grupos de
instrucciones s o hasta hallar un instrucción break, para luego continuar el proceso con las instrucciones
que siguen al switch. El comportamiento del switch, sin embargo, se adapta en cada caso a las
restricciones del lenguaje incluyendo el concepto de igualdad que éste aplique.
C
Capítulo 8: Operadores Definidos
199
En el lenguaje C el término e debe calcular a un valor de tipo integral (los varios tipos de enteros y el
tipo char) o “enumerated”. Las condiciones deben ser constant-expressions cuyo valor debe poderse
calcular durante la compilación del programa (por lo que no deben tener variables ni, en general,
evocaciones a funciones).
JAVA
En el lenguaje JAVA la expresión e debe tener como resultado un valor de tipo byte, short, char, int,
enumerated o string. Las condiciones g deben ser constantes o literales del mismo tipo al que evalúe e.
PHP
En el lenguaje PHP la expresiones e es comparada con los valores de los casos ck usando el operador de
comparación “==” (que tiene un comportamiento diferente al del operador “===”) para todas las
expresiones a los que se aplique (incluyendo expresiones con variables y funciones). El caso de defecto,
puede estar, además, colocado en cualquier posición y aún sólo se tiene en cuenta si los demás fallan.

###  |= F=≠≈≤≥ F V

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
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 operandos con los tipos apropiados.
6. El programador puede reutilizar 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, junto con la definición del operador se debe proveer una
declaración que indique su perfil (indicando tanto los tipos para los operandos, como el tipo
para el resultado de su aplicación).
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.
Capítulo 8: Operadores Definidos
200
8.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; y, de no ser esto posible, el intérprete genera un mensaje de error.

La definición siguiente define un operador cuyo resultado tiene un tipo indeterminado:

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


;Value: op1
1 ]=> (op1 1)
;Value: 1
1 ]=> (op1 2)
;Value: “abb”

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.
Capítulo 8: Operadores Definidos
201
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.

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

8.5.1.1 Predicado de Tipo


Si en un lenguaje débilmente tipado se desea que un operador verifique el tipo de sus
operandos, es necesario contar con un operador que establezcan si un valor es de un tipo
determinado.
A los operadores que permiten establecer si un valor es de un tipo determinado, los
denominaremos “predicado de tipo”.

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


SCHEME ofrece una serie de predicados de tipo nativos; así los operadores siguientes:

boolean? _
number? _
complex? _
real? _
rational? _
integer? _
char? _
string? _
pair? _
list? _
vector? _
bit-string? _
symbol? _
cell? _
record? _
Establecen si su operando es del tipo correspondiente.

8.5.2 Perfil de los operadores en MAUDE


Tal como se indicó antes (ver ¡Error! No se encuentra el origen de la referencia.)
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.
Capítulo 8: Operadores Definidos
202
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.
8.5.2.1 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ón119 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>.
8.5.2.2 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> .

119 En el sentido de que todos los elementos de un sort están contenidos en otro sort diferente.
Capítulo 8: Operadores Definidos
203
Donde:
 <identificador_de_variable> Es un nombre que en adelante será asociado a
una variable.
 <lista_de_identificadores_de_variable> Es una lista de
<identificador_de_variable> separados por espacio.
8.5.2.3 Declaración de operadores en MAUDE
Por ser importantes tanto el sort del valor resultante como el sort de sus operandos, al
momento de definirse un operador, MAUDE provee construcciones específicas para
declarar los operadores y definir su perfil.
Tal como se refiere en [Clavel 2007, Sec 3.4], la declaración de operadores se apoya en
construcciones de una de las dos formas siguientes:
op <plantilla> : <sorts_dominio> -> <sort_rango> [<atributos_del _operador>] .
ops <plantillas> : <sorts_dominio> -> <sort_rango> [<atributos_del _operador>] .
Donde:
 <plantilla> Es el nombre del operador constituido por una secuencia de uno o
más identificadores del operador (separados entre si por caracteres
especiales120 o espacio en blanco).
 <plantillas> Es una secuencia de dos o mas <plantilla>, separadas entre si por
caracteres especiales o por espacios en blanco121.
 <sorts_dominio> es una secuencia de cero o más identificadores de sort,
separados entre si por espacios en blanco, que identifican el dominio de la
función asociada al operador.
 <sort_rango> Es un nombre de sort que identifica el rango de la función
asociada al operador.
 <atributos_del_operador> es una secuencia de cero o más identificadores de
propiedad, separados entre si por coma. En caso de ser cero los
identificadores de propiedad se omiten los paréntesis cuadrados. Cada una
de los posibles atributos del operador será discutida en el resto del trabajo
cuando el significado de la propiedad corresponda al tópico en discusión.
Para una discusión completa de dichos atributos el lector puede consultar a
[Clavel 2007, sec 4.4].
Las construcciones que comienzan con op se usan para definir un operador, mientras que
las construcciones que comienzan con ops permiten definir varios operadores (de igual
dominio y rango).

Los operadores definidos antes requieren de una declaración previa para el perfil del operador y de una
declaración de tipo para las variables usadas en la definición:

120 { } [ ] , son caracteres especiales que separan identificadores.


121 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 8: Operadores Definidos
204
fmod CUADR is
protecting FLOAT .
op cua : Float -> Float .
op sumcua : Float Float -> Float .
vars X Y : Float .
eq cua(X) = X * X .
eq sumcua(X, Y) = cua(X) + cua(Y) .
endfm
Maude> rewrite in CUADR : sumcua(2.0, 3.0) .
rewrites: 6 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result FiniteFloat: 1.3e+1

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

8.5.2.3.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 5.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>.
Cuando no hay ocurrencias del caracter “_” en <plantilla>, las aplicaciones del operador sólo
pueden usar notación prefija (ver 5.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.

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

op |_| : Float -> Float .


Capítulo 8: Operadores Definidos
205
vars X Y : Float .

ceq | X | = X if(X > 0.) .


ceq | X | = - X if(X <= 0.) .

8.5.2.3.2 Constantes
Cuando hay cero identificadores de sort en <sorts_dominio> se dice que el operador es una
constante del sort, y se usa para representar de forma explícita un elemento determinado del
mismo.

Una constante no es otra cosa que un operador de aridad cero. Así, la declaración siguiente define la
constante 0 de los números enteros:

op 0 : -> Int .

8.5.2.3.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 la sobrecarga de operadores usando la misma
plantilla con diferentes sorts para el dominio y el rango.

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

op _+_ : string string -> string .

Es un operador nativo que concatena dos strings:

Maude> reduce in STRING : "abc" + "def" .


rewrites: 1 in 4832458223ms cpu (0ms real) (0 rewrites/second)
result String: "abcdef"

8.5.2.3.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].
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.
Capítulo 8: Operadores Definidos
206
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.
8.5.2.3.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 5.2.3.1 ).
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:

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


precedencia del operador para 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.

En la declaración siguiente [Clavel 2007, sec. 3.9], se le da a los operadores suma y multiplicación un
Capítulo 8: Operadores Definidos
207
valor de precedencia y un patrón de gathering determinado:

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


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

8.5.3 Perfil de los Predicados en SWI PROLOG.


El SWI PROLOG es débilmente tipado, con lo que sólo al momento de llevarse a cabo los
cálculos asociados al proceso de resolución, se verifica que las operaciones puedan
aplicarse a los argumentos (ya calculados); y, de no ser esto posible, el intérprete genera un
mensaje de error.

La definición siguiente define un predicado cuyo resultado no tiene un tipo determinado:

pred1(1,1) . % -- 1 es un entero
pred1(2,"ab") . % -- "ab" es un arreglo de enteros
pred1(3,X) :- string_to_list(X, "ab") . % -- X es un string
pred1(4,"a") . % -- "a" es el entero 97
Que posibilita escribir predicados que al calcularse podrían dar lugar a errores de ejecución o a
resultados inesperados:

pred2(X1,X2,Y) :- pred1(X1,X11), pred1(X2,X21), Y is X11+X21 .

?- pred2(1,2,Y).
ERROR: '.'/2: Type error: `[]' expected, found `[98]' ("x" must hold one character)

2 ?- pred2(1,3,Y).
ERROR: is/2: Type error: `number' expected, found `ab'

3 ?- pred2(1,4,Y).
Y = 98.

Por ser un lenguaje débilmente tipado, el SWI PROLOG ofrece “predicados de tipo”.

Al igual que el SCHEME, el SWI PROLOG ofrece una serie de predicados de tipo nativos (ver
[swi_ref_man secc 4.6 ]), que permiten saber si un nombre de variable <var> se asocia o no con un valor
(si está o no “ligada”), y si un término <t> es o no de un tipo determinado:

var(<var>)
nonvar(<var>)
integer(<t>)
float(<t>)
rational(<t>)
rational<t>,<numerador>, <denominador>)
number<t>)
atom(<t>)
blob(<t>, <tipo>)
string(<t>)
Capítulo 8: Operadores Definidos
208
Cabe notar que como muestra el ejemplo anterior “aab” no es un string sino un arreglo de enteros (ver
[swi_ref_man secc 4.2.3]) .

?- string("aab").
false.

No implica esto que le PROLOG en si mismo deba ser débilmente tipado, por lo que es
posible hallar implementaciones fuertemente tipadas del mismo.

Medio ambiente y asignación.


El valor resultante del cálculo de un término sometido al intérprete en un lenguaje
funcional, depende de los operadores y de los valores que componen el término. Los
símbolos que representan valores deben, además, estar asociados con 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.
Como se explico en el numeral anterior, al ejecutarse una evocación de un operador
definido, las variables usadas en su definición se emparejan con los valores de la evocación,
por los que al llevarse a cabo los cálculos indicados en la definición, estas variables
también representan valores específicos de su dominio.
En algunos lenguajes es posible, sin embargo, someter al intérprete término en los que los
valores son representados por medio de símbolos diferentes a los literales del lenguaje.
Estos símbolos son tradicionalmente denominados como “variables” en un sentido similar
al utilizado en los lenguajes procedurales122. Para que sea posible usar una variable en un
término sometido a ser calculado, sin 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”123.
En este numeral nos referiremos al significado de los símbolos que ocurren en los términos
involucrados en un paso de reescritura, como el “medio ambiente” del paso de reescritura
(o el “medio ambiente” en el que se calcula el término involucrado en el paso de
reescritura).
La manera como, en cada lenguaje, se determina este medio ambiente es el motivo de
discusión de las subsecciones siguientes.

122 En los lenguajes procedurales las “variables” son rótulos que están asociadas a un único valor del dominio cuando se
inicia la ejecución del cuerpo de una función, pero son variables debido a que pueden reasociarse a otro valor diferente,
durante la ejecución del cuerpo de dicha función, al ejecutarse una instrucción de asignación. Sin embargo los
argumentos “formales” que aparecen en la definición de las funciones, representan a todos los valores de su tipo de la
misma manera que lo hacen las variables cuantificadas de los axiomas.
123Nótese que en un lenguaje procedural, un símbolo de variable representa un lugar de memoria que siempre contiene un
valor (dejado allí por quien antes hizo uso del lugar, y que puede ser un proceso no relacionado con el actual) Así, cuando
se usa una variable que no ha sido explícitamente asignada en el programa, éste tiene un comportamiento impredecible.
Capítulo 8: Operadores Definidos
209
8.6.1 Medio ambiente en SCHEME.
En la versión de SCHEME analizada, se provee un medio ambiente inicial con las variables
y símbolos predefinidos en el lenguaje. A medida que el usuario interactúa con el medio
ambiente, evocando los operadores nativos del lenguaje denominados “formas especiales”,
extiende124 el medio ambiente, hasta definir el medio ambiente en el que se ejecutan las
reescrituras [Hanson 2002, sec. 1.2.3].
Debido a que para el MIT SCHEME, tanto la definición de los operadores como su
evocación se llevan a cabo a través de un mismo interfaz125, dichas definiciones y
evocaciones forman una secuencia continua de interacciones donde se mezclan definiciones
y evocaciones de operadores en un orden arbitrario. Al someter un término para su cálculo,
es además posible usar nombres de “variable” en lugar de constantes para referirse a los
valores involucrados en el cálculo. La única condición de orden entre las definiciones y las
evocaciones de los operadores, es que tanto los operadores como las variables usadas en los
términos sometidos a cálculo, hayan sido definidos antes de su utilización. Así, las
condiciones bajo las cuales se somete un término pueden cambiar ya sea redefiniendo sus
operadores, por medio de axiomas de igualdad, o redefiniendo el valor de sus variables, por
medio de un operador de asignación nativo al lenguaje. En consecuencia, el resultado del
cálculo de un término dependerá de la secuencia de interacciones del usuario con el
intérprete, antes de someter el término a ser calculado.
Esta secuencia de interacciones es, entonces, la que define, o “extiende” el medio ambiente
de los cálculos. En los subnumerales que siguen se presenta la manera como se extiende el
medio ambiente por la evocación de las diferentes formas especiales ofrecidas en el
SCHEME
8.6.1.1 Introducción de Símbolos y de su Significado.
Cada vez que el usuario propone un nuevo operador por medio de una evocación del
operador define (ver, “Definición de Operadores en SCHEME”), introduce un nuevo
símbolo en el medio ambiente que había inmediatamente antes de la evocación,
asociándolo con la operación definida.
El operador de definición puede usarse también, para introducir símbolos de variable y
asociarlos al valor resultante de evaluar una expresión en el medio ambiente de la
evocación.

Dada la definición de la variable pi en el ejemplo 48, la definición siguiente:

1 ]=> (define pi2 (* 2 pi))


;Value: pi2
Asociará a la nueva variable pi2 el valor de la expresión calculada:

124No se hará aquí una definición precisa del concepto “extender” el medio ambiente, esperando que quede clara al leer el
texto que sigue.
125Esto es cierto aún si el programa se incluye ya que esto no es más que una secuencia de evocaciones que se lleva a
cabo de forma automática.
Capítulo 8: Operadores Definidos
210
1 ]=> pi2
;Value: 6.283082

Es posible, sin embargo, usar el operador define para introducir un nuevo símbolo al medio
ambiente sin “asignarlo” a valor u operador alguno. Sin embargo, sólo símbolos definidos
y asignados a un significado126 pueden usarse en las definiciones y cálculos siguientes.

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

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

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


;Unbound variable: pi212
; To continue.......

126En esto difiere el SCHEME de los lenguajes procedurales ya que una variable debe ser necesariamente asignada antes
de usarse, sin que tome el valor del espacio de memoria donde reside su valor.
Capítulo 8: Operadores Definidos
211
8.6.1.3 Medio ambiente de una evocación.
Al llevarse a cabo el cálculo de la evocación de un operador, los operandos se evalúan en el
medio ambiente en que se llevó a cabo la evocación. Luego de evaluados los operandos, se
reescribe la evocación usando el (único) axioma asociado al operador. El término
resultante de la reescritura se calcula, a su vez, en el medio ambiente en que se definió el
operador, extendido con la asociación de las variables que constituyen los argumentos
formales a los valores de los operandos ya evaluados (ver [Hanson 2002, sec. 1.2.4]). En
otras palabras, las variables usadas en el cuerpo de la definición de un operador no
necesariamente tienen que ser argumentos formales en dicha definición.

La definición de un operador en SCHEME se lleva a cabo en un medio ambiente determinado por los
operadores ejecutados antes que el define correspondiente. Así, la definición del operador f que se
muestra a continuación:

1 ]=> (define x 4)
...
1 ]=> (define y 2)
...
1 ]=> (define (f y z) (+ x y z))

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

8.6.1.4 Extensiones locales del medio ambiente durante el cálculo.


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
Capítulo 8: Operadores Definidos
212
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 let127, 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.

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

127 La diferencia entre let, let* y letrec estriba en que el medio ambiente de evaluación de <valor> para una <variable>
determinada se aumenta con las variables definidas antes que ella en el caso del let* y con todas las variables definidas en
el caso de letrec.
Capítulo 8: Operadores Definidos
213
A ser evaluado en el medio ambiente E3 = {x/4, y/3, z/4}.
La extensión del medio ambiente inducida por el let da finalmente lugar a la evaluación del término
siguiente:

(+ x y)
Que ahora debe ser evaluado en el medio ambiente E4 = E3 + {x/7, y/4} = {x/7, y/4, z/4}

;Value: 11

Una vez finalizado el cálculo de la evocación del operador, el medio ambiente se restituye
al existente antes de la evocación. En efecto, el medio ambiente resultante al extender el
medio ambiente de la definición con la asociación de los argumentos formales a los valores
de los operandos es local al término al que se reescribe el operador. Es posible, sin
embargo, usar la operación de asignación dentro del cuerpo del operador para redefinir el
medio ambiente de la definición.

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

8.6.1.5 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
Capítulo 8: Operadores Definidos
214
la definición en el momento de la evocación. Si se tiene en cuenta que este estado puede
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 más 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 procedural128. 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 profundidad129 (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.
8.6.2 Medio ambiente en MAUDE.
Tanto en el REPL como en un achivo de inclusión un operador puede ser definido varias
veces, siendo la última la que se usa en los cálcuos posteriores.

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)

128Si el lector se pregunta si existe una forma especial para lleva a cabo procesos repetitivos, la repuesta es SI. La forma
especial do puede verse en [Hanson 2002, sec. 2.9].
129La única restricción sobre los define es que deben ocurrir al principio del cuerpo donde aparecen [Hanson 2002, sec.
2.4]
Capítulo 8: Operadores Definidos
215
;Value: 144

El medio ambiente en el que se lleva a cabo el cálculo de un término sometido al intérprete


de MAUDE, está completamente definido por los módulos del programa. Para ello dichos
módulos deben ser incluidos en el intérprete antes de que se lleven a cabo cálculos. Los
módulos de programa definen los operadores que pueden ser evocados en los términos a ser
calculados, sin definir valor alguno para las variables que se involucran en su definición.
Los términos sometidos deben ser términos base, sin variables. El cálculo de los términos,
por su parte, no tiene “efectos colaterales”, por lo que no asigna o reasigna valor alguno a
los rótulos ni a las “variables”. En consecuencia el valor calculado para un término
sometido, no depende de los términos calculados con anterioridad.
Al ser evocado un operador definido en el programa, el intérprete procede con la reescritura
de cada evocación, siguiendo el procedimiento descrito en la sección 5.4.1 “Substitución,
Particularización y Emparejamiento”. Durante este proceso, todas las variables del lado
derecho del axioma que se use para la reescritura, toman su valor del emparejamiento que
iguala el lado izquierdo del axioma con la evocación del operador. Así, dado que las
ecuaciones satisfacen la condición impuesta en la sección 5.3.3: “Ecuaciones” (ver Capítulo
5) para los SRT130, el medio ambiente en el que se calcula el cuerpo de la definición de un
operador es totalmente definido por los valores de los argumentos con los que se evoca el
operador sin que haya valoración o revaloración de variables como producto de los
cálculos efectuados con anterioridad.
Es posible, sin embargo, definir en el programa operadores de aridad cero (sin operandos),
y utilizar axiomas en el programa para reescribirlos a términos base específicos. En este
sentido, estos operadores pueden usarse como substitutos de dichos términos base, en los
términos a ser calculado. No es posible, sin embargo, cambiar el valor asociado a dichos
operadores, ya que de definirlo más de una vez en el programa, se tendría una ambigüedad
en su significado que es reportada por el intérprete durante la ejecución; y, lo que es más
importante, no existe un operador de asignación que permita modificar, en ejecución,
el significado de los operadores que aparecen en los términos que se calculen con base
en las ecuaciones del programa131.
8.6.3 Medio ambiente en PROLOG.

Estrategia de Evaluación.
Dado un término complejo a ser calculado por el intérprete de un lenguaje funcional, cabe
siempre la pregunta siguiente: ¿en que orden se van a aplicar los axiomas asociados con los
(varios) operadores del término durante el proceso de reescritura?. Nótese que este orden

130MAUDE permite usar un tipo de condición de igualdad (que se denomina “matching equations” [Clavel 2007, Sec
4.3]) en las condiciones de una ecuación condicional, que permite usar términos con variables que no aparecen al lado
izquierdo de la ecuación condicional. En este caso la valoración de estas variables proviene del proceso de selección del
axioma y del emparejamiento, sin que influyan de forma alguna de los cálculos efectuados con anterioridad.
131 Es posible, sin embargo, usar operadores de nivel meta que manipulen los elementos de un programa como valores
(strings) que luego son ejecutados. Con esto se pueden escribir programas que evoluciona con su uso.
Capítulo 8: Operadores Definidos
216
es esencialmente arbitrario ya que el proceso de reescritura puede, en principio, aplicarse a
cualquiera de los subtérminos del término a ser calculado132.
Desde el punto de vista del árbol sintáctico, esto significa que el mecanismo de derivación
debe reescribir los operadores del término complejo en una secuencia específica,
decidiendo, por ejemplo, si reescribe primero los operadores de la parte inferior del árbol, si
reescribe primero los operadores de la parte superior del árbol, si los reescribe en algún
orden diferente predeterminado, o si reescribe varios operadores en forma paralela (o
simultánea).
Para definir este orden, cada implementación usa una “estrategia de evaluación”. Son
estrategias típicas, la de calcular primero los operandos de los operadores antes de abordar
el cálculo de los mismos, que ha sido denominada “evaluación en orden aplicativo”133, y la
de abordar primero el cálculo de los operadores antes de abordar el cálculo de los
operandos, que ha sido denominada “evaluación en orden normal”134 [Abelson 1985 sec.
1.1.3].

Dada la definición de los operadores sumcua y cua de los ejemplos anteriores, en el cálculo del
término siguiente:

sumcua(2+1, 8/2)
Se puede proceder en orden normal, en cuyo caso la secuencia de reescrituras dará lugar a la derivación
siguiente:

cua(2+1)+cua(8/2)
(2+1) * (2+1) + (8/2) * (8/2)
3*3+4*4
9 + 16
25
O se puede proceder en orden aplicativo, en cuyo caso la secuencia de reescrituras dará lugar a la
derivación siguiente:

sumcua(3, 4)
cua(3) + cua(4)
3*3+4*4
9 + 16
25
Formas de evaluación que para el ejemplo difieren sólo en el número de veces que deben efectuarse las
operaciones involucradas en los operandos de la aplicación del operador sumcua.

132 Si el SRT es convergente, para el resultado final del cálculo el orden de aplicación de los axiomas es, además,
irrelevante.
133 O “estricto”
134 O “perezosa”
Capítulo 8: Operadores Definidos
217
Si bien del ejemplo anterior parece desprenderse que el orden de evaluación aplicativo es
más eficiente que el orden de evaluación normal, este no es siempre el caso. En particular
frente a un operador de selección ocurre precisamente lo contrario.

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.

Considere la ejecución del programa siguiente en MAUDE:

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

eq fun(4,0) .

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

Un ejemplo sencillo tomado de [Clavel 2007, sec 4.4.7] ilustra el uso de la estrategia de evaluación en
un operador de selección para evitar el cálculo de la parte de la evocación no seleccionada.
Capítulo 8: Operadores Definidos
219
fmod EXT-BOOL is
protecting BOOL .
op _and-then_ : Bool Bool -> Bool [strat (1 0)] .
op _or-else_ : Bool Bool -> Bool [strat (1 0)] .
var B : [Bool] .
eq true and-then B = B .
eq false and-then B = false .
eq true or-else B = true .
eq false or-else B = B .
endfm
En ese código se define una versión de los operadores lógicos que tienen la ventaja de evaluar el
segundo operando sólo si se necesita. Esto se deber a que la estrategia de evaluación indica evaluar
primero el primer operando, y luego el operador. Al evaluar el operador el axioma seleccionado puede
definir el resultado final sin necesidad de evaluar el segundo operando, o depender del valor del segundo
operando, en cuyo caso obliga a su evaluación.

8.7.3 Estrategia de evaluación en PROLOG


En PROLOG los términos son construcciones sintácticas que pueden ser atómicos o
estructurados. Los operadores de igualdad o desigualdad sirven para comparar los términos
de forma sintáctica.

El operador t1=t2 verifica si t1 y t2 unifican. Tiene por tanto un sentido sintáctico y produce como
respuesta los valores que deben sustituir a las variables no ligadas de t1 y t2 que permiten la unificación.

3 ?- 4=2+2 .
false.

4 ?- X=2+2 .
X = 2+2.

2 ?- 2+2 = X .
X = 2+2.

3 ?- 2+X = 2+2 .
X=2

1 ?- 2+4/X = Y+4/7 .
X = 7,
Y = 2.

2 ?- 2+4/X = Z+4/Y .
X = Y,
Z = 2.
En SWI PROLOG, las variables de igual nombre entre t1 y t2 se consideran ligadas al mismo
cuantificador, así135:

135Puede entenderse que el = es un predicado que posibilita la unificación y sus variables están en el alcance de los
mismos cuantificadores.
Capítulo 8: Operadores Definidos
220
1 ?- 2+4/X = X+4/Z .
X = 2,
Z = 2.

En consecuencia para hallar el mgu entre t1 y t2 el usuario debe asegurarse de que no tengan variables
en común.
El operador == tiene también un sentido meramente sintáctico, verificando si sus operandos se asocian
con el mismo término, pero no puede ser usado para instanciar una variable, así:

4 ?- 2+2 == 4.
false.

1 ?- X==2+2.
false.

5 ?- X=2+2, 3+2/(2+2) == 3+2/X .


X = 2+2.

1 ?- 2+X = 2+2, X==2.


X=2

6 ?- X=2+2, X=4.
false.
El operador =\= es la negación de == .

El predicado is, sirve para forzar la evaluación de expresiones aritméticas.

Frente a la consulta:

1 ?- is(5, +(3,2)).
El intérprete responde: true.
También es posible escribir la misma consulta con notación infija así:

2 ?- 5 is 3+2 .
true.
Mejor aun, se puede colocar una variable como primer argumento del predicado is, para que por
unificación el SWI PROLOG determine su valor136, por ejemplo con la consulta siguiente:

3 ?- X is 3+2 .
El intérprete responde X = 5.
Pero no puede colocarse una expresión como primer argumento

2 ?- 3+1 is 1+3 .
false.

136En lo que sigue consideraremos que la unificación para cálculos aritméticos se llevan a cabo con una serie de reglas
nativas que denominaremos de forma genérica *.
Capítulo 8: Operadores Definidos
221
7 ?- (3+2) is 5 .
false
El segundo argumento del predicado is debe estar completamente instanciado en el momento de
ejecutarse. Así, para las consultas siguientes:

4 ?- 3+2 is X .
ERROR: is/2: Arguments are not sufficiently instantiated

6 ?- 5 is X .
ERROR: is/2: Arguments are not sufficiently instantiated
El intérprete responde con un error de instanciación.
Cabe anotar, que los operadores lógicos de comparación aritmética, =:= fuerzan el cálculo de sus
operandos pero requieren que sus argumentos estén instanciados (ver [Blackburn 01: secc 5.4] ), así:

5 ?- 4 =:= 2+2 .
true.

1 ?- 1+3 =:= 2*2 .


true.

8 ?- X =:= 2+2 .
ERROR: =:=/2: Arguments are not sufficiently instantiated

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

137 Denominada descomposición progresiva de funciones en el marco de la programación “estructurada”.


Capítulo 8: Operadores Definidos
222
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
7.7.1.4), permitiéndole a un operador estructurar sus propios componentes sin conflictos de
nombre con operadores de igual nombre definidos en otros lugares diferentes. Esta
característica apoya la mantenibilidad, ya que cambios a operadores definidos dentro de
otro operado sólo pueden afectar a dicho operador.

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.
8.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”)138 (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
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.
Para que la relación entre módulos sea efectiva, es necesario que dichos módulos sean
previamente incluidos en el intérprete. Los módulos MAUDE deben estar contenidos en

138 Los nombres en castellano corresponden a la traducción libre del autor de los nombres de los módulos en MAUDE
Capítulo 8: Operadores Definidos
223
archivos de texto139. Un archivo de texto puede contener uno o varios módulos completos.
Una vez iniciada una sesión del intérprete de MAUDE, se deben incluir los módulos desde
lo archivos usando los comandos in o load [Clavel 2007, cap. 18]. Se pueden incluir los
módulos de varios archivos usando de forma repetida dichos comandos. Es posible,
también, colocar comandos en archivos de texto que sean ejecutados al incluir el archivo
donde aparecen; con ello que es posible incluir múltiples archivos con un sólo comando de
inclusión. Además, si en el comando usado para ejecutar el intérprete se coloca como
argumento el nombre de un archivo de texto, su contenido es automáticamente incluido
luego de iniciado el programa.
Los módulos que definen los tipos nativos se incluyen automáticamente de primeros en el
intérprete140, por lo que están siempre disponibles para relacionarlos con los módulos del
programa que serán incluidos posteriormente.
En las subsecciones que siguen describiremos el contenido de los módulos funcionales y
los operadores que permiten establecer relaciones entre módulos.
8.8.2.1 Definición y contenido de módulos en MAUDE
Un módulo funcional se define así:
fmod <nombre> is <cuerpo_del_modulo> endfm
Donde:
 <nombre> Es el identificador del módulo, usualmente colocado en letra
capital.
 <cuerpo_del_modulo> Es un conjunto de operadores de relación entre
módulos (ver sección 5.3.1), seguido de una especificación en lógica
ecuacional multisort {(S,),X,E} (ver sección 5.3.1).
Los símbolos de sort, S son las declaraciones de los sort propuestos por el programador
(ver “Declaraciones de sort en MAUDE” (sec. 7.5.2.1)).
Los símbolos de operación  son las declaraciones de los operadores propuestos por el
programador (ver “Declaraciones de operadores en MAUDE” (sec. 7.5.2.3)).
El conjunto de variables X son las declaraciones de variables del módulo (ver
“Declaraciones de variables en MAUDE” (sec. 7.5.2.2)).
El conjunto de aserciones E comprende en MAUDE tres tipos de aserción, así:
 Las ecuaciones con las que se definen los operadores (ver “Declaraciones
de operadores en MAUDE” (sec. 7.3.4)).
 Las relaciones de subconjunto entre los sort, que son tratadas en 8.5.2.1
 Las ecuaciones de membresía condicional, que son tratadas en 10.2.5 y
11.4.4.
8.8.2.2 Relaciones entre módulos en MAUDE

139Es posible incluirlos desde la línea de comandos, aunque por las pocas facilidades del editor de la línea de comandos,
esto carece de sentido.
140 De un archivo denominado prelude.maude, que debe estar a disposición del interprete a través de la variable path del
sistema operativo...
Capítulo 8: Operadores Definidos
224
Las relaciones entre los módulos son establecidas por medio de construcciones que
soportan la definición de jerarquías de inclusión entre módulos, una algebra de módulos y
técnicas de programación parametrizada (ver [Clavel 2007, sec. 6]).
En las secciones siguientes se presentan los dos primeros tipos de construcciones. Una
descripción detallada de la programación parametrizada se encuentra en el Capítulo 11.
8.8.2.3 Inclusión de Módulos en MAUDE
En MAUDE es posible incluir un módulo en otro, mediante el uso de las construcciones
siguientes:
protecting <expresion_de_modulo> .
extending < expresion_de_modulo> .
including < expresion_de_modulo> .
Donde:
 <expresion_de_modulo> es el identificador de un módulo o la evocación
de un operador entre módulos que de lugar a un módulo.
Cuando en un módulo se incluye otro modulo, los operadores y sorts, del módulo incluido
se ponen a disposición del módulo que lo incluye. Las variables por su parte siguen siendo
locales al módulo y no se ponen a disposición del módulo que incluye.
Las diferencias entre los tres modos de inclusión, están por fuera del alcance de este texto.
El lector interesado puede consultarlas en [Clavel 2007, sec. 6.1]. En términos resumidos
protecting significa que las declaraciones del módulo incluido, no pueden ser modificadas;
including, significa que se puede modificar el módulo incluido, y extending, está en el
medio de las dos anteriores.
8.8.2.4 Algebra de Módulos en MAUDE
La construcción de la forma siguiente permite construir un módulo a partir de dos ya
existentes:
<expresion_de_modulo> + <expresion_de_modulo>
El módulo construido incluye los elementos de los módulos representados por los
operandos.
La construcción de la forma siguiente permite cambiar los identificadores de un módulo:
<expresion_de_modulo> * ( <lista_de_cambios_de_nombre> )
Donde:
 <lista_de_cambios_de_nombre> es una lista de <cambio_de_nombre>
separados por coma.
 <cambio_de_nombre> es una construcción de una de las formas
siguientes:
sort <nombre> to <nombre>
op <nombre> to <nombre>
op <nombre> to <nombre> [ <atributos_de_sintaxis> ]
op <perfil_operador> to <plantilla>
op <perfil_operador> to <plantilla> [<atributos_de_sintaxi>]
Capítulo 8: Operadores Definidos
225
label <nombre> to <nombre>
Donde:
 <nombre> son el nuevo y viejo identificador.
 <perfil_operador> es el perfil del operador (ver sección “Declaración de
Operadores en MAUDE” sección 7.5.2.3) cuya <plantilla> se desea
modificar.
 <atributos_de_sintaxis> pueden ser los atributos prec, gather y format.
Para una explicación más precisa del efecto de los cambios de nombre y en particular de la
necesidad de cambiar los atributos del operador el lector debe referirse a [Clavel 2007, sec.
6.2.2].

Resumen del Capítulo.


La potencia real de los lenguajes funcionales surge de las facilidades que tienen para
proponer nuevos operadores y nuevos tipos de datos con sus operadores asociados. En
efecto, la posibilidad de definir nuevos operadores es fundamental tanto para el reuso del
código elaborado, que pueda ser aplicable en múltiples programas, como para darle apoyo
lingüístico a una arquitectura de descomposición progresiva de funciones en una pieza de
software compleja.
En el marco de los lenguajes funcionales la definición de un operador será asimilada a una
(o varias) ecuación(es) de la forma ti = td, cuya semántica será siempre la de usar una de
dichas ecuaciones para reescribir una evocación del operador principal de ti,
substituyéndola por el término td luego de aplicarle la substitución de variables que,
aplicada a ti lo hace idéntico al término de la evocación (la substitución que “empareja” a ti
con la evocación), así:
 En el lenguaje SCHEME un operador se define con una sola ocurrencia de la
forma (define (ti) (td)), siendo ti un término con un sólo operador, en el que
todos los argumentos son nombres de variable.
 En el lenguaje MAUDE un operador se define con una o varias ocurrencias
de las formas eq ti = td ., y ceq ti = td if(tb) siendo ti un término cuyo operador
principal es el definido y cuyos argumentos pueden ser otros términos,
constantes o nombres de variable.
Para que sea posible variar el término que reescribe una evocación de un operador en
función de los valores de los argumentos usados en la evocación, la definición de un
operador debe tener implícitas tantas ecuaciones como “casos” de variación existan, así:
 En el lenguaje SCHEME el término td de la definición puede tener la forma
de uno de los dos términos de selección siguientes: (if (tb1) td1 td2) o (cond
((tb1) (td1)) ((tb2) (td2)) ...). Ellas permiten seleccionar al término tdi como el
lado derecho de la ecuación cuando su tbi asociado valora al boleano
verdadero luego de sustituir sus variables de la misma forma que ha de
hacerse en tdi. Es, además, posible que tdi sea a su vez un término de
selección para anidar múltiples criterios de selección.
 En el lenguaje MAUDE la ocurrencia de múltiples instancias de las formas
eq ti = td ., y ceq ti = td if(tb), deben implicar que sólo un término td es
Capítulo 8: Operadores Definidos
226
seleccionado para la reescritura. La ecuación ti = td usada, en efecto, debe
ser la única en la que se cumple que existe una substitución de las variables
de ti que lo hacen idéntico al término de la evocación, y que, además, el
valor del tb, evaluado bajo la misma substitución de variables, toma el valor
de verdadero.
Todos los valores asociados con los argumentos de la evocación de un operador tienen un
tipo. Ello no implica, sin embargo, que todos los lenguajes restringen de igual manera el
tipo de los valores con los que es posible evocar un operador, así:
 Los tipos en SCHEME son latentes, en lugar de ser manifiestos [Hanson
2002, secc. 1.3], por lo que el tipo se asocia a los valores y no a las variables
que los representan. En suma el SCHEME es un lenguaje “débilmente
tipado”. Así, al definir los operadores no se indica el tipo de sus argumentos
o del resultado de su aplicación. Para conocer el tipo del valor asociado a
una variable se debe usar un “predicado de tipo”.
 En contraste al SCHEME, el lenguaje MAUDE es un lenguaje “fuertemente
tipado”. Todas las variables deben ser declaradas y asociadas con un tipo (o
“sort”), y la definición de los operadores debe estar acompañada por una
declaración que indica el sort de sus argumentos y resultado. La asociación
del operador con sus tipos permite reusar el nombre de los operadores en
operadores para diferentes tipo (“sobrecargando” dicho operador). La
declaración del operador permite, además, señalar la posición de los
argumentos en relación al nombre del operador para definirlo con notación
“infija”. Para resolver las posibles ambigüedades al definir el árbol
sintáctico de un término que involucre un operador definido con notación
infija, el lenguaje permite definirle al operador un valor de precedencia, y a
sus argumentos un orden de evaluación (o “gathering”), incluso en relación
con la evaluación del operador mismo.
El orden en que se evalúa (o reescribe) una evocación de un operador en relación con la
evaluación de sus argumentos (en caso de que sean términos complejos) constituye la
“estrategia de evaluación del operador”. Esta estrategia influye tanto en la eficiencia de los
cálculos como en la terminancia del cálculo mismo. Esta estrategia difiere
significativamente entre los lenguajes analizados, así:
 En SCHEME se lleva a cabo el cálculo de los argumentos antes de aplicar la
reescritura al operador. Esta estrategia denominada “orden aplicativo” (vs.
“orden normal”), promueve la eficiencia al evitar que un argumento deba
calcularse varias veces luego de reescrito el operador. Los operadores que
seleccionan entre sus operandos (cond e if), son considerados “formas
especiales” y evalúan primero el argumento del cual depende la selección
para evitar evaluar los argumentos no seleccionados (esto, además, garantiza
la terminancia para operadores definidos recursivamente, ver Capítulo 9)
 En contraste al SCHEME, el lenguaje MAUDE le permite al programador
determinar la estrategia de evaluación de forma particular para cada uno de
los operadores que declara. Para ello al declarar el operador se incluye en la
sección de “atributos del operador” una secuencia de enteros (i1 i2 i3 ..)
indicando la secuencia en que se evalúan los argumentos señalados por el
valor del entero (0 para el operador, 1 para el 1er argumento, etc..).
Capítulo 8: Operadores Definidos
227
El valor resultante del cálculo de un término depende de los operadores y de los valores de
los operandos. El valor de las variables que aparezcan como operandos es determinado por
el “medio ambiente de evaluación” del término. La manera como se define este medio
ambiente difiere significativamente entre los lenguajes analizados, así:
 En SCHEME el medio ambiente de evaluación esta determinado, tanto por
el valor de los argumentos usados en la evocación, como por instrucciones
de asignación de rótulos a valores ejecutadas antes de la evocación. Así, por
un lado, el valor de las variables usadas como argumentos en la definición
del operador es el valor de los argumentos que les correspondan en la
evocación, y por el otro, el valor de las variables que no aparezcan como
argumentos en la evocación esta determinado por las últimas ejecuciones de
las asignaciones (define v t) que asignen valores t a las variables v.
 En contraste al SCHEME, el lenguaje MAUDE sólo permite que el valor de
las variables usadas como argumentos en la definición del operador sea el
valor de los argumentos que les correspondan en la evocación. No es,
entonces, posible usar, en los términos que definen a un operador, variables
diferentes a las usadas como argumentos en su definición. Es posible, sin
embargo, definir operadores sin argumentos que son considerados como
“constantes”, para luego asociarles un valor por medio de su definición. El
valor asociado a las constantes es, sin embargo, inmodificable.
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. La manera como se definen los módulos
difiere significativamente entre los lenguajes analizados, así:
 En SCHEME la unidad fundamental de modularización es el operador o
“procedimiento”. Es también posible definir operadores dentro de otro
operador, que son visibles sólo dentro del operador en que se definen, sin
conflictos de nombre con otros operadores de igual nombre definidos en otra
parte.
 En el lenguaje MAUDE la unidad básica de estructuración de los programas
es el módulo. Un módulo contiene los elementos sintácticos y las aserciones
que definen una teoría en lógica. Las relaciones entre los módulos se
definen por medio de operaciones entre módulos, así: un módulo puede ser
incluido otro modulo, para que este último disponga de los elementos que el
incluido define; y es posible, además, “sumar” dos módulos para crear uno
nuevo seleccionando de cada uno los elementos de interés que pueden
renombrarse para evitar conflictos.

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

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 cálculo es, en consecuencia, un factor importante al definir
operadores de forma recursiva y el programador debe conocerla para evitar programas que
desborden los recursos disponibles.
Por otro lado, los programas que usan operadores recursivos se aplican a problemas que
deben resolverse llevando a cabo cálculos que se repiten una y otra vez por un número de
veces que es, en general, determinado por los datos involucrados en el problema. En
consecuencia el tiempo que se demora la ejecución del programa, dependerá de los datos
involucrados en el problema. La forma que toma dicha dependencia es, en consecuencia,
un factor clave al momento de definir operadores recursivos y el programador debe
conocerla para evitar ejecuciones que se demoren más de lo previsto.
En los apartados siguientes se introduce la definición recursiva de operadores en el marco
de los lenguajes estudiados, para luego analizar detalladamente las consecuencias de las
posibles formas de definir la recursión. La presentación se lleva a cabo con base en
problemas específicos, para los que, primero, se construyen diferentes soluciones, y luego,
se estudia el comportamiento espacio-temporal del proceso de cálculo que ellas determinan.
La presentación va, principalmente, orientada a que el lector desarrolle las habilidades
necesarias para llevar a cabo definiciones recursivas de operadores que sean útiles, sin
entrar en estudios teóricos que están por fuera del alcance del trabajo.

141 Ver definición de “Recursión” en Wikipedia http://wikipedia.org/


Capítulo 9: Definición Recursiva de Operadores.
231
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.
9.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.

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.

El proceso de reescritura de una evocación del operador definido en el ejemplo anterior seria como la
que se ilustra a continuación:

;;( sum_n2 1 3)
;;(+ (* 1 1) (sum_n2 2 3))
;;(+ 1 (+ (* 2 2) (sum_n2 3 3)))
;;(+ 1 (+ 4 (+ (* 3 3) (sum_n2 4 3))))
;;…
;;…
Capítulo 9: Definición Recursiva de Operadores.
232
Para evitar esta circunstancia, basta con notar que:
i

n
n i
2
 i 2 {3}

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

La selección en SCHEME debe efectuarse en el marco del único axioma de definición usando un
selector.

(define (Sm-n2 I J)
(if (= I J) (* I I)
(+ (* I I) (Sm-n2 (+ I 1) J) )
)
)

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

(sum_n2 1 6)
;;(+ (* 1 1) (sum_n2 2 6)
;;(+ 1 (+ (* 2 2) (sum_n2 3 6)))
;;(+ 1 (+ 4 (+ (* 3 3) (sum_n2 4 6))))
;;(+ 1 (+ 4 (+ 9 (+ (* 4 4) (sum_n2 5 6)))))
;;(+ 1 (+ 4 (+ 9 (+ 16 (+ (* 5 5) (sum_n2 6 6))))))
;;(+ 1 (+ 4 (+ 9 (+ 16 (+ 25 (* 6 6))))))
;;(+ 1 (+ 4 (+ 9 (+ 16 (+ 25 36)))))
;;(+ 1 (+ 4 (+ 9 (+ 16 61))))
;;(+ 1 (+ 4 (+ 9 77)))
;;(+ 1 (+ 4 86))
;;(+ 1 90)
;; 91

En MAUDE, por otro lado, basta con incluir el nuevo axioma, limitando el uso del anterior al caso que
corresponde, así:

op Sn2_ _ : Int Int -> Int .


vars I J : Int .
ceq Sn2 I J = (I * I) + (Sn2 (I + 1) J) if(I < J) .
eq Sn2 I I = (I * I) .

El proceso de reescritura de una evocación del operador en MAUDE, es muy similar al del operador en
SCHEME por lo que dejamos al lector la tarea de analizarlo.
Capítulo 9: Definición Recursiva de Operadores.
233

La versión recursiva en SWI PROLOG del predicado que suma los cuadrados de los números situados
entre dos enteros I y J (I>=J), es la siguiente:

sm_n2_r(I,I,R):- R is I*I .
sm_n2_r(I,J,R):- J>I, I1 is I+1, sm_n2_r(I1,J,R1), R is R1+I*I .
El comportamiento en memoria del proceso de resolución SLD determinado por la versión recursiva del
predicado del ejemplo anterior, puede ilustrarse por medio de una tabla que muestra la evolución del
objetivo para una evocación específica del predicado, así:

Paso regla substitución objetivo


0 sm_n2_r(1,4,X)
1 2 1/I, 6/J, X/R 4>1, I1 is 1+1, sm_n2_r(I1,4,R1), X is R1+1*1
2-3 * 2/I1 sm_n2_r(2,4,R1), X is R1+1*1
4 2 2/I, 6/J, R1/R 4>2, I1 is 2+1, sm_n2_r(I1,4,R1’), R1 is R1’+2*2, X is R1+1*1
5-6 * 3/I1 sm_n2_r(3,4,R1’), R1 is R1’+2*2, X is R1+1*1
7 2 3/I, 6/J, R1’/R 4>3, I1 is 3+1, sm_n2_r(I1,4,R1’’), R1’ is R1’’+3*3, R1 is R1’+2*2, X is R1+1*1
8-9 * 4/I1 sm_n2_r(4,4,R1’’), R1’ is R1’’+3*3, R1 is R1’+2*2, X is R1+1*1
10 1 4/I, R1’’/R R1’’ is 4*4, R1’ is R1’’+3*3, R1 is R1’+2*2, X is R1+1*1
11 * 16/R1’’ R1’ is 16+3*3, R1 is R1’+2*2, X is R1+1*1
12 * 25/R1’ R1 is 25+2*2, X is R1+1*1
13 * 29/R1 X is 29+1*1
14 * 30/X □

Donde las filas de la tabla ilustran el resultado del proceso para uno o varios pasos de la resolución, en la
primera columna de la tabla se muestran los pasos que conducen al resultado de la fila, en la segunda
columna se indica el número de orden en el programa PROLOG de la regla a ser usada en la(s)
resolución(es), en la tercera columna se muestra la substitución de variables necesaria para la(s)
unificación(es) --de la primera meta y la cabeza de la regla--, y en la cuarta columna el objetivo
resultante.
El lector debe notar, además, que antes de llevar a cabo la resolución se efectuaron cambios de nombre a
las variables (agregándoles ’), para evitar las colisiones.

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.
9.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 de
operadores 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
Capítulo 9: Definición Recursiva de Operadores.
234
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.

La Tabla 8.1 se puede expresar en SCHEME de la manera siguiente:

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

El proceso de reescritura de una evocación del operador hallar-raíz-cuadrada sería como la que se
ilustra a continuación:

(hallar-raíz-cuadrada 1.0 3)
;;(hallar-raíz-cuadrada 2.0 3)
;;(hallar-raíz-cuadrada 1.75 3)
Capítulo 9: Definición Recursiva de Operadores.
235
;;(hallar-raíz-cuadrada 1.7321 3)
….
….

Para que el proceso finalice y de como resultado una buena aproximación de la raíz, es
suficiente con colocar una pregunta de parada que evite la substitución recursiva en el
momento en que se haya alcanzado la aproximación, y entregue como resultado el valor del
argumento que la contiene.
Así, si consideramos que una buena aproximación de x es un valor que elevado al
cuadrado difiere en menos de dos cifras decimales del valor de x, podemos completar el
procedimiento anterior para obtener el procedimiento buscado, de la manera siguiente:

El operador raíz-cuadrada en SCHEME quedaría de la manera siguiente:

(define (raiz-cuadrada X) (raiz2-ite 1. X))


(define (raiz2-ite Y X)
(if (< (abs (- (* Y Y) X)) 0.001) Y
(raiz2-ite (/ (+ (/ X Y) Y) 2.) X )
)
)

El operador √ (Rz) en MAUDE quedaría de la manera siguiente:

op Rz_ _ : Float Float -> Float .


vars X Y : Float .
ceq Rz X Y = Y if(abs(Y * Y - X) < 0.0001) .
ceq Rz X Y = (Rz X ((X / Y + Y) / 2.)) if(abs(Y * Y - X) >= 0.0001) .

.....

Al usar este procedimiento obtendríamos, por ejemplo:

El resultado de una evocación del operador raíz-cuadrada definido en el ejemplo anterior sería como el
que se ilustra a continuación:

(hallar-raíz-cuadrada 1.0 3)
;;(hallar-raíz-cuadrada 2.0 3)
;;(hallar-raíz-cuadrada 1.75 3)
;;(hallar-raíz-cuadrada 1.7321 3)
;;1.7321….
Capítulo 9: Definición Recursiva de Operadores.
236
La forma del Proceso de Cálculo (uso de memoria).
La importancia de la manera como se desenvuelve el proceso de cálculo para un operador
definido, puede intuirse comparando los procesos de reescritura correspondientes al cálculo
de los operadores definidos en la sección anterior (Ejemplo 32 y Ejemplo 35).
En el caso del operador que calcula la suma de los cuadrados comprendidos entre dos
enteros dados (Ejemplo 32), es fácil observar que, durante la substitución, el término de la
derivación crece hasta alcanzar un tamaño máximo y luego comienza a reducirse para
alcanzar la respuesta deseada. Este crecimiento se debe a que la operación suma que se
introduce en cada paso de reescritura, no puede llevarse a cabo inmediatamente debido a
que no se dispone del valor del segundo sumando. Este sumando, en efecto, se calcula
primero para la última operación suma introducida, que, al ser efectuada, obtiene el
segundo sumando de la penúltima suma introducida, y así sucesivamente. Nótese también
que el tamaño máximo alcanzado por el término durante el proceso depende del valor de
los argumentos, ya que el proceso de substitución debe introducir j-i veces la operación
suma antes de que se pueda calcular el último sumando.
En el caso del operador que obtiene la raíz cuadrada de un número dado, (Ejemplo 35), es
fácil observar que, durante la substitución el término de la derivación no crece como el del
caso anterior, sino que se mantiene estable manteniendo el mismo número de operadores y
operandos, hasta que se obtiene la respuesta deseada. Esta estabilidad se debe a que el
efecto neto de cada paso de substitución es el de cambiar el valor del primer argumento de
la evocación de la función, haciéndolo evolucionar hasta el valor deseado.
A los procesos que presentan inestabilidad en la memoria se le ha denominado “proceso
recursivo” mientras que A los procesos que presenta estabilidad se les ha denominado
“proceso iterativo” [Abelson 85 1.2.1]. El lector debe notar que esta definición refiere a la
forma como se comporta el proceso en la memoria y no a la forma como se define el
operador (que arriba denominamos definición recursiva)142.
Un proceso iterativo es, sin duda, más deseable que un proceso recursivo ya que no se corre
el riesgo de, que para ciertos valores de los argumentos, el cálculo se haga imposible por
agotamiento de la memoria. Cabe entonces preguntarnos si: ¿es posible modificar la
definición recursiva de un operador que genera procesos recursivo para que genere
procesos iterativos?; y si esto es posible, si: ¿es posible en todos los casos?.
La respuesta a la primera de las preguntas anteriores es SI, y lo probaremos modificando la
definición del operador encargado de obtener la suma de los cuadrados, para que los
procesos que determina sean iterativos.
La respuesta a la segunda pregunta es NO, y, aunque no lo probaremos en este trabajo, en
los ejemplos, mostraremos problemas cuya solución estará siempre basada en operadores
que determinan procesos recursivos. En este caso culparemos al problema, en si mismo, y

142 En un lenguaje procedural toda especificación recursiva determina un proceso recursivo debido al apilamiento del
“stack” que ocurre en cada evocación. Para poder definir procesos iterativos, el programador debe usar las construcciones
de repetición de grupos de comandos (for, do, while, etc..). En un capítulo posterior interpretaremos estas construcciones
en el marco de un lenguaje funcional, probando que no adicionan semántica alguna al lenguaje de programación.
Capítulo 9: Definición Recursiva de Operadores.
237
lo consideraremos de un grado de complejidad más alto que el que tiene soluciones a través
de procesos iterativos.
Para concebir la solución iterativa podemos apoyarnos, inicialmente, en la naturaleza del
problema. En efecto si desarrollamos un poco la fórmula {2}, que fue base para concebir el
procedimiento:
j j
sum   n  i  ((i  1)  ((i  2)  ((i  3)   n 2 ))) {5}
2 2 2 2 2

n i n i  4

Es fácil ver que tomando ventaja de la propiedad asociativa del operador suma, podríamos
plantear otra forma distinta de desarrollar la fórmula durante el cálculo., así:
j j
sum   n 2  (((i 2  (i  1) 2 )  (i  2) 2 )  (i  3) 2 )   n 2 {6}
n i n i  4

La ventaja de esta forma de desarrollar la fórmula es que nos permite intuir que las sumas,
introducidas de izquierda a derecha en el proceso deben reducirse a un valor sin esperar a
que se introduzca el último sumando, dejando, en cada iteración, un valor ya totalizado y
una sumatoria mas pequeña por calcular.
Para separar este valor totalizado de la parte por totalizar, debemos usar un “lugar de
memoria” que lo almacene y que en cada iteración lo aumente adicionándole un término
más de la sumatoria143.
Este lugar de memoria no es otra cosa que un operando del operador.

El procedimiento SCHEME que se muestra a continuación reserva un operando para “acumular” los
términos de la sumatoria.

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

143Esto es precisamente lo que se hace al programar este operador en un lenguaje procedural. La diferencia es que para
contar con el “lugar de memoria” se debe declarar una variable, para usarlo se debe contar con instrucciones “tome de” el
lugar de la memoria el valor, “coloque en” dicho lugar el resultado; y para repetir el proceso una y otra vez se deben tener
construcciones que de forma específica indiquen la repetición. Así, en suma, el lenguaje procedural requiere de un mayor
número de construcciones sintácticas que el funcional.
Capítulo 9: Definición Recursiva de Operadores.
238
Donde, por claridad, se resaltó el hecho de que antes de cada paso de reescritura la estrategia de
evaluación propia del SCHEME obliga a totalizar el acumulador.

En esta última definición el término de la derivación es, en efecto, de tamaño estable como
deseábamos, y, al igual que en el caso de la raíz cuadrada, el valor del resultado se va
obteniendo de forma paulatina en el primer argumento.
Al igual que en el caso de la raíz cuadrada, para que el proceso entregue el resultado desde
el argumento que lo contiene, es necesario completar la definición con una condición que
suspenda la evocación recursiva luego del paso en que se alcanza el resultado. Además,
para garantizar que la evocación al procedimiento se lleva a cabo con el valor inicial
correcto para el acumulador, la evocación debe efectuarse desde otro procedimiento cuya
única función es encargarse de “inicializar” el acumulador.

La versión iterativa del operador de sumatoria en SCHEME es, en consecuencia, como sigue:

(define (Sm-n2i I J) (aux_smn2i 0 I J))


(define (aux_smn2i A I J)
(if (= I J) (+ A (* I I))
(aux_smn2i (+ A (* I I)) (+ I 1) J )
)
)

La versión iterativa del operador de sumatoria en MAUDE, es como sigue:

fmod C8-SUMN2I is
protecting INT .
op Sn2I_ _ : Int Int -> Int .
op sum-i : Int Int Int -> Int .
vars I J T : Int .
eq Sn2I I J = sum-i(0,I,J) .
ceq sum-i(T,I,J) = sum-i((T + I * I),(I + 1),J) if(I <= J) .
eq sum-i(T,I,I) = T .
endfm
La versión iterativa del predicado PROLOG para el cálculo de la sumatoria es la siguiente:

sm_n2_i(I,J,R):- sm_n2_i(I,J,0,R) .
sm_n2_i(I,I,S,R):- R is S+I*I .
sm_n2_i(I,J,S,R):- J>I, I1 is I+1, S1 is S+I*I, sm_n2_i(I1,J,S1,R) .

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  n 2  (( j  3) 2  (( j  2) 2  (( j  1) 2  j 2 ))) {7}
2

n i n 1
Capítulo 9: Definición Recursiva de Operadores.
239
Con lo que es fácil concebir la definición del ejemplo siguiente:

La versión iterativa del operador de sumatoria en MAUDE, que acumula los términos de la sumatoria de
atrás hacia adelante es como sigue:

fmod C8-SUMN2I-BK is
protecting INT .
op Sn2IB_ _ : Int Int -> Int .
op sum-ib : Int Int Int -> Int .
vars I J T : Int .
eq Sn2IB I J = sum-ib(0,I,J) .
ceq sum-ib(T,I,J) = sum-ib((T + J * J),I,(J - 1)) if(I <= J) .
eq sum-ib(T,I,I) = T .
endfm

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.
Capítulo 9: Definición Recursiva de Operadores.
240
9.3.1 Forma del proceso: acumulador asociativo y no asociativo.
Los dos ejemplos siguientes 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
1 
2
3
22 
3 
2

Estos dos ejemplos difieren básicamente en el operador de más alto nivel que en el primer
caso es la suma y en el segundo la división. Es importante notar desde ahora que el
primero es un operador asociativo mientras que el segundo no lo es.
Nos concentraremos en obtener un operador que calcule una aproximación del valor de la
serie para un número dado de términos, truncando la fracción infinita. Así, queremos
definir dos operadores que llamaremos sum_pi(n) y cont_frac(n), que reciben un entero n
como argumento y devuelven el valor dado respectivamente por las expresiones:
n
1 1 1 1 1
Sn       {10}
k 1 ( 4k  3)(4k  1) 1  3 5  7 9  11 (4n  3)  (4n  1)
Y
1
Fn  {11}
2
1 
2
3
22 
n

n2
9.3.1.1 Acumulador recursivo
Ya en este punto es evidente que existe una similitud entre los dos casos ( Sn y Fn). En
efecto, para cualquier valor de n>1, el valor de la función se construye llevando a cabo
algún tipo de “acumulación” (con el operador “+” o con el operador “/”) de los términos
genéricos de una serie, diferentes en cada caso, cuyo valor depende de su posición.
Así, un posible acercamiento para construir un operador que genere un proceso recursivo,
es identificar en la fórmula, una serie de subfórmulas auxiliares S*(k, n) y F*(k, n) que se
relacionan entre sí por ocurrencias del término genérico.
Estas subfórmulas, que se entienden como “acumular los términos desde k hasta n”, se
muestran gráficamente en las figuras 2.2 y 2.3.
Capítulo 9: Definición Recursiva de Operadores.
241

1 1 1 1 1
Sn      0
1  3 5  7 9  11 ( 4( n  1)  3)  ( 4( n  1)  1) ( 4n  3)  ( 4n  1)

S*(n+1, n)
S*(n, n)
S*(3, n)
S*(n-1, n)

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

1
Fn 
2
12 
3
22 
n2

...
n 1
(n  2) 2 
n
(n  1) 2 
n 0
2

F*(n+1,n)
F*(n,n)
F*(2,n)
F*(n-1,n)
F*(n-2,n)

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

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) 
(4k  3)(4k  1) k  F * (k  1, n)
2

S * (n  1, n)  0 F * (n  1, n)  0
Descubrir las funciones auxiliares y la manera como se relacionan equivale a obtener la
versión recursiva del operador, ya que para definirlo basta expresar dichas funciones y
relaciones en el lenguaje de programación.
Capítulo 9: Definición Recursiva de Operadores.
242
La versión recursiva de los operadores sum_pi(n) y cont_frac(n), en SCHEME expresan directamente
las relaciones mostradas arriba, así:

(define (sum-pi-rec K N)
(if (> K N) 0.
(+ (/ 1. (* (- (* 4. k) 3.) (- (* 4. k) 1.)))
(sum-pi-rec (+ K 1) N)
)
)
)

(define (cont-frac-rec K N)
(if (> K N) 0.
(/ K (+ (* K K ) (cont-frac-rec (+ K 1) N) ))
)
)

Donde sum_pi_rec y cont_frac_rec expresan las funciones S*(k, n) y F*(k, n), respectivamente.
Y Finalmente:

(define (sum-pi N)(* 8. (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.

La versión recursiva de los operadores sum_pi(n) y cont_frac(n), en MAUDE expresa las mismas
relaciones en una sintaxis similar a las de las fórmulas matemáticas, así:

fmod C8-PI-FRAC is

protecting FLOAT .

vars I J T : Float .

op Spi1 _ : Float -> Float .


op Spi-R _ _ : Float Float -> Float .
eq Spi1 J = 8. * (Spi-R 1. J) .
ceq Spi-R I J = (1. / ((4. * I - 1.)*(4. * I - 3.)))
+ (Spi-R (I + 1.) J) if(I < J) .
eq Spi-R I I = (1. / ((4. * I - 1.)*(4. * I - 3.))) .

op Frc1 _ : Float -> Float .


op Frc-R _ _ : Float Float -> Float .
eq Frc1 J = Frc-R 1. J .
ceq Frc-R I J = I / ((I * I) + (Frc-R (I + 1.) J)) if(I < J) .
eq Frc-R I I = I / (I * I) .
endfm
Donde sum-pi-rec y cont-frac-rec expresan las funciones S*(k, n) y F*(k, n), respectivamente.
Capítulo 9: Definición Recursiva de Operadores.
243

La versión recursiva en SWI PROLOG, de los predicados correspondientes al calculo de la sumatoria y


fracción continua de los ejemplos anteriores, se muestran a continuación:
Para el cálculo de π/8 por medio de una sumatoria:

sum_pi8_rec(K,N,0.0):- K>N .
sum_pi8_rec(K,N,R):- K1 is K+1, sum_pi8_rec(K1,N,R1), R is R1+1.0/((4.0*K-3.0)*(4.0*K-1.0)) .
sum_pi_rec(N,R):- sum_pi8_rec(1,N,R8), R is R8*8.0 .
La ejecución para 300 términos, da como resultado los valores siguientes como aproximación a π:

3 ?- sum_pi_rec(300,X).
X = 3.1399259880805297 .
Para el cálculo de la fracción:

frc_r(N,R):- frc_r(1,N,R) .
frc_r(I,N,0.0):- I>N .
frc_r(I,N,R):- I1 is I+1, frc_r(I1,N,R1), R is I/((I*I)+R1) .
La ejecución para 5 niveles, da como resultado los valores siguientes:

1 ?- frc_r(5,X).
X = 0.683766096685666 .

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

La versión iterativa del operador sum_pi(n) en MAUDE tiene la misma estructura que la de la suma de
los cuadrados de Ejemplo 238 , sólo que el término a ser acumulado es diferente, así:

fmod C8-PI-FRAC is

protecting FLOAT .

vars I J T : Float .
Capítulo 9: Definición Recursiva de Operadores.
244
op Spi2 _ : Float -> Float .
op Spi-i _ _ _ : Float Float Float -> Float .
eq Spi2 J = 8. * (Spi-i 1. J 0.) .
ceq Spi-i I J T = (Spi-i (I + 1.) J
(T + (1. / ((4. * I - 1.)*(4. * I - 3.))))) if(I < J) .
eq Spi-i I I T = T + 1. / ((4. * I - 1.)*(4. * I - 3.)) .

endfm
La versión iterativa del predicado en SWI PROLOG, correspondiente al cálculo de la sumatoria es la
que se muestra a continuación:

sum_pi8_ite(K,N,R):- sum_pi8_ite(K,N,0.0,R) .
sum_pi8_ite(K,N,S,S):- K>N .
sum_pi8_ite(K,N,S,R):- K1 is K+1, S1 is S+1.0/((4.0*K-3.0)*(4.0*K-1.0)), sum_pi8_ite(K1,N,S1,R) .
sum_pi_ite(N,R):- sum_pi8_ite(1,N,R8), R is R8*8.0 .

Donde el lector debe notar que la variable que se asocia con la respuesta, R, sólo se toma del valor
acumulado al momento llevarse a cabo la unificación con la segunda regla del programa.

4 ?- sum_pi_ite(300,X).
X = 3.1399259880805284

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.
1 1
Así, como valor, no es útil para hallar y este a su vez no es útil para encontrar
12 2
1  2
2

2
1
, pues haría falta la estructura completa para colocar el nuevo valor en el lugar
2
1 
2
3
22 
32
apropiado de la expresión.
Esta posibilidad, sin embargo, aparece claramente si llevamos a cabo el proceso de
acumulación de abajo hacia arriba (en lugar de atrás hacia delante como en el caso de la
sumatoria de los cuadrados).
Así, como se muestra en la figura 2.1, empezaremos por encontrar el último cociente,
correspondiente a n y luego con ese cociente hallaremos el penúltimo cociente,
correspondiente a n-1, haciendo la suma y división necesaria. El proceso continuará hasta
que encontremos el valor de la primera fracción.
Capítulo 9: Definición Recursiva de Operadores.
245
1
Fn 
2
12 
3
22 
n2
... 
n 1
( n  2) 2 
n
(n  1) 2 
n2
Figura 2.1. El proceso de construcción de cont-frac iterativo.

La versión iterativa del operador cont_frac(n) en MAUDE tiene la misma estructura que el de sum-
pi(n), con la diferencia de que ahora es necesario acumular de abajo hacia arriba (o de atrás para
adelante), así:

fmod C8-PI-FRAC is

protecting FLOAT .

vars I J T : Float .

op Frc2 _ : Float -> Float .


op Frc-i _ _ : Float Float -> Float .
eq Frc2 I = Frc-i I 0. .
ceq Frc-i I T = (Frc-i (I - 1.) (I / ((I * I) + T)) ) if(I > 0.) .
eq Frc-i 0. T = T .

endfm
Donde la dirección del cálculo simplificó el número de argumentos, ya que para detener el cálculo
bastaba que el índice del proceso llegara a cero.
La versión iterativa en SWI PROLOG, del predicado correspondientes al calculo de la fracción continua
se muestran a continuación:

frc_i(I,R):- frc_i(I,0.0,R) .
frc_i(0,A,A) .
frc_i(I,A,R):- I1 is I-1, A1 is I/((I*I)+A), frc_i(I1,A1,R) .
Donde el lector debe notar que la acumulación se lleva a cabo de abajo hacia arriba en la fórmula, por lo
que sólo se requiere un contador; y que la variable que se asocia con la respuesta, R, sólo se toma del
valor acumulado A al momento llevarse a cabo la unificación con la segunda regla del programa.

2 ?- frc_i(5,X).
X = 0.683766096685666 .

Nótese que ambos procedimientos comparten la misma estructura:


“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”.
Capítulo 9: Definición Recursiva de Operadores.
246
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

La precisión del proceso de Cálculo


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 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    {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.

La versión iterativa del operador coseno(x,n) en MAUDE con la misma estructura que la de la suma de
los cuadrados del ejemplo 238, es el siguiente:

fmod C8-COSENO is

protecting FLOAT .
protecting INT .
protecting CONVERSION .

vars I J K N : Int .
vars T X : Float .
…..
op coseno1 : Float Int -> Float .
op Cos-i : Float Int Int Float -> Float .
Capítulo 9: Definición Recursiva de Operadores.
247
eq coseno1(X,N) = Cos-i(X,N,1,1.) .
ceq Cos-i(X,N,K,T) = Cos-i(X,N,(K + 1),
(T + ((- 1.) ^ float(K)) *
((X ^ float(2 * K)) / ((2 * K) !)))
) if(K < N) .
eq Cos-i(X,N,N,T) = T .

endfm
.....
Suponiendo que existen procedimientos predefinidos _! (que calcula el factorial) y _^_ (que eleva un
número a una potencia).

Esta definición, sin embargo, NO SIEMPRE LOGRA SU COMETIDO, induciendo a que


se aborte el proceso o a imprecisiones en el cálculo (para el coseno(x,n), cuando se usan
valores grandes de x y n).

Comparando el cálculo del coseno usando nuestra función, con el cálculo usando la función nativa del
lenguaje, así:

Maude> rewrite in C8-COSENO : coseno1(2.0, 10) .


rewrites: 588 in 1628036047000ms cpu (5ms real) (0 rewrites/second)
result FiniteFloat: -4.1614683654756973e-1

Maude> rewrite in C8-COSENO : cos(2.0) .


rewrites: 1 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result FiniteFloat: -4.1614683654714241e-1
Pueden verse pequeñas diferencias en el resultado para un número pequeño de términos144, pero al
aumentar el número de términos de la serie el asunto empeora ya que el cálculo puede llegar a ser
imposible:

Maude> rewrite in C8-COSENO : coseno1(2.0, 1000) .


rewrites: 1320958 in 1628036047000ms cpu (7016ms real) (0 rewrites/second)
result [Float]: Cos-i(2.0, 1000, 513, -4.1614683654714246e-1 + 1.0 * (Infinity
/ Infinity))

9.4.1.1 Primer cambio al cálculo del coseno: mejora en la precisión.


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 ,   ,   ,   ,  ,   {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 o imposible.
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

144 Posiblemente debido que el cálculo en el procesador aritmético no se lleva a cabo por series de taylor.
Capítulo 9: Definición Recursiva de Operadores.
248
demasiado agudo, para notar que el resultado de estos cálculos tiende a producir valores
demasiado grandes para la capacidad de almacenamiento de los valores en la mayoría de las
implementaciones. Las limitaciones en la capacidad de almacenamiento de valores se
traducen en algunos casos en el almacenamiento de valores equivocados y en otros en una
pérdida de precisión145.
Así a, pesar de que el valor de X^(2 * K) / (2 * k)!) es pequeño, en general, no se calcula
correctamente debido a que el cálculo de sus dos componentes ya presenta errores de
precisión. Las expresiones matemáticas, pese a ser claras y concisas, no son
necesariamente la mejor manera, ni siquiera la más exacta, de calcular.
Para eliminar los problemas de precisión en el cálculo del término optaremos por mantener
los valores intermedios del proceso de cálculo lo mas pequeños posibles. Para ello nos
apoyaremos en el hecho de que, por un lado, los términos de la secuencia {16}, van
disminuyendo su valor hasta un punto en que se hace despreciable, y que, por otro, es fácil
ver que cada término puede encontrarse a partir del anterior, mediante un producto
adecuado.
Por ahora, tratemos de expresar, en términos matemáticos, este hecho. Primero separemos
el primer término de la suma que llamaremos a0:
n
(1) k x 2 k n
(1) k x 2 k n
cos(x)  S n   1   1   ak {17}
k 0 (2k )! k 1 (2k )! k 1

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:
 x2   x2  x2   x4  x2   x6  x2   x 2 ( n 1)  x 2 
 1,   ,   ,   ,   ,  ,  (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.

145 En las implementaciones de nuestros lenguajes, los valores se almacenan usando un espacio fijo y predeterminado en
la memoria. Existen, sin embargo, lenguajes como Mathematica
(http://www.wolfram.com/products/mathematica/index.html) y Maxima (http://maxima.sourceforge.net/), que proveen
implementaciones que almacenan los valores en espacios de memoria variables, manteniendo un grado de precisión
predeterminado.
Capítulo 9: Definición Recursiva de Operadores.
249
El cálculo del término de orden n en la serie {19}, lo podemos llevara a cabo de forma precisa por
medio del operador definido en MAUDE siguiente:

op termino : Float Int -> Float .


op ter-ite : Float Int Int Float -> Float .
eq termino(X,N) = ter-ite(X,1,N,1.) .
ceq ter-ite(X,K,N,Tr) =
ter-ite( X,(K + 1),N,
((- Tr) * ((X * X) / ((2. * float(K)) *
(2. * float(K) - 1.))))
) if(K <= N) .
ceq ter-ite(X,K,N,Tr) = Tr 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 coseno2 : Float Int -> Float .


op Cos-i2 : Float Int Int Float -> Float .
eq coseno2(X,N) = Cos-i2(X,N,1,1.) .
ceq Cos-i2(X,N,K,T) = Cos-i2(X,N,(K + 1),(T + termino(X,K))) if(K <= N) .
ceq Cos-i2(X,N,K,T) = T if(K > N) .
.....

Con lo que el cálculo de coseno(x,n), es ahora siempre posible para valores altos de n, así146:

Maude> rewrite in C8-COSENO : coseno2(2.0, 1000) .


rewrites: 6014004 in 1628036047000ms cpu (19936ms real) (0 rewrites/second)
result FiniteFloat: -4.1614683654714246e-1

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,
el tiempo que se tarda el cálculo asociado a una evocación de estos operadores es, entonces,
independiente del valor de los operandos. 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*1 donde C es el tiempo de ejecución del caso más desfavorable

Dadas las definiciones que se muestran a continuación:

.....
eq cua(x) = x * x .
eq sumcua(x, y) = cua(x) + cua(y) .
.....

146El cálculo con la serie de taylor da diferencias significativas, con respecto al cálculo con el operador nativo del
lenguaje, para valores grandes de X (v.g. 30 radianes). El lector debe examinar si puede reducir estas diferencias
reduciendo el valor en radianes a uno menor que 2π (usando el residuo de dividirlo por 2π).
Capítulo 9: Definición Recursiva de Operadores.
250
La evocación del operador sumcua, con argumentos escalares dará lugar a un proceso de cálculo que
lleva a cabo 2 reescrituras, 2 multiplicaciones y 1 suma, independientemente de los valores de los
argumentos:

Esto se debe a que las definiciones de los operadores, se apoyaron siempre en términos
construidos con operadores diferentes, que, a su vez, fueron definidos con base en otros
operadores diferentes, sin que en la cadena de definiciones sucesivas apareciera de nuevo
alguno de los operadores definidos147.
Por otro lado, la evocación de los operadores recursivos genera procesos en los que el
número de reescrituras y cálculos elementales, depende no sólo del número operaciones
especificadas en su definición y en la definición de los operadores que participan en
cálculo, sino también del número de veces que el operador se evoca a si mismo durante el
proceso. Tal como puede observarse en los ejemplos presentados antes, este último número
depende, en general, del valor de los operandos involucrados en la evocación. En otras
palabras, el tiempo de ejecución de un operador recursivo puede depender del valor de sus
operandos.
Con unos pocos ejemplos sencillos (ver 9.6), es fácil probar que, para un mismo problema,
el tiempo de ejecución de diferentes implementaciones puede ser tan dramáticamente
diferentes, que algunas de ellas carezcan completamente de valor práctico.
En términos absolutos el tiempo de ejecución de un operador depende de múltiples factores,
entre ellos los siguientes:
 La velocidad del computador donde se ejecuta.
 El lenguaje de programación.
 La implementación del lenguaje de programación.
 Los valores de los argumentos (o sea el “caso” al que se aplica).
 El “algoritmo” resultante de la definición del operador.
 La forma en que el programador define el “algoritmo”.
Para reducir la complejidad de esta dependencia, y capturar por medio del concepto de
“algoritmo” el factor de mayor relevancia frente a la eficiencia, se ha planteado el
“principio de invarianza” [Brassard 88, sec. 1.3]. Según este principio, dos códigos
diferentes cualesquiera (es decir que difieran en el lenguaje, el computador, la
implementación del lenguaje y forma de definir el algoritmo), que implementen un mismo
operador, implementan también el mismo “algoritmo” si las funciones que relaciona el
tiempo de ejecución con los valores de los argumentos (que en adelante denominaremos
“funciones de rendimiento”), están mutuamente acotadas por una constante
multiplicativa148. Así, para las implementaciones A y B de un mismo algoritmo α se
cumple que existen constantes c1 y c2 que satisfacen los predicados siguientes:

147En general será importante establecer si la función que relaciona el tiempo de ejecución con el valor de los operandos
esta acotada superiormente por una la función de dicho valor de la forma C*1 donde C es un valor constante.
148El concepto de algoritmo es de hecho, bastante intangible, y con frecuencia interpretado erróneamente como la
implementación de una solución en un lenguaje procedural. A este respecto, debemos destacar que toda implementación
implementa un algoritmo particular, pero que diferentes implementaciones pueden implementar un mismo algoritmo a
pesar de sus diferencias (en lenguaje y en la disposición de sus elementos).
Capítulo 9: Definición Recursiva de Operadores.
251
TαA(M).<= c1*TαB(M)
TαB(M).<= c2*TαA(M)
Donde:
 Tαk(M) es la función de rendimiento de la implementación k del algoritmo α.
 M es el conjunto de elementos que describen el caso de ejecución.
En términos simples el principio de invarianza plantea que, el efecto de mejorar todos los
factores que afectan el rendimiento, con excepción del algoritmo y del caso, puede
resumirse en que, a lo sumo, las mejoras en el tiempo de ejecución estarán acotadas por una
constante multiplicativa
Otra simplificación útil es la de caracterizar el caso por unas pocas medidas de magnitud
(v.g. m y n siendo ambos enteros), y considerar, dentro de los casos de un tamaño dado,
sólo al de la peor disposición de sus elementos (o al de la disposición mas frecuente o al de
la disposición “promedia”). En consecuencia el esfuerzo de desarrollo de programas debe
enfocarse a obtener algoritmos en los que la función de rendimiento, para un cierto tipo de
disposición de los elementos, sea la “mejor posible”.
Una manera de definir cual es la “mejor” entre dos funciones de rendimiento, para el caso
de funciones con una sola variable independiente, es usar el concepto matemático del
“orden de una función”149.
El “orden” de una función f(n) denominado O(f(n)), es el conjunto de funciones t(n)
definido de la manera siguiente [Brassard 88, sec. 2.1]:

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 constante de f(n), a partir de un valor de n.
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, ln(n), n1/2, n, n*ln(n), n2, n3,..nk,…2n
Ya que sus órdenes respectivos se relacionan de la manera siguiente:

149 O “Big O notation”, http://en.wikipedia.org/wiki/Big_O_notation


Capítulo 9: Definición Recursiva de Operadores.
252
O(1)  O(ln(n))  O(n1/2)  O(n)  O(n*ln(n))  O(n2)  O(n3) … O(nk)…  2n
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.

Los dos operadores definidos en el ¡Error! No se encuentra el origen de la referencia. :

eq cua(x) = x * x .
eq sum_cua(x, y) = cua(x) + cua(y) .
Tienen una función de rendimiento en O(1), ya que su tiempo de ejecución es independiente del valor
de los datos (asumiendo que el producto lo sea).
El operador definido en el Ejemplo 39:

eq Σn2 I J = sum_ite(0, I, J) .
ceq sum_ite(Total, I, J) = sum_ite((Total + I * I), (I + 1), J) if(i<=j) .
ceq sum_ite(Total, I, J) = Total if(i>j) .
.....
Tienen una función de rendimiento en O(n), siendo n el número de sumandos, ya que la evocación
recursiva de sum_ite se lleva a cabo n+1 veces y el O(n+1) es el mismo O(n).
El operador del Ejemplo 36:

(define (raíz-cuadrada Y Y)
( If (< (abs (- (* Y Y) Y )) 0.001) Y
(raíz-cuadrada (/ (+ Y (/ x Y)) 2) x ) )
)
)
Tienen una función de rendimiento en O(n1/2), siendo n el número de cifras decimales de la precisión
deseada, ya que la taza de convergencia del método de newton-rapson es cuadrática ( ver
http://en.citizendium.org/wiki/Newton%27s_method )

En lo que sigue nos preocuparemos por valorar la eficiencia de los operadores que se
definan por medio del orden de sus funciones de rendimiento. En cada caso procuraremos
situar dichas funciones en el marco del orden de las funciones presentadas más arriba. Para
ello nos apoyaremos en planteamientos intuitivos sin entrar en mayores disquisiciones
teóricas. El objetivo de nuestra valoración es tanto el de alertar al lector sobre la
importancia del asunto, como el de dotarlo de una intuición básica que le permita evitar la
definición de algoritmos groseramente ineficientes. Para una mejor discusión del tema
remitimos al lector a [Brassard 88].
9.5.1.1 Segundo cambio al cálculo del coseno: mejora en la eficiencia.
Si bien resolvimos el grave problema de cálculo que tenía la solución inicial para el
operador que calculaba el coseno, todavía queda un problema de eficiencia.
En efecto, si analizamos el número de veces que se lleva a cabo la reescritura, para el caso
de la última versión iterativa, podemos ver que durante el proceso de acumulación de los
términos de la serie, esta se lleva a cabo N+2 veces, al igual que en el caso del cálculo de la
suma de los cuadrados.
Capítulo 9: Definición Recursiva de Operadores.
253
Sin embargo el cálculo propio de cada término de la serie requiere de K+2 reescrituras
siendo K el índice del término en la serie. Así, una aproximación al número total de
reescrituras, es el siguiente:
Nro_Reescrituras = 2+3+4+......N+(N+1)+(N+2) = (N+4)*(N+2)/2 {20}
Este número contrasta con el requerido para el cálculo de la suma de los cuadrados que era
básicamente N+2. En términos de la teoría del orden, se puede demostrar que ahora
nuestro algoritmo es del orden de N2, cuando los que teníamos antes eran del orden de N.
Para corregir este problema de eficiencia, modificaremos la última definición del operador
coseno(x,n), acoplando el cálculo de los términos de la serie, con el cálculo de la serie
misma. A esta estrategia la denominaremos “tejer” las definiciones de los operadores
termino(n), y coseno(x,n) del Ejemplo 48. La especificación separada de los operadores y
su posterior tejido, facilita una estrategia de programación en la que las diferentes partes del
programa se conciben de forma independiente, para juntarlas luego de que se han definido
y probado150.
La posibilidad de llevar a cabo este tejido se debe a que en el proceso definido por
termino(n) van apareciendo de forma sucesiva los términos de la serie, y en el proceso de
coseno(x,n) se van utilizando de forma sucesiva dichos términos. Así, si se acoplan el
cálculo de los términos con su uso, se pueden fundir las dos iteraciones en una sola.

Para tejer los dos operadores del ejemplo anterior, basta con acoplar el cálculo de los términos parciales
con su uso. Para ello basta con recibir como argumento de Cos-i3 el término requerido en cada
iteración, usarlo, y luego actualizarlo en la evocación recursiva para que en la iteración siguiente esté
disponible el término siguiente, así:

op coseno3 : Float Int -> Float .


op Cos-i3 : Float Int Int Float Float -> Float .
op siguiente : Float Float Int -> Float .

eq siguiente(X,Tr,K) = (- Tr) * ((X * X) / (float(2 * K) *


float((2 * K) - 1))) .
eq coseno3(X,N) = Cos-i3(X,N,1,1.,0.) .
ceq Cos-i3(X,N,K,Tr,T) =
Cos-i3(X,N,(K + 1),siguiente(X,Tr,K),(T + Tr)) if(K < N) .
eq Cos-i3(X,N,N,Tr,T) = T + Tr
.....
Donde pueden verse diferencias significativas en el número de reescrituras151, así:

Maude> rewrite in C8-COSENO : coseno3(2.0, 1000) .


rewrites: 13990 in 1628036047000ms cpu (74ms real) (0 rewrites/second)
result FiniteFloat: -4.1614683654714246e-1

9.5.1.2 Tercer cambio al cálculo del coseno: precisión en la condición de salida

150Naturalmente que si el tejido se llevara a cabo de forma automática, el problema de la programación se acercaría
significativamente al de concebir el modelo matemático del problema.
151La persistencia de los valores de tiempo de cpu para las cuatro funciones programadas (...in 1628036047000ms),
parece indicar que éste se calcula de forma incorrecta.
Capítulo 9: Definición Recursiva de Operadores.
254
En las definiciones del operador coseno anteriores usamos un número de iteraciones fijo
suministrado por el usuario como argumento al operador. Sin embargo, un mejor criterio
para detener el proceso de aproximación, sería poder decir si la aproximación es
suficientemente buena, según algún criterio.
Se puede demostrar que los términos de la sucesión tienden a cero cuando n tiende a
infinito. Entonces, lo que haremos será detener el proceso cuando la última pareja (término
positivo + término negativo), sumen un valor cercano a cero. Así, podemos estar seguros
que la aproximación es buena, pues, el resto de término no sumados, no aportan mucho.

Para garantizar la precisión del valor calculado, se debe incluir en la sumatoria un número suficiente de
términos. En esta versión se acumulan términos hasta llegar a que la última pareja, término positivo +
término negativo, adicionen a la sumatoria un valor despreciable, así:

op coseno4 : Float -> Float .


op Cos-i4 : Float Int Float Float Float -> Float .
eq coseno4(X) = Cos-i4(X,2,1.,((- 1.) * (X * X) / 2.),1.) .
ceq Cos-i4(X,K,Tra,Tr,T) =
Cos-i4(X,(K + 1),Tr,siguiente(X,Tr,K),(T + Tr))
if(abs(Tr + Tra) > 10.e-12) .
ceq Cos-i4(X,K,Tra,Tr,T) = T + Tr if(abs(Tr + Tra) <= 10.e-12) ......
Nótese que ahora que el proceso acumula sólo el número de términos necesarios para garantizar la
precisión, haciendo el proceso es significativamente más rápido.

Maude> rewrite in C8-COSENO : coseno4(2.0) .


rewrites: 173 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result FiniteFloat: -4.1614683654714246e-1

El código PROLOG correspondiente al cálculo anterior es el que se muestra a continuación:

siguiente(X,Tr,K,Trs) :- Trs is (-Tr)*((X*X)/((2.0*K)*((2.0*K)-1.0))) .


coseno4(X,R) :- cos_i4(X,2,1.0,((-1.0)*(X*X)/2.0),1.0,R) .
cos_i4(X,K,Tra,Tr,T,R) :- (abs(Tr+Tra)<10.0e-12), R is T+Tr .
cos_i4(X,K,Tra,Tr,T,R) :- K1 is K+1, T1 is T+Tr, siguiente(X,Tr,K,Trs), cos_i4(X,K1,Tr,Trs,T1,R) .
Al ejecutarlo:

1 ?- coseno4(2.0,R).
R = -0.41614683654714246

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

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 el operador Fib(n).
9.6.1 Proceso recursivo.
Como es usual, es posible obtener un operador que determina un proceso recursivo
plasmando en el lenguaje funcional el modelo matemático del problema.

Una primera aproximación al operador Fib(n) se obtiene al escribir el modelo matemático en {20} en un
lenguaje funcional.
En SCHEME tendríamos la siguiente definición para Fib(n):

(define (fib1 N)
(if (= N 0) 0
(if (= N 1) 1
(+ (fib1 (- N 1)) (fib1 (- N 2)))
))
)

La definición correspondiente en MAUDE es la siguiente:

fmod C8-FIBONACCI is

protecting INT .

op fib1 : Int -> Int .


vars N : Int .

eq fib1(0) = 0 .
eq fib1(1) = 1 .
ceq fib1(N) = fib1(N - 1) + fib1(N - 2) if(N > 1) .

endfm

Donde es importante notar el comportamiento del tiempo de ejecución respecto a N:


Capítulo 9: Definición Recursiva de Operadores.
256
Maude> rewrite in C8-FIBONACCI : fib1(10) .
rewrites: 529 in 1628036047000ms cpu (3ms real) (0 rewrites/second)
result NzNat: 55

Maude> rewrite in C8-FIBONACCI : fib1(20) .


rewrites: 65671 in 1628036047000ms cpu (390ms real) (0 rewrites/second)
result NzNat: 6765

Maude> rewrite in C8-FIBONACCI : fib1(30) .


rewrites: 8077609 in 1628036047000ms cpu (48463ms real) (0 rewrites/second)
result NzNat: 832040

La forma del proceso que determina esta definición es sin duda la de un proceso recursivo.

Una evocación del operador definido en el ejemplo anterior procede como se muestra a continuación.

(fib 5)
;;(+ (fib 4) (fib 3) )
;;(+ (+ (fib 3) (fib 2)) (+ (fib 2) (fib 1)) )
;;(+ (+ (+ (fib 2) (fib 1)) (+ (fib 1) (fib 0))) (+ (+ (fib 1) (fib 0)) 1) )
;;(+ (+ (+ (+ (fib 1) (fib 0)) 1) (+ 1 0) (+ (+ 1 0) 1) )
;;(+ (+ (+ (+ 1 0) 1) 1 ) (+ 1 1) )
;;(+ (+ (+ 1 1) 1 ) 2 )
;;(+ (+ 2 1 ) 2 )
;;(+ 3 2 )
;;5

Que sin duda corresponde a un proceso recursivo. Nótese que para calcular (fib 5), es necesario
calcular 3 veces a (fib 0) y a (fib 1):

Para evaluar el número de veces que se lleva a cabo la reescritura bajo la definición
recursiva de fib(n), debemos notar que este depende de n, y que la función número de
reescrituras vs. N, que denominaremos Nr(n), satisface las ecuaciones siguientes:

Nr(1) = 1 si n = 0
Nr(0) = 1 si n = 1
Nr(n) = 1 + Nr(n-1) + Nr(n-2) si n > 1 {21}
Estas ecuaciones pueden resolverse de la forma que se muestra en [Brassard 88], dando
como respuesta una función que tiene el orden de n!.
El efecto de este tipo de orden puede fácilmente intuirse si se miran los valores que toma el
número de repeticiones para n=10,100,1000, etc.
En efecto, este operador es inútil en términos prácticos, por el tiempo de ejecución asociado
a valores moderados de n.
9.6.2 Proceso iterativo.
Como es bien conocido por todo programador, es fácil definir un operador que calcule los
números de Fibonacci por medio de un proceso iterativo. Este operador se apoya
Capítulo 9: Definición Recursiva de Operadores.
257
simplemente en mantener almacenado dos números consecutivos para calcular con ellos el
siguiente.

Una primera versión de un operador fib(n) que determine un proceso iterativo se obtiene manteniendo
en la memoria los dos últimos números calculados, que son utilizados para calcular el siguiente.
En SCHEME tendríamos la siguiente definición para un fib(n) iterativo:

(define (fib2 N) (if (= N 0) 0 (fib2-ite 0 1 N)))


(define (fib2-ite FAA FA N)
(if (= N 1) FA
(fib2-ite FA (+ FA FAA) (- N 1))
)
)
La definición correspondiente en MAUDE es la siguiente:

fmod C8-FIBONACCI is
protecting INT .

op fib2 : Int -> Int .


op fib-it : Int Int Int -> Int .

vars N Fa Faa Int .

eq fib2(0) = 0 .
eq fib2(1) = 1 .
ceq fib2(N) = fib-it(0,1,N) if(N > 1) .
ceq fib-it(Faa,Fa,N) = fib-it(Fa,(Fa + Faa),(N - 1)) if(N > 1) .
eq fib-it(Faa,Fa,1) = Fa .
endfm
Cuyos tiempos de ejecución para los mismos casos del ejemplo anterior son los siguientes:

Maude> rewrite in C8-FIBONACCI : fib2(10) .


rewrites: 40 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result NzNat: 55

Maude> rewrite in C8-FIBONACCI : fib2(20) .


rewrites: 80 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result NzNat: 6765

Maude> rewrite in C8-FIBONACCI : fib2(30) .


rewrites: 120 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result NzNat: 832040

Del ejemplo anterior puede verse que el proceso iterativo generado es significativamente
más eficiente, así:

Una evocación del operador definido en el ejemplo anterior procede como se muestra a continuación.

(fib 5)
;;(fib-iter 1 0 4 )
;;(fib-iter 1 1 3 )
Capítulo 9: Definición Recursiva de Operadores.
258
;;(fib-iter 2 1 2 )
;;(fib-iter 3 2 1 )
;;5

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.

Una versión más avanzada del operador fib(n) es basado en la “Matriz de Fibonacci” que puede
calcularse por medio de un algoritmo del orden O(ln n). El algoritmo se basa en el cálculo de la Matriz
de Fibonacci (http://en.wikipedia.org/wiki/Fibonacci_number )

 fib(n  1)
n
1 1 fib(n) 
1 0   fib(n) fib(n  1
  
El cálculo de An, para A un real o una matriz, puede llevarse a cabo por medio de un algoritmo del O(ln
n) [Abelson 85, sec. 1.2.4].

Para evaluar el número de veces que se lleva a cabo la reescritura bajo la definición
iterativa de fib(n), basta con notar que en cada paso de reescritura de fib-iter, el valor del
argumento K se divide por 2, que K se inicia en N y que la reescritura que produce el
resultado se lleva a cabo cuando K llega a 1. Así el número de reescrituras es el número al
que hay que elevar 2 para que de N, o sea log2(N).
El efecto de este tipo de orden puede fácilmente intuirse si se miran los valores que toma el
log2(n) para n=10,100,1000, etc.
Para este momento debe ser obvio que el precio a pagar por el incremento en la eficiencia
del operador definido, es el de abandonar la simplicidad de la especificación matemática
del problema original, buscando en métodos, posiblemente más complejos, ventajas
computacionales inexistentes en el modelo original.

Utilidad de la recursión
Aunque los ejemplos anteriores muestran una clara superioridad, en lo que a eficiencia se
refiere, de los procesos iterativos sobre los procesos recursivos, no debemos concluir que la
definición de procesos recursivos es inútil.
Capítulo 9: Definición Recursiva de Operadores.
259
En primer lugar, de los ejemplos anteriores, debe ser claro que es significativamente más
fácil concebir una solución recursiva que concebir una iterativa. Prueba de esto es la
naturalidad con que surge el primer procedimiento para calcular los números de Fibonacci,
en contraste con el segundo que genera el proceso iterativo más eficiente, pero un poco más
difícil de idear y entender, y ni que hablar de la dificultad asociada al último algoritmo
presentado.
La recursión es, en efecto, un instrumento útil para entender y diseñar la solución a
problemas específicos. Una vez concebida una solución recursiva, se puede intentar
transformarla a una versión iterativa152.
El ejemplo del cambio de moneda presentado por Abelson en [Abelson 1985 sección 1.2.2],
que resumimos a continuación, es una muestra patente del poder de la recursión como
medio para hallar una solución a un problema.
El problema es el siguiente: ¿De cuantas maneras diferentes se puede cambiar una cantidad
de dinero dada, si contamos con monedas de 20, 50, 100, 200 y 500 pesos?.
Este problema tiene una solución relativamente sencilla, si se plantea de forma recursiva.
Este planteamiento se basa en los hechos siguientes:
 El conjunto £(V,20 a 500) formado por todas las maneras de cambiar V con las
cinco denominaciones, puede ser dividido en dos conjuntos separados: las maneras
que emplean al menos una moneda de 500 y las maneras que no utilizan ninguna
moneda de 500.
 El conjunto de todas las maneras que no usan monedas de 500 no es otra cosa que el
conjunto £(V,20 a 200) formado por todas las maneras de cambiar V con las
monedas de 20 a200.
 El conjunto de todas las maneras de hacer el cambio con al menos una moneda de
500 es equipotente con el conjunto £(V-500,20 a 500) de todas las manera de hacer
el cambio con monedas de 20 a 500, de la cantidad inicial menos 500.
Así, para el número de formas de cambiar el dinero original se puede plantear una ecuación
que la relaciona de forma recursiva, con el número de formas de cambiar menos dinero y de
usar menos denominaciones:
|£(V,20 a 500)| = |£(V,20 a 200)| + |£(V-500,20 a 500)|
Para escribir un procedimiento que efectúe este cálculo, basta con definir un mecanismo
para manejar las denominaciones e incluir los casos particulares. Remitimos al lector
interesado a [Abelson 1985 sección 1.2.2], para una versión en SCHEME de este
procedimiento.

Resumen del Capítulo.


A la definición de operadores que de forma directa o indirecta usan el operador definido en
su definición la denominaremos “definición recursiva de operadores” y a los operadores
así definidos los denominaremos “operadores recursivos”.

152 Que de hecho no siempre es posible.


Capítulo 9: Definición Recursiva de Operadores.
260
Son ejemplos de definición recursiva, los dos siguientes:
 La definición de un operador que sume una serie de términos, por ejemplo el
cuadrado de los números entre dos valores i y j con i<j , que puede definirse
como la adición del primer término a la sumatoria de los siguientes.
 La obtención de un operador que calcule la raíz cuadrada de un número por
el método de Newton-Rapson, que puede definirse usando un operador que
tiene en uno de sus argumentos una aproximación a la raíz y que, de forma
progresiva, mejora el valor de la aproximación en evocaciones posteriores
del mismo operador.
Dos elementos claves de estas definiciones recursivas son las siguientes
 Al evocar de nuevo el operador hace sobre un caso mas pequeño, así: la
sumatoria se reevoca con un término menos; y el cálculo de la raíz se
reevoca con un valor de la aproximación más cercano a la solución
 Se usa una instrucción de selección para terminar la evocación recursiva
cuando se encuentra disponible la respuesta, así: en la sumatoria cuando se
han acumulado todos los términos; y en la raíz cuando se ha llegado a una
aproximación satisfactoria.
Una diferencia importante entre los dos operadores definidos es que, para la sumatoria, el
término resultante de los diferentes pasos de reescritura crece hasta un tamaño máximo, que
depende del valor de los argumentos, para luego decrecer hasta la respuesta; en cambio,
para la raíz, dicho término no crece, sino que se mantiene estable durante todo el proceso de
reescritura, manteniendo el mismo número de operadores y operandos hasta que se obtiene
la respuesta deseada. A los procesos que presentan inestabilidad en la memoria se le ha
denominado procesos recursivos mientras que a los procesos que presenta estabilidad se les
ha denominado procesos iterativos.
Para lograr que el operador que obtiene el valor de la sumatoria determine un proceso
iterativo, basta con usar un argumento del operador (u “acumulador”) para totalizar de
forma progresiva los términos de la serie, a medida que se evoca de nuevo el operador, y
terminar el proceso cuando dicho argumento alcance el total buscado.
Son ejemplos de operadores que pueden plantearse de forma recursiva e iterativa, uno que
suma los términos de una serie que converge a π/8, y el de una fracción continua que
converge a un valor similar. En el caso de la fracción continua, es importante notar que el
operador que determina un proceso iterativo, debe llevar a cabo el cálculo de abajo hacia
arriba iniciando el acumulador con el último divisor de la fracción.
Para desarrollar la capacidad de definir operadores útiles el programador debe tener
conciencia de los peligros potenciales de sus especificaciones. El cálculo del valor del
coseno, con base en su desarrollo en serie de Taylor tiene por ejemplo un peligro potencial,
a saber: de calcularse los términos de la serie según la fórmula matemática, se cae en el
riesgo de calcular valores intermedios, que por ser de gran magnitud, se representan y
almacenan de forma imprecisa. Para evitar este tipo de problemas el programa debe
determinar un proceso en el que los valores intermedios del cálculo sean lo mas pequeño
posible.
Una característica de los operadores recursivos es que el tiempo de ejecución puede
depender del valor de sus argumentos, existiendo el riesgo de que para ciertos valores el
Capítulo 9: Definición Recursiva de Operadores.
261
cálculo sea imposible. El “principio de invarianza” plantea que al ejecutar dos códigos
diferentes (por el lenguaje, el computador, la implementación, las instrucciones mismas),
implementan un mismo “algoritmo” si las funciones que relaciona el tiempo de ejecución
con los valores de los argumentos están mutuamente acotadas por una constante
multiplicativa, significando que tiene la misma “forma”. La forma de esta función (o sea el
algoritmo) es, en consecuencia, el elemento más relevante frente al problema de la
eficiencia.
Para caracterizar la forma de la función de rendimiento se ha propuesto la teoría del “orden
de las funciones”. En breve el orden de una función de rendimiento es el conjunto de
funciones que pueden considerarse de igual o mejor rendimiento. Con ello es posible
plantear una serie ordenada de funciones cuyos órdenes contienen los de las funciones
anteriores en la serie, por ejemplo: O(1)  O(ln(n))  O(n1/2)  O(n)  O(n*ln(n))  O(n2)
 O(n3) … O(nk)…  2n. Para caracterizar la eficiencia de un algoritmo basta, entonces,
con establecer cual es la función más adelante de la serie que contiene el orden de la
función de rendimiento del algoritmo. Los operadores que determinan un número fijo de
operaciones al ejecutarse están en O(1). Para hallar el orden de los demás operadores, basta
con establecer el número de veces que se repite el subgrupo de instrucciones del O(1) que
se repita más veces durante el proceso, como una función de los argumentos. El orden de
esta función será entonces el orden del algoritmo.
El operador correcto inicialmente planteado para el cálculo del coseno, tiene el orden
O(N2), siendo N el número de términos sumados de la serie. Es, sin embargo fácil bajar el
orden a O(N), “tejiendo” el cálculo del término con el cálculo de la serie. Otra mejora es la
de acumular sólo los términos requeridos para garantizar la precisión, terminando la
acumulación al momento en que se comiencen a acumular parejas de términos (uno
positivo y el otro negativo) con una diferencia insignificante.
Un ejemplo clásico del efecto del orden del algoritmo en el tiempo de ejecución, son las
tres formas de calcular el número de la serie de Fibonacci localizado en la posición N de la
serie. En efecto los tres algoritmos tienen respectivamente los órdenes O(2N), O(N), y
O(log N), siendo el primero imposible de usar para valores grandes de N, mientras que el
último puede usarse para prácticamente cualquier valor (limitado, sin embargo, por la
precisión del resultado). A los algoritmos cuyo orden corresponde a una función de la serie
más arriba que la de una potencia constante de N, se les denomina “no polinómicos” (N.P.)
y corresponden a programas prácticamente inútiles.

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

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:
Capítulo 9: Definición Recursiva de Operadores.
262
- 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 )
7 - ¿por que no es necesario que en la solución MAUDE del Ejemplo 31 todas las
ecuaciones sean condicionales?.
8. Elabore la versión en SCHEME de los ejemplos Ejemplo 40, Ejemplo 44, y Ejemplo 45.
9- Elabore la versión que procede de atrás para adelante para el caso del Ejemplo 29 y del
Ejemplo 31.
10- Elabore un código en MAUDE que defina los operadores _! (que calcula el factorial) y
_^_ (que eleva un número a una potencia).
11- Lleve a cabo los ejercicios 1.16 a 1.19 referido en [Abelson 85 sec. 1.2.4]
12- Verifique la validez del operador coseno(n) definido, para una gama de valores de x
que alcance valores de x > 1000.
13- . Verifique la capacidad de su intérprete para almacenar los valores calculados, por
medio de un programa que calcule las diferencias de dos valores consecutivos. Analice en
que momento estas generan un valor obviamente errado.
14- Reelabore la definición del operador coseno del Ejemplo 46, primero cambiando la
ecuación eq coseno1(X, N) = coseno-ite(X,N,1,1) por la ecuación eq coseno(X, N)
= coseno-ite(X,N,1,0) y luego por la ecuación eq coseno(X, N) = coseno-
ite(X,N,2,1).
15- En el operador que calcula los términos de la serie del coseno definido en el Ejemplo
51, se usó la expresión Tr * (-1) * (X * 2 / ((2 * k) * (2 * k – 1))) para obtener cada término
a partir del anterior. En esta expresión se lleva a cabo dos veces la subexpresión (2*k) .
Una manera de evitar esta doble multiplicación es usar como variable iteradora a J=2*K
(que corresponde en cada término al exponente de X), y que debe ahora avanzar de 2 en 2
en cada iteración, para llegar hasta JMX=2*N. Reelabore el operador termino(N) teniendo
en cuenta este detalle.
16- Defina operadores que obtengan Xn/n!, X(2n-1)/(n+1)!, y X2n/(2n-2)!
Capítulo 9: Definición Recursiva de Operadores.
263
17- En la definición del operador coseno(X) del Ejemplo 52:
a- como debe cambiarse la definición en cada uno de los casos siguientes:
En lugar de (0., 1., (- X * X / 2.), 2, X) se tiene (1.., 1., (- X * X / 2.), 2, X).
En lugar de Ta * (X^2 / ((2 * K) * (2 * K – 1)) se tiene Ta * (-1) * (X^2 / ((2 * K) * (2 * K – 1))
b- ¿puede cambiarse?:
siguiente(Ta, K), siguiente(siguiente(Ta, K), (K + 1)) por siguiente(Ta, K), siguiente(Ta, (K + 2)).

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 53, para valores de n=6,7,8,9. Grafique la función
número de veces vs. n.
19- Realizar los siguientes ejercicios de la sección 1.1. de [Abelson 85], que está disponible
en línea en la WWW (ver referencia)
1.6, 1.8
20- Escriba un procedimiento SCHEME para llevar a cabo el cálculo siguiente:
- La suma de los n primeros números naturales pares.
- El valor de x n , dados x y n
n
1
- La sumatoria de los inversos de los n primeros números naturales: i
i 1

n
- El factorial de un número entero positivo: n!  i
i 1

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:
Capítulo 9: Definición Recursiva de Operadores.
264
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

k 2 k  1
3

25. Suponga que cuenta con un procedimiento (mcd a b), que toma dos enteros positivos
a y b y devuelve el máximo común divisor entre ellos. Escriba un procedimiento (primo?
n) que verifique si un entero positivo n es primo o no. Compárelo con el primer
procedimiento para este mismo fin desarrollado en la sección 1.2.6 de [Abelson 85].
26. Elabore procedimientos SCHEME para obtener el valor de la expresión siguiente dados
x y n. Escriba tanto un procedimiento que determine un proceso recursivo como un
proceso iterativo.

0
n+xn e
....................
.....................
(k+1)+...
k+xk e
.....e
...
7+ ....
6+x6 e
5
5+x e
4+x4 e
3+x3 e
2
2+x e
1+x1 e
27. Para cada uno de los cuatro grupos de ecuaciones que siguen, vistas como la definición
MAUDE del operador del punto anterior, seleccione las opciones a y b que les
corresponden en las listas de términos que siguen a las ecuaciones.
eq exp(X, N) = exp-ite(0, X, N) .
ceq exp-ite(T, X, K) = exp-ite(_________a_________, X, (K - 1)) if(K>0) .
eq exp-ite(T, X, 0) = _____b_______ .

eq exp(X, N) = exp-ite(1, X, N) .
ceq exp-ite(T, X, K) = exp-ite(_________a_________, X, (K - 1)) if(K>0) .
eq exp-ite(T, X, 0) = _____b_______ .

eq exp(X, N) = exp-ite(0, X, N) .
ceq exp-ite(T, X, K) = exp-ite(________a__________, X, (K - 1)) if(K>1) .
eq exp-ite(T, X,1) = _____b_______ .
Capítulo 9: Definición Recursiva de Operadores.
265
eq exp(X, N) = exp-ite(1, X, N) .
ceq exp-ite(T, X, K) = exp-ite(________a__________, X, (K - 1)) if(K>1) .
eq exp-ite(T, X,1) = _____b_______ .

a: ( N + X ^ N * T)
a: ( N + X ^ N * e ^ T)
a: e ^ ( N + X ^ N * T)
a: X ^ N * e ^ (N + T)
a: e ^ ( N + X ^ N * e ^ T)

b: T
b: ln(T)
b: e^T
b: 1+X*e^T
b: 1+X*T
Capítulo 10
Valores y Tipos Compuestos
Capítulo 10: Valores y Tipos Compuestos.
268
Introducción
Todo lenguaje de programación ofrece tipos nativos de datos que el programador utiliza
para definir datos concretos asociados a los elementos de sus problemas. Para estos tipos,
el lenguaje ofrece una serie de operaciones que el programador usa para manipular y
transformar los datos. Al utilizar estos datos y sus operaciones asociadas, el programador
no necesita conocer ni la manera como los datos se representan en el computador, ni la
manera en que las operaciones se llevan a cabo. En este sentido diremos que el
programador se “abstrae” de conocer los detalles asociados a la implementación de los
tipos de datos que utiliza [Arango 97].
Por otro lado, en los capítulos anteriores, solo hemos tenido contacto con datos numéricos
escalares y en particular, con números enteros y números con decimales. Una característica
de estos tipos de dato es que son vistos como una unidad por las operaciones que los
manipulan sin permitir el acceso directo a sus partes componentes (si es que las tienen).
Estos tipos de dato son, sin embargo, insuficientes para representar la información asociada
a muchos problemas de interés. Considere, por ejemplo, la representación de un punto en
un espacio cartesiano, la representación de un número complejo, o la representación de la
cola de compradores en un supermercado.
Para representar estos elementos de información, es necesario contar con datos compuestos
que le ofrezcan al programador, tanto mecanismos que le permitan acceder y manipular las
partes que lo componen (que denominaremos en lo que sigue como “vías de acceso”),
como operaciones que le permitan operar con el compuesto como un todo.
Es conveniente, además, que estos datos compuestos le sean ofrecidos al programador
como “tipos abstractos” de datos en el sentido citado arriba153, permitiéndole su utilización
sin tener que preocuparse por la manera como se almacenan en memoria, la forma de unir
los componentes, o por la manera como se llevan a cabo las operaciones.
En este capitulo se analizan las maneras de definir y representar datos compuestos en los
lenguajes funcionales analizados, junto con la manera para definir las operaciones que se
les aplican.
Enfatizaremos el grado en que cada lenguaje permite considerar estos datos como
verdaderos tipos de datos propuestos por el programador, permitiendo su uso posterior en
condiciones similares a las de los tipos nativos.

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

153En este trabajo usaremos la denominación “tipo abstracto” en el sentido citado, sin desmedro de asociarlo luego a una
teoría en una lógica ecuacional que define las propiedades de operaciones definidas sin asociarlas a un tipo específico de
datos (y que son, por tanto, aplicables a cualquier álgebra que sea modelo de la teoría).
Capítulo 10: Valores y Tipos Compuestos.
269
año, mes y día; un estudiante es descrito por su nombre, un numero de identificación, una
dirección, un teléfono, una fecha de nacimiento, etc...
Para tratar con estas entidades, es de gran utilidad poder considerar el conjunto de valores
que las describen como unidades de valor o datos compuestos. Al considerarlas de esta
manera, podemos definir variables que hagan referencia a un conjunto de dichos valores,
usarlas como argumentos al evocar operadores, y sustituir variables con entidades
particulares para el emparejamiento de axiomas.
La mayoría de los lenguajes de programación ofrecen construcciones para manipular este
tipo de compuesto, denominándolos “registros”154 (“records”), o “estructuras”
(“structures”). En lo que sigue nos referiremos a ellos como “estructuras” o “datos
estructurados”.
Caracterizaremos una estructura por las siguientes propiedades:
 Ser un compuesto que tiene un número determinado de componentes155.
 Los componentes pueden ser de diferentes tipos (incluyendo tipos
compuestos).
 Cada componente se asocia al todo en un “lugar” determinado.
 Las vías de acceso a las componentes se apoyan en el “lugar” de la
componente dentro de la estructura, que es identificado por medio de un
número o un rótulo.
Para facilitar los cálculos con datos estructurados el lenguaje debe ofrecer, además,
mecanismos para darle la categoría de tipo de dato al conjunto de valores estructurados que
tengan la misma estructura de composición. A estos tipos los denominaremos en lo qiue
sigue como “tipos estructurados”. Para definir tipos estructurados el lenguaje debe proveer
lo siguiente:
 Un mecanismo para “construir” valores estructurados a partir de los valores
de sus componentes.
 Un mecanismo para imponer condiciones adicionales de pertenencia al tipo
a las tuplas que estructuren un número igual de componentes con los
mismos tipos.
 Un mecanismo para obtener o “seleccionar” los valores de las componentes
a partir de un valor estructurado.
 La posibilidad de definir operadores que actúen sobre datos del tipo
estructurado.
 Un modo para representar de forma explícita datos estructurados
particulares, o valores base, dentro del tipo.
En esta sección presentaremos las facilidades ofrecidas por cada uno de los lenguajes
analizados para crear y manipular tipos estructurados. Para ello nos apoyaremos en un
ejemplo específico. Para este ejemplo presentaremos, en cada lenguaje, las construcciones
que definen los elementos referidos en el parágrafo anterior, señalando las ventajas y
desventajas relativas de las mismas.

154 Por referencia al hecho de que tradicionalmente constituyen la unidad básica de lectura/escritura de datos en archivos.
155 Aunque no necesariamente fijo.
Capítulo 10: Valores y Tipos Compuestos.
270

Supongamos que nos es necesario lleva a cabo numerosos cálculos que involucran magnitudes
vectoriales en dos dimensiones. Si contáramos con un tipo abstracto correspondiente a los vectores, la
especificación de estos cálculos se simplificaría enormemente.
El tipo abstracto asociado a los vectores debe proveernos de los tres elementos siguientes:
Un nuevo tipo de datos, que denominaremos “Vector”.
Una manera de construir una instancia de vector a partir de dos números reales.
Una manera de acceder a los reales que forman un vector.
Una manera para representar vectores, por ejemplo los vectores V1 = 2i+5j y V2 = 3i+4j,
Un conjunto de operadores aplicable a los vectores que representemos, por ejemplo los operadores suma
de vectores (+), producto escalar (.) y el operador de igualdad (=) aplicado a vectores.

10.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 de
“declaración de tipo”.
10.2.1.1 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.
Capítulo 10: Valores y Tipos Compuestos.
271
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:
 <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.

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

(define Vector (make-record-type "Vector" (list '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?.

10.2.1.2 Declaración de un tipo compuesto estructurado en MAUDE


La declaración de 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 8.5.2.1.

La declaración de nuestro tipo Vector en MAUDE se lleva a cabo declarando a un identificador como el
medio para hacer referencia al tipo, así:

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

Para definir un operador constructor de valores de tipo Vector se usa el operador nativo record-
constructor que toma como argumento el valor de tipo record-type creado al declarar el tipo Vector,
así:
(define make-Vector (record-constructor Vector))
Donde asociamos el símbolo make_Vector al operador resultante de evocar record-constructor, para
poder usar dicho operador al crear instancias de Vector, así:

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

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

10.2.2.2 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 defoodos como “constructor”. 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.
Capítulo 10: Valores y Tipos Compuestos.
273
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].

La declaración del constructor para el sort Vector en MAUDE, puede tomar ventaja de la notación infija
para que los términos base mantengan la forma tradicional, así:

fmod C9-VECTOR is
protecting FLOAT .
sort Vector .
op _i+_j : Float Float -> Vector [ctor] .
endfm
Permitiendo que los vectores V1 y V2 se representen de forma tradicional por medio de los términos
siguientes:
2. i+ 5. j y 3. i+ 4. j

Maude> rewrite in C9-VECTOR : 2.0 i+ 5.0 j .


rewrites: 0 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result Vector: 2.0 i+ 5.0 j

10.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. Para ello los
lengujes deben ofrecer la manera de definir operadores de tipo “selector”.
10.2.3.1 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> .
Capítulo 10: Valores y Tipos Compuestos.
274

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.

10.2.3.2 Selección de componentes en MAUDE


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

Los operadores selectores para las componentes del vector se pueden declarar y definir fácilmente en
MAUDE con los mismos elementos que se usan para definir y declarar cualquier otro operador:

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.
10.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.
10.2.4.1 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” u operadores de un tipo abstracto de datos, son necesarios y
suficientes los operadores constructores y selectores del tipo que está siendo definido.
Capítulo 10: Valores y Tipos Compuestos.
275
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-vector V1 V2)


(make-Vector (+ (get-Vi V1) (get-Vi V2)) (+ (get-Vj V1) (get-Vj V2))))

(define (producto-escalar-vector V1 V2)


(+ (* (get-Vi V1) (get-Vi V2)) (* (get-Vj V1) (get-Vj V2))))

(define (igual-vector V1 V2)(and(= (get-Vi V1)


(get-Vi V2)) (= (get-Vj V1) (get-Vj V2))))

Dados estos operadores se pueden lleva a cabo operaciones sobre valores previamente
creados.

Dado el constructor y los operadores definidos se puede operar sobre valores del tipo sin especificar
cada vez como llevar a cabo las operaciones, así:

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


...
1 ]=> (define V2 (make-Vector 3 4 ))
...
1 ]=> (get-Vi (suma-vector V1 V2))
;Value: 5
...

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.

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-vector 6 9)
;The object 1 is not a record of type vector
……..

10.2.4.2 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.
Capítulo 10: Valores y Tipos Compuestos.
276
Para definir operadores sobre instancias de un tipo propuesto por el programador es necesario y
suficiente usar los constructores. En particular, no es necesario usar los selectores, dado que el
emparejamiento es suficiente para asociar los valores de las componentes de los valores agregados, con
las variables que aparecen al evocar los constructores en el lado izquierdo de las ecuaciones.

fmod C9-VECTOR is
protecting FLOAT .
sort Vector .
op _i+_j : Float Float -> Vector [ctor] .
op _+_ : Vector Vector -> Vector .
op _*_ : Vector Vector -> Float .
vars Xi Xj Yi Yj : Float .

eq (Xi i+ Xj j) + (Yi i+ Yj j) = (Xi + Yi) i+ (Xj + Yj) j .


eq (Xi i+ Xj j) * (Yi i+ Yj j) = (Xi * Yi) + (Xj * Yj) .

endfm
Los operadores pueden ser usados para llevar a cabo cálculos, así:

Maude> rewrite in C9-VECTOR : 3.0 i+ 2.0 j + 2.0 i+ 1.0 j .


rewrites: 3 in 7635026285ms cpu (0ms real) (0 rewrites/second)
result Vector: 5.0 i+ 3.0 j

10.2.4.2.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 se refiere a que la correcta definición de los operadores, debe implicar que
el resultado de los cálculos efectuados con los operadores cuyo rango sea un tipo definido,
debe ser un término base de un operador constructor de dicho tipo. 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.
10.2.5 Invariantes de Tipo
Es frecuente que al definir un tipo compuesto estructurado, se desee restringir los valores
de las componentes que conforman los valores del tipo, a unos que satisfagan condiciones
específicas.

Considere el tipo compuesto estructurado “Fecha”, usado para representar las fechas del calendario
Juliano.
Las fechas pueden representarse con tres valores, uno para el año, uno para el mes y uno para el día.
Así, si se opta por representar las fechas usando números enteros (v.g. año 2013, mes 5, día 31), es
posible definir el tipo de las fechas usando una estructura que contenga tres valores enteros como
componentes.
En SCHEME:
Capítulo 10: Valores y Tipos Compuestos.
277
;;; tipo compuesto estructurado "Fecha"
;;; ------------------------------------------

(define Fecha (make-record-type "Fecha" (list 'Dia 'Mes 'Anio )))


(define Fecha?(record-predicate Fecha))
(define make-Fecha (record-constructor Fecha))
(define get-Dia (record-accessor Fecha 'Dia))
(define get-Mes (record-accessor Fecha 'Mes))
(define get-Anio (record-accessor Fecha 'Anio))
En MAUDE

fmod C9-FECHA is

protecting INT .
protecting STRING .

sort Fecha .

op _de_de_ : Int Int Int -> Fecha [ctor] .


op _de_del_ : Int String Int -> Fecha [ctor] .

endfm
El problema de definir las fechas de esta manera, es que cualquier terceta de enteros puede ser
considerada como una fecha. Así:

1 ]=>(Fecha? (make-Fecha 100 100 100))


;Value: #t

Maude> rewrite in C9-FECHA : 100 de 100 de 100 .


rewrites: 0 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result Fecha: 100 de 100 de 100

Se hace entonces necesario contar con una manera para señalar como erradas, ciertas
instancias de los tipos compuestos estructurados, que hayan sido definidos usando los
mecanismos presentados en los numerales anteriores.
Para ello debemos asegurarnos que las componentes de los valores de un tipo compuesto
estructurado, cumplan con una serie de condiciones. A estas condiciones nos referiremos en
lo que sigue como las “invariantes del tipo”.
10.2.5.1 Invariantes de tipo en SCHEME
Por ser el SCHEME un lenguaje débilmente tipado, es necesario evocar explícitamente el
predicado del tipo (ver 8.5.1.1), para verificar si un valor pertenece o no al tipo esperado.
En consecuencia, para verificar las invariantes del tipo en un tipo definido, es necesario
incorporarlas como parte de la definición del predicado del tipo.

A continuación se muestra la definición del predicado de tipo para el tipo Fecha en SCHEME:
Capítulo 10: Valores y Tipos Compuestos.
278
(define (Fecha? X)
(boolean/and
((record-predicate Fecha) X)
(>= (get-Anio X) 0)
(and (> (get-Mes X) 0) (<= (get-Mes X) 12))
(and (> (get-Dia X) 0) (<= (get-Dia X) 31))
(and (or (<= (get-Dia X) 28)
(and (<= (get-Dia X) 29) (or (not (= (get-Mes X) 2))
(bisiesto? (get-Anio X))))
(and (<= (get-Dia X) 30) (not (= (get-Mes X) 2)))
(and (<= (get-Dia X) 31) (not (or (= (get-Mes X) 2)
(= (get-Mes X) 4)
(= (get-Mes X) 6)
(= (get-Mes X) 9)
(= (get-Mes X) 11)
)
)
)
)
)
)
)

Que permite discriminar entre las fechas correctas y las incorrectas, Así:

1 ]=>(Fecha? (make-Fecha 100 100 100))


;Value: ()

10.2.5.2 Invariantes de Tipo en MAUDE.


Por ser el MAUDE un lenguaje fuertemente tipado el intérprete se encarga de clasificar
todos los términos base asociándolos con un tipo. Con ello el intérprete es capaz de
detectar tanto el uso incorrecto de los operadores, cuando se les evoca con argumentos del
tipo equivocado; como el operador que debe usarse, cuando se evoca un operador
sobrecargado para varios tipos.
Bajo este enfoque, es posible plantear que si un valor de un tipo compuesto estructurado no
cumple con las invariantes de tipo, entonces no debería pertenecer al tipo; y por tanto, el
intérprete debería ser quién evite que participe de forma errada en las operaciones definidas
sobre dicho tipo.
A este efecto, el lenguaje MAUDE ofrece un mecanismo para excluir de un sort aquellos
valores definidos con los constructores que no cumplen las invariantes del sort. Este
mecanismo se apoya en dos elementos, a saber: la relación de subsort (ver 8.5.2.1), y las
ecuaciones de “membresía” y de “membresía condicional”. Estos dos elementos,
permiten declarar que algunas instancias del constructor de un sort (y como tal miembros
del sort), son también miembros de un subsort del, sort cuando cumplen con ciertas
condiciones específicas.
Las ecuaciones de membresía y de membresía condicional tienen la forma siguiente:
mb <termino> : <sort> .
cmb <termino> : <sort> if <temino_boleano> .
Capítulo 10: Valores y Tipos Compuestos.
279
Donde:
 <termino> Es el término cuyo sort esta siendo redefinido.
 <sort> Es un subsort del sort asociado con <termino>.
 <temino_boleano> Es un término que al ser evaluado determina si
<termino> pertenece o no al sort <sort>.
Dadas estas ecuaciones un término cualquiera que empareje con <termino> es
reclasificado, de forma dinámica, en un subsort de su sort original.

El siguiente ejemplo muestra el uso de la ecuación de membresía condicional para distinguir entre las
instancias correctas y las incorrectas del constructor para las fechas presentado en el ejemplo anterior.

fmod C9-FECHA is

protecting INT .
protecting STRING .
protecting BOOL .

sort Fecha Fecha? .


subsort Fecha < Fecha? .

op _de_de_ : Int Int Int -> Fecha? [ctor] .


op bisiesto? : Int -> Bool .

vars M D A : Int .

eq bisiesto?(M) = (M == 0) or ((M rem 1000) == 0) or ((M rem 4) == 0) .


cmb (D de M de A) : Fecha
if(
(A >= 0) and
((M > 0) and (M < 13)) and
((D > 0) and (D < 32)) and
((D < 29) or
((D == 29) and ((M =/= 2) or bisiesto?(A) )) or
((D == 30) and (M =/= 2)) or
((D == 31) and (not ((M == 2) or (M == 4) or (M == 6) or
(M == 9) or (M == 11))) )
)
) .

….
….

endfm

Nótese que el sort Fecha? (fechas dudosas), sirve como paso intermedio para construir el sort Fecha
(fechas correctas). El operador _de_de_, sirve para definir el constructor de las fechas correctas e
incorrectas, y la ecuación de membresía condicional permite distinguir entre las dos.

Maude> rewrite in C9-INVARIANTE_TIPO : 10 de 5 de 2012 .


rewrites: 2 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result Fecha: 10 de 5 de 2012

Maude> rewrite in C9-INVARIANTE_TIPO : 31 de 5 de 2012 .


rewrites: 1 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result Fecha?: 31 de 5 de 2012
Capítulo 10: Valores y Tipos Compuestos.
280

De esta manera, es responsabilidad del intérprete, garantizar que sólo los valores que
cumplen con las invariantes del tipo, se usen como argumento en las operaciones relativas
al mismo.

Para garantizar que sólo se usen fechas correctas con los operadores relativos a las fechas, es suficiente
con garantizar que tanto el perfil de dichos operadores como las variables usadas en su definición, sólo
haga referencia al tipo de las fechas correctas.

fmod C9-FECHA is

…..
…..
var F : Fecha .

op yr : Fecha? -> Int .


op mt : Fecha? -> Int .
op dy : Fecha? -> Int .
op zeller-gr : Int Int Int -> Int .
op zeller-jl : Int Int Int -> Int .

op dia-nombre : Fecha -> String .


op dia-s : Int -> String .

eq dia-nombre(F) = dia-s(zeller-gr(dy(F),mt(F),yr(F))) .

ceq yr(D de M de A) = A if(M > 2) .


ceq yr(D de M de A) = A - 1 if(M <= 2) .
ceq mt(D de M de A) = M if(M > 2) .
ceq mt(D de M de A) = M + 12 if(M <= 2) .
eq dy(D de M de A) = D .

eq zeller-gr(D,M,A) = ( D + (((M + 1) * 26) quo 10) + A + (A quo 4) +


6 * (A quo 100) + (A quo 400)
) rem 7 .
eq zeller-jl(D,M,A) = ( D + (((M + 1) * 26) quo 10) + A + (A quo 4) + 5)
rem 7 .

eq dia-s ( 1 ) = "DOMINGO" . eq dia-s ( 2 ) = "LUNES" .


eq dia-s ( 3 ) = "MARTES" . eq dia-s ( 4 ) = "MIERCOLES" .
eq dia-s ( 5 ) = "JUEVES" . eq dia-s ( 6 ) = "VIERNES" .
eq dia-s ( 0 ) = "SABADO" .

endfm

Bajo esta circunstancia el cálculo del día de la semana sólo se puede llevar a cabo con fechas correctas,
así:

Maude> rewrite in C9-FECHA : dia-nombre(29 de 2 de 2012) .


rewrites: 97 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result String: "MIERCOLES"

Maude> rewrite in C9-FECHA : dia-nombre(30 de 2 de 2012) .


rewrites: 71 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result [String]: dia-nombre(30 de 2 de 2012)
Capítulo 10: Valores y Tipos Compuestos.
281
NOTA: Vale la pena anotar que bajo la implementación utilizada para verificar los ejemplos156, el
cálculo se lleva a cabo aún con fechas equivocadas si la ecuación que lo define así lo indica. En efecto,
si se define el operador dia-nombre(..) con base en el constructor de fechas (que construye fechas
correctas e incorrectas), en lugar de definirse con base en una variable tipo Fecha (correcta), así :

eq dia-nombre(D de M de A) =
dia-s(zeller-gr(dy(D de M de A),mt(D de M de A),yr(D de M de A))) .

El intérprete prosigue con el cálculo sin verificar el tipo del argumento.

Maude> rewrite in C9-FECHA : dia-nombre(30 de 2 de 2012) .


rewrites: 96 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result String: "JUEVES"

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 (v.g. “arreglos”, “vectores” o “archivos” ), hasta construcciones
para que programador defina estructuras de diversos grados de complejidad en memoria y
en disco (v.g. 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).

156 Implementación para windows sobre ECLIPSE® del proyecto MOMENT, versión: MaudeFW_2.6.exe,
Capítulo 10: Valores y Tipos Compuestos.
282
 Se puede acceder a los componentes individuales por medio de un
mecanismo que denominaremos “vía de acceso”.
 Entre las componentes de un compuesto y, posiblemente, entre las
componentes de varios compuestos pueden ocurrir conexiones o
“relaciones”.
Para facilitar los cálculos con datos iterados el lenguaje debe ofrecer, también, mecanismos
para darle la categoría de tipo de dato a todos los conjuntos de valores iterados que tengan
la misma estructura de composición. Para ello debe proveer lo siguiente:
 Un mecanismo para “construir” de forma progresiva valores iterados a partir
de los valores de sus componentes.
 Un mecanismo para obtener o “seleccionar” los valores de las componentes
a partir de un valor iterado.
 La posibilidad de definir operadores que actúen sobre datos del tipo iterado.
En esta sección presentaremos las facilidades ofrecidas por cada uno de los lenguajes
analizados, para crear y manipular datos compuestos iterados. Para ello nos apoyaremos en
iterados con relaciones específicas entres sus componentes. Para estos tipos de iterados
presentaremos, en cada lenguaje, las construcciones que permiten crearlos y manipularlos,
haciendo énfasis en la manera de definir los elementos referidos en el párrafo anterior.
10.3.1 Conexión entre componentes: Pares.
Para componer datos se debe contar con mecanismos de agregación y desagregación. El
mecanismo de agregación más simple es el que junta o “pega” dos valores para formar una
pareja. El mecanismo de desagregación mas simple es el que toma una pareja y obtiene o
“selecciona” sus componentes.
La importancia del mecanismo de manipulación de parejas, es que a partir de él, algunos
lenguajes implementan los mecanismos para manipular estructuras más complejas, y en
particular las iteradas.
En esta sección se presenta, entonces, la manera de agregar y desagregar parejas en los
lenguajes analizados.
10.3.1.1 Parejas en SCHEME.
En concordancia con la estrategia general del lenguaje, el SCHEME ofrece los operadores
nativos especializados a la gestión de parejas que se describen a continuación
Un operador encargado de formar una pareja.
(cons <objeto_1> <objeto_2> )
Donde:
 <objeto_1> y <objeto_2> son dos valores de tipos arbitrarios, que pueden
incluir valores compuestos, que constituyen las componentes de la pareja.
Dos operadores encargados de obtener de una pareja su primer o segundo componente,
respectivamente.
(car <pareja> )
(cdr <pareja> )
Capítulo 10: Valores y Tipos Compuestos.
283
Donde:
 <pareja> es el valor compuesto de tipo pareja del que se desea obtener las
componentes.
Dos operadores encargados de reasignar el primero o el segundo componente de una pareja
ya creada.
(set_car! <pareja> <objeto>)
(set_cdr! <pareja> <objeto>)
Donde:
 <pareja> es el valor compuesto de tipo pareja al que se le desea cambiar un
componente.
 <objeto> es el nuevo valor que tomará el componente a ser modificado.
Un operador que permite establecer si un valor es o no es una pareja.
(pair? <objeto> )
Donde:
 <objeto> es el valor que se desea verificar como pareja.

Dado el constructor y los operadores definidos se puede operar sobre valores del tipo sin especificar
cada vez como llevar a cabo las operaciones, así:

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
Capítulo 10: Valores y Tipos Compuestos.
284
verdadera posición de cada objeto. Además hace evidente la imposibilidad para acceder a
ciertos elementos del compuesto directamente. En la figura 4.1, podemos observar, por
ejemplo, dos maneras de combinar los números 1, 2, 3, y 4 utilizando parejas.

1 2 3 4 1 2 3 4

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

10.3.1.2 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 membresí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.

Para definir mecanismos que permitan la gestión de parejas, basta con usar operadores constructores y
selectores de igual forma que para el caso de los compuestos estructurados (solo que ahora tienen dos
componentes).
El siguiente código podría ser usado para manipular parejas de enteros:

fmod C9-PAR is
protecting INT .
sort Par-Int .
op _|_ : Int Int -> Par-Int [ctor] .
op =>_ : Par-Int -> Par-Int .

vars I J : Int .

ceq =>(I | J) = (J | I) if(I > J) .


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

endfm

Nótese que no es necesario definir operadores de selección, ya que para obtener las partes de una pareja
tomaremos ventaja del emparejamiento usando más de un operador al lado derecho de la ecuación.
El operador definido toma como argumento a una pareja y de como resultado a otra pareja que tenga los
componentes en orden ascendente.

Maude> rewrite in C9-PAR : => (5 | 3) .


Capítulo 10: Valores y Tipos Compuestos.
285
rewrites: 2 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result Par-Int: (3 | 5).Par-Int

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
la instrucción de asignación ya que no existen variables distintas a las cuantificadas en los
axiomas.
10.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 diferentes157.
 La conexión es dirigida, en el sentido de que asigna un rol diferente a cada
uno de los dos elementos que conecta (v.g. anterior y siguiente, padre e hijo,
etc...).
 Los elementos conectados pertenecen al mismo compuesto iterado.
Con lo anterior 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.

157Justificado por el hecho de que una conexión entre más de dos componentes pueden representarse por varias
conexiones entre dichos componentes.
Capítulo 10: Valores y Tipos Compuestos.
286
La importancia de estos tipos de compuestos iterados, es que, por un lado, permiten
modelar diversos tipos de elementos en el dominio del problema, y por otro, se pueden
manipular con códigos estandarizados que se abstraen del significado específico de la
relación.
Así, en lo que sigue, nos limitaremos a ejemplificar la manera de gestionar compuestos tipo
LISTA y tipo ARBOL en cada uno de los lenguajes seleccionados. Con ello sentaremos las
bases suficientes para la gestión de los demás tipos, y mostraremos las capacidades
relativas de los diferentes lenguajes frente a este tipo de compuesto.

Gestión de Listas.
Para la gestión de listas, como es usual para un compuesto, debemos contar con los
mecanismos necesarios para construirlas y seleccionar sus componentes.
Estos mecanismos, sin embargo, no son tan simples como los que se presentaron más
arriba. En particular:
 La selección de un componente no puede basarse ahora, en un nombre
asociado al componente, sino que debe fundamentarse ya sea en su posición
en la lista, o en una característica asociada al valor del componente mismo.
 La selección de un sólo componente no es, tampoco, suficiente. Es
necesario contar con mecanismos que permitan obtener sublistas que
satisfagan condiciones específicas.
 La construcción de la lista debe poder llevarse a cabo de forma paulatina,
con base en operadores que introduzcan, supriman y modifiquen
componentes.
 Debe ser posible reposicionar los elementos en la lista de modo que esta
satisfaga condiciones específicas (v.g. que los elementos estén en orden).
 Debe ser posible, tanto combinar los elementos de varias listas, como
separar los elementos de una lista para obtener nuevas listas con propiedades
determinadas.
En las secciones que siguen, presentaremos los mecanismos que ofrece cada lenguaje para
construir listas y definir operadores que lleven a cabo las tareas de los tipos referidos.
10.4.1 Declaración de Listas.
Para darle categoría de tipo a una lista el lenguaje debe poder ofrecernos construcciones
que lo permitan.
10.4.1.1 Tipo nativo Lista en SCHEME.
El MIT_SCHEME le da soporte a las listas como un tipo nativo por lo que no es necesario
usar construcción alguna para declararlo. Además, por ser el SCHEME un lenguaje
débilmente tipado, no es necesario distinguir las listas según el tipo de componente. En
efecto las listas son listas del tipo genérico object.
Para establecer si un valor es o no es una lista el SCHEME ofrece el operador siguiente.
(list? <objeto> )
Donde:
Capítulo 10: Valores y Tipos Compuestos.
287
 <objeto> es el valor que se desea verificar como lista.
10.4.1.2 Declaración de un tipo Lista en MAUDE
El lenguaje MAUDE ofrece en el preludio (ver [Clavel 2007, sec. 2.2]), la especificación
de un tipo genérico denominado List{X}. Este tipo es paramétrico en el tipo del
componente, pudiendo instanciarse a una lista de valores de cualquier tipo. La
parametrización de tipos será, sin embargo, un motivo de estudio en el Capítulo 10, por lo
que prescindiremos de este aspecto en el presente.
La especificación de la lista nativa es, en todo caso, llevada a cabo en MAUDE como la
especificación de un tipo cualquiera propuesto por el programador. Así, en lo que sigue
usaremos una especificación similar para ilustrar las capacidades del lenguaje en la
definición de este tipo de compuesto. Nuestra especificación difiere de la de la lista nativa,
en que considera, sólo listas con un tipo definido de componente (de tipo Int).
La declaración de un nuevo tipo en MAUDE se lleva a cabo declarando un identificador
como el nombre a ser usado para referirse a un sort propuesto por el programador. Para
ello se usa la construcción descrita en la sección 8.5.2.1.

La declaración de un tipo asociado con todas las listas de enteros, que llamaremos ListInt en MAUDE
se lleva a cabo declarando a un identificador como el medio para hacer referencia al tipo, así:

sort ListInt .

10.4.2 Constructores de la lista.


Para tener listas específicas, es necesario construirlas partiendo de sus componentes reales.
10.4.2.1 Constructores de listas en SCHEME.
Una lista en SCHEME es un par cuyo primer componente es el primer elemento de la lista
y su 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 siguiente158:

’()

La construcción para una lista que contiene 1, 2, 3 y 4 sería entonces como sigue:

158Tala como lo refiere [Hanson 2002, sec. 10.1], en la versión de SCHEME usada en este trabajo se distingue la lista
vacía del símbolo nil, usado en otras versiones de SCHEME , tal como la referida en [Abelson 85 , sección 2.2.1]
Capítulo 10: Valores y Tipos Compuestos.
288
(cons 1
(cons 2
(cons 3
(cons 4 ’()))))
Que visualizada por medio de cajas luciría de la manera siguiente:

1 2 3 4

SCHEME provee adicionalmente algunos operadores nativos para construir listas.


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> <objeto> )
Donde:
 <objeto> Es el componente que se repite.
 <numero> Es el número de veces que se repite el componente <objeto>.

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.
Capítulo 10: Valores y Tipos Compuestos.
289

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

Es importante entender, también, que sólo el uso adecuado del cons da lugar a listas.

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

(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 ’()))
Donde debe notarse que se usó el valor ’(), para señalar el final de la cadena.

10.4.2.2 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 10.2.2.2). El constructor
Capítulo 10: Valores y Tipos Compuestos.
290
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 76). El efecto de este encapsulamiento será evidente a medida que se definan
operadores sobre la lista.
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 elementos siguientes
(ver [Clavel 2007, SEC 7.12.1]):
 Un constructor que pega dos listas en lugar de un elemento y una lista.
 Declarar usando la relación de subsort (sección 8.5.2.1), que el conjunto de
los elementos base es un subconjunto de la lista con el objeto de incluir
como listas las listas de un sólo elemento.
 Una constante para la lista vacía.
 Los atributos ecuacionales assoc y id dentro de los <atributos_del _
operador> (ver 7.5.2.3) en el constructor de la lista.

La declaración del constructor para el sort ListInt en MAUDE, usará el espacio vacío como símbolo de
operación. Se definirá el operador nilLint para referirse a la lista vacía, así:

fmod LISTA-INT is
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


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

endfm
De esta manera una serie de enteros “pegados” con el espacio en blanco, constituyen un elemento de la
lista de enteros:

Maude> rewrite in LISTA-INT : 2 4 5 .


rewrites: 0 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result ListInt: 2 4 5

El efecto del atributo ecuacional assoc, es que dos listas con los mismos componentes son
consideradas iguales sin importar cuales son sus sublistas159.

159 Los atributos ecuacionales assoc e idem no pueden ser usados juntos.
Capítulo 10: Valores y Tipos Compuestos.
291
Dadas las dos sublistas siguientes:

(3 5 7)
(2 1)
Y las sublistas siguientes:

(3 5)
(7 2 1)
La dos listas resultantes de pegar cada pareja:

(3 5 7) (2 1)

(3 5) (7 2 1)
Son consideradas iguales (mismos elementos en el mismo orden) y pueden representarse sin necesidad
de encapsulamiento alguno:

3 5 7 2 1

El efecto del atributo ecuacional id, es el de señalar una constante como el elemento
identidad para el operador (ver [Clavel 2007, SEC 4.4.1]). Así, toda aplicación del
operador que tenga como uno de sus operandos al elemento identidad es rescrita, antes de
que tenga lugar cualquier otra reescritura, al otro operando de la operación.

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 no se considerará la posibilidad de tener listas que siempre terminan en
nilInt como ocurre con las listas en SCHEME. En los ejemplos se solicita al lector, como ejercicio, que
indique el efecto de eliminar el atributo de identidad y considere la forma de construir una lista que
mimifique las listas nativas en SCHEME

10.4.2.3 Constructores de listas en PROLOG


Con base en la definición del predicado nativo is_list(), (ver [swi_ref_man: secc-4.28 Built-in
list operations]) :
Capítulo 10: Valores y Tipos Compuestos.
292
is_list(X) :- var(X), !, fail.
is_list([]).
is_list([_|T]) :- is_list(T).

Se induce que sólo variables instanciadas pueden ser listas, y que lo son sólo si están
instanciadas al átomo [], o a un término complejo bajo el operador (o “functor”) de dos
argumentos [_|_], en el que el segundo argumento es una lista.
Nótese que bajo esta definición, [] representa la lista vacía, y los dos argumentos del
functor [_|_], representan respectivamente la cabeza y la cola de la lista. De lo anterior se
infiere que las listas PROLOG son estructruralmente idénticas a las listas en SCHEME, por
lo que los códigos que gestionan las listas pueden realizarse de forma similar a la
presentada para dicho lenguaje.
Así, la lista compuesta por los enteros 1, 2 y 3 se escribiría así:
[1 | [2 | [3 | [] ] ] ]
Y tendría el árbol sintáctico siguiente:
.

.
1

.
2

3 []

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

En la definición siguiente el operador largo obtiene el tamaño de la lista, recorriéndola en orden directo.
Nótese que no es necesario obtener la cabeza de las lista ya que su valor no afecta el resultado., así:

(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
(+ (* (car L) (car L))(sum-cua (cdr L)))
)
)

Donde el operador cua, fue definido en el ¡Error! No se encuentra el origen de la referencia..

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.

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 cuando se encuentra el elemento buscado, así:

(define (pertenece? E L)
(if (null? L) #f
(if (= E (car L))#t
(pertenece? E (cdr L))
Capítulo 10: Valores y Tipos Compuestos.
294
)
)
)

A pesar de que el proceso asociado al operador del ejemplo anterior, es muy semejante a
los asociados con los dos operadores que le anteceden, en este caso, debe tenerse en cuenta
al determinar el orden del proceso que el recorrido puede terminar luego de visitar
cualquiera de las componentes de la lista. En efecto, el número de veces que se lleva a
cabo la reescritura es, en este caso, aleatorio, pudiendo ser un número cualquiera entre 1 y
N. En estos casos es conveniente valorar el orden del proceso con el peor de los casos del
fenómeno aleatorio. Así, diremos que el orden del proceso en el peor de los casos es O(N).
Nótese que en la definición de los dos primeros operadores se utilizó un proceso recursivo.
Igualmente podría haberse usado un proceso iterativo.

Los operadores siguientes obtienen el tamaño de una lista de enteros y la suma de los cuadrados de sus
elementos, por medio de un proceso ITERATIVO, así:

(define (largo-i L)
(if (null? L) 0 (lrg-it L 0))
)
(define (lrg-it L N)
(if (null? L) N
(lrg-it (cdr L) (+ N 1))
)
)

(define (sum-cua-i L)
(if (null? L) 0 (sucua-it L 0))
)
(define (sucua-it L T)
(if (null? L) T
(sucua-it (cdr L) (+ (* (car L) (car L)) T))
)
)

Los recorridos en orden inverso en SCHEME no son tan simples como el recorrido en
orden directo. Esto se debe a que no existe un operador nativo, equivalente al car, que de
como resultado inmediato el último componente, ni un operador nativo, equivalente al cdr,
que de como resultado la sublista que queda quitando el último componente.
Una posible estrategia para lleva a cabo este recorrido es usar una función auxiliar que
provea en cada iteración el elemento adecuado y la sublista adecuada.

En la definición siguiente el operador largo obtiene el tamaño de la lista, recorriéndola en orden inverso
y usando un proceso iterativo, así:
Capítulo 10: Valores y Tipos Compuestos.
295
(define (largo L) (largo_ite 0 L))

(define (largo_ite T L)
( if (null? L) T (largo_ite (+ 1 T) (cdr_inverso L)) )
)
En esta definición se usó el operador cdr_inverso que tiene como objeto reducir la lista en un
componente pero suprimiendo el último, en lugar del primero. Este operador tendría, sin embargo, un
nivel de complejidad algorítmica O(N), mientras que el operador cdr tiene una complejidad algorítmica
O(1).

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

En la especificación siguiente el operador > |..| obtiene el tamaño de la lista, recorriéndola en orden
directo, mientras que el operador < |..| obtiene el tamaño de la lista, recorriéndola en orden inverso.

fmod LISTA-INT is
protecting INT .
sort ListInt .
subsort Int < ListInt .
op nilLint : -> ListInt [ctor] .
op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op >|_| : ListInt -> Int .
op <|_| : ListInt -> Int .
var L : ListInt . var I : Int .
eq >| nilLint | = 0 .
eq >| I | = 1 .
eq >| I L | = 1 + >| L | .
eq <| nilLint | = 0 .
eq <| I | = 1 .
eq <| L I | = 1 + >| L | .
endfm

Donde es importante notar que el emparejamiento determina que se asocie el primero o último elemento
de la lista a la variable I (que solo se puede asociar a un entero), y la sublista restante a la variable L (que
Capítulo 10: Valores y Tipos Compuestos.
296
debe asociarse al resto para emparejar).
Nótese también que es necesaria una ecuación particular para el caso de una lista con un sólo elemento,
ya que, como se explicó en el Ejemplo 82, en las listas cuyo elemento identidad es el nilLint se asume
que solo aparece nilLint en la lista vacía (a diferencia del SCHEME en el que el nilLint es siempre el
último elemento de la lista). Se deja al lector la tarea de definir el operador en orden inverso para el caso
en que la lista tiene siempre el nilLint al final.

Maude> rewrite in LISTA-INT : <| 2 4 5 6 | .


rewrites: 7 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result NzNat: 4
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.

fmod LISTA-INT-SUMCUA is
protecting INT .
sort ListInt .
subsort Int < ListInt .
op nilLint : -> ListInt [ctor] .
op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op SCua_ : ListInt -> Int .
var I : Int . var L : ListInt .

eq SCua I = I * I .
eq SCua nilLint = 0 .
eq SCua (I L) = I * I + SCua L .
endfm
Que produce el resultado que se muestra a continuación:

Maude> rewrite in LISTA-INT-SUMCUA : SCua (2 3 4) .


rewrites: 8 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result NzNat: 29
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:

fmod LISTA-INT-PERTENECE is
protecting INT .
protecting BOOL .
sort ListInt .
subsort Int < ListInt .
op nilLint : -> ListInt [ctor] .
op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op _in_ : Int ListInt -> Bool .
vars I J : Int . var L : ListInt .
eq I in nilLint = false .
eq I in I = true .
eq I in (I L) = true .
ceq I in J = false if(I =/= J) .
ceq I in J L = I in L if(I =/= J) .
endfm
Que produce los resultados que se muestran a continuación:

Maude> rewrite in LISTA-INT-PERTENECE : 4 in 2 5 6 .


rewrites: 6 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
Capítulo 10: Valores y Tipos Compuestos.
297
result Bool: false

Maude> rewrite in LISTA-INT-PERTENECE : 4 in 2 4 6 .


rewrites: 3 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result Bool: true

El número de reescrituras requeridas para llevar a cabo los procesos definidos en el ejemplo
anterior es el tamaño de la lista. Si asumimos que las operaciones en cada reescritura del
proceso, incluida el emparejamiento, es O(1)160, el orden de los procesos definidos para el
pero de los casos, es O(N) siendo N el tamaño de la lista. Se deja al lector como ejercicio
validar la conjetura de que el emparejamiento es siempre O(1).
Nótese que a diferencia del SCHEME, en este caso, el orden del proceso no cambia con el
sentido del recorrido. Esto gracias a que la selección de las componentes se apoyó en el
emparejamiento en ambos casos. La posibilidad de emparejar una variable entera con el
último elemento de la lista es, por otro lado, debida a que por la propiedad asociativa todos
los elementos de la lista están al mismo nivel de encapsulamiento. Así la posible ganancia
en el orden sería una consecuencia directa del uso del atributo ecuacional assoc en la
declaración del constructor.
Se deja al lector como ejercicio plantear en MAUDE la definición de los operadores, de tal
manera que determinen un proceso iterativo.
10.4.3.3 Recorridos básicos sobre Listas en PROLOG.
Para los dos primeros operadores, al igual que en el SCHEME se plantea un proceso que en
cada paso procesa el elemento de la cabeza de la lista. Para acceder a este elemento, sin
embargo, ahora se usa la unificación.

En la definición siguiente el operador largo obtiene el tamaño de la lista, recorriéndola en orden directo.
Nótese que no es necesario obtener la cabeza de las lista ya que su valor no afecta el resultado., así:

largo([],0) .
largo([_|T],R) :- largo(T,R1), R is 1+R1 .
En la definición siguiente el operador sum_cua obtiene la suma de los cuadrados de los componentes
de la lista, recorriéndola en orden directo. Nótese que se usa un término con variables del operador [_|_]
para que pro medio de la unificación las variables X y T tomen el valor de la cabeza y de la cola de la
lista respectivamente, así:

sum_cua([],0) .
sum_cua([X|T],R) :- sum_cua(T,R1), R is X*X+R1 .
En ejecución.
1 ?- largo([1,4,6,7],X).

160 Esta suposición no es validada en este trabajo, y depende de la manera como el intérprete represente la lista. Así, si los
elementos de la lista se representan en unidades de memoria de igual tamaño, el emparejamiento que asocia una variable
al primer elemento y al último será O(1); en contraste, si los elementos de la lista se representan en unidades de diferente
tamaño, el emparejamiento para el primer elemento será de O(1) y el emparejamiento para el último elemento
probablemente será O(N)
Capítulo 10: Valores y Tipos Compuestos.
298
X = 4.

2 ?- sum_cua([1,2,3],X).
X = 14.

En el caso del tercer operador, el recorrido se interrumpe cuando se obtiene el resultado


esperado.

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 es el mismo que se definió en SCHEME, así:

pertenece(X,[X|_]) .
pertenece(X,[_|T]) :- pertenece(X,T) .

En ejecución.

1 ?- pertenece(2,[1,3,7]).
false.

2 ?- pertenece(2,[1,2,3]).
true .

Nótese que en la definición de los dos primeros operadores se utilizó un proceso recursivo.
Igualmente podría haberse usado un proceso iterativo.

Los operadores siguientes obtienen el tamaño de una lista de enteros y la suma de los cuadrados de sus
elementos, por medio de un proceso ITERATIVO, así:

largo_i(L,R) :- largo_i(L,R,0) .
largo_i([],R,R) .
largo_i([_|T],R,A) :- A1 is A+1, largo_i(T,R,A1) .

sum_cua_i(L,R) :- sum_cua_i(L,R,0.0) .
sum_cua_i([],R,R) .
sum_cua_i([X|T],R,A) :- A1 is A+X*X, sum_cua_i(T,R,A1) .
En ejecución.

1 ?- largo_i([1,3,7],R).
R = 3.

2 ?- sum_cua_i([1,2,3],R).
R = 14.0.

10.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.
Capítulo 10: Valores y Tipos Compuestos.
299
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.
10.4.4.1 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
proveer, sin embargo, un valor especial de retorno en caso de que la componente buscada
no se encuentre en la lista.

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 (iesimo I L)
(if (null? L) -1
(if (= I 1) (car L)
(iesimo (- I 1) (cdr L))
)
)
)

Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I de una lista
es elemento de orden I-1 de la cola de la lista cuando I es diferente de 1.
En la definición siguiente el operador primero-menor-que obtiene el primer componente (de izquierda
a derecha), cuyo valor satisface la condición de ser menor que un valor dado, retornando -1 en caso de
que éste no exista.

(define (primero-menor-que I L)
(if (null? L) -1
(if (< (car L) I) (car L)
(primero-menor-que I (cdr L))
)
)
)

La similitud de las dos definiciones presentadas nos hace pensar en la posibilidad de crear
un operador genérico que busque un elemento recibiendo como argumento la condición de
acierto. Este problema, será tratado en el capítulo 10.
La diferencia del tercer operador con los dos anteriores es que ahora se deben tener en
cuenta el valor de dos componentes consecutivas de la lista.
Capítulo 10: Valores y Tipos Compuestos.
300

En la definición siguiente, el operador primero-menor-que-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 (primero-menor-que-anterior L)
(if (or (null? L) (null? (cdr L))) -1
(if (< (car (cdr L)) (car L)) (car (cdr L))
(primero-menor-que-anterior (cdr L))
)
)
)

Donde la condición de inexistencia debe contemplar tanto el caso de que la lista sea vacía como el caso
de que no haya sino un elemento haciendo imposible la comparación.

El cuarto operador, por su parte, no sólo debe recorrer la lista, sino que debe construir otra
usando el operador cons.

En la definición siguiente, el operador select-menor-que obtiene una lista que contiene los elementos
de la lista dada cuyo valor es menor que un umbral dado.

(define (select-menor-que I L)
(if (null? L) '()
(if (< (car L) I) (cons (car L) (select-menor-que I (cdr L)))
(select-menor-que I (cdr L))
)
)
)

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

En la definición siguiente, el operador select-menor-que-i obtiene una lista que contiene los elementos
de la lista dada cuyo valor es menor que un umbral dado, por medio de un proceso iterativo.

(define (select-menor-que-i I L) (sel-men-it I L '()))


(define (sel-men-it I L R)
(if (null? L) R
(if (< (car L) I) (sel-men-it I (cdr L) (cons (car L) R))
(sel-men-it I (cdr L) R)
)
)
)

Donde la lista resultante tiene los elementos debidos pero en el orden inverso al que tenían en la lista
original.
Capítulo 10: Valores y Tipos Compuestos.
301
Por estar basados en recorridos simples sobre la lista, es fácil ver que el orden de los
procesos asociados a las definiciones de esta sección, es en el peor de los casos O(N),
siendo N el tamaño de la lista.
10.4.4.2 Selección sobre Listas en MAUDE.
Al igual que antes los dos primeros operadores se apoyan en recorridos que se interrumpen
al encontrar la componente buscada. En este caso, sin embargo, proveemos un valor
especial del tipo definido en la teoría como señal de que la componente buscada no se
encuentra en la lista.

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.

fmod LISTA-INT-IESIMO is
protecting INT .
sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc ] .
op _[_] : ListInt Int -> Int .
op error-indice : -> Int .

vars I J E : Int . var L : ListInt .

eq E[1] = E .
eq (E L)[1] = E .
ceq (E L)[I] = L[I + (- 1)] if(I =/= 1) .
ceq E[I] = error-indice if(I =/= 1) .
eq nilLint[I] = error-indice .
endfm

Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I de una lista
es elemento de orden I-1 de la cola de la lista cuando I =/= 1.

Maude> rewrite in LISTA-INT-IESIMO : (6 7 8 9)[4] .


rewrites: 10 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result NzNat: 9
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.

fmod LISTA-INT-MENORQUE is
protecting INT .
sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc ] .
op {1Xen_/X<_} : ListInt Int -> Int .
op no-existe-menor-que _ : Int -> Int [ctor] .

vars I J E : Int . var L : ListInt .


Capítulo 10: Valores y Tipos Compuestos.
302
ceq {1Xen I /X< E } = I if(I < E) .
ceq {1Xen (I L) /X< E } = I if(I < E) .
ceq {1Xen (I L) /X< E } = {1Xen L /X< E } if(I >= E) .
ceq {1Xen I /X< E } = no-existe-menor-que E if(I >= E) .
eq {1Xen nilLint /X< E } = no-existe-menor-que E .
endfm
Que produce los resultados que se muestran a continuación:

Maude> rewrite in LISTA-INT-MENORQUE : {1Xen 3 3 5 /X< 5} .


rewrites: 2 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result NzNat: 3

Maude> rewrite in LISTA-INT-MENORQUE : {1Xen 6 7 9 8 /X< 4} .


rewrites: 12 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result Int: no-existe-menor-que 4

Para acceder a dos valores consecutivos de la lista se usará, como siempre, la unificación.

En la definición siguiente, el operador {X<X1en _} obtiene el primer componente cuyo valor es menor
que el valor del componente anterior, retornando un valor especial de error en caso de que éste no exista.

fmod LISTA-INT-MENORQUE-ANTERIOR is
protecting INT .

sort ListInt .
subsort Int < ListInt .
op nilLint : -> ListInt [ctor] .
op _ _ : ListInt ListInt -> ListInt [ctor assoc ] .
op {X2<X1en_} : ListInt -> Int .
op no-existe-X2-menor-que-X1 : -> Int [ctor] .

vars I J E : Int . var L : ListInt .

ceq {X2<X1en (I E L)} = E if(E < I) .


ceq {X2<X1en (I E )} = E if(E < I) .
ceq {X2<X1en (I E L)} = {X2<X1en (E L)} if(E >= I) .
ceq {X2<X1en (I E )} = no-existe-X2-menor-que-X1 if(E >= I) .
eq {X2<X1en I} = no-existe-X2-menor-que-X1 .
eq {X2<X1en nilLint} = no-existe-X2-menor-que-X1 .

endfm

Donde la condición de inexistencia debe contemplar tanto el caso de que la lista sea vacía como el caso
de que no haya sino un elemento haciendo imposible la comparación.

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

En la definición siguiente, el operador {*Xen_/X<_} obtiene una lista que contiene los elementos de la
lista dada cuyo valor es menor que un umbral dado.

fmod LISTA-INT-MENORQUE is
Capítulo 10: Valores y Tipos Compuestos.
303
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op {*Xen_/X<_} : ListInt Int -> ListInt .

vars I J E : Int . var L : ListInt .

ceq {*Xen I /X< E } = I if(I < E) .


ceq {*Xen (I L) /X< E } = (I {*Xen L /X< E }) if(I < E) .
ceq {*Xen (I L) /X< E } = {*Xen L /X< E } if(I >= E) .
ceq {*Xen I /X< E } = nilLint if(I >= E) .
eq {*Xen nilLint /X< E } = nilLint .

endfm
Que produce los resultados que se muestran a continuación:

Maude> rewrite in LISTA-INT-MENORQUE : {*Xen 2 4 6 7 1 2 8 /X< 7} .


rewrites: 18 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result ListInt: 2 4 6 1 2

Maude> rewrite in LISTA-INT-MENORQUE : {*Xen 8 4 6 7 8 /X< 4} .


rewrites: 17 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result ListInt: nilLint
Donde el lector debe notar el efecto del atributo identidad, que permite eliminar el nilLint al final de la
lista excepto cuanto la lista resultante es vacía.

La especificación del operador del ejemplo anterior, que define un proceso iterativo no
ofrece dificultad alguna en MAUDE.

En la definición siguiente, el operador {*Xen_/X<_} del ejemplo anterior es definido de manera que
genere un proceso iterativo.

fmod LISTA-INT-MENORQUE-IT is
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op {*Xen_/X<_} : ListInt Int -> ListInt .
op {*Xen_/X<_it_} : ListInt Int ListInt -> ListInt .
vars I J E : Int . var L1 L2 : ListInt .

eq { *Xen L1 /X< E } = { *Xen L1 /X< E it nilLint } .


eq {*Xen nilLint /X< E it L1 } = L1 .
ceq {*Xen I /X< E it L1 } = { *Xen nilLint /X< E it L1 } if(I >= E) .
ceq {*Xen I /X< E it L1 } = { *Xen nilLint /X< E it (I L1) } if(I < E) .
ceq {*Xen (I L1) /X< E it L2 } = { *Xen L1 /X< E it L2 } if(I >= E) .
ceq {*Xen (I L1) /X< E it L2 } = { *Xen L1 /X< E it (I L2) } if(I < E) .
endfm
Capítulo 10: Valores y Tipos Compuestos.
304
Que produce los resultados que se muestran a continuación:

Maude> rewrite in LISTA-INT-MENORQUE-IT : {*Xen 2 6 1 8 7 3 /X< 5} .


rewrites: 17 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result ListInt: 3 1 2
Donde la lista resultante tiene los elementos en orden inverso.

Por estar basados en recorridos simples sobre la lista, es fácil ver que el orden de los
procesos asociados a las definiciones de esta sección, es para el peor de los casos O(N),
siendo N el tamaño de la lista.
10.4.4.3 Selección sobre Listas en PROLOG.
Los dos primeros dos operadores, recorren la lista, elemento por elemento, hasta hallar el
buscado o agotar la lista.

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.

iesimo(1,[X|_],X) .
iesimo(_,[],-1) .
iesimo(I,[_|T],X) :- I1 is I-1, iesimo(I1,T,X) .
Donde el lector puede notar, de nuevo, que la idea clave de la definición es que el elemento de orden I
de una lista es elemento de orden I-1 de la cola de la lista cuando I es diferente de 1.
En la definición siguiente el operador primero_menor_que obtiene el primer componente (de
izquierda a derecha), cuyo valor satisface la condición de ser menor que un valor dado, retornando -1 en
caso de que éste no exista.

primero_menor_que(I,[X|_],X) :- X<I .
primero_menor_que(_,[],-1) .
primero_menor_que(I,[_|T],X) :- primero_menor_que(I,T,X) .
En ejecución.

1 ?- iesimo(3,[1,2,3,4,5],X).
X=3.

2 ?- primero_menor_que(5,[6,7,4,6,1],X).
X=4.

3 ?- iesimo(7,[1,2,3],X).
X = -1 .

4 ?- primero_menor_que(4,[7,8,9],X).
X = -1
Es fácil observar que el comportamiento de los procesos de la búsqueda planteados es esencialmente el
de un proceso iterativo, estable en memoria. Para ilustrar este hecho presentamos a continuación la tabla
que muestra la evolución del objetivo para la primera de las evocaciones mostradas del predicado
iesimo, así:

Paso regla substitución objetivo


0 iesimo(3,[1,2,3,4,5],R)
1 3 3/I, [2,3,4,5]/T, R/X I1 is 3-1, iesimo(I1, [2,3,4,5],R)
Capítulo 10: Valores y Tipos Compuestos.
305
2 * 2/I1 iesimo(2, [2,3,4,5],R)
3 3 2/I, [3,4,5]/T, R/X I1 is 2-1, iesimo(I1, [3,4,5],R)
4 * 1/I1 iesimo(1, [3,4,5],R)
5 1 3/X, 3/R □

Donde las substituciones de la última unificación determinan el valor de la respuesta (R=3).

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.

En la definición siguiente, el operador primero_menor_que_anterior obtiene el primer componente


cuyo valor es menor que el valor del componente anterior, retornando -1 en caso de que éste no exista.

primero_menor_que_anterior([],-1) .
primero_menor_que_anterior([_|[]],-1) .
primero_menor_que_anterior([X|[Y|_]],Y) :- Y<X .
primero_menor_que_anterior([_|T],X) :- primero_menor_que_anterior(T,X) .
Donde la condición de inexistencia debe contemplar tanto el caso de que la lista sea vacía como el caso
de que no haya sino un elemento haciendo imposible la comparación.
En ejecución.

1 ?- primero_menor_que_anterior([],X).
X = -1.

2 ?- primero_menor_que_anterior([1],X).
X = -1 .

3 ?- primero_menor_que_anterior([1,2,3],X).
X = -1 .

4 ?- primero_menor_que_anterior([7,9,5,8,4],X).
X=5

El cuarto operador, por su parte, no sólo debe recorrer la lista, sino que debe construir otra
usando el operador _ | _ .

En la definición siguiente, el operador select_menor_que obtiene una lista que contiene los elementos
de la lista dada cuyo valor es menor que un umbral dado.

select_menor_que(_,[],[]) .
select_menor_que(E,[X|T],[X|W]) :- X<E, select_menor_que(E,T,W) .
select_menor_que(E,[_|T],W) :- select_menor_que(E,T,W) .
En ejecución.

1 ?- select_menor_que(4,[],X).
X = [] .

2 ?- select_menor_que(4,[7,1,6,3],R).
R = [1, 3]
Para ilustrar la manera como ocurre el retroceso y se construye la respuesta, presentamos a continuación
la tabla que muestra la evolución del objetivo para la segunda evocación mostrada del predicado
Capítulo 10: Valores y Tipos Compuestos.
306
select_menor_que, así:

Paso regla substitución objetivo


0 select_menor_que(4,[7,1,6,3],R)
1 2 4/E, 7/X, [1,6,3]/T, [7 | W]/R 7<4, select_menor_que(4, [1,6,3],W)
2 * falla
1 3 4/E, [1,6,3]/T, W/R select_menor_que(4, [1,6,3],W)
*2 2 4/E, 1/X, [6,3]/T, [1 | W´]/W 1<4, select_menor_que(4, [6,3],W´)
3 * select_menor_que(4, [6,3],W´)
4 2 4/E, 6/X, [3]/T, [6 | W´´]/W´ 6<4, select_menor_que(4, [3],W´´)
5 * falla
*4 3 4/E, [3]/T, W´´/W´ select_menor_que(4, [3],W´´)
5 2 4/E, 3/X, [ ]/T, [3 | W´´´]/W´´ 3<4, select_menor_que(4, [ ],W´´´)
6 * select_menor_que(4, [ ],W´´´)
7 1 [ ]/W´´´ □

Donde un paso i con * corresponden al nuevo paso i luego de un retroceso. Nótese que la respuesta se
construye con base en las substituciones de las variables en las diferentes unificaciones, así:

R = W = [1 | W´] = [1 | W´´] = [1 | [3 | W´´´]] = [1 | [3 | [ ]]] = [1,3]

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

En la definición siguiente, el operador select_menor_que_i obtiene una lista que contiene los
elementos de la lista dada cuyo valor es menor que un umbral dado, por medio de un proceso iterativo.

select_menor_que_i(E,L,R) :- select_menor_que_i(E,L,R,[]) .
select_menor_que_i(_,[],R,R) .
select_menor_que_i(E,[X|T],R,A) :- X<E, select_menor_que_i(E,T,R,[X|A]) .
select_menor_que_i(E,[_|T],R,A) :- select_menor_que_i(E,T,R,A) .
Donde la lista resultante tiene los elementos debidos pero en el orden inverso al que tenían en la lista
original.
En ejecución.

1 ?- select_menor_que_i(4,[],X).
X = [] .

2 ?- select_menor_que_i(4,[7,1,6,8,3,6,2],X).
X = [2, 3, 1]

10.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.
Capítulo 10: Valores y Tipos Compuestos.
307
A los operadores encargados de incluir, excluir y modificar los componentes del iterado,
los hemos denominado “operadores modificadores” del iterado.
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.
10.4.5.1 Modificadores de listas en SCHEME.
Los cuatro primeros operadores se pueden definir fácilmente, por medio de un recorrido
que en cada paso determine 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.

En la definición siguiente, el operador insert coloca un componente dado en una posición dada de la
lista, incluyéndolo al principio si la posición dada es menor o igual a cero, e incluyéndolo de último si la
posición dada es mayor que el tamaño de la lista.

(define (insert E I L)
(if (null? L) (list E)
(if (<= I 1) (cons E L)
(cons (car L) (insert E (- I 1) (cdr L)))
)
)
)

Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I en la lista, es
el elemento de orden I-1 en la cola de la lista.
En la definición siguiente el operador borre suprime el componente localizado en una posición dada de
la lista, o no suprime ninguno si la posición dada es inválida.

(define (borre I L)
(if (null? L) L
(if (= I 1) (cdr L)
(cons (car L) (borre (- I 1) (cdr L)))
)
)
)
Capítulo 10: Valores y Tipos Compuestos.
308
En la definición siguiente el operador borre-e suprime los componentes que tienen un valor dado.

(define (borre-e E L)
(if (null? L) L
(if (= (car L) E) (borre-e E (cdr L))
(cons (car L) (borre-e E (cdr L)))
)
)
)

Donde el lector puede notar que el operador borra, no sólo la primera ocurrencia del elemento sino todas
ellas.

Si el lector sigue la línea de pensamiento que aplicamos antes para derivar el orden de los
procesos asociados con los operadores definidos con base en recorridos, podrá fácilmente
intuir que el orden de los procesos para los operadores definidos en el ejemplo anterior es
O(N).

En la definición siguiente, el operador pode elimina los elementos repetidos de la lista dejando la última
ocurrencia de cada elemento. Para ello usa el operador pertenece? definido previamente en el Ejemplo
84.

(define (pode L)
(if (null? L) L
(if (pertenece? (car L) (cdr L)) (pode (cdr L))
(cons (car L) (pode (cdr L)))
)
)
)

Donde el lector puede notar que la definición del operador difiere de la del anterior sólo en que la
condición de eliminación del componente es diferente.
La posibilidad de crear un operador que reciba como argumento, de forma genérica, la condición de
eliminación, será tratada en el capítulo 10

Para derivar el orden del proceso asociado al operador definido en el ejemplo anterior, se
debe tener en cuenta que en cada paso de la reescritura especificada, se evoca a otro
operador que induce, a su vez, tantas reescrituras como el tamaño de la lista que recibe.
Así, el número de reescrituras total para el peor de los casos es (N-1)+(N-2)+(N-3)+.....+2,
por lo que el orden del proceso es ahora O(N2).
Para definir el operador que adiciona al final de una lista otra lista dada, se debe tener en
cuenta que los elementos de la lista adicionada deben quedar en el nivel de profundidad
adecuado. En efecto, la simple aplicación del operador cons a las dos listas dadas, sólo
resultaría en una pareja cuyas componentes serían las dos listas, sin que dicha pareja se
constituya en una lista.
Capítulo 10: Valores y Tipos Compuestos.
309
En la definición siguiente, el operador append obtiene una lista que contiene los elementos de dos listas
dadas.

(define (append L1 L2)


(if (null? L1) L2
(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.
10.4.5.2 Modificadores de listas en MAUDE.
Tal como en el caso del SCHEME los cuatro primeros operadores se pueden definir
fácilmente, por medio de un recorrido que en cada paso indica si el elemento visitado hace
parte o no de la lista resultante o si debe ser substituido por otro elemento diferente.

En la definición siguiente, el operador _[_]<=_ coloca un componente dado en una posición dada de la
lista, incluyéndolo al principio si la posición dada es menor o igual a cero, e incluyéndolo de último si la
posición dada es mayor que el tamaño de la lista.

fmod LISTA-INT-INSERT-IESIMO is
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


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

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) .
eq nilLint[I]<= E = E .
endfm

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.

Maude> rewrite in LISTA-INT-INSERT-IESIMO : (2 4 5)[2]<= 7 .


Capítulo 10: Valores y Tipos Compuestos.
310
rewrites: 6 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result ListInt: 2 7 4 5
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.

fmod LISTA-INT-SUPRIMA-IESIMO is
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc ] .
op _[_]=> : ListInt Int -> Int .

vars I J E : Int . var L : ListInt .

ceq L[I]=> = L if(I < 1) .


eq (J L)[1]=> = L .
ceq (J L)[I]=> = J (L[I + (- 1)]=> ) if(I > 1) .
ceq J[I]=> = J if(I > 1) .
eq J[1]=> = nilLint .
eq nilLint[I]=> = nilLint .
endfm

Maude> rewrite in LISTA-INT-SUPRIMA-IESIMO : (2 3 5)[2]=> .


rewrites: 6 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result ListInt: 2 5

En la definición siguiente el operador _not-in_ suprime los componentes que tienen un valor dado.

fmod LISTA-INT-NO-PERTENECE is
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


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

vars I J : Int . var L : ListInt .

eq I not-in nilLint = nilLint .


eq I not-in I = nilLint .
eq I not-in (I L) = I not-in L .
ceq I not-in J = J if(I =/= J) .
ceq I not-in (J L) = J (I not-in L) if(I =/= J) .
endfm

Donde el lector puede notar que el operador borra, no sólo la primera ocurrencia del elemento sino todas
ellas.

Maude> rewrite in LISTA-INT-NO-PERTENECE : 4 not-in 3 4 5 6 4 .


rewrites: 8 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result ListInt: 3 5 6
Capítulo 10: Valores y Tipos Compuestos.
311
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 87.

fmod LISTA-INT-PODE is
protecting INT .
protecting BOOL .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc id: nilLint] .
op _in_ : Int ListInt -> Bool .
op set : ListInt -> ListInt .

vars I J : Int . var L : ListInt .

eq I in nilLint = false .
eq I in I = true .
eq I in (I L) = true .
ceq I in J = false if(I =/= J) .
ceq I in J L = I in L if(I =/= J) .

eq set(nilLint) = nilLint .
eq set(I) = I .
ceq set(I L) = I set(L) if(not(I in L)) .
ceq set(I L) = set(L) if(I in L) .

endfm

Donde el lector puede notar que la definición del operador difiere de la del anterior sólo en que la
condición de eliminación del componente es diferente.

Maude> rewrite in LISTA-INT-PODE : set(3 4 5 6 3 4) .


rewrites: 56 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result ListInt: 5 6 3 4

Dado que en la definición de los operadores del ejemplo anterior, se especifican reescrituras
similares a las de su correspondiente definición en SCHEME, los procesos asociados a
dichos operadores tiene el mismo orden que sus correspondientes en SCHEME. Nótese
que aquí, de nuevo, se parte de asumir que el emparejamiento es siempre O(1).
El operador que adiciona al final de una lista otra lista dada en MAUDE, simplemente
aplica el constructor de listas definido antes. La simplicidad de esta definición es una
consecuencia directa de la eliminación de los niveles de profundidad para los componentes,
asociada a la forma como se definió en MAUDE el constructor de la lista.

En la definición siguiente, el operador _|_ adiciona a los elementos de una lista otra lista dada dando
como resultado una lista.

op _|_ : ListInt ListInt -> ListInt .


vars L1 L2 : ListInt .
eq L1 | L2 = L1 L2 .
Capítulo 10: Valores y Tipos Compuestos.
312
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 (que por simplicidad asumimos O(1)).
10.4.5.3 Modificadores de listas en PROLOG.
Los cuatro primeros operadores se pueden definir fácilmente, por medio de un recorrido
que en cada paso determina si el elemento visitado hace parte o no de la lista resultante o si
debe ser substituido por otro elemento diferente.
Nótese, además, que el resultado de la operación debe ser siempre una lista.
Capítulo 10: Valores y Tipos Compuestos.
313
En la definición siguiente, el operador insert coloca un componente dado en una posición dada de la
lista, incluyéndolo al principio si la posición dada es menor o igual a cero, e incluyéndolo de último si la
posición dada es mayor que el tamaño de la lista.

insert(_,E,[],[E|[]]) .
insert(I,E,L,[E|L]) :- I=<1 .
insert(I,E,[X|T],[X|W]) :- I1 is I-1, insert(I1,E,T,W) .
Donde el lector puede notar que la idea clave de la definición es que el elemento de orden I en la lista, es
el elemento de orden I-1 en la cola de la lista. Al ejecutarse:

1 ?- insert(3,6,[],X).
X = [6] .

2 ?- insert(3,6,[1,3],X).
X = [1, 3, 6] .

3 ?- insert(3,6,[1,4,7,4,2],X).
X = [1, 4, 6, 7, 4, 2]
En la definición siguiente el operador borre suprime el componente localizado en una posición dada de
la lista, o no suprime ninguno si la posición dada es inválida.

borre(_,[],[]) .
borre(I,L,L) :- I<1 .
borre(1,[_|T],T) .
borre(I,[X|T],[X|W]) :- I1 is I-1, borre(I1,T,W) .
Al ejecutarse:

1 ?- borre(5,[],X).
X = [] .

2 ?- borre(-4,[1,2,3],X).
X = [1, 2, 3] .

3 ?- borre(2,[1,2,3],X).
X = [1, 3]
En la definición siguiente el operador borre_e suprime todas las componentes que tienen un valor dado.

borre_e(_,[],[]) .
borre_e(E,[E|T],W) :- borre_e(E,T,W) .
borre_e(E,[X|T],[X|W]) :- borre_e(E,T,W) .
Al ejecutarse:

1 ?- borre_e(4,[],X).
X = [] .

2 ?- borre_e(4,[1,4,3,5,4,6,4],X).
X = [1, 3, 5, 6]
En la definición siguiente, el operador pode elimina los elementos repetidos de la lista dejando la última
ocurrencia de cada elemento.

pode([],[]) .
pode([E|T],W) :- pertenece(E,T), pode(T,W) .
pode([E|T],[E|W]) :- pode(T,W) .
Al ejecutarse.
Capítulo 10: Valores y Tipos Compuestos.
314
1 ?- pode([],X).
X = [].

2 ?- pode([1,2,3],X).
X = [1, 2, 3].

3 ?- pode([1,2,1,3,1,1,5],X).
X = [2, 3, 1, 5]

Al igual que en el SCHEME, para definir el operador que adiciona al final de una lista otra
lista dada, se debe tener en cuenta que los elementos de la lista adicionada deben quedar en
el nivel de profundidad adecuado.

En la definición siguiente, el operador append obtiene una lista que contiene los elementos de dos listas
dadas.

append([],L2,L2) .
append(L1,[],L1) .
append([X|T],L,[X|W]) :- append(T,L,W) .
Donde la evocación recursiva del operador definido tiene como único objeto profundizar en los niveles
de encapsulamiento de L1, para llegar al nivel donde aparece la lista vacía. Esta lista es, entonces,
substituida por la lista L2. Al ejecutarse:

1 ?- append([],[1,2,3],X).
X = [1, 2, 3] .

2 ?- append([1,2,3],[],X).
X = [1, 2, 3] .

3 ?- append([1,2,3],[4,5,6],X).
X = [1, 2, 3, 4, 5, 6]
Para ilustrar la manera como se construye la respuesta, presentamos a continuación la tabla que muestra
la evolución del objetivo para la tercera evocación mostrada del predicado append, así:

Paso regla substitución objetivo


0 append([1,2,3],[4,5,6],R).
1 3 1/X, [2,3]/T, [1 | W]/R append([2,3],[4,5,6],W).
2 3 2/X, [3]/T, [2 | W´]/W append([3],[4,5,6],W´).
3 3 3/X, [ ]/T, [3 | W´´]/W´ append([ ],[4,5,6],W´´).
4 1 [4,5,6]/L2, [4,5,6]/W´´ □

Nótese de nuevo que la respuesta se construye con base en las substituciones de las variables en las
diferentes unificaciones, así:

R = [1 | W] = [1 | [2 | W´]] = [1 | [2 | [3 | W¨´]]] = [1 | [2 | [3 | [4,5,6]]]] = [1,2,3,4,5,6]

10.4.6 Transformación de la lista


Algunos de los procesos útiles sobre la lista se facilitan si el orden de las componentes
cumple ciertas condiciones. En particular la selección de un componente con base en su
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).
Capítulo 10: Valores y Tipos Compuestos.
315
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.
10.4.6.1 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.

En la definición siguiente, el operador intercambie recorre la lista en orden directo intercambiando los
elementos que estén en desorden.

(define (intercambie L)
(if (or (null? L) (null? (cdr L))) L
(if (< (car L) (car (cdr L)))
(cons (car L) (intercambie (cdr L)))
(cons (car (cdr L)) (intercambie (cons (car L) (cdr (cdr L)))))
)
)
)

Donde el lector puede notar que el intercambio se suspende cuando sólo queda un elemento, y que luego
de una comparación, y un posible intercambio, el elemento mayor de cada pareja vuelve a participar en
la comparación, y posible intercambio, con el elemento siguiente a la pareja.

Por llevar a cabo un recorrido simple, y asumiendo que el proceso en cada paso del
recorrido es del orden O(1), el orden del proceso asociado con el operador del ejemplo es
O(N).
Un primer algoritmo de ordenamiento puede concebirse fácilmente con base en el operador
anterior. En efecto, el lector puede verificar experimentalmente que la aplicación reiterada
del operador anterior conduce eventualmente a una lista ordenada.

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.

(define (buble-sort L)
(if (or (null? L) (null? (cdr L))) L
(buble-sort-ite L (largo L))
)
)
(define (buble-sort-ite L N)
Capítulo 10: Valores y Tipos Compuestos.
316
(if (= N 0) L
(buble-sort-ite (intercambie L) (- N 1))
)
)

Donde el lector puede notar que el intercambio se lleva a cabo tantas veces como el tamaño de la lista.

Dado que la reescritura que evoca el intercambio se lleva a cabo N veces, y que el
algoritmo de intercambio en si mismo tiene el orden O(N), el sort tendrá el orden O(N2).
El algoritmo de ordenamiento más usado actualmente, es el conocido con el nombre de
“Quick Sort” [Hoare 61]. El Quick-Sort es usado además como un ejemplo clásico de la
expresividad de los lenguajes funcionales [Sylvan 2007]. En lugar de describir el algoritmo
en palabras, presentamos la definición del operador correspondiente, con la expectativa de
que sea lo suficientemente descriptivo.

En la definición siguiente, el operador quick-sort lleva a cabo un ordenamiento de la lista por el método
del “Quick Sort”.

(define (quick-sort L)
(if (or (null? L) (null? (cdr L))) L
(append (quick-sort (select-menor-que (car L) (cdr L)))
(cons (car L)
(quick-sort
(select-mayor-igual-que (car L) (cdr L))))
)
)
)

Donde se usaron los operadores de selección select-menor-a-v y select-mayor-igual-a-v, definidos,


el primero en el Ejemplo 94, 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 fuentes161.
10.4.6.2 Transformación de listas en MAUDE.
El primer operador es un recorrido simple que, en cada iteración, debe acceder a dos
valores consecutivos de la lista y declarar la posición de uno de ellos en la lista resultante.

En la definición siguiente, el operador <-> recorre la lista en orden inverso intercambiando los
elementos que estén en desorden.

161 Consultar por ejemplo en http://es.wikipedia.org/wiki/Quicksort


Capítulo 10: Valores y Tipos Compuestos.
317
fmod LISTA-INT-BUBLE is
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc ] .
op <->_ : ListInt -> ListInt .

vars I J 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 I E) = (<-> (L E)) I if(E >= I) .
ceq <-> (L I E) = (<-> (L I)) E if(E < I) .

endfm.....
Donde el lector debe notar el uso de paréntesis para indicar la lista a la que se aplica el operador en cada
caso (la expresión (<-> L E)aplica el operador <-> sólo al L).

Maude> rewrite in LISTA-INT-BUBLE : <-> (3 6 5 4 8) .


rewrites: 8 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result ListInt: 8 3 6 5 4

Por llevar a cabo un recorrido simple, y asumiendo que el proceso en cada paso del
recorrido es del orden O(1), el orden del proceso asociado con el operador del ejemplo es
O(N).
Al igual que en SCHEME el primer algoritmo de ordenamiento se concibe fácilmente con
base en la aplicación reiterada del operador anterior. El lector puede verificar
experimentalmente que la aplicación reiterada del operador anterior conduce eventualmente
a una lista ordenada.

En la definición siguiente, el operador B>> lleva a cabo un ordenamiento de la lista evocando de forma
repetida el operador intercambie definido arriba.

fmod LISTA-INT-BUBLE is
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


op _ _ : ListInt ListInt -> ListInt [ctor assoc ] .
op <->_ : ListInt -> ListInt .

vars I J E : Int . var L : ListInt .

…..

op B>>_ : ListInt -> ListInt .


op auxB>>_ : ListInt -> ListInt .
eq B>> nilLint = nilLint .
Capítulo 10: Valores y Tipos Compuestos.
318
ceq B>> L = auxB>> (<-> L) if(L =/= nilLint) .
eq auxB>> (E L) = E (auxB>> (<-> L)) .
eq auxB>> E = E .

endfm

Donde el lector puede notar que el algoritmo toma ventaja de que el proceso de intercambio, efectuado
en el orden inverso, siempre deja el elemento menor de la lista de primero.

Maude> rewrite in LISTA-INT-BUBLE : B>> (4 7 3 8 9 11) .


rewrites: 41 in 1628036047000ms cpu (1ms real) (0 rewrites/second)
result ListInt: 11 9 8 7 4 3

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

En la definición siguiente, el operador Q>> lleva a cabo un ordenamiento de la lista por el método del
“Quick Sort”.

fmod LISTA-INT-QUICKSORT is
protecting INT .

sort ListInt .
subsort Int < ListInt .

op nilLint : -> ListInt [ctor] .


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

vars I J E : Int . var L1 L2 L : ListInt .


…..

eq Q>> nilLint = nilLint .


eq Q>> E = E .
ceq Q>> (E L) = (Q>> { *Xen L /X< E })
E
(Q>> { *Xen L /X>= E }) if(L =/= nilLint) .
endfm

Donde se usaron los operadores de selección {*Xen_/X<_} y {*X en_/X>=_}, definidos, el primero en
el Ejemplo 98, y el segundo como ejercicio para el lector.

Tal como fue referido arriba el orden del Quick Sort es, en el peor de los casos O(N2) y en
el caso promedio es O(N logN).
10.4.6.3 Transformación de listas en PROLOG.
Para invertir la lista usaremos un proceso simple iterativo.
Capítulo 10: Valores y Tipos Compuestos.
319

En la definición siguiente, el operador ivertir parte de una lista vacía sobre la que coloca los elementos
de la lista original.

invertir([],[]) .
invertir([E|[]],[E|[]]) .
invertir(L,R) :- invertir(L,R,[]) .
invertir([X|T],R,LI) :- invertir(T,R,[X|LI]) .
invertir([],R,R) .
En ejecución:

1 ?- invertir([1,2,3],X).
X = [3, 2, 1]

El segundo operador es un recorrido simple que, en cada iteración, debe acceder a dos
valores consecutivos de la lista y declarar la posición de uno de ellos en la lista resultante.

En la definición siguiente, el operador intercambie recorre la lista en orden directo intercambiando los
elementos que estén en desorden.

intercambie([],[]) .
intercambie([E|[]],[E|[]]) .
intercambie([X|[Y|T]],[Y|W]) :- X>Y, intercambie([X|T],W) .
intercambie([X|[Y|T]],[X|W]) :- intercambie([Y|T],W)
Donde el lector puede notar que el intercambio se suspende cuando sólo queda un elemento, y que luego
de una comparación, y un posible intercambio, el elemento mayor de cada pareja vuelve a participar en
la comparación, y posible intercambio, con el elemento siguiente a la pareja.
En ejecución:

1 ?- intercambie([],X).
X = [].

2 ?- intercambie([1,2,3],X).
X = [1, 2, 3] .

3 ?- intercambie([3,2,1],X).
X = [2, 1, 3]
Para ilustrar la manera como ocurre el retroceso y se construye la respuesta, presentamos a continuación
la tabla que muestra la evolución del objetivo para la tercera evocación mostrada del predicado
intercambie, así:

Paso regla substitución Objetivo


0 intercambie([3,2,1],X).
1 3 3/X´, 2/Y, [1]/T, [2 | W]/X 3>2, intercambie([3|[1]],W).
2 * intercambie([3,1],W).
3 3 3/X´, 1/Y, [ ]/T, [1 | W´]/W 3>1, intercambie([3|[ ]],W´).
4 * intercambie([3],W´).
5 1 3/E, [3]/W´ □

Construcción de la respuesta:

X = [2 | W] = [2 | [1 | W´]] = 2 | [1 | [3]]] = [2,1,3]


Capítulo 10: Valores y Tipos Compuestos.
320
Un primer algoritmo de ordenamiento puede concebirse fácilmente con base en el operador
anterior.

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.

bubble_sort([],[]) .
bubble_sort([E|[]],[E|[]]) .
bubble_sort(L,R) :- largo(L,T), bubble_sort(L,R,T) .
bubble_sort(L,L,0) .
bubble_sort(L,R,I) :- intercambie(L,W), I1 is I-1, bubble_sort(W,R,I1) .
Donde el lector puede notar que el intercambio se lleva a cabo tantas veces como el tamaño de la lista.
En ejecución:
1 ?- bubble_sort([],X).
X = [] .

2 ?- bubble_sort([2],X).
X = [2] .

3 ?- bubble_sort([3,2,1],X).
X = [1, 2, 3]

El algoritmo de ordenamiento más usado actualmente, es el conocido con el nombre de


“Quick Sort” [Hoare 61].

En la definición siguiente, el operador quick-sort lleva a cabo un ordenamiento de la lista por el método
del “Quick Sort”.

quick_sort([],[]) .
quick_sort([E|[]],[E|[]]) .
quick_sort([E|T],R) :- select_menor_que(E,T,Mn), quick_sort(Mn,MnS),
select_mayoroig_que(E,T,My), quick_sort(My,MyS),
append(MnS,[E|MyS],R) .
En ejecución:

1 ?- quick_sort([],X).
X = [].

2 ?- quick_sort([3],X).
X = [3] .

3 ?- quick_sort([3,2,1],X).
X = [1, 2, 3] .

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

La construcción para un árbol binario de búsqueda que lista que tiene como sus componentes los
números enteros 1, 2, 5, 9 y 11, con el número 5 en la raíz, sería entonces como sigue:

(define A1
(cons 5 (cons (cons 2 (cons
(cons 1 (cons '() '()))
(cons 3 (cons '() '()))
)
)

(cons 9 (cons
(cons 7 (cons '() '()))
'()
)
)
)
)
)

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.

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 R AI AD) (cons R (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 R) (cons R (cons '() '())))


Capítulo 10: Valores y Tipos Compuestos.
323
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 que los
argumentos sean del tipo correcto. El uso correcto 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, secc.
2.1.2].

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 predicado de tipo


nativo que valide si un objeto es un árbol. Esto se debe a que el árbol no es una estructura
nativa en lenguaje. Este predicado debe, entonces, definirse de forma explícita, definición
que se deja como ejercicio al lector
10.5.2.2 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.

En la especificación MAUDE siguiente se propone un sort asociado con todos los árboles binarios de
búsqueda con componentes enteras, que llamaremos BTreeInt, se define un constructor par el árbol y
una constante para representar el árbol vacío, así:

fmod ARBOL-BINARIO-INT is
protecting INT .

sort BTreeInt .

op nilBTInt : -> BTreeInt [ctor] .


Capítulo 10: Valores y Tipos Compuestos.
324
op <_> _ _ : Int BTreeInt BTreeInt -> BTreeInt [ctor] .
op A1 : -> BTreeInt .

var A : BTreeInt . var E I : Int .


eq A1 =
(< 7 > (< 3 > (< 2 > (< 1 > nilBTInt nilBTInt)
nilBTInt
)
(< 4 > nilBTInt
(< 6 > nilBTInt nilBTInt)
)
)
(< 8 > nilBTInt
(< 15 > (< 13 > nilBTInt nilBTInt)
nilBTInt
)
)
) .

endfm
Donde el árbol representado por la constante A1 es el que se muestra en la figura siguiente:

3 8
2 4 15

1 6 13

Maude> rewrite in ARBOL-BINARIO-INT : A1 .


rewrites: 1 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result BTreeInt: < 7 > < 3 > < 2 > < 1 > nilBTInt nilBTInt nilBTInt < 4 >
nilBTInt < 6 > nilBTInt nilBTInt < 8 > nilBTInt < 15 > < 13 >
nilBTInt
nilBTInt nilBTInt

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.
10.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
Capítulo 10: Valores y Tipos Compuestos.
325
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.
10.5.3.1 Localización de un Componente del Árbol en SCHEME.
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.

En la definición siguiente, el operador esta-en? recorre el árbol de la manera referida arriba para
verificar si un elemento se encuentra o no, en el árbol.

(define (esta-en? E A)
(if (null? A)#f
(if (= E (get-root A))#t
(if (< E (get-root A)) (esta-en? E (get-ArI A))
(esta-en? E (get-ArD A))
)
)
)
)

Una característica importante del proceso asociado al operador del ejemplo anterior, es que
no debe visitar todos los elementos del árbol para hallar el que busca. Esto se debe a que en
cada nudo visitado desecha una de las ramas y prosigue el recorrido visitando sólo
elementos de la otra. El efecto de este comportamiento es una sensible reducción en le
tiempo de proceso, con respecto al operador equivalente de la lista.
Para tener una intuición del orden del algoritmo, el lector debe notar que el número de
visitas, correspondiente con el número de reescrituras determinadas por la definición,
depende de la “forma” del árbol.
En efecto, es posible que de forma sistemática, ningún componente del árbol tenga hijos a
su izquierda (o a su derecha), constituyéndose en un árbol “degenerado”. En este caso la
relación entre los elementos es equivalente a la que tendrían en una lista. Para este caso, el
orden del proceso de localización de un elemento es, en el peor de los casos, O(N), siendo
N el número de componentes del árbol.
Es posible también, que los sub-árboles que parten de cada nodo del árbol tengan el mismo
número de elementos, constituyéndose en un árbol perfectamente “balanceado”162. En este
caso el número de visitas que deben efectuarse para llega a una hoja del árbol es siempre
log2N, siendo N el número de componentes en el árbol163. En este caso el orden del
proceso de localización de un elemento es, en el peor de los casos O(log2N).
10.5.3.2 Localización de un Componente del Árbol en MAUDE.

162 Caso en el que el número de componentes del árbol debe ser una potencia de N.
163 Esta relación es fácil de intuir y de demostrar, si se organizan los componentes formando capas o “niveles”
correspondientes con el número de ancestros que tienen, y se tiene en cuenta que, en un árbol perfectamente balanceado,
en cada nivel el número de elementos es el doble de los que hay en el nivel anterior.
Capítulo 10: Valores y Tipos Compuestos.
326
En el ejemplo siguiente se define un operador que indica si un componente con un valor de
identificación dado se encuentra o no se encuentra en el árbol.

El operador _in?_ verifica si un entero dado se encuentra en el árbol, así:

op _in?_ : Int BTreeInt -> Bool .

var T TI TD : BTreeInt . var 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.
10.5.4 Inserción de un Componente en el Árbol Binario de Búsqueda.
Cada vez que se inserta un nuevo componente, en el árbol, este debe situarse en el lugar
adecuado para mantener las relaciones entre los nodos y sus hijos. Este lugar, por otra
parte, no es otro que el lugar donde el componente debería estar de ser buscado.
Capítulo 10: Valores y Tipos Compuestos.
327
10.5.4.1 Inserción de un Componente en el Árbol en SCHEME.
La inserción de un nuevo componente en SCHEME es, esencialmente, un recorrido de
búsqueda del elemento a ser insertado, que va declarando en cada visita la composición del
árbol resultante, y lo inserta al momento de no encontrarlo en el árbol.

En la definición siguiente, el operador inserte recorre el árbol buscando el elemento a ser insertado y lo
coloca en lugar que le corresponde al momento de no hallarlo, así:

(define (inserte E A)
(if (null? A) (make-e-BTree E)
(if (= E (get-root A)) A
(if (< E (get-root A))
(make-BTree (get-root A) (inserte E (get-ArI A))
(get-ArD A)
)
(make-BTree (get-root A) (get-ArI A)
(inserte E (get-ArD A))
)
)
)
)

Por estar el algoritmo de inserción, fundamentado en una localización del elemento a ser
insertado, si se supone que el orden del proceso efectuado en cada visita es O(1), entonces
el orden del proceso de inserción sería el mismo que el de la localización de un elemento
con base en su valor de identificación.
La forma del árbol, por otro lado, depende del orden en que se insertan sus componentes.
En efecto, si éstas se insertan en orden por su valor de identificación el árbol resultante es
degenerado; en cambio, si se insertan de forma aleatoria el árbol tiende a tener una forma
cercana a la de uno balanceado. Una manera de garantizar que el árbol mantiene una forma
cercana a la balanceado es la de mejorar el algoritmo de inserción. El lector interesado
puede consultar en [Arango 2006, SEC, 9.3] para hallar mejores algoritmos de inserción.
10.5.4.2 Inserción de un Componente en el Árbol en MAUDE.
En el ejemplo siguiente se define un operador que inserta un componente en el lugar que le
corresponde en el árbol, siguiendo la misma estrategia que el correspondiente en SCHEME.

El operador _in_ inserta en el árbol un entero dado, así:

op _in_ : Int BTreeInt -> BTreeInt .

var T TI TD : BTreeInt . var E ER : Int .


Capítulo 10: Valores y Tipos Compuestos.
328
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) .
Donde:

Maude> rewrite in ARBOL-BINARIO-INT : 12 in A1 .


rewrites: 12 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result BTreeInt: < 7 > < 3 > < 2 > < 1 > nilBTInt nilBTInt nilBTInt < 4 >
nilBTInt < 6 > nilBTInt nilBTInt < 8 > nilBTInt < 15 > < 13 > < 12 >
nilBTInt nilBTInt nilBTInt nilBTInt

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.

Resumen del Capítulo.


Para representar datos tales como un punto en un espacio cartesiano, un número complejo,
o una cola de compradores en un supermercado, es necesario contar con tipos de datos
compuestos que ofrezcan, tanto mecanismos que le permitan acceder y manipular las partes
que lo componen, como operaciones que le permitan operar con el compuesto como un
todo. Estos tipos deben ser, además, abstractos, permitiendo 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.
Los tipos compuestos “estructurados” están caracterizados por que sus elementos son
“tuplas” que tiene un número acotado de componentes, las componentes pueden ser de
diferentes tipos (incluyendo tipos compuestos), se asocian al todo en un “lugar”
determinado, y pueden ser accedidas ya sea por su posición o por un rótulo que las
identifica.
Los lenguajes funcionales usualmente ofrecen mecanismos para: darle categoría de tipo al
conjunto de tuplas que tengan la composición adecuada y cumplan con las “invariantes del
tipo”; construir instancias del tipo a partir de valores para sus componentes; obtener el
valores de sus componentes a partir de una instancia del tipo estructurado, definir
operadores que actúen sobre instancias del tipo; un modo para representar de forma
explícita instancias particulares (o valores base) del tipo. Los lenguajes analizados, sin
embargo, llevan a cabo estas tareas de forma diferente, a saber:
 El lenguaje MIT SCHEME ofrece un conjunto de operadores nativos
orientados a cada una de las tareas, así: el operador make-record-type retorna
un “descriptor” del tipo permitiendo declarar el tipo; el operador record-
predicate retorna un operador que verifica si un elemento pertenece al tipo
(definido por el descriptor); el operador record-constructor retorna un
operador que permite construir una instancia del tipo a partir de valores para
sus componentes; el operador record-accessor retorna un operador que
permite obtener el valor de una componente específica a partir de una
instancia del tipo. Para introducir las invariantes del tipo, sin embargo, es
necesario redefinir o completar el operador obtenido de ejecutar el operador
nativo record-predicate. El lenguaje, por otro lado, no posee mecanismos
nativos para representar de forma explícita un valor de un tipo estructurado.
Capítulo 10: Valores y Tipos Compuestos.
329
 En el lenguaje MAUDE se puede declarar directamente el nombre del tipo
estructurado como un tipo más de la teoría, usando la instrucción sort. Los
constructores, selectores y operadores sobre o con elementos del tipo, no son
otra cosa que operadores definidos de la manera tradicional. Los
constructores se deben declarar con el atributo ctor y no se les debe asociar
reescritura alguna. Una evocación sin variables de un constructor
representa, además, una instancia particular del tipo. Para adicionar las
invariantes del tipo se debe usar una construcción denominada “ecuación de
membresía condicional”.
 En el lenguaje PROLOG las funciones juegan el rol de constructores de
forma similar a los del lenguaje MAUDE. La selección de los elementos
componentes se lleva a cabo por medio de la unificación. No siendo un
lenguaje fuertemente tipado, el SWI PROLOG, requiere al igual que el
lenguaje SCHEME, de la construcción de predicados de tipo al objeto de
verificar las invariantes para una evocación correcta del constructor.
Los tipos compuestos iterados están caracterizados por que sus instancias son conjuntos de
elementos, que tiene un número no determinado de componentes, usualmente del mismo
tipo (pudiendo ser, a su vez, tipos compuestos). A los componentes individuales se puede
acceder por medio de las “vías de acceso”. Entre los componentes del iterado pueden
ocurrir conexiones o “relaciones” formando PAREJAS, que según sus características
determinan que el iterado tenga una estructura en forma de LISTA. ÁRBOL o GRAFO.
Los lenguajes funcionales usualmente ofrecen mecanismos para: darle la categoría de tipo
de dato a todos los conjuntos de valores iterados que tengan la misma estructura de
relaciones, “construir” de forma progresiva valores iterados a partir de componentes
individuales, obtener o “seleccionar” los componentes individuales dentro de una instancia
del iterado, y definir operadores que actúen sobre un iterado visto como un todos.
Los lenguajes analizados ofrecen de forma nativa el tipo LISTA general con un conjunto de
operadores asociados. Para ilustrar las diferencias entre los enfoques de dichos lenguajes,
se presentan códigos asociados con un iterado de tipo LISTA de enteros, que efectúan los
siguientes tipos de tarea:
 Declaración del tipo y definición de constructores.
 Recorridos básicos a la lista para obtener su tamaño, hallar la suma de
términos asociados con los elementos de la lista, y establecer si un valor
específico ocurre o no en la lista.
 Selección de elementos de la lista para obtener uno de los siguientes
elementos: el elemento que aparece en una posición dada o que cumple una
condición específica, el primer elemento que cumple una condición dada en
relación con los que le siguen o le anteceden, y otra lista con los elementos
que cumplen con una condición específica.
 Modificación de la lista para: incluir un nuevo elemento en una posición
dada, borrar el elemento que aparece en una posición dada, excluir los
elementos que tienen una condición específica, eliminar los valores
repetidos, y adicionar al final de una lista los elementos de otra lista.
Capítulo 10: Valores y Tipos Compuestos.
330
 Transformación de la lista para: intercambiar los elementos consecutivos en
desorden y para ordenar la lista por el método de la “burbuja” o el “Quick
Sort”
Finalmente para completar el cuadro sobre los tipos iterados se presentan los elementos que
permiten definir un tipo iterado cuyos elementos sean ÁRBOLES BINARIOS DE
BÚSQUEDA, en los lenguajes analizados.

Ejercicios propuestos
Datos compuestos estructurados.
1- En [Abelson 85, sec. 2.1.1 ay 2.1.2] se definen directamente los constructores y
selectores de un tipo estructurado, con base en pares, sin utilizar el tipo registro ofrecido
pro el SCHEME. Lea las secciones correspondientes y lleve a cabo los ejercicios
siguientes: 2.1, 2.2, 2.3.
2- Aritmética de Intervalos: Realizar basándose en los lenguajes analizados, los siguientes
ejercicios de la sección 2.1.4 en [Abelson 85, sec. 2.1.4]: 2.7, 2.8, 2.9, 2.10, 2.11.
3- Punto en un plano: Elabore en MAUDE y en SCHEME un tipo compuesto estructurado
que represente un punto en un plano. Defina operadores para:
Hallar las coordenadas cartesianas del punto.
Hallar las coordenadas polares del punto.
Hallar la distancia entre dos puntos.
Hallar el área de un rectángulo cuyos vértices son dos puntos dados.
4- Rectángulo en un plano: Elabore en MAUDE y en SCHEME un tipo compuesto
estructurado que represente un rectángulo en un plano con base en dos puntos que
representen dos esquinas diagonalmente opuestas del rectángulo cualquiera que estas
sean. Elabore luego operadores para:
Hallar el área de un rectángulo.
Hallar el rectángulo más pequeño que incluye dos rectángulos dados.
Desplazar el rectángulo una distancia dada representada por un punto.
Deformar el rectángulo con base en un desplazamiento de la esquina superior izquierda
representado por medio de un punto.
Determinar si un punto dado es o no es interior al rectángulo.
4- Rectángulo normalizado: Asuma que los puntos que representan el rectángulo en el
ejemplo anterior, son siempre su esquina superior izquierda y su esquina inferior
derecha. Reelabore para este caso los operadores del ejercicio anterior.
4- Elabore en SCHEME y MAUDE un tipo compuesto estructurado que registre la
información relativa a un estudiante del curso “Lenguajes Declarativos”.
Pares
1- SCHEME: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2.2]: 2.25, 2.26.
Capítulo 10: Valores y Tipos Compuestos.
331
2- Dibuje la representación de cajas de las listas referidas y resultantes en los ejercicios
2.26, 2.27 de [Abelson 85, sec. 2.2.2]
Listas
Definición SCHEME
1-: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2]: 2.17, 2.18, 2.21, 2.22, 2.34.
Definición en MAUDE:
1- Cambie el constructor de listas en MAUDE por el siguiente:
op _ _ : Int ListInt -> ListInt [ctor ] .
Explique el efecto que tendría éste cambio en los ejemplos sobre listas en MAUDE.
Reelabore los ejemplos en MAUDE para que operen con base en el nuevo constructor.
2- Para un operador con el siguiente perfil:
op sel : ListInt -> Int .
Elabore ecuaciones que den como resultado los elementos siguientes:
Primer elemento de la lista.
Suma del primero y tercer elemento de la lista.
Diferencia entre el segundo elemento y el penúltimo.
Suma de las diferencias entre el primero y el último y el segundo y el penúltimo.
El valor del elemento del medio.
Listas de enteros:
Elabore en los lenguajes analizados operadores que lleven a cabo las tareas que se muestran
a continuación. Elabore operadores que lleven a cabo la tarea por medio de un proceso
tanto recursivo como iterativo. Indique el orden de cada una de las soluciones definidas.
1- Dados dos vectores X y Y de N componentes:
X1, X2, X3,....., Xi,..... Xn
Y1, Y2, Y3,....., Yi,..... Yn
Correspondientes a las coordenadas cartesianas de los vértices de un polígono cerrado:
Hallar el perímetro aplicando la fórmula siguiente:
Perímetro = i (Xi+1 – Xi)2 + (Yi+1 – Yi)2
Hallar el área aplicando la fórmula siguiente:
Area = i (Yi * Xi+1 - Xi * Yi+1)
2- Suprima un elemento dado de un vector, contemplando la posibilidad de que dicho
elemento ocurra más de una vez en el vector.
3- Suprima las ocurrencias múltiples de los elementos en una lista, dejando como resultado
una lista con los mismos elementos pero sin ocurrencias múltiples.
Capítulo 10: Valores y Tipos Compuestos.
332
15- Declare un operador podarDerecha, que elimine ocurrencias múltiples en una lista
dejando únicamente la primera ocurrencia, y defina su funcionalidad mediante
ecuaciones.
4- Intercale los elementos de dos listas ordenadas para obtener una nueva lista ordenada con
los elementos de las dos listas originales.
5- Separe los elementos de una lista en los que son menores que un valor dado (el umbral),
y los que son mayores o iguales que ese valor dado, dejando como respuesta dos listas,
una con los mayores que el umbral y otra con los menores que el umbral.
6- Suprimir un elemento de un vector, dada su posición en el vector.
7- Incluir un elemento en un vector delante del elemento que tiene una posición dada.
8- Sumarle a todos los elementos de un vector hasta una posición dada, un valor dado.
9- Sumarle un valor dado al primer componente de un vector que tenga un valor mayor que
otro valor dado.
14- Obtenga el valor de las fórmulas del ejercicio 5 del Capítulo 8, pero considerando que
los valores de x son obtenidos de forma sucesiva de una lista de valores dados.
11- Reconozca la existencia en una lista de un segmento dado, dejando como respuesta la
posición del carácter donde aparece el segmento.
13- Coloque la primera mitad de la lista luego de la segunda sin cambiar el orden de los
elementos de cada mitad.
Listas de Registros:
Para una lista de componentes, cada uno de ellos representando a un estudiante del curso
“Lenguajes Declarativos”, lleve a cabo las tareas siguientes en los diferentes lenguajes
analizados:
1- Defina el tipo de dato con su correspondiente constructor.
2- Elabore operadores para lleva a cabo las tareas siguientes.
Hallar el promedio de notas de los estudiantes del curso.
Hallar la diferencia entre el promedio de notas de los estudiantes que están viendo el
curso por primera vez, y los estudiantes que están repitiendo el curso.
Seleccionar los M estudiantes con mejores notas, para un valor de M suministrado.
Para una lista de componentes, cada uno de ellos representando un punto en el plano, lleve
a cabo las tareas siguientes en los diferentes lenguajes analizados:
1- Defina el tipo de dato con su correspondiente constructor.
2- Elabore operadores para lleva a cabo las tareas siguientes.
Hallar el área y el perímetro de la poligonal cerrada formada por los puntos, asumiendo
que el último tramo de la poligonal une el último punto con el primero.
Determine si un punto dado se halla al interior de la poligonal cerrada formada por los
puntos.
Capítulo 10: Valores y Tipos Compuestos.
333
Listas de listas
1-: Realice los ejercicios siguientes de [Abelson 85, sec. 2.2]: 2.27, 2.28, 2.32.
2- Cree un constructor en MAUDE que le permita representar listas de listas, y lleve a cabo
los ejercicios del punto anterior con base en dicha representación.
Árbol Binario de Búsqueda.
1-.Desarrolle un operador, que cuente el número de nodos de un árbol binario de búsqueda.
2-.Desarrollar un operador que tome como argumentos un árbol y un identificador. En caso
de que el identificador se encuentre en el árbol, lo elimine del árbol. Hay que tener
cuidado con las posibles ramas que se desprenden del nodo a eliminar, estas deben ser
reubicada. La función debe retornar el árbol reorganizado.
3- Desarrolle un operador de inserción que mantenga el árbol balanceado.
1. Considere la definición de los predicado prefijo, sufijo y sublista que se muestran
a continuación:
prefijo(P, L) :- append(P, _, L) .
sufijo(S, L) :- append(_, S, L) .
sublista(SubL, Lista) :- prefijo(P, Lista), sufijo(SubL, P) .
Construir el árbol de búsqueda para la consulta :- sublista([2,3,4],[0,1,2,3,4,5]).

2. Definir un predicado extraer que tome tres argumentos (X, L1, L2) que se satisfaga
cuando L2 sea el resultado de extraer todas las ocurrencias de X en L1.

3. Considere 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) .
Construir el árbol de búsqueda para la consulta agregar(3,[1,2,3,4],[1,3,2,3,4]).

4. Elabore dos programas PROLOG que obtengan el factorial de un número entero dado, el
primero debe definir un proceso recursivo y el segundo un proceso iterativo. Construir dos
árboles de búsqueda para la consulta factorial(4,F), basándose en cada una de las
implementaciones elaboradas.
Capítulo 10: Valores y Tipos Compuestos.
334
5. Resolver el taller de listas que se encuentra en la página asociada al texto, utilizando
predicados definidos en PROLOG.

6. Considere la siguiente implementación del QickSort:


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

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


partir([A], [A], []) .
partir([A,B|T], [A|Ta], [B|Tb]) :- partir(T, Ta, Tb) .

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

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


Construya el árbol de búsqueda para la consulta quicksort([7,3,9,10],R).(Si considera
que puede predecir adecuadamente el resultado de partir o merge, no es indispensable que
desarrolle la totalidad del árbol).
Capítulo 11
Programación Paramétrica y
Relaciones entre Tipos
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
336
Introducción
Los operadores pueden verse como patrones de proceso aplicables a las múltiples
combinaciones posibles de valores para sus operandos. 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 operador. En efecto, es posible abstraerse también de los operadores
particulares y de los tipos de los valores particulares, involucrados en la definición misma
de operador.
La utilidad de abstraer el tipo de los valores y los operadores usados, 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; así, 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. Además, si fuera posible
abstraerse del criterio de escogencia, al definir los operadores de selección de elementos, se
evitaría tener que escribir de forma explícita un operador para cada criterio
La utilidad de abstraerse de los operadores particulares usados en la definición de un
operador, 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 21,
sección 9.8): 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 redefinirlo cada vez que cambie la función.
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.

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. 8.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.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
337
11.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
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.

El operador largo definido en la sección 9.4.3.1 puede aplicarse tanto a listas de enteros como a listas de
vectores (ver sec. 9.2.1.1), así:

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.

El operador largo definido en la sección 9.4.3.1 puede aplicarse tanto a listas de enteros y vectores (ver
sec. 9.2.1.1), así:

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
sobrecarga de operadores (ver 8.5.2). Así, cuando a los elementos de una lista se les deba
aplicar una operación que dependa del tipo, se debe, ya sea escribir un operador específico
para las listas de cada tipo, o averiguar dentro de una función genérica aplicable a varias
listas, por el tipo del componente para aplicar la operación asociada con el tipo164.

164 Fraccionar una aplicación que manipula diversos tipos de datos, en un conjunto de funciones genéricas que aplican un
“mismo” proceso a todos los datos de la aplicación (v.g. lectura de datos -> validación-de datos -> cálculos ->
presentación de resultados), constituye una “descomposición funcional del programa”, que separa en diversos módulos los
tratamientos que se realizan a un tipo específico de datos. Modernamente se prefiere descomponer las aplicaciones por
módulos que reúnen las funciones que llevan a cabo todos los tratamientos de un tipo de datos. A esta descomposición se
le denomina “descomposición por tipos de dato” y conduce a la “descomposición por objetos”.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
338

Un operador que le suma un valor a las componentes de una lista de Vectores, debe usar un operador
específico para las listas de Vectores., así:

(define (sume-valor-lista-vector LV V)
(if (null? LV) nil
(cons (sume-valor-vector (car LV ) V ) (sume-valor-lista-vector (cdr LV ) V ) )
)
)
Donde la función sume-valor-vector, es la adecuada al tipo Vector (que el lector deberá elaborar por
su propia cuenta).
Un operador genérico aplicable a varias listas deberá aplicar la operación adecuada al tipo de
componente. Así, un operador que le suma un valor a las componentes de una lista mixta de enteros y
Vectores, debe usar el operador adecuado en cada caso, preguntando, de forma explícita, por el tipo del
componente.

(define (sume-valor-lista LV V)
(if (null? LV) nil
(cond
( (Vector? (car LV ))
(cons (sume-valor-vector (car LV ) V ) (sume-valor-lista (cdr LV ) V ) )
)
( (integer? (car LV ))
(cons (+ (car LV ) V ) (sume-valor-lista (cdr LV ) V) )
)
)
)
)

11.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.
11.2.2.1 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.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
339
La definición de los operadores sum_pi_rec(n) y cont_frac_rec(n), presentados en la sección 9.3.1
para calcular el valor de las fórmulas siguientes.

1 1
 (4n  3)(4n  1) y
2
n 1
12 
3
22 
3 
2

Siguen un patrón típico de los procesos que denominamos de “acumulación”, tratados en la sección
9.3.1.
Este patrón puede observarse fácilmente en las definiciones, si se tiene en cuenta que éstas sólo difieren
en la parte que se muestra en negrita a continuación.

(define (sum_pi_rec k n)
(if (> k n) 0
(+ (/ 1 (* (- (* 4 k) 3) (- (* 4 k) 1))) (sum_pi_rec (+ k 1) n)
)
)
)

(define (cont_frac_rec k n)
(if (> k n) 0
(/ k (+ (* k k) (cont_frac_rec (+ k 1) n)))
)
)
Es posible, entonces, pensar que un operador genérico de acumulación, podría capturar lo que es común
a dichos procesos, y que para especializarlo a cada proceso de acumulación particular, bastaría
suministrarle en una evocación, como argumento, la parte que los hace diferentes.

Para capturar la parte común a todos los operadores de “acumulación”, podemos construir
un operador genérico de acumulación, parametrizado en la parte que hace cada
acumulación diferente de las demás.

Para obtener un operador genérico de acumulación, aplicable a los dos casos del ejemplo anterior, basta
con abstraer la parte común a las acumulaciones presentadas, parametrizándola por medio de un
operador, así:

(define (acumule-rec k n f )
(if (> k n) 0
(f k (acumule-rec (+ k 1) n)
)
)
)
Donde la variable f es usada como operador.
Para obtener las acumulaciones deseadas se deben definir los operadores adecuados a cada caso y darlos
como argumento al evocar el operador genérico, así:

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


(define (aux-frac k S) (/ k (+ (* k k) S) )
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
340
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
9.3.1.1.
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.

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

11.2.2.2 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
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
341
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.
Un operador creado por medio de una expresión lambda puede ser evocado como
cualquier otro operador.

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.

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

11.2.2.3 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.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
342
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.

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.

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.

Abstracción de teorías en MAUDE.


El lenguaje MAUDE provee un mecanismo de abstracción que 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 8.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ón165.
Un módulo parametrizado puede ser instanciado asociando cada uno de sus (diferentes)
parámetros con un módulo real a ser incluido. MAUDE requiere, sin embargo, que un
módulo real incluido a través de un parámetro satisfaga una serie de condiciones definidas
para el parámetro. Las condiciones definidas para el parámetro son descritas en una

165La abstracción de módulos incluidos permite también la abstracción de rótulos asociados con axiomas (ver [Clavel
2007, secs 4.5.2]).
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
343
“teoría”. Así, cada parámetro de un módulo parametrizado se asocia con una teoría que
debe satisfacer el modulo real incluido, permitiendo que varios parámetros se asocien con
la misma teoría pero (por ser únicos) cada parámetro se asocia con una sola teoría. La
teoría define, además, los rótulos que serán usados en el módulo inclusor para hacer
referencia a los elementos del módulo incluido.
Para indicar la manera como los elementos de un módulo real satisfacen las condiciones de
una teoría, se debe crear una “vista” de la teoría sobre el módulo real. Entonces al
instanciar un módulo parametrizado se substituye cada parámetro con el nombre de una
vista de la teoría del parámetro sobre el modulo real. La vista define, además, cuales son
los elementos específicos del módulo real a los que se hace referencia con los rótulos
definidos en la teoría.
Como ejemplo de la parametrización en MAUDE, presentamos a continuación los
elementos básicos de la especificación de un módulo paramétrico, en el que se define una
lista genérica con sus operaciones respectivas usando la lista que se definió en el capítulo
anterior de forma específica para los enteros. Este módulo puede ser, en consecuencia,
utilizado para crear listas de elementos de cualquier tipo. Para una especificación completa
de la lista genérica nativa en MAUDE el lector debe consultar a [Clavel 2007, sec 7.12.4].

La declaración de la lista genérica sigue los alineamientos presentados en el capítulo anterior, pero
teniendo en cuenta los elementos asociados con la parametrización para hacer genérico el elemento de la
lista, así:

fmod LISTA{X :: TRIV} is


protecting BOOL .
protecting INT .
sort Lista{X} .
subsort X$Elt < Lista{X} .
op nilList : -> Lista{X} [ctor] .
op _ _ : Lista{X} Lista{X} -> Lista{X} [ctor assoc id: nilList] .

vars I : Int .
vars E H : X$Elt .
vars L L1 L2 : Lista{X} .

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


eq | nilList | = 0 .
eq | E L | = 1 + | L | .

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


op error-en-indice : -> X$Elt .
eq (E L)[1] = E .
ceq (E L)[I] = L[I + (- 1)] if(I =/= 1) .
eq nilList[I] = error-en-indice .

op {*Xen_/X<_} : Lista{X} X$Elt -> Lista{X} .


op {*Xen_/X<_it_} : Lista{X} X$Elt Lista{X} -> Lista{X} .
eq { *Xen L1 /X< E } = { *Xen L1 /X< E it nilList } .
eq {*Xen nilList /X< E it L1 } = L1 .
ceq {*Xen (H L1) /X< E it L2 } = { *Xen L1 /X< E it L2 } if(H >= E) .
ceq {*Xen (H L1) /X< E it L2 } = { *Xen L1 /X< E it (H L2) } if(H < E) .
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
344
endfm

Donde: X es el parámetro que representa el módulo a ser incluido, y es usado para cualificar, dentro del
módulo parametrizado, a los elementos del módulo incluido. Por ejemplo X$Elt hace referencia a un sort
del módulo incluido a través del parámetro X. TRIV es la teoría que determina las condiciones que deben
satisfacer los elementos del módulo incluido a través de X, siendo Elt un rótulo definido en la teoría para
ser asociado con un sort del módulo incluido. Nótese que la vista indicará cual es el sort del módulo real
incluido al que se hará referencia con el rótulo Elt, y si existiera más de un parámetro asociado con la
teoría TRIV, Elt podría hacer referencia a dos sort diferentes, para parámetros diferentes.
Lista{X} es un sort definido en el módulo parametrizado y es cualificado con X debido a que
representará diferentes sorts para instancias diferentes del parámetro X.
Para obtener una lista de enteros, basta crear un módulo que instancie el módulo paramétrico usando una
vista de TRIV, sobre los enteros así:

fmod LISTA-INT is
protecting LISTA{Int} .
endfm
Donde Int es el nombre de una vista de TRIV sobre INT.

Maude> rewrite in LISTA-INT : 1 2 4 .


rewrites: 0 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result Lista{Int}: 1 2 4

Maude> rewrite in LISTA-INT : | 1 2 3 | .


rewrites: 7 in 1628036047000ms cpu (0ms real) (0 rewrites/second)
result NzNat: 3

Tanto la vista Int como la teoría TRIV están incluidas en el archivo preludio.maude (ver ¡Error! No se
encuentra el origen de la referencia.) de MAUDE [Clavel 2007, sec 6.3], y se reproducen a continuación:

fth TRIV is
sort Elt .
endfth

view Int from TRIV to INT is


sort Elt to Int .
endv
El lector puede notar que la teoría TRIV sólo impone la existencia del sort Elt, que es proyectado en la vista
al sort Int (es costumbre usar como nombre de la vista, el del sort que representa). La vista Int, por su parte
sólo requiere proyectar Elt a Int ya que los operadores de igual nombre (<,>,>=,<=) se proyectan por
defecto)

En las secciones que siguen se describe en más detalle, los elementos que involucra la
parametrización de los módulos, resumiendo el contenido y usando los ejemplos del
manual de MAUDE [Clavel 2007, sec 6.3]. Para una descripción más detallada se remite al
lector a dicho documento.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
345
11.3.1 Parametrización de módulos.
Un módulo funcional parametrizado se define de la manera siguiente:
fmod <nombre>{<lista_de_parametros>} is <cuerpo_del_modulo> endfm
Donde:
 <nombre> Es el identificador del módulo, usualmente colocado en letra
capital.
 <lista_de_parametros> Es una lista de <Parametro> separados por coma “,”.
 <cuerpo_del_modulo> Es el cuerpo de un módulo funcional tal como fue
descrito en la sección 8.8.2.1.
Cada <Parametro> es una construcción de la forma:
<Xi> :: <Ti>
Donde:
 <Xi> Es el identificador del parámetro en la posición i de la lista de
parámetros.
 <Ti> Es el identificador de la teoría asociada al parámetro en la posición i en
la lista de parámetros.
Los identificadores Xi de los parámetros deben ser únicos en la lista de parámetros. Los
identificadores Ti de las teorías asociadas a los parámetros pueden, por su parte, repetirse.
De esta manera, cada parámetro es usado para incluir un módulo diferente, pero pueden
incluirse varios módulos diferentes bajo las condiciones definidas en una misma teoría.
En el <cuerpo del módulo> pueden ser usados los operadores y sorts definidos en las
teorías asociadas a los parámetros. Sin embargo, cuando se usan los nombres de los sort
definidos en una teoría, ellos deben cualificarse con el nombre del parámetro asociado con
dicha teoría. Así, si dentro del módulo parametrizado se desea usar el sort S definido en la
teoría T, asociada con el parámetro de posición i, se debe hacer referencia a éste sort con el
nombre Xi$S, siendo Xi el nombre del parámetro de posición i.
Nótese que cuando la teoría T aparece asociada, tanto al parámetro de posición i como al
parámetro de posición j, a un sort S definido en T, se le hace referencia tanto por medio del
nombre Xi$S como por medio del nombre Xj$S dentro del módulo parametrizado. Esto no
implica que se esté haciendo referencia al mismo sort de dos formas distintas, ya que las
teorías son sólo un medio para definir las condiciones que deben satisfacer los módulos
incluidos a través de los parámetros. Es, por tanto, posible que se incluyan dos módulos
diferentes a través de parámetros diferentes, bajo la misma teoría. Y si éste es el caso de la
teoría T y de los parámetros de posición i y j, los nombres Xi$S y Xj$S estarán haciendo
referencia a sorts definidos en dos módulos importados completamente diferentes (el uno
importado con el parámetro Xi y el otro importado con el parámetro Xj, ambos satisfaciendo
la teoría T)
Por otro lado, un módulo parametrizado puede ser incluido en otro módulo más de una vez,
instanciándolo de diferentes formas (es decir incluyéndole a través de los parámetros
diferentes módulos). Entonces, si se tiene en cuenta que un sort declarado dentro de un
módulo parametrizado no es, en general, el mismo para diferentes instancias del módulo
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
346
(debido a que puede depender de los parámetros), se hace necesario poder distinguir este
sort para diferentes instancias del módulo parametrizado que se incluyan en otro módulo.
Para distinguir entre estos sorts basta con cualificar el nombre del sort, primero dentro del
módulo parametrizado con el nombre de los parámetros de los que depende, en todas las
ocurrencias de dicho nombre, y segundo con cualificar el nombre del sort dentro de los
módulos que incluyen instancias del módulo parametrizado, con el nombre de las vistas con
que se instancian los parámetros de los que depende el sort. Un sort depende de un
parámetro si se relaciona con un sort de la teoría asociada al parámetro (v.g. existe una
relación de subsort con dicho sort, o en el dominio de los constructores del sort ocurre el
sort de la teoría).

Los módulos funcionales que se muestran a continuación, basados en ejemplos de [Clavel 2007, sec
6.3.3], ilustran la manera de especificar módulos funcionales parametrizados.
En el primer módulo se ilustra el uso de parámetros para definir el tipo de los elementos de un conjunto.

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 (Set) depende
del parámetro X debido a que es superconjunto del sort X$Elt de X . En consecuencia el sort Set es
cualificado con el único parámetro del módulo (Set{X}). Así, de incluirse conjuntos de dos tipos de
elementos diferentes, instanciando el sort parametrizado anterior (con dos vistas diferentes), los sort Set
de los dos tipos de conjunto se distinguirán por estar cualificados con la vista. En el módulo siguiente se
incluye dos veces el módulo parametrizado anterior, instanciado con la vista int y con la vista float, y las
dos versiones del sort Set definido en dicho módulo, se distinguirán por estar cualificados con la vista:

fmod X is
protecting SET{int} .
protecting SET{float} .

op opx : Set{int} Set{float} -> Set{float} .

endfm
En el segundo módulo se ilustra el uso de más de un parámetro bajo la misma teoría, así:

fmod 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 .
...
...
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
347
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.
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.

11.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ódulo166 . 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 8.8.2.1, 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.

166 Pueden ser usados al nivel del metalenguaje para llevar a cabo demostraciones de teoremas de forma controlada.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
348
Con el objeto de ilustrar el uso de las teorías y sus relaciones, reproducimos aquí las teorías
presentadas en [Clavel 2007, sec 6.3.1], que ilustran la definición de los operadores de
orden _<_ y _<=_ para conjuntos total y parcialmente ordenados.

La teoría SPOSET define el operador _<_ como un operador irreflexivo, antisimétrico y transitivo, por
medio de axiomas no ejecutables.

fth SPOSET i s
protecting BOOL .
sort Elt .
op _<_ : Elt Elt -> Bool .
vars X Y Z : Elt .
ceq X < Z = true if (X < Y /\ Y < Z) [nonexec label transitive] .
ceq X = Y if (X < Y /\ Y < X) [nonexec label antisymmetric] .
eq X < X = false [nonexec label irreflexive] .
endfth
La teoría POSET adiciona a SPOSET el operador _<=_ como un operador irreflexivo, definido en
términos de los operadores definidos antes.

fth POSET is
including SPOSET .
op _<=_ : Elt Elt -> Bool .
vars X Y : Elt .
eq X <= X = true [nonexec] .
ceq X <= Y = true if(X < Y) [nonexec] .
ceq X = Y if( X <= Y /\ X < Y = false) [nonexec]
endfth
La teoría TOSET adiciona un axioma que garantiza que el orden sea total.

fth TOSET is
including POSET .
vars X Y : Elt .
ceq X <= Y = true if(Y <= X = false) [nonexec label total] .
endfth
Nótese que los axiomas incluidos en las teorías anteriores no participan en procesos de reescritura. Ellos
sólo afirman las propiedades que deben cumplir los operadores de orden a ser incluidos en un módulo
parametrizado, por medio de un parámetro asociado con la teoría.

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


prelude.maude (ver ¡Error! No se encuentra el origen de la referencia. ).

La teoría elemental TRIV (tomadas de [Clavel 2007, sec 7.11.1]) no impone condición alguna a los
miembros de su único sort, el sort Elt:

fth TRIV
sort Elt .
endfth
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
349
La teoría DEFAULT (tomadas de [Clavel 2007, sec 7.11.2]) adiciona a TRIV un elemento distinguido
para el sort Elt:

fth DEFAULT is
including TRIV .
op 0 : -> Elt .
endfth
Las teorías STRICT-WEAK-ORDER y STRICT-TOTAL-ORDER (tomadas de [Clavel 2007, sec
7.11.3]), define las propiedades del operador _<_ para conjuntos débilmente y totalmente ordenados:

fth STRICT-WEAK-ORDER is
protecting BOOL .
including TRIV .
op _<_ : Elt Elt -> Bool .
vars X Y Z : Elt .
ceq X < Z = true if( X < Y /\ Y < Z) [nonexec label transitive] .
eq X < X = false [nonexec label irreflexive] .
ceq X < Y or Y < X or Y < Z or Z < Y = true if( X < Z or Z < X) [nonexec label
incomparability-transitive] .
endfth

fth STRICT-TOTAL-ORDER is
including STRICT-WEAK-ORDER .
vars X Y : Elt .
ceq X = Y if(X < Y = false /\ Y < X = false) [nonexec label total] .
endfth

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

11.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:
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
350
view <nombre> from <fuente> to <destino> is
<mapeo>
endv

Donde:
 <nombre> Es el identificador de la vista, donde es costumbre usar el mismo
nombre asociado con <destino>167.
 <fuente> Es el nombre asociado a una teoría, o una expresión que evalúe a
una teoría. En lo que sigue nos referiremos a ella como la fuente.
 <destino> Es el nombre asociado a un módulo o a una teoría, o una
expresión que evalué a uno de ellos. En lo que sigue nos referiremos a este
módulo o teoría como el destino.
 <mapeo> Es una serie de <expresiones-de-mapeo> separadas por
espacios,
Las <expresiones-de-mapeo> proyectan los sorts y operadores de la teoría referida en
<fuente> a los sorts y operadores del módulo o teoría referida en <destino>.
La proyección de un sort se define por medio de una expresión de la forma siguiente:
sort <nombre-fuente> to <nombre-destino> .

Donde:
 <nombre-fuente> Es el nombre de un sort de la fuente.
 <nombre-destino> Es el nombre de un sort del destino.
Dada la proyección de un sort de la fuente a un sort del destino, el sort del destino será
representado por el sort de la fuente, tanto dentro de la fuente como dentro de los módulos
parametrizados que incluyen el destino a través del parámetro asociado a la fuente. Los
sorts que no aparezcan en el mapeo se asumen proyectados a los sorts de igual nombre.
Para cada sorts de la fuente debe existir un sort correspondiente en el destino. Además si el
sort S es subsort del sort T en fuente, los sorts correspondientes en el destino S´ y T´ deben
mantener la misma relación de subsort. Así, si dos sort S y T de la fuente pertenecen a un
mismo “kind” (ver [Clavel 2007, sec 3.5]) entonces los sort correspondientes del destino
deben asimismo pertenecer a un mismo kind.
La proyección de un operador se define por medio de una construcción de la forma
siguiente:
op <plantilla-fuente> to <plantilla-destino> .

op <plantilla-fuente> : <sorts-fuente-dominio> -> <sort-fuente-rango>


to <plantilla-destino> .
op <termino-fuente> to term <termino-destino> .

167Es decir pueden existir vistas con el mismo nombre de módulos sin que se produzca ambigüedad. No deben existir, sin
embargo, nombres de parámetro con el mismo nombre de vistas ya que ambos pueden usarse para instanciar módulos
parametrizados creando ambigüedad.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
351
Donde:
 <plantilla-fuente> Es la plantilla de un operador declarado en la fuente.
 <plantilla-destino> Es la plantilla de un operador declarado en el destino.
 <sorts-fuente-dominio> Son los sorts que constituyen en la fuente el
dominio del operador.
 <sorts-fuente-rango> Es el sorts que constituyen en la fuente el rango del
operador.
 <termino-fuente> Es un término con un sólo operador definido con base en
un operador y variables de la fuente.
 <termino-destino> Es un término definido con base en operadores y
variables del destino. Las variables de <termino-destino> deben, sin
embargo, aparecen en <termino-fuente>.
Dada la proyección de un operador de la fuente a un operador del destino, el operador del
destino será representado por el operador correspondiente en la fuente, tanto dentro de la
fuente como dentro del módulo parametrizado que incluye el destino a través del parámetro
asociado a la fuente, así:
 La primera de las construcciones afecta a todos los operadores
sobrecargados con las plantillas referidas. Así, un operador con la plantilla
<plantilla-fuente> de la fuente se proyecta al operador con <plantilla-
destino> en destino, cuyos sorts de dominio y rango correspondan bajo el
mapeo de los sort.
 La segunda construcción permite el mapeo del operador con el dominio y
rango especificados168.
 La tercera construcción permite proyectar un operador de la fuente a un
término en el destino. El sort de una variable en <termino-destino> debe
ser el sort al que se proyecta el sort de la misma variable en <termino-
fuente>. Además, el sort (o kind) del término <termino-fuente> en la
fuente debe estar proyectado al sort (o kind) del término <termino-destino>
en el destino. Las variables usadas en los términos <termino-fuente> y
<termino-destino>, pueden ser declaradas en la vista con el sort de la
fuente, siendo implícita, por la proyección de los sorts, la declaración de la
variable correspondiente al sort del destino.
Los operadores que no aparezcan en el mapeo se asumen proyectados a los operadores de
igual nombre, dada su correspondencia de dominio y rango bajo la proyección de los sorts.
Cada operador de la fuente o de una subteoría de la fuente (una teoría importada en la
fuente) debe tener un operador correspondiente en el destino. Los operadores definidos en
submódulos de la fuente (un módulo importado en la fuente) no pueden, sin embargo, ser
mapeados por medio de la vista al destino169. Entre los operadores correspondientes de la

168En [Clavel 2007, sec 6.3.2] se refiere además, que esta forma “afecta no sólo al operador con dicha aridad y coaridad,
sino a toda la familia de operadores sobrecargados con subsorts” (ver 8.5.2.1). No presenta, sin embargo, ejemplo alguno
de esta circunstancia.
169Nótese que los módulos importados en la fuente pueden tener operadores declarados y definidos (por reescritura) que,
a diferencia de los operadores declarados en la teoría (o las teorías incluidas), no pueden ser redefinidos en los módulos
destino de la vista (por otras formas de reescritura). De hacerlo se genera un mensaje de advertencia.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
352
fuente y el destino, se debe conservar tanto la aridad del operador, como los sorts de su
dominio y rango, dada la proyección de los mismos. Otras restricciones al mapeo de
operadores pueden verse en [Clavel 2007, sec 6.3.2].
Por cada vista se debe cumplir que los axiomas de la fuente, interpretados en el destino bajo
las proyecciones definidas en la vista, se cumplen en el destino. La satisfacción de estos
axiomas (denominados “prof. obligations”), no es, sin embargo, verificada por el
intérprete.
Así como MAUDE ofrece de forma nativa en el archivo prelude.maude, una serie de
teorías, ofrece también una serie de vistas a dichas teorías.

La teoría TRIV se proyecta a una serie de módulos nativos [Clavel 2007, sec 7.11.1]. Así, la vista
siguiente:

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.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
353
11.3.4 Creación de instancias de módulos paramétricos.
Dado un módulo parametrizado, basta sustituir sus parámetros por vistas de otros módulos
a las teorías respectivas, para obtener un módulo que puede ser usado en procesos de
reescritura o incluido en otro módulo.

Para obtener un modulo que defina un conjunto de enteros, basta con substituid en el módulo SET{X ::
TRIV}, definido arriba, el parámetro por la vista que proyecta el modulo de los enteros a la teoría TRIV.
Así, la especificación siguiente:
..
protecting SET{Int} .
..
Incluye un módulo que define conjuntos de enteros, mientras que la especificación siguiente:
..
protecting SET{String} .
..
Incluye un módulo que define conjuntos de strings.
Nótese que en ambos casos, el parámetro real con que se instancia al módulo parametrizado, es una vista
de un módulo a la teoría asociada al parámetro formal. Esta vista fue, de forma arbitraria, nombrada
igual que el módulo por lo que, en apariencia, el parámetro real es el módulo mismo. El lector no debe,
sin embargo, olvidar que la vista es el medio para indicar la manera como el módulo satisface las
condiciones del parámetro por lo que el parámetro real debe ser la vista del módulo a la teoría y no el
módulo mismo.

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

Para incluir en un módulo parametrizado otro módulo parametrizado, instanciado con parámetros del
módulo que lo incluye, es suficiente que los parámetros correspondientes se asocien con la misma
teoría.
Así, en el módulo siguiente:

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
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
354
las vistas asociadas a los parámetros correspondientes.

Puede ser útil, sin embargo, que las teorías asociadas con los parámetros del módulo
inclusor sean diferentes a las teorías que se asocian con los parámetros del módulo
incluido. Este es el caso de un módulo incluido cuyos parámetros se asocian con subteorías
de las asociadas a los parámetros del módulo inclusor.
Para resolver el conflicto de teorías, se debe usar una vista de las teorías del módulo
incluido a las correspondientes teorías del módulo inclusor. Estas vistas garantizan que las
teorías del modulo incluido son, en efecto, subteorías de las correspondientes en el módulo
inclusor. El modulo incluido se instancia, entonces, con las vista entre teorías, dando como
resultado un módulo incluido que, ahora, es parametrizado con las teorías del módulo
inclusor, y puede, en consecuencia instanciarse usando sus parámetros.

En el módulo siguiente tomado de [Clavel 2007, sec 6.3.4]170 se extiende el módulo SET{X::TRIV}
referido en los ejemplos anteriores al módulo SET-MAX{X::TOSET}, para implementar un operador
que obtenga el máximo de los valores del conjunto.

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 TRIV a
TOSET (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 (v.g. Set{..}), al usarse en el inclusor, se
cualifican con los nombres de los parámetros usados en la instanciación (v.g. Set{TOSET}{T} ). Con
ello se evitan conflictos que podrían surgir si se usara más de una instancia del módulo incluido.

170 Se toma incompleto para inducir al lector a consultar la referencia.


Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
355
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
inclusor171[Clavel 2007, sec 6.3.4].

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.

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

171 Esta restricción parece ligarse al intérprete, más bien que al leguaje mismo.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
356
Relaciones entre Tipos.
La relación de contención entre tipos es declarada en MAUDE por medio de la instrucción
subsort (ver 8.5.2.1 ).
Si bien la relación de subsort fue utilizada en 10.4.2.2 para declarar las componentes
elementales de la estructura iterada lista, ella puede también ser utilizada para definir
operadores asociados a funciones parcialmente definidas sobre un tipo, darle apoyo a la
recuperación de errores, indicar de forma más precisa el efecto de aplicar operadores (ya
definidos) a subconjuntos del tipo, y definir tipos con los elementos de un tipo ya existente
que satisfagan alguna condición.
En esta sección se presentan estos usos para la relación de subsort.
11.4.1 “Kinds” y Gestión de Errores.
La instrucción subsort permite definir jerarquías de contención entre tipos.

La declaración siguiente:

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 errados172 siendo
indefinido el sort al que pertenecen. MAUDE permite estos términos concediéndoles el
“beneficio de la duda” en el sentido de que si al ser simplificados pertenecen a un sort
definido, se consideran correctos.

172Por ejemplo un término que tiene un operando de un supersort del sort prescrito para el operador. Vg. 4/0 tiene un
divisor del sort Nat (naturales) debiendo ser NzNat (naturales diferentes de cero) que es un subsort de Nat.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
357
Es posible, además, efectuar simplificaciones al nivel del kind, de tal manera que términos
errados pueden convertirse a términos de error definidos para facilitar las operaciones de
depuración del programa. Para hacer referencia a un kind basta colocar entre parénesis
cuadrados ([ ..]) el nombre de un sort del kind o una lista de nombres de sorts del kind.
MAUDE al hacer referencia a un kind usa la lista de los sorts maximales del kind.
11.4.2 Sobrecarga de operadores en subtipos.
Dada la posibilidad de sobrecargar los operadores (ver 8.5.2.3.3 ), es posible distinguir dos
tipos de sobrecarga en el contexto de las relaciones de subtipo.
El primer tipo de sobrecarga ocurre cuando los sorts del dominio no están relacionados (no
pertenecen al mismo kind) con los correspondientes de la declaración original (ver Ejemplo
8 ). Este tipo de sobrecarga es denominado “sobrecarga ad-hoc” (“ad-hoc overloading”
[Clavel 2007, sec 3.6]).
El segundo tipo de sobrecarga ocurre cuando los sorts del dominio son subsorts de los
correspondientes sorts en la declaración original. Este tipo de sobrecarga es denominado
“sobrecarga de subsorts” (“subsort overloading” [Clavel 2007, sec 3.6]).
La razón de usar sobrecarga de subsorts, es poder adicionar restricciones al
comportamiento del operador original para el caso de argumentos más específicos (los del
subsort).
Para evitar expresiones ambiguas, MAUDE exige que al sobrecargar un operador, si los
correspondientes argumentos del dominio pertenecen al mismo kind, entonces los
argumentos del rango deben, también, pertenecer al mismo kind173.
Cuando un operador es una sobrecarga de subsorts de otro operador declarado, los atributos
de la declaración, con excepción del atributo ctor y atributos con metadatos (ver [Clavel
2007, sec 4.5.2]), deben ser los mismos que los de la declaración original. Para evitar
reescribir estos atributos se puede usar el atributo ditto. El atributo ditto indica que el
operador tiene los mismos atributos (excepto com y atributos con metadatos) que la
declaración original.
En MAUDE es posible sobrecargar las constantes. Sin embargo, para evitar ambigüedades
al momento de usarlas como argumentos reales, ellas (y en general todos los términos)
pueden cualificarse con el sort al que pertenecen.
Para cualificar un término se debe usar una expresión de la forma siguiente:
(<termino>).<sort> .
Donde:
 <termino> Es el término a ser cualificado con el sort.
 <sort> Es el sort que cualifica al término.
11.4.3 Preregularidad
Bajo la relación de subsort, un término puede pertenecer a varios sorts. Así:

173 Pudiendo ser los kind de cada pareja diferentes.


Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
358
 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.

El siguiente ejemplo, tomado de [Clavel 2007, sec 3.8] muestra esta situación, así:

sorts A B C D .
subsorts A < B C < D .
op a : -> A .
op f : B -> B .
Donde el término f(a) tiene sorts B, C, D siendo minimales tanto B como C.

La propiedad de una signatura de que todo término bien formado tenga un solo sort
minimal se denomina “preregularidad”. La preregularidad es una condición deseable en
una especificación y es verificada por MAUDE, generando advertencias cuando no es
satisfecha174.
11.4.4 Ecuaciones de Membresía y de Membresía Condicional
En los capítulos anteriores la relación de subsort fue utilizada para declarar que los
elementos de un sort MAUDE ya creado, hacían parte de los elementos de uno de sus
supersorts. El objeto de esta declaración fue el de definir los elementos del nuevo supersort
con base en los elementos del sort ya creado. En particular, esta fue la manera como se
construyó la lista en 10.4.2.2.
Es posible, sin embargo, declarar también que un subconjunto de los elementos de un sort
ya creado, constituye los elementos de un subsort a ser definido. Para llevar a cabo esto
MAUDE ofrece dos tipos de construcciones: la ecuación de membresía y la ecuación de
membresía condicional.
La ecuación de membresía y la ecuación de membresía condicional, permiten declarar que
algunos términos específicos de un sort determinado, por ellos mismos o cuando satisfacen
una condición definida, constituyen (o dan como resultado) elementos de un subsort del
sort que les correspondía originalmente.

174Tal como se indica en la preregularidad es verificada teniendo en cuenta la ocurrencia de los atributos ecuacionales
assoc, comm y id:, que obliga su verificación en la clase de equivalencia de todos los términos que son iguales bajo los
axiomas implícitos en dichos atributos.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
359
Las ecuaciones de membresía y de membresía condicional tienen la forma siguiente:
mb <termino> : <sort> .
cmb <termino> : <sort> if <temino_boleano> .
Donde:
 <termino> Es el término cuyo sort esta siendo redefinido.
 <sort> Es un subsort del sort asociado con <termino>.
 <temino_boleano> Es un término que al ser evaluado determina si
<termino> pertenece o no al sort <sort>.
Dadas estas ecuaciones un término cualquiera que empareje con <termino> es
reclasificado, de forma dinámica, en un subsort de su sort original.

El siguiente ejemplo, tomado de [Clavel 2007, sec 4.2] muestra el uso de la ecuación de membresía
simple. Así, la especificación siguiente:

...
sort Nat3 .
subsort Nat3 < Nat .
var M3 : Nat3 .
mb 0 : Nat3.
mb (s s s M3) : Nat3 .

Define que algunos de los elementos del sort de los naturales (Nat) pertenecen al sort de los naturales
múltiplos de 3 (3*Nat).

El siguiente ejemplo, basado en el presentado en [Clavel 2007, sec 3.5 y 4.3] muestra el uso de la
ecuación de membresía condicional.
Así, la especificación siguiente implementa un álgebra para nodos arcos y caminos en un grafo. Un
grafo está constituido por un conjunto de nodos, y un conjunto de conexiones entre nodos llamados
arcos. Una serie de arcos conectados entre si determina un camino en el grafo. Finalmente un camino
cerrado es aquel que comienza y termina en el mismo grafo.

fmod GRAFO is
sorts Nodo Arco .
ops origen fin : Arco -> Nodo .

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) .
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
360
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.
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.

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

Una posible declaración del módulo paramétrico para una lista ordenada genérica que denominaremos
LISTA-ORD, requeriría que los componentes de la lista sean ordenables. Por ello debemos exigir a su
único parámetro, satisfacer la teoría TOSET que define un orden total bajo el operador _>_.
El módulo se apoyaría en el módulo que define la lista genérica presentado en el Ejemplo 138, el
módulo LISTA (ver Nota al final del ejemplo sobre un posible problema al usar este módulo). Este
módulo es importado e instanciado con una vista a la teoría TOSET para luego ligar su parámetro con
el del módulo LIST-ORD.

fmod LISTA-ORD{X::TOSET}

protecting LISTA{TOSET}{X} .
sort StdList{X} .
subsort StdList{X} < Lista{TOSET}{X} .
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
361
vars N : X$Elt .
vars SL : StdList{X} .
mb nil : StdList{X} .
cmb (N SL) : StdList{X} if (SL==nil) or (N <= head(SL)) .

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


..
..
op merge_list : StdList{X} StdList{X} -> StdList{X} .
..
..
endfm
Donde el operador sort_list se encargaría de llevar a cabo el ordenamiento de una lista, convirtiéndola en
una instancia de una lista ordenada, mientras que el operador merge_list se encargaría de llevar 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: Sin embargo, tal como lo hace notar Clavel en (ver [Clavel 2007, sec 14. 2.8]), por razones de
implementación, la ecuación de memebrecía condicional interactúa con los atributos assoc e iter de
forma indeseable, por lo que no es seguro que la lista ordenable presentada se pueda basar en la lista
definida de la manera presentada en Ejemplo 138, en lugar de basarse en listas definidas bajo la filosofía
cabeza cola a la manera del LISP,

11.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 [Clavel
2007, sec 4.4.4]. 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 operadores que no son constantes, al menos
un argumento debería ser polimórfico para evitar ambigüedades.
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.

En [Clavel 2007, sec 4.4.4] se presenta el siguiente ejemplo, donde se define una lista que puede ser
utilizada con elementos de diversos tipos, así:
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
362
fmod HET-LIST is
sort List .
op nil : -> List .
op _ _ : Universal List -> List [ctor poly (1)] .
endfm

Resumen del Capítulo.


La capacidad de definir operadores, es un mecanismo para parametrizar procesos
abstrayéndolos de los valores involucrados en los mismos. Es también posible abstraerse
de los operadores particulares y de los tipos de los valores particulares, involucrados en la
definición de un operador. En una lista, por ejemplo, al definir el operador que obtiene su
tamaño es conveniente abstraerse del tipo de los elementos de la lista, para definir este
operador una sola vez, de forma genérica para todas las listas evitando hacerlo cada vez que
se cambie dicho tipo. Igual ocurre con los operadores de selección de elementos a los que
conviene “pasarles” como argumento el criterio de selección evitando escribir de forma
explícita un operador para cada criterio.
En el lenguaje SCHEME se ofrecen dos mecanismos de abstracción, distintos al del valor
de los argumentos, a saber:
 Abstracción del tipo: por ser un lenguaje “débilmente tipado”, los
operadores pueden evocarse con argumentos 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.
 Abstracción de operadores: es posible considerar que algunos de los
argumentos de la definición de un operador representan a un operador en
lugar de representar un valor. Así, dentro de una definición es posible usar
un argumento como si fuera un operador, evocándolo con sus respectivos
operandos. Al evocarse un operador así definido es necesario suministrarle
un operador real como operando. Para suministrarle como argumento a otro
operador un operador previamente definido, basta colocar el nombre del
operador como argumento de la evocación. Para suministrarle como
argumento a otro operador un operador que no esté previamente definido, es
posible usar una expresión “lambda” como argumento en la evocación. Una
expresión lambda puede usarse, también, como el término al que reescribe la
evocación de un operador, para que este de cómo resultado un operador en
lugar de un valor.
El lenguaje MAUDE ofrece un mecanismo de abstracción que se apoya en parametrizar sus
unidades de modularización, es decir los módulos. La parametrización de módulos permite
definir módulos abstrayendo algunos de los módulos que el módulo definido incluye. Así,
al momento de “evocar” un módulo parametrizado (es decir al incluirlo en un módulo
cualquiera) se puede indicar cuales serán algunos de los módulos que usa, con sólo indicar
cuales han de ser los módulos reales, que se asocian con los parámetros de la definición.
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. En este
mecanismo de parametrización se deben tener en cuenta los elementos siguientes:
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
363
 Al definir un módulo parametrizado, cada uno de los parámetros debe estar
asociado con una “teoría” (el tipo del parámetro), pudiendo estar varios
parámetros asociados con la misma teoría. Las teorías definen las
condiciones que debe satisfacer un módulo real para poder ser incluido en un
módulo parametrizado a través de un parámetro, ellas definen las
propiedades sintácticas y semánticas que deben satisfacer dichos módulos.
Las teorías son módulos funcionales en los que sus axiomas son
no_ejecutables, ellas pueden incluir otros módulos u otras teorías. En el
cuerpo del módulo parametrizado pueden ser usados los operadores y sorts
definidos en las teorías asociadas a los parámetros, los tipos, sin embargo,
deben cualificarse con el nombre del parámetro asociado con dicha teoría.
 Para poder incluir un módulo real en una evocación 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 real a ser incluido, y
luego usar el nombre de la vista como argumento al evocar el módulo
parametrizado. Una vista, proyecta los nombres de los sorts y operadores
definidos en la teoría, a los sorts y operadores del módulo real. Para cada
sort de la teoría debe existir un sort correspondiente en el módulo real y se
deben preservar en el módulo real las relaciones existentes entre los módulos
en la teoría. Los sorts y operadores de la teoría que no aparezcan en el
mapeo se asumen proyectados a los sorts de igual nombre en el módulo real.
Los operadores definidos en los módulos importados en la teoría no pueden,
sin embargo, ser mapeados por medio de la vista al módulo real. Entre los
operadores correspondientes de la teoría y el módulo real, se debe conservar
tanto la aridad del operador, como los sorts de su dominio y rango, dada la
proyección de los mismos
 Un módulo parametrizado puede ser incluido en otro módulo más de una
vez, instanciándolo con diferentes módulos reales. Para distinguir los sort
declarados dentro de un módulo parametrizado, entre las diferentes
instancias del módulo parametrizado que se incluyan en otro módulo, se
deben cualificar los nombres de dichos sort, dentro de los módulos que
incluyen las instancias, con el nombre de las vistas con que se instancian los
parámetros de los que depende el sort.
 Es posible crear vistas de una teoría a otra teoría, para resolver conflictos al
incluir en un módulo parametrizado (A) como modulo real una instancia de
otro módulo parametrizado (B) que se instancia con parámetros del primer
módulo parametrizado (A), en cuyo caso se debe garantizar que las teorías
asociadas a los parámetros del módulo inclusor, correspondan con las de los
parámetros del módulo incluido siendo estas teorías diferentes. Para
resolver el conflicto de teorías, se debe usar una vista de las teorías del
módulo incluido a las correspondientes teorías del módulo inclusor.
Si bien la relación de subsort en MAUDE fue utilizada para declarar las componentes
elementales de la estructura iterada lista, y para definir subtipos con los elementos de un
tipo ya existente que satisfagan alguna condición, ella puede también ser utilizada para
definir operadores asociados a funciones parcialmente definidas sobre un tipo, darle apoyo
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
364
a la recuperación de errores, e indicar de forma más precisa el efecto de aplicar operadores
(ya definidos) a subconjuntos del tipo.

Ejercicios propuestos
Ejercicios: sección 1.3.* de (Abelson 1985)
1. La regla de Simpson, es otra manera para aproximar el valor de una integral definida
entre a y b. En términos matemáticos, dado un número natural par, n:
h
 f (a)  f (a  h)  f (a  2h)    f (a  nh)
b
a
f ( x)dx 
3
Donde h=(b-a)/n. Defina un procedimiento que tome como argumentos, f, a, b, n y que
retorne el valor de la integral definida, aproximado mediante la regla de Simpson. Si lo hizo
sin utilizar el procedimiento sumatoria definido en la sección 3.2, reescríbalo para que
haga uso de tal abstracción.
2. El procedimiento sumatoria (sección 3.2) genera un proceso recursivo. El
procedimiento puede ser reescrito para que lleve a cabo un proceso iterativo. Muestre como
hacer esto llenando los campos faltantes en la siguiente definición:
(define (sumatoria termino a sig b)
(define (iter a resultado)
(if <??>
<??>
(iter <??> <??>)))
(iter <??> <??>))
3. Escriba un procedimiento productoria, análogo a sumatoria, que retorne el producto
de los valores de una función en puntos sobre un rango dado.
a. Muestre como definir factorial en términos del nuevo procedimiento productoria.
b. Use el procedimiento productoria, para encontrar una aproximación a , usando
la fórmula
 2  4  4  6  6  8

4 3  3  5  5  7  7
c. Si su procedimiento productoria genera un proceso iterativo, escriba uno que
genere un proceso recursivo. Y si su procedimiento genera uno recursivo, escriba
uno que genere un proceso iterativo.
4. Muestre que sumatoria y productoria son casos especiales de un concepto más
general llamado acumular, que combina una colección de términos, usando alguna función
general de acumulación:
(acumular combinador nulo termino a sig b)
Acumular toma como argumentos la misma especificación de rango y término que
sumatoria, junto con un procedimiento combinador (de dos argumentos) que especifica
como el término actual se combina con la acumulación de los términos precedentes; y nulo
es un valor que especifica la base a usar cuando terminen los valores.
Capítulo 11: Programación Paramétrica y Relaciones entre Tipos
365
a. Escriba el procedimiento acumular.
b. Reescriba los procedimientos sumatoria y productoria, en términos de
acumular.
5. En el ejercicio 3 del módulo 2, se planteo una fórmula general para una fracción continua
infinita.
a. Escriba un procedimiento frac-inf, con el siguiente perfil (frac-inf N D a b),
donde N y D son procedimiento, con argumento n, que calculan el n-ésimo valor para
Ni y Di, y [a, b] determina el rango de enteros.

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).
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.
Capítulo 12
PROLOG como Lenguaje de
Búsqueda
Capítulo 12: PROLOG como lenguaje de búsqueda
368
Introducción.
La resolución SLD del lenguaje PROLOG es no sólo un proceso de reescritura, sino
también un proceso de búsqueda que encuentra la combinación de hechos y reglas del
programa que prueban verdadero el objetivo original.
Esta circunstancia permite describir de forma simple, procesos que llevan a cabo consultas
complejas sobre bases de datos (conjuntos de hechos), y solucionan problemas que
requieren hallar, por ensayo y error, una combinación precisa de datos entre múltiples
posibles.
En este capítulo presentaremos una serie de ejemplos que toman ventaja de la búsqueda
para lograr solucionar problemas nuevos, o lograr soluciones más simples a problemas ya
planteados.

Consultas Deductivas sobre una Base de Datos.


Los hechos base de un programa PROLOG, constituyen elementos de información
almacenada, en el mismo sentido en que lo hace una Base de Datos.

Los hechos siguientes:.

ama(juan, maría) .
ama(pedro, claudia) .
ama(pedro, luisa) .
ama(maría, julio) .
ama(julio, luisa) .
ama(verónica, hector) .
ama(hector, verónica) .
ama(claudia, hector) .
ama(pedro, verónica) .
ama(julio, verónica) .
Constituyen un registro de las relaciones existentes en un dominio de aplicación, que bien podrían estar
almacenados en una tabla de una base de datos relacional, por ejemplo la tabla “ama” con columnas
“ama” y “amado”.

ama
ama amado
juan María
pedro Claudia
....

Cualquier objetivo PROLOG que se demuestre cierto con base en los hechos, no es otra
cosa que una consulta sobre la Base de Datos correspondiente.

Los objetivos siguientes:

1 ?- ama(juan,verónica).
Capítulo 12 : PROLOG como lenguaje de búsqueda
369
false.

2 ?- ama(juan,maría).
true.
No son otra cosa que una consulta de existencia para un registro específico de la base de datos.
El objetivo siguiente:

3 ?- ama(X,maría).
X = juan.
Obtiene la primera línea de la tabla que tiene a maría en la columna “amado”

Un elemento de especial importancia en PROLOG, es que un objetivo puede demostrarse


cierto con base en varias combinaciones distintas de reglas y hechos. Sin embargo, la
resolución SLD, tal como fue presentada arriba, tiene como fin obtener una sola de dichas
combinaciones y, por ello, el valor obtenido para de las variables de la consulta es uno sólo.

El objetivo siguiente:

1 ?- ama(X,verónica) .
X = hector .
Puede demostrarse cierto con tres hechos diferentes del programa, a saber:

ama(hector, verónica) .
ama(pedro, verónica) .
ama(julio, verónica) .
Sin embargo, sólo el primero de los tres fue tenido en cuenta en el proceso, dando como respuesta un
solo valor para la variable de la consulta.

Para hacer que la resolución SLD, demuestre el objetivo con otra combinación de reglas y
consultas, y, en consecuencia, produzca otro valor para las variables, basta con introducir
“;” luego de obtener el resultado inicial.

Si luego de obtener un resultado positivo, se introduce “;” se obtiene un nuevo resultado, así:

1 ?- ama(X,verónica) .
X = hector ;
X = pedro ;
X = julio.

2 ?-
Donde vale anotar que el intérprete sólo da la posibilidad de introducir el “;” si existen otras
combinaciones de éxito.

La introducción del “;” equivale a forzar un retroceso luego del éxito, para que la
resolución SLD busque otro camino de demostración alternativo. Así, el “;” equivale a
indicarle al proceso que el éxito es considerado un fracaso por el usuario de la consulta.
Capítulo 12: PROLOG como lenguaje de búsqueda
370

Si se observa el orden en que se obtienen las diferentes respuestas a la consulta siguiente:

1 ?- ama(X,verónica),ama(X,Y),Y\==verónica .
X = pedro,
Y = claudia ;
X = pedro,
Y = luisa ;
X = julio,
Y = luisa ;
false.

Puede comprobarse que corresponden al orden en que se efectúa el retroceso, luego de que cada éxito es
rechazado por el usuario. Así, luego de rechazar la primera pareja X = pedro, Y = claudia, la resolución
SLD se retracta de la unificación de ama(pedro,Y), obteniendo un nuevo valor para Y. Rechazado
también este último resultado, y no existiendo otra opción para ama(pedro,Y), se retracta la unificación
ama(pedro,verónica) obteniendo un nuevo valor para X.
El PROLOG ofrece, también, el predicado findall(_,_,_) para recolectar en una meta de una regla las
diferentes soluciones a una consulta poniéndolas a disposición de las siguientes metas de la regla. Para
más información al respecto remitimos al lector a [Blackburn 01: secc 11.2].

La adición de reglas asociadas con los datos contenidos en una base de datos, posibilita
deducir información de la base de datos sin que dicha información esté explícitamente
registrada en los hechos. A este tipo de Bases de Datos se le ha denominado Bases de
Datos “deductivas” [Celma 95],[Ceri 90] .

La regla siguiente define el concepto de “rivales”.

rivales(X,Y) :- ama(X,Z),ama(Y,Z),X\==Y .
Y permite deducir quienes son rivales, a partir de las relaciones amorosas:

2 ?- rivales(X,Y)
X = pedro, Y = julio ;
X = julio, Y = pedro ;
X = verónica, Y = claudia ;
X = hector, Y = pedro ;
X = hector, Y = julio ;
X = claudia, Y = verónica ;
X = pedro, Y = hector ;
X = pedro, Y = julio ;
X = julio, Y = hector ;
X = julio, Y = pedro ;
false.
Cabe notar, que por la simetría de la relación si X y Y son rivales, también lo son Y y X:

Un objetivo formado por una secuencia de metas posibilita efectuar consultas más
complejas.
Capítulo 12 : PROLOG como lenguaje de búsqueda
371
Los registros almacenados en una base de datos pueden ser considerados como átomos de predicados
asociados con cada tipo de registro (o tabla de la BD). Así, los siguientes seis grupos de registros:

estudia cursa lugar_salon


estudiante carrera estudiante curso salon campus
juan civil maria calculo c-104 agronomia
maria medicina juan algoritmos b-240 minas
jose matematicas juan fisica a-120 agronomia
maria algoritmos d-100 minas
jose calculo

salón_curso area origen


curso salon carrera Area estudiante region
calculo d-100 civil ingenieria juan costa
fisica a-120 medicina salud maria centro
algoritmos b-240 matematicas ciencias jose centro
quimica ciencias
Pueden ser considerados como átomos de los predicados: estudia(_,_), cursa(_,_), lugar_salon(_,_),
salon_curso(_,_), area(_,_), origen(_,_), y, en consecuencia, como hechos de un programa PROLOG
(tomados en el mismo orden en el que aparecen en las tablas).
Sobre este programa PROLOG, se pueden efectuar consultas como las siguientes:
¿Cuales cursos toma María en el campus de Minas?

8 ?- cursa(maria,CURSO),salon_curso(CURSO,S),lugar_salon(S,minas) .
CURSO = calculo, S = d-100 ;
CURSO = algoritmos, S = b-240 ;
false.
¿En que campus podrían verse juan y maria?

10 ?- cursa(juan,X), salon_curso(X,LX), cursa(maria,Y), salon_curso(Y,LY),


lugar_salon(LX,CAMPUS), lugar_salon(LY,CAMPUS) .
X = algoritmos, LX = b-240, Y = calculo, LY = d-100, CAMPUS = minas ;
X = algoritmos, LX = b-240, Y = algoritmos, LY = b-240, CAMPUS = minas ;
false.
Cabe sugerir en este punto, que el orden de las metas en la consulta puede cambiar sustancialmente el
número de fallas y retrocesos, siendo ciertas disposiciones de metas más eficientes que otras. Se deja al
lector la tarea de analizar el camino que toma la resolución para diferentes disposiciones de las metas.

El PROLOG provee, además, predicados para actualizar el conjunto de hechos y reglas, de


forma dinámica durante la ejecución.

Los comandos assert, (retract,) asserta, y assertz toman como argumento un hecho o regla y lo
incluyen (o retiran) del programa (ver [Blackburn 01: secc 11.1]). Ellos requieren, sin embargo, que el
predicado que redefinen sea declarado en el programa como “dinámico”. Así, si en el programa del
ejemplo anterior se incluye, la cláusula siguiente:

:- dynamic cursa/2 .
Es posible introducir y quitar hechos durante la ejecución:

2 ?- cursa(X,fisica) .
Capítulo 12: PROLOG como lenguaje de búsqueda
372
X = juan.

3 ?- assert(cursa(jose,fisica)) .
true.

5 ?- cursa(X,fisica) .
X = juan ;
X = jose.

6 ?- retract(cursa(juan,fisica)) .
true .

7 ?- cursa(X,fisica) .
X = jose.

8 ?-
Nótese que el cambio en la base de hechos sólo tiene efecto durante la ejecución (el archivo con los
hechos originales queda inmodificado)175.

El proceso de resolución SLD puede ser no-terminante.

Considere los hechos y las reglas siguientes:

padre(jose,ana) .
padre(pedro,maria) .
padre(jose,pedro) .
ancestro(X,Y) :- padre(X,Y) .
ancestro(X,Y) :- ancestro(X,Z),padre(Z,Y) .
Es claro que ana y maria tienen a jose como ancestro común ya que es padre de ana y abuelo de maria.
Sin embargo, al llevar a cabo la consulta siguiente:

1 ?- ancestro(A,maria),ancestro(A,ana) .
ERROR: Out of local stack
La resolución SLD es incapaz de hallar la respuesta correcta, cayendo en ciclo infinito que es inducido
por la transitividad inherente al concepto de ancestro(). Para ilustrar la manera como ocurre la falla en el
proceso, presentamos a continuación la tabla que muestra la evolución del objetivo.

Paso regla Substitución Objetivo


0 ancestro(A,maria),ancestro(A,ana)
1 4 A/X, maria/Y padre(A,maria),ancestro(A,ana)
2 2 pedro/A ancestro(pedro,ana)
3 4 pedro/X, ana/Y padre(pedro,ana)
4 ? Falla
3* 5 pedro/X, ana/Y ancestro(pedro,Z),padre(Z,ana)
4 4 pedro/X, Z/Y padre(pedro,Z),padre(Z,ana)
5 2 maria/Z padre(maria,maria)
6 ? Falla
4* 5 pedro/X, Z/Y ancestro(pedro,Z´),padre(Z´,Z),padre(Z,ana)
5 4 pedro/X, Z´/Y padre(pedro,Z´),padre(Z´,Z),padre(Z,ana)
6 2 maria/Z´ padre(maria,Z),padre(Z,ana)
7 ? Falla

175Cabe resaltar que el PROLOG, en si mismo, no es un gestor de Bases de Datos, ya que no incorpora de forma nativa
los elementos necesarios para preservar las restricciones de integridad, crear vistas, optimizar consultas, gestión de
usuarios, etc…
Capítulo 12 : PROLOG como lenguaje de búsqueda
373
5* 5 pedro/X, Z´/Y ancestro(pedro,Z´´),padre(Z´´,Z´), padre(Z´,Z),padre(Z,ana)
6 4 pedro/X, Z´´/Y padre(pedro,Z´´),padre(Z´´,Z´), padre(Z´,Z),padre(Z,ana)
7 2 maria/Z´´ padre(maria,Z´), padre(Z´,Z),padre(Z,ana)
8 ? Falla
....
....

Donde se puede observar que el proceso tratar de resolver el objetivo introduciendo, de forma reiterada,
un miembro más en la cadena de ancestría.
Un simple cambio en el orden de las metas de la consulta, produce el resultado esperado, así:

2 ?- ancestro(A,ana),ancestro(A,maria) .
A = jose
De lo anterior se induce que el proceso de resolución SLD no sólo puede ser ineficiente para ciertas
consultas, sino que, por si mismo, es incapaz de dar cuenta de todas las situaciones

Para controlar los fallos y retrocesos del proceso SLD, el PROLOG ofrece los predicados
de “!” (corte) y “fail” (negación), así:
 !, colocado como predicado (o meta) en la cola de una regla obliga al
proceso de resolución a mantener las substituciones de las variables de la
regla que se hayan efectuado antes del corte, en caso de que el proceso falle
luego del corte. En otras palabras un retroceso no puede tratar de unificar de
nuevo ninguno de los predicados que aparecen en la regla antes del corte,
incluyendo la cabeza de la regla; así, los valores de las variables de la regla
ya ligadas, no pueden ser redefinidos.
 fail, colocado como predicado (o “meta”) en la cola de una regla, induce un
fracaso generando un retroceso en el proceso.
Para mayor información sobre estos predicados ver [Blackburn 01: secc 10].

Búsqueda de datos y combinaciones de datos.


La falla y el retroceso facilitan definir procesos en los que se trata de hallar el valor o la
combinación de valores, de un conjunto de valores determinado, que satisface(n) una
propiedad dada.
En esta sección se plantea que conceptualmente la solución a este problema se fundamenta
en los elementos siguientes:
 Contar con un mecanismo para obtener uno a uno los diferentes valores o
agrupaciones de valores del conjunto.
 Contar con un mecanismo para verificar si un valor o agrupación de valores
obtenido satisface la propiedad.
 Aplicar de forma reiterada el mecanismo de selección y verificación, hasta
hallar el caso o los casos que satisfacen la propiedad.
En lo que sigue ilustraremos una forma de implementar este tipo de procesos en PROLOG
tomando ventaja de la falla y el retroceso. Para ello se presenta primero, de forma general,
la manera de obtener los casos desde una lista como representación del conjunto, luego se
aplica dicha manera a la solución de un caso de estudio específico.
Capítulo 12: PROLOG como lenguaje de búsqueda
374
Para el caso en que el conjunto es una lista, el mecanismo para obtener los valores uno a
uno, no es otra cosa que el predicado que permite establecer si un valor se encuentra en la
lista.

El operador pertenece definido antes para establecer si un valor se encuentra en una lista de valores:

pertenece(X,[X|_]) .
pertenece(X,[_|T]) :- pertenece(X,T) .

Permite obtener, uno a uno, los valores de la lista. Para ello basta con evocar el predicado usando una
variable para el valor cuya pertenencia se quiere verificar, y luego, rechazar (con “;”) una y otra vez la
respuesta obtenida, así:

1 ?- pertenece(X,[1,2,3]) .
X=1;
X=2;
X=3;
false.

2 ?-

Para hallar en un conjunto, un elemento que satisfaga una condición dada es suficiente con
obtener los valores, uno a uno, y verificar la condición (fallando en caso contrario).

El operador halle_menor_que obtiene el primer elemento de una lista que cumple la condición de ser
menor que un valor dado:

halle_menor_que(U,L,R) :- pertenece(X,L), X<U, R is X .


En ejecución:

2 ?- halle_menor_que(4,[5,2,6,7,1],X).
X=2.
Para obtener todos los valores basta con rechazar de forma sucesiva la respuesta obtenida:

2 ?- halle_menor_que(4,[5,2,6,7,1],X).
X=2;
X=1;
false.

Para recolectar todos los valores puede usarse el predicado nativo findall, así:

halle_menores_que(U,L,R) :- findall(X,halle_menor_que(U,L,X),R) .
En ejecución:

1 ?- halle_menores_que(4,[5,2,3,6,7,1],R) .
R = [2, 3, 1].
Capítulo 12 : PROLOG como lenguaje de búsqueda
375
Para el caso en que el conjunto es una lista, y se desea obtener parejas de valores de la lista,
el mecanismo para obtener uno a uno las diferentes parejas de valores, puede construirse
fácilmente a partir del predicado de pertenencia.

El predicado copy_2 toma una pareja de valores de la lista, permitiendo que los dos elementos de la
pareja se tomen de la misma posición de la lista:

copy_2(L,[X|[Y|[]]]) :- pertenece(X,L), pertenece(Y,L) .


En ejecución:

1 ?- copy_2([1,2,3],R) .
R = [1, 1] ;
R = [1, 2] ;
R = [1, 3] ;
R = [2, 1] ;
R = [2, 2] ;
R = [2, 3] ;
R = [3, 1] ;
R = [3, 2] ;
R = [3, 3] ;
false.
El predicado pick_2 toma una pareja de valores de la lista, evitando que los dos elementos de la pareja se
tomen de la misma posición de la lista:

pick_2(L,[X|[Y|[]]]) :- pertenece(X,L,T), pertenece(Y,T,_) .


Nótese que dicho predicado se apoya en una versión del predicado pertenece que permite obtener tanto
un elemento de la lista como la lista de los elementos que le siguen, así:

pertenece(X,[X|T],T) .
pertenece(X,[_|T],R) :- pertenece(X,T,R) .
En ejecución:

2 ?- pick_2([1,2,3],R) .
R = [1, 2] ;
R = [1, 3] ;
R = [2, 3] ;
false

Para hallar en un conjunto, una pareja de elementos que satisfaga una condición dada es
suficiente con obtener las parejas, una a una, y verificar la condición (fallando en caso
contrario).

El operador halle_parque_sumev obtiene el primer par de una lista que cumple la condición de que sus
componentes suman un valor dado:

halle_parque_sumev(V,L,R) :- copy_2(L,[X|[Y|[]]]), V is X+Y, R = [X|[Y|[]]] .


En ejecución:

1 ?- halle_parque_sumev(3,[0,1,2,3,4],R).
R = [0, 3] .
Capítulo 12: PROLOG como lenguaje de búsqueda
376
Para obtener todos los valores basta con rechazar de forma sucesiva la respuesta obtenida:

1 ?- halle_parque_sumev(3,[0,1,2,3,4],R).
R = [0, 3] ;
R = [1, 2] ;
R = [2, 1] ;
R = [3, 0] ;
false.

Para recolectar todos los valores puede usarse el predicado nativo findall, así:

halle_losparque_sumev(V,L,R) :- findall(X,halle_parque_sumev(V,L,X),R) .
En ejecución:

1 ?- halle_losparque_sumev(3,[0,1,2,3],R) .
R = [[0, 3], [1, 2], [2, 1], [3, 0]].

Para el caso en que el conjunto es una lista, y se desea obtener tuplas de n valores de la
lista, se debe generalizar el predicado propuesto en los ejemplos anteriores.

El predicado copy_n toma una tupla de n valores de la lista, permitiendo que los elementos de una tupla
se tomen de la misma posición de la lista:

copy_n(0,_,[]) .
copy_n(N,L,[X|A]) :- N>0, pertenece(X,L), N1 is N-1, copy_n(N1,L,A) .
En ejecución:

4 ?- copy_n(3,[1,2],R) .
R = [1, 1, 1] ;
R = [1, 1, 2] ;
R = [1, 2, 1] ;
R = [1, 2, 2] ;
R = [2, 1, 1] ;
R = [2, 1, 2] ;
R = [2, 2, 1] ;
R = [2, 2, 2] ;
false.
El predicado pick_2 toma una pareja de valores de la lista, evitando que los dos elementos de la pareja se
tomen de la misma posición de la lista:

pick_n(0,_,[]) .
pick_n(N,L,[X|A]) :- pertenece(X,L,T), N1 is N-1, pick_n(N1,T,A) .
En ejecución:

5 ?- pick_n(3,[1,2,3,4],R) .
R = [1, 2, 3] ;
R = [1, 2, 4] ;
R = [1, 3, 4] ;
R = [2, 3, 4] ;
false.
Capítulo 12 : PROLOG como lenguaje de búsqueda
377
Para hallar en un conjunto, una tupla de elementos que satisfaga una condición dada es
suficiente con obtener las tuplas, una a una, y verificar la condición (fallando en caso
contrario).

El operador halle_tuplaque_orden obtiene la primer tupla de una lista que cumple la condición de que
sus componentes estén ordenados en orden estricto creciente:

halle_tuplaque_orden(N,L,R) :- copy_n(N,L,X), ordenada(X), R = X .


ordenada([_|[]]) .
ordenada([X|[Y|T]]) :- Y > X , ordenada([Y|T]) .
En ejecución:

1 ?- halle_tuplaque_orden(3,[1,2,3,4],R) .
R = [1, 2, 3] .
Para obtener todos los valores basta con rechazar de forma sucesiva la respuesta obtenida:

1 ?- halle_tuplaque_orden(3,[1,2,3,4],R) .
R = [1, 2, 3] ;
R = [1, 2, 4] ;
R = [1, 3, 4] ;
R = [2, 3, 4] ;
false.
Para recolectar todos los valores puede usarse el predicado nativo findall, así:

halle_tuplasque_orden(N,L,R) :- findall(X,halle_tuplaque_orden(N,L,X),R) .
En ejecución:

1 ?- halle_tuplasque_orden(3,[1,2,3,4],R) .
R = [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]].

La búsqueda de una o varias soluciones dentro de un universo de posibles alternativas, es


un problema central en los sistemas “inteligentes”, y de común ocurrencia en la literatura
relativa al PROLOG.
A continuación presentamos una solución fundamentada en los elementos del numeral
anterior, a algunos casos de ejemplo que aparecen descritos en otros documentos. El objeto
de la presentación no es el de presentar una mejor solución al problema, sino el de
caracterizarlos de forma genérica como problemas de búsqueda. Con ello esperamos que el
lector pueda reconocer las soluciones de la literatura, como variantes de la solución
presentada en las que se toma ventaja de la naturaleza del problema para optimizar los
procesos de selección de casos, o, simplemente, se adicionan elementos de visualización.
Un primer problema es el del coloreo de mapas (ver [Fisher 01-12 secc 2.1, 2.9] ).

Un ejemplo clásico de este tipo de problema es el del coloreo de mapas. El problema consiste en hallar
la forma de colorear las diferentes regiones de un mapa, a partir de un conjunto finito de colores,
evitando que dos regiones contiguas queden con el mismo color.
Para representar los elementos del problema, usaremos los siguientes elementos de datos:
Capítulo 12: PROLOG como lenguaje de búsqueda
378
 Un número (N) que representa el número de regiones del mapa.
 Una lista de pares de regiones (LPRG) que representa las relaciones de contigüidad entre las
mismas. Así, si en la lista se halla el par [i,j], la región i y la región j son contiguas en el mapa.
 Una lista de nombres de colores (CLRS) , asumidos como diferentes.
Para representar una alternativa de solución usaremos los siguientes elementos de datos:
 Una lista (CLREO) de N colores que corresponden a la asignación de colores a las N regiones
del mapa.
Para verificar si una alternativa es correcta, se debe verificar que si dos regiones aparecen en una de las
parejas de contigüidad ellas deben tener colores diferentes:

get_color(1,[C|_],C) .
get_color(I,[_|T],C) :- I>1, I1 is I-1, get_color(I1,T,C) .
verifique_coloreo([],_) .
verifique_coloreo([[I,J]|T],CLREO) :- get_color(I,CLREO,CI), get_color(J,CLREO,CJ), CI\=CJ,
verifique_coloreo(T,CLREO) .
Para hallar un coloreo adecuado, basta con generar uno a uno los posibles coloreos verificando que sean
correctos hasta hallar uno que lo sea:

1 ?- halle_parque_sumev(3,[0,1,2,3,4],R).
R = [0, 3] .
Para obtener todos los valores basta con rechazar de forma sucesiva la respuesta obtenida:
halle_coloreo_mapa(N,LPRG,CLRS,CLREO) :-
copy_n(N,CLRS,CL), verifique_coloreo(LPRG,CL), CLREO = CL .
ejemplo_fisher(C) :-
halle_coloreo_mapa(5,[[1,2],[1,3],[1,4],[1,5],[2,3],[2,4],[3,4],[4,5]],[red,green,blue,yellow],C) .
En ejecución:

3 ?- halle_coloreo_mapa(4,[[1,2],[1,3],[2,4],[3,4]],[rojo,verde],C) .
C = [rojo, verde, verde, rojo] ;
C = [verde, rojo, rojo, verde] ;
false.

1 ?- ejemplo_fisher(C).
C = [red, green, blue, yellow, green] ;
C = [red, green, blue, yellow, blue] ;
C = [red, green, yellow, blue, green] ;
C = [red, green, yellow, blue, yellow]

Acertijos
El siguiente ejemplo, el acertijo de la zebra, consiste en la construcción de un predicado
que permita resolver el siguiente acertijo:
“Hay una calle con tres casas contiguas de tres colores diferentes. Una es roja, la otra azul y
la otra verde. Gente de diferente nacionalidad vive en las diferentes casas y todos tienen
diferentes mascotas. Además se sabe también que:
 El inglés vive en la casa roja.
 El jaguar es la mascota de la familia española.
 El japonés vive a la derecha del dueño del caracol.
 El dueño del caracol vive a la izquierda de la casa azul.
Capítulo 12 : PROLOG como lenguaje de búsqueda
379
¿Quien es el dueño de la zebra?”
Lo que se busca es un predicado zebra, que tome un argumento que corresponda a la
nacionalidad del dueño de la zebra, de manera que al hacer la consulta

:- zebra(NAC) .
El intérprete responda NAC=..., con la respuesta correcta al acertijo.
Para resolverlo haremos uso de algunos de los predicados construidos en las secciones
anteriores. Realmente no queda más que agregar. En medio del código se incluyeron
comentarios (aquellas líneas encabezadas por %%) para mayor claridad.

zebra(N) :-
%% La calle es representada como una lista de 3 casas.
%% Una casa es representada como una lista de 3 propiedades:
%% color, nacionalidad y mascota.
%% Hay una casa roja en la calle.
miembro([roja,_,_], [Casa1,Casa2,Casa3]),
%% Hay una casa Azul en la calle.
miembro([azul,_,_], [Casa1,Casa2,Casa3]),
%% Hay una casa verde en la calle.
miembro([verde,_,_], [Casa1,Casa2,Casa3]),
%% El inglés vive en la casa roja.
miembro([roja,inglés,_], [Casa1,Casa2,Casa3]),
%% El jaguar es la mascota de la familia española.
miembro([_,español,jaguar], [Casa1,Casa2,Casa3]),
%% El japonés vive a la derecha del dueño del caracol.
sublista([[_,_,caracol],[_,japonés,_]], [Casa1,Casa2,Casa3]),
%% El dueño del caracol vive a la izquierda de la casa azul.
sublista([[_,_,caracol],[azul,_,_]], [Casa1,Casa2,Casa3]),
%% La zebra es la mascota de N.
miembro([_,N,zebra], [Casa1,Casa2,Casa3]) .
Vale la pena construir un árbol de búsqueda, al menos incompleto, para ilustrar la búsqueda
que se genera al plantear la consulta.

:- zebra(N) .
Como conocemos el funcionamiento de el predicado miembro, y también el de sublista,
no hará falta exponer en el árbol toda la búsqueda generada por estos predicados, tan solo
recobraremos el resultado que sabemos arrojarán. En la figura siguiente se muestra el árbol
incompleto generado.
Capítulo 12: PROLOG como lenguaje de búsqueda
380
zebra(N)

N = _01

miembro([roja,_,_],[Casa1,Casa2,Casa3]),...,
miembro([_,_01,zebra],[Casa1,Casa2,Casa3])

Casa1 = [roja,_02,_03]

miembro([azul,_,_],[[roja,_02,_03],Casa2,Casa3]),...

Casa2 = [azul,_04,_05]

miembro([verde,_,_],[[roja,_02,_03],[azul,_04,_05],Casa3]),...

Casa3 = [verde,_06,_07]

miembro([roja,inglés,_],[[roja,_02,_03],[azul,_04,_05],[verde,_06,_07]]),...

_02 = ingles

miembro([_,español,jaguar],[[roja,ingles,_03],[azul,_04,_05],[verde,_06,_07]]),...
_04 = español _06 = español
_05 = jaguar _07 = jaguar
sublista([[_,_,caracol],[_,japonés,_]],
[[roja,ingles,_03],[azul,español,jaguar],[verde,_06,_07]]),...

sublista([[_,_,caracol],[_,japonés,_]],
[[roja,ingles,_03],[azul,_04,_05],[verde,español,jaguar]]),...
_03 = caracol
_04 = japonés
sublista([[_,_,caracol],[azul,_,_]],
[[roja,ingles,caracol],[azul,japonés,_05],[verde,español,jaguar]]),...

miembro([_,_01,zebra],
[[roja,ingles,caracol],[azul,japonés,_05],[verde,español,jaguar]])
_01 = japonés
_05 = zebra

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

Resumen del Capítulo.


La resolución SLD del lenguaje PROLOG además de ser un proceso de reescritura, es
también un proceso de búsqueda. Esta circunstancia permite describir de forma simple,
procesos que llevan a cabo consultas complejas sobre bases de datos (conjuntos de hechos),
y solucionan problemas que requieren hallar, por ensayo y error, una combinación precisa
de datos entre múltiples posibles.
Los hechos base de un programa PROLOG, constituyen elementos de información
almacenada en el mismo sentido en que lo hace una Base de Datos. Cualquier objetivo
PROLOG que se demuestre cierto con base en los hechos, no es otra cosa que una consulta
sobre la Base de Datos correspondiente. La adición de reglas asociadas con los datos
contenidos en una base de datos, posibilita, además, deducir información de la base de
datos sin que dicha información esté explícitamente registrada en los hechos. A este tipo de
Bases de Datos se le ha denominado Bases de Datos “deductivas”.
Capítulo 12 : PROLOG como lenguaje de búsqueda
381
Un objetivo puede demostrarse cierto con base en varias combinaciones distintas de reglas
y hechos. La introducción de un “;” luego del éxito de una consulta equivale a forzar un
retroceso luego del éxito, para que la resolución SLD busque otro camino de demostración
alternativo. Con ello el PROLOG puede, hallar otra respuesta a la misma consulta distinta
a la hallada previamente. El PROLOG ofrece, también, el predicado findall(_,_,_) para
recolectar en una meta de una regla las diferentes soluciones a una consulta poniéndolas a
disposición de las siguientes metas de la regla
El PROLOG provee, además, predicados (como assert y retract) para actualizar el conjunto
de hechos y reglas, de forma dinámica durante la ejecución.
El proceso de resolución SLD puede ser no-terminante. Para controlar los fallos y
retrocesos del proceso SLD, el PROLOG ofrece los predicados de “!” (corte) y “fail”
(negación), así:
 !, colocado como predicado (o meta) en la cola de una regla obliga al
proceso de resolución a mantener las substituciones de las variables de la
regla que se hayan efectuado antes del corte, en caso de que el proceso falle
luego del corte.
 fail, colocado como predicado (o “meta”) en la cola de una regla, induce un
fracaso generando un retroceso en el proceso.
La falla y el retroceso facilitan definir procesos en los que se trata de hallar el valor o la
combinación de valores, de un conjunto de valores determinado, que satisface(n) una
propiedad dada. Conceptualmente la solución a este problema se fundamenta en los
elementos siguientes: un mecanismo para obtener uno a uno los diferentes valores o
agrupaciones de valores del conjunto; un mecanismo para verificar si un valor o agrupación
de valores obtenido satisface la propiedad; aplicar de forma reiterada el mecanismo de
selección y verificación hasta hallar el caso o los casos que satisfacen la propiedad.
Para obtener los valores de una lista, un a uno, puede usarse el predicado que valida si un
elemento pertenece a la lista evocándolo con una variable en el lugar de un elemento
específico. La unificación, el fallo y el retroceso se encargarán por si mismas de asociar a
la variable los diferentes elementos de la lista. Combinaciones de este predicado, pueden
ser usadas para generar parejas, tripletas etc..
Para verificar si los valores generados satisfacen la propiedad y aplicar de forma reiterada
el mecanismo de selección y verificación hasta hallar el caso o los casos que la satisfacen,
basta con incluir el predicado correspondiente a la verificación y dejar a la falla y al
retroceso el problema de la reiteración.
La falla y el retroceso se convierten, entonces, en mecanismos para solucionar problemas
de búsqueda que pueden asociarse con selecciones, y soluciones a “acertijos” complejos.
Indice.
 acoplamiento, 29  BOOL, 53, 58
 agregación, 38  Boole, 48
 alcance del cuantificador, 83  calculo, 52
 alfabeto, 120  cálculo relacional, 31
 Alfabeto, 53  categorías conceptuales, 20
 alfabeto de símbolos, 49  Church-Roser, 128
 Alfabeto de símbolos, 79  Church-Rosser, 119
 álgebra Boleana, 48  clase, 37
 álgebra multisort con signatura, 129  clases, 36
 álgebra relacional, 31  Clases de Equivalencia, 129
 algoritmo, 250  cláusula, 69
 análisis, 20, 27  clave foránea, 31
 anomalía de inserción, 29  coaridad, 120
 Aplicaciones, 8  compiladores, 7
 árbol sintáctico, 50  completamente parentetizada, 56
 Árbol sintáctico, 55  Completitud, 66
 Árbol Sintáctico, 105  Completitud Suficiente, 276
 archivo, 28  computador, 3
 Archivos, 4  Conclusión, 66
 argumentos formales, 200  conector monádico, 57
 aridad, 81, 103, 120  conectores diádicos, 57
 Aristóteles, 47  conectores lógicos, 54
 arquitectura de los datos, 30  confluencia, 119, 124
 Arquitectura funcional, 24  Confluencia, 128
 Arquitectura Relacional, 32  conjunto universal, 87
 asignación, 209, 210, 213  consecuencia lógica, 53, 63
 asociación, 38  consistencia, 66
 Asociatividad, 107  constante, 120, 205
 átomos, 54, 58  Constantes, 79
 atributos, 36  construcción, 20
 atributos clave, 31  constructor, 272, 273
 atributos del operador, 203  contingente, 61
 atributos ecuacionales, 205  contradicción, 61, 63
 axioma, 64  crisis del software, 2
 Axioma, 115  criterios de demostración, 66
 Axiomas, 114  criterios demostrativos, 53
 Backus Naur, 50  Criterios Demostrativos, 112
 BIOS, 7  criterios formativos, 50, 122
 BNF, 50, 55  Criterios formativos, 104
Indice
384
 Criterios Formativos, 54, 81  equivalentes, 63
 criterios transformativos, 53  Especificaciones formales, 23
 Cuantificadores, 81  estado, 36
 Cuarta Generación, 14  estructuras, 269
 Data Definition Languages, 15  evaluación en orden aplicativo, 216
 DBMS, 30  evaluación en orden normal, 216
 débilmente tipado, 200  eventos, 38
 débilmente tipados, 199  expresiones regulares, 50
 declaración, 199  Extensión, 88
 Declaración, 113  fases del proyecto, 20
 declaración de operadores, 203  fbf, 49
 declaración de tipo, 270, 271  fbf abierta, 86
 deducción, 63  fbf cerrada, 86
 definición recursiva de operadores,  Fibonacci, 254
230, 231, 233  forma normal conjuntiva, 69
 Demostración, 115  forma normal disyuntiva, 70
 denota, 51  formas estructuradas, 24
 Derivación, 115  formas estructuradas de
 desacoplo de impedancia, 15 secuenciamiento, 13
 descomposición progresiva, 24, 187  formula bien formada, 49
 DFD, 25  Frege, 48
 DFL, 25  fuertemente tipado, 202
 DHF, 24  fuertemente tipados, 199
 diagrama de composición  función de rendimiento, 251
jerárquica de funciones, 24  funciones discontinuas, 191
 diagrama de Flujo de Control, 25  Generalización, 91
 diagramas de Flujo de Datos, 25  gestores de bases de datos, 30
 diseño, 20, 27  herencia, 36
 Dispositivos de almacenamiento a  Herencia, 37
largo plazo, 4  Herencia Completa, 37
 Dispositivos de Entrada/Salida, 3  Herencia múltiple, 37
 Dispositivos de Procesamiento, 3  hilos de ejecución, 5
 Dispositivos internos de  igualdad, 111, 123
Almacenamiento, 4  implantación, 20
 Dominio de interpretación, 87  Inferencia, 52, 62, 90
 dominio semántico, 51  insatisfacible, 61
 ecuaciones, 120  instancia, 37
 ecuaciones condicionales, 123  instrucción de asignación, 13
 ecuaciones simples, 123  instrucción de Entrada/Salida, 13
 El modelo de clases, 39  instrucción del procesador, 5
 emparejamiento, 124  instrucciones de secuenciamiento, 13
 Emparejamiento, 115, 125  interpretación, 51, 59
 encapsulamiento, 37  Interpretación, 58
 Equivalencia Semántica, 63  interpretes, 172
Indice
385
 intérpretes, 7  modelo Entidad/Relación, 33
 invariantes de tipo, 278  Modelo Relacional, 30
 invariantes del tipo, 277  modularidad, 187
 IPO, 25  modularización, 221
 Jevons, 48  módulo, 222
 Leibniz, 48  módulo funcional, 223
 lenguaje de máquina, 6  módulos funcionales, 222
 Lenguaje de Máquina, 9  módulos sistémicos, 222
 lenguaje de programación, 6  Modus tollendo ponens, 61
 Lenguaje Ensamblador, 10  Modus tollendo tollen, 61
 Lenguaje natural, 22  Morgan, 48
 lenguajes de programación, 2  Newton Rapson, 234
 Lenguajes de Programación, 8  normalización, 31
 Lenguajes Declarativos, 14  notación infija, 204
 Lenguajes Procedurales, 11  notación prefija, 204
 Ley de la adición, 61  OASIS, 39
 Ley de la exportación, 61  Objeto, 36
 Ley de la importación, 61  objetos, 36
 Ley de la separación, 61  observaciones, 38
 Ley de la simplificación, 61  OCL, 39
 Ley de sustitutividad, 113  ocurrencia, 126
 Ley del absurdo, 61  Ocurrencias de variable, 85
 Ley del silog. hipotético, 61  OMG, 39
 Ley reflexiva, 112  operación, 120
 Ley simétrica, 112  operaciones de maquina, 5
 Ley transitiva, 112  Operadores, 103
 Ligaduras de las variables, 85  operadores definidos, 186
 literal, 69  operando, 103
 Lógica Clausal, 94  orden de precedencia, 56
 lógica de proposiciones, 53  orden de una función, 251
 Lógica Dinámica, 95  Organon, 47
 Lógica Ecuacional, 94, 111  Orientada Por Objetos, 36
 Lógica Ecuacional Multisort, 120  paradigma arquitectónico, 21
 mantenibilidad, 222  Paradigma de Agentes, 41
 Máquina de Estados, 5  Paradigma de Entidades, 28
 máquinas de estado finitas, 50  Paradigma de Funciones, 21
 medio ambiente, 31, 208, 209, 211,  Paradigma de Instrucciones, 21
213, 215  Paradigma de Objetos, 35
 Memorias de acceso directo, 4  paradigma relacional, 30
 métodos, 36  paradigmas arquitectónicos, 2
 mini especificaciones, 23  paradigmas de gestión, 2, 20
 modelo, 60  Paréntesis, 106
 modelo de casos de uso, 41  parsers, 51
 modelo de transición de estados, 40  particularización, 124, 125
Indice
386
 Particularización, 91  Relaciones, 38
 Particularización de variables, 113  Relaciones de uso, 38
 Peano, 48  relaciones entre los módulos, 224
 Peirce, 48  Remplazo, 115
 perfil, 103  Report Generation Languages, 15
 Perfil, 114  reusabilidad, 222
 plantilla estándar, 81  ROM, 4
 Plantillas, 80  salto bajo condición, 13
 plantillas infijas, 81  satisfacible, 61
 polimorfismo, 38  SCHEME, 173
 Pre y Post condiciones, 31  Segunda Generación, 10
 precedencia, 107  selección, 191
 precedencia del operador, 206  selector, 273, 274
 Precedencia entre asociatividades,  semántica, 52
108  Semántica, 51, 58, 59, 60, 87, 110,
 precisión, 246, 248 129
 predicado de tipo, 201, 271  semántica de un conjunto de fbfs, 62
 Predicados, 80  semánticamente equivalente, 53
 Premisa, 66  semánticamente equivalentes, 63
 Primera Generación, 9  sentido de asociatividad, 57
 primitivas, 54  seudo-código, 24
 principio de invarianza, 250  signatura multisort, 120
 procedimientos, 221  Signatura Multisort, 120
 proceso computacional, 5  silogismo, 47
 proceso iterativo, 236, 243, 246, 256  símbolos lógicos, 51, 54
 proceso recursivo, 236, 239, 240, 255  símbolos no lógicos, 51, 54
 procesos recursivos, 258  SIMULA, 36
 programación estructurada, 21  Sintaxis, 49, 53, 78
 Programas, 6  Sistema Operativo, 7
 Proposiciones, 53  Sistemas Dinámicos, 38
 prueba, 20  SO, 7
 Prueba de existencia, 91  sobrecarga de operadores, 201, 205
 Query Languages, 15  sort, 120, 203
 Quinta Generación, 15  Sorts, 79
 RAM, 4  SQL, 14
 recursión, 230  SRT, 117, 124
 reemplazo, 124  substitución, 124
 Reescritura, 127  Subtérmino, 115
 Reflexión, 111  Sustituibilidad, 111
 registros, 28, 269  swim lines, 25
 Registros del procesador, 4  Tablas de verdad, 64
 reglas de inferencia, 66  tautología, 61, 63
 relación de reescritura, 124  Tautología, 66
 relación de subsort, 202  teoría, 64
Indice
387
 Teoría, 113  Transitividad, 111
 Tercera Generación, 11  UML, 39
 terminancia, 119, 124  válida, 61
 Terminancia, 128  valor semántico, 51
 términos, 120, 121  variable, 202, 209
 Términos, 79, 102  variable libre, 86
 Términos Complejos, 105  variable ligada, 86
 Términos compuestos, 80  variables, 121, 208, 215
 tiempo de ejecución, 250  Variables, 79
 tipo, 121, 199  variables proposicionales, 53
 tipos abstractos, 268  VDM, 23
 Top-Down, 27  verificación, 20
 Traductores de Lenguajes de  Z, 23
Programación, 7
Bibliografía.
EN.REFLIST[Aho 2007] Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman,
“Compilers Principles, Techniques, & Tools”, Second Edition, Pearson Education,
Inc, ISBN 0-321-48681-1, 2007.
[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 http://xue.medellin.unal.edu.co/farango/.
[Baber 02] Robert L. Baber, “Mathematically Rigorous Software Design”, McMaster
University, Department of Computing and Software an electronic textbook, 2002
September 9, http://www.cas.mcmaster.ca/~baber/Courses/46L03/MRSDLect.pdf
[Blackburn 01] Patrick Blackburn, Johan Bos, and Kristina Striegnitz, “Leanr Prolog
Now”, http://www.learnprolognow.org/, 2001
[Booch 96] Grady Booch, “Análisis y Diseño Orientado a Objetos con Aplicaciones 2ª
edición”, Addison-Wesley, 1996
[Booch 05] Grady Boochhttp://www.amazon.com/Unified-Modeling-Language-User-
Guide/dp/0321267974, James Rumbaughhttp://www.amazon.com/Unified-Modeling-
Language-User-Guide/dp/0321267974, Ivar
Jacobsonhttp://www.amazon.com/Unified-Modeling-Language-User-
Guide/dp/0321267974, “The Unified Modeling Language User Guide (2nd Edition)”,
Addison-Wesley Professional, 2005.
[Bourbaki 50]
[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.
[CAML] http://caml.inria.fr/
[Canos 97] José Hilario Canós Cerdá, “Oasisun lenguaje único para bases de datos
orientadas a objetos ”, Tesis Doctoral, Universitat Politècnica de València,
Director: Isidro Ramos Salavert, 1997
[Celma 95] Matilde Celma Giménez, “La Lógica en el desarrollo de las Bases de Datos”,
Departamento de Sistemas Informáticos y Computación, Universidad Politécnica de
Valencia, España, 1995, (*.ppt), users.dsic.upv.es/~mcelma/conferencia.ppt
[Ceri 90] Stefano Ceri,http://www.amazon.com/Programming-Databases-Surveys-
Computer-Science/dp/3642839541 Georg Gottlob, Letizia Tanca,“Logic
Bibliografía
390
Programming and Databases (Surveys in Computer Science)”, Springer Verlag,
1990.
[Chang 73] EN.REFLIST[Church 36] Church, Alonzo; Rosser, J. Barkley, "Some
properties of conversion", Transactions of the American Mathematical Society 39
(3): 472–482, May http://www.jstor.org/stable/1989762
[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 http://maude.cs.uiuc.edu/
[Codd 70] Edgar Frank Codd.. "A relational model of data for large shared data banks".
Communications of the ACM 13 (6): 377, 1970
[Codd 82] Edgar Frank Codd "Relational database: A practical foundation for
productivity", Communications of the ACM 25 (2): 109, 1982.
[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:
www.software2007.com/grafix/pdf/State-Software-Industry-2007.pdf
[Dijkstra 68] Edsger Dijkstra, “GoTo Considedred Harmfull”, Comunications of the ACM,
11(3): 147-148, 1968. disponible en:
http://www.thocp.net/biographies/papers/goto_considered_harmful.htm
[Dijkstra 70] Edsger Dijkstra, “NOTES ON STRUCTURED PROGRAMMING”,
Technological University Eindhoven, The Netherlands, Department of
Mathematics, T.H.-Report 70-WSK-03, 1970, disponible en:
http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD249.PDF
[ERLANG] http://www.erlang.org/doc/
[Fisher 01-12] Jhon Fisher, “PROLOG Tutorial”,
http://www.csupomona.edu/~jrfisher/www/prolog_tutorial/contents.html
[Gibbs 94] W. Wayt Gibbs, “Software's Chronic Crisis”, staff writer. Copyright Scientific
American; September 1994;
http://www.cis.gsu.edu/~mmoore/CIS3300/handouts/SciAmSept1994.html
[Goguen 92]Joseph A. Goguen, José Meseguer: “Order-Sorted Algebra I: Equational
Deduction for Multiple Inheritance, Overloading, Exceptions and Partial
Operations”. Theor. Comput. Sci. 105(2): 217-273 (1992)
[Gra 2001] Paul Graham, “The roots of LISP”, 2001,
http://www.paulgraham.com/rootsoflisp.html
[Grassmann 97] Grassmann, W. K. (1997). “Matemática discreta y lógica”, Prentice Hall.
[Hanson 2002] Chris Hanson, the MIT Scheme Team and a cast of thousands, “MIT
Scheme Reference Manual”, Massachusetts Institute of Technology, Edition 1.96,
Bibliografía
391
for Scheme Release 7.7.0, 13 March 2002, disponible en
http://www.swiss.ai.mit.edu/projects/scheme/.
[HASKEL] http://www.haskell.org/haskellwiki/Haskell
[Harel 2000] David Harel, Dexter Kozen, Jersy Tiuryn, “Dynamic Logic”, Massachusetts
Institute of Thecnology, 2000,
[Hoare 61] C A R Hoare. Algorithm 63, Partition; Algorithm 64, Quicksort; Algorithm 65,
Communications of the ACM, 4(7):321-322, Jul 1961.
[Hoare 69] C.A.R. Hoare, “An Axiomatic Basis for Computer Programming”, CACM
12(10), 1969
[Hut 04] Michael Hut, Mark Ryan, “Logic in Computer Science- Modelling and Reasoning
About Systems” Cambridge Unversity Press, New York, 2004.
[Hopcroft 01] Jhon E. Hopcroft, Rajeev Motwani, Jeffrey D. Ullman, “Introduction to
automata theory, languages, and computation”, Addison Wesley, 2001.
[IBM 74] IBM Corporation, HIPO—A “Design Aid and Documentation Technique”,
Publication Number GC20-1851, IBM Corporation, White Plains, NY, 1974. (ver:
http://www.hit.ac.il/staff/leonidm/information-systems/ch64.html)
[Jacobson 92] Ivar Jacobson et al, “Object Oriented Software Engineering: A Use Case
Driven Approach”, 1992)
[Jacobson 98] Ivar; Grady Booch; James Rumbaugh, “The Unified Software Development
Process”. Addison Wesley Longman, 1998, ISBN 0-201-57169-2.
[Fitzgerald 04] John Fitzgerald, Peter Gorm Larsen, Paul Mukherjee, Nico Plat, Marcel
Verhoef, “Validated Designs for Object-oriented Systems”, Springer, 2004
[Jones 90] Cliff B Jones, “Systematic Software Development Using Vdm”, Second Edition,
The University, Manchester, England, Prentice Hall International, 1990.
[Kinnersley 1991-now] http://people.ku.edu/~nkinners/LangList/Extras/langlist.htm,
[Klint 07] Paul Klint, “Quick Introduction to Term Rewriting”, http://www.meta-
environment.org/doc/books/extraction-transformation/term-rewriting/term-
rewriting.html.
[Klotz 80] Klotz, I, “The N-Ray Affair”, Scientific American, Mayo 1980.
[Lennart 94] Aqvist Lennart Aqvist, "Deontic Logic" in D. Gabbay and F. Guenthner, ed.,
Handbook of Philosophical Logic: Volume II Extensions of Classical Logic.
Kluwer,1994
[Letelier 00] Patricio Letelier Torres, Pedro Sánchez Palma, Isidro Ramos Salavert. Oscar
Pastor López, “OASIS Versión 3.0 Un enfoque formal para el modelado conceptual
orientado a objeto”, Universidad Polítécnica de Valencia, DSIIC, proyecto
MENHIR, 2002
[Levenez ?-now] Éric Lévénez, “Computer Languages History”,
http://www.levenez.com/lang/
Bibliografía
392
[Carlsson 12] Mats Carlsson et al, .Laboratory, I. S. “SICStus Prolog User’s Manual”,
Swedish Institute of Computer Science,
http://www.sics.se/sicstus/docs/latest/pdf/sicstus.pdf.
[Martin 85] by James J. Martin, “Fourth-Generation Languages: Principles (Fourth
Generation Languages)”, Prentice Hall, ISBN: 0133296733 (ISBN13:
9780133296730), 1985
[Martin 97] Martin James, Odell Jaimes. “Métodos orientados a objetos: conceptos
fundamentals”, México: Prentice Hall Hispanoamérica, 1997
[McCracken 64 ] McCracken Daniel D.; William S. Dorn . “Numerical Methods and
Fortran Programming” (1 ed.). Wiley,1964.
[Mess 92]José Meseguer: “Conditioned Rewriting Logic as a United Model of
Concurrency”. Theor. Comput. Sci. 96(1): 73-155 (1992)
[Microsoft F#] http://research.microsoft.com/en-us/projects/fsharp/
[Miller 1956] Miller, G. A. (1956). "The magical number seven, plus or minus two: Some
limits on our capacity for processing information". Psychological Review 63 (2):
81–97. doi:10.1037/h0043158. PMID 13310704
[ML (SML)] http://www.smlnj.org/
[MAUDE] http://maude.cs.uiuc.edu/
[MAUDE 2007] “The Maude System”, Computer Science Laboratory, Departament of
Computer Science, University of Ilinois at Urbana-Champaign,
http://maude.cs.uiuc.edu/, página Web.
[McC 97] John McCarthy, “History of Lisp” Artificial Intelligence Laboratory, Stanford
University 12 February 1979, http://www-
formal.stanford.edu/jmc/history/lisp/lisp.html
[Meyer 98] Bertran Meyer, “Construcción de Software Orientado a Objetos, Segunda
Edición”, PRENTICE HALL, Madrid, 1999.
[Nwana 96] Nwana, H. S. “Software Agents: An Overview”, Cambridge University Press,
Knowledge Engineering Review, Vol. 11, No. 3, pp. 205–244. 1996.
[Ole-Johan 70] Ole-Johan Dahl, Bjørm Myhrhaug, and Kristen Nygaard, “SIMULA
Information: Common Base Language”, Norwegian Computing Center, OSLO 3 -
NORWAY, 1970, http://www.fh-jena.de/~kleine/history/languages/Simula-
CommonBaseLanguage.pdf
[OMG UML] Object Management Group, “Unified Modeling Lanaguge”,
http://www.uml.org/
[Pastor 95] Pastor López Oscar, Ramos Salavert Isidro, “OASIS 2.1.1, A Class-Definition
Language to Model Information Systems Using Object-Oriented Approach”,
Departamento de Sistemas Informáticos y Computación, Universidad Politécnica de
Valencia, España, 1995.
[Pressman 05] Pressman Roger, “Ingeniería del Software (6ª ed.)”, McGraw-Hill /
Interamericana de México, 2005.
Bibliografía
393
[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.
[Ram 93] Isidro Ramos, Oscar Pastor, Jose Cuevas, Jaume Devesa, “Objects as Observable
Processes”, Actas del 3rd. Workshop on the Deductive Approach to Information
System Design, Cataluña, 1993.
[Rumbaugh 91] James E. Rumbaugh et al, “Object-Oriented Modeling and Design”. , With
others. Prentice Hall, 1991, ISBN 0-13-629841-9.
[SCHEME 2003] “Scheme”, Massachusetts Institute of Technology,
http://www.swiss.ai.mit.edu/projects/scheme/, página Web mantenida por Chris
Hanson, last update: October 2003.
[swi_ref_man] SWI-Prolog documentation, http://www.swi-prolog.org/pldoc/refman/
[Sylvan 2007] http://www.haskell.org/complex/why_does_haskell_matter.html
[TIOBE ¿-now] The Software Quality Company, TIOBE Index fo <fecha>,
http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html,
http://www.tiobe.com/tiobe_index?page=index
[Terese 2003] Terese, “Term Rewriting Systems”, edited by Marc Bezem, Jan Willem
Klop, and Roelde Vrijer, Cambridge Tracts in Theoretical Computer Science, vol.
55. Cambridge University Press, 2003
[Toyama 90] Yoshihito Toyama, “Term rewriting systems and the Church-Rosser
property”, Doctoral Thesis, Tohoku University, 1990.
[Ullman 86] Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools, Addison-
Wesley, 1986. ISBN 0-201-10088-6
[Ullman 79] Jhon E. Hopcroft, Jefrey D. Ullman, “Introduction to automata theory,
languages and computation”, Addison-Wesley, 1979.
[Vangheluwe 02] Hans Vangheluwe, Juan de Lara, Pieter j. Mosterman, “An Introduction
to Multi-Paradigm Modelling and Simulation”, Proc. AIS2002. Pp, 2002, Pagina
web ATOM3
[Woodc 96] Jim Woodcock, University of Oxford, Jim Davies,University of Oxford,
“Using Z Specification, Refinement, and Proof”, Prentice Hall (ISBN 0-13-948472-
8), 1996, http://www.usingz.com/, http://www.lsi.usp.br/~edualves/Material-de-
Aula/Alao/zedbook.pdf
[Yourdon 79] Yourdon, E. and Constantine, “Structured Design”, Prentice-Hall,
Englewood Cliffs, NJ, 1979

También podría gustarte