Está en la página 1de 94

1. INTRODUCCIÓN ..................................................................................................................................................................

1.1. ¿QUÉ ES UN LP? ................................................................................................................................................................... 2


1.2. ABSTRACCIONES EN LOS LP.................................................................................................................................................... 2
1.2.1. Abstracciones de datos..................................................................................................................................................... 2
1.2.2. Abstracciones de control. .................................................................................................................................................. 2
1.3. PARADIGMAS DE COMPUTACIÓN ................................................................................................................................................ 3
1.4. DEFINICIÓN DE LENGUAJE ........................................................................................................................................................ 4
1.5. TRADUCCIÓN DEL LENGUAJE ..................................................................................................................................................... 5
1.6. DISEÑO DEL LENGUAJE ............................................................................................................................................................ 6
1. Introducción
1.1. ¿Qué es un LP?
 Un LP es un sistema notacional para describir computaciones en una forma legible tanto para la máquina como para el
ser humano.
 La computación, incluye todo tipo de operaciones de computadora, incluyendo la manipulación de datos, el
procesamiento de texto y el almacenamiento y recuperación de la información.
 La legibilidad por parte de la máquina, depende de la estructura del lenguaje. Ha de tener una estructura lo
suficientemente simple como para que permita una traducción eficiente.
 La legibilidad por parte del ser humano, requiere que un LP proporcione abstracciones de las acciones de las
computadoras fáciles de comprender.
 Una consecuencia de lo anterior es que los LP tienen tendencia a parecerse a los lenguajes naturales. De esta forma un
programador puede basarse en su comprensión del lenguaje natural para tener una idea inmediata de la computación
que se está describiendo.

1.2. Abstracciones en los LP


 Las abstracciones juegan un papel esencial para dotar de legibilidad a los programas para los seres humanos.
 Las abstracciones de los LP se agrupan en dos clases generales: abstracción de datos y abstracción de control. Éstas a
su vez, se agrupan en tres niveles: básicas (reúnen información de máquina más localizada), estructuradas (reúnen
información más global sobre la estructura del programa) y unitarias (reúnen información sobre alguna parte completa de
un programa).

1.2.1. Abstracciones de datos


 Resumen las propiedades de los datos (cadenas, números, árboles de búsqueda).
 Básicas:
o Resumen la representación interna de valores de datos comunes en una computadora (complemento a dos,
IEEE754, ASCII).
o Las localizaciones en la memoria de la computadora que contienen valores de datos, se abstraen dándoles un
nombre llamado variables.
o El tipo de valor de datos también recibe un nombre y se conoce como tipo.
o A las variables se les dan nombres y tipos de datos mediante una declaración.

Ej.: int x;

 Estructuradas:
o La estructura de los datos es el método principal para la abstracción de colecciones de valores de datos
relacionados entre sí.
Ej.: registros, arreglos, tipos estructurados.

 Unitarias:
o Incluyen al encapsulado de datos y la ocultación de la información, a menudo relacionado con los tipos de datos
abstractos.
Ej: paquete, clase.

o Una propiedad adicional de la abstracción de datos unitarios es su reutilización.


o Típicamente estas abstracciones representan componentes o contenedores y se convierten en la base de
mecanismos de bibliotecas de lenguaje.

1.2.2. Abstracciones de control.


 Resumen propiedades de la transferencia de control, es decir, la modificación de la trayectoria de ejecución de un
programa con base en una situación determinada (bucles, sentencias condicionadas, llamadas a procedimientos).

2 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Básicas:
o Son las sentencias de un lenguaje que combinan unas cuantas instrucciones de máquina en una sentencia
abstracta más comprensible.
Ej.: x = x + 3;

 Estructuradas:
o Dividen un programa en grupos de instrucciones que están anidadas.

Ej.: if, case, switch.

o Una ventaja es que se pueden anidar estas estructuras dentro de otras estructuras de control.
o Los mecanismos de bucle o ciclos estructurados se presentan de muchas formas, incluyendo los ciclos while, for
y do de C y C++, los ciclos repeat de Pascal y el enunciado loop de Ada.
o Otro mecanismo útil es el procedimiento (subrutina o subprograma).
o Se definirse un procedimiento dándole un nombre y asociándolo con las acciones que se van a llevar a cabo
(declaración de procedimiento).
o El procedimiento debe ser llamado (invocación o activación) en el punto en que las acciones deben ejecutarse.
o Una función puede considerarse un procedimiento pero que devuelve un valor o resultado a su invocador.
o En C y C++ a los procedimientos se les llama funciones nulas.

 Unitarias:
o Consiste en efectuar abstracciones de control con la finalidad de incluir una colección de procedimientos que
proporcionan servicios relacionados lógicamente con otras partes del programa y que forman una parte
unitaria, o independiente, del programa.
o Lo descrito anteriormente es en esencia lo mismo que una abstracción de datos de nivel unitario. La única
diferencia consiste en que aquí el enfoque está en las operaciones más que en los datos.
o Un tipo de abstracción de control que resulta difícil de clasificar son los mecanismos de programación en
paralelo, por ejemplo, las tareas (task) de ADA son una abstracción unitaria, pero los hilos o hebras de Java se
consideran estructuradas.

1.3. Paradigmas de computación


 Programación imperativa o procedural:
o Propiedades:
 La ejecución secuencial de instrucciones.
 El uso de variables en representación de localizaciones de memoria.
 El uso de la asignación para cambiar el valor de las variables.
o Al requisito de que la computación sea descrita como una secuencia de instrucciones se le llama cuello de
botella de Von Neumann, dado que limita la capacidad de un lenguaje de indicar computación en paralelo.

 Programación orientada a objetos:


o Un objeto puede describirse vagamente como una colección de localizaciones de memoria junto con todas las
operaciones que pueden cambiar los valores de dichas localizaciones.
o Representan una computación como la interacción o comunicación entre un grupo de objetos, cada uno de ellos
comportándose como su propia computadora, con su propia memoria y sus propias operaciones.
o En muchos lenguajes, éstos se agrupan en clases que representan todos los objetos con las mismas
propiedades. Entonces, se crean los objetos como instancias (ejemplos particulares) de una clase.
o Los métodos son las funciones u operaciones que contienen esas clases.
o En una clase, primero se define un constructor, el cual asigna memoria y proporciona los valores iniciales para
los datos de un objeto.
o Permiten a los programadores escribir código reutilizable y ampliable
o Los lenguajes orientados a objetos más importantes son Java, C++ y Smalltalk.
3 Longinos Recuero Bustos (http://longinox.blogspot.com)
 Programación funcional:
o Basa la descripción de las computaciones en la evaluación de funciones o en la aplicación de
funciones a valores conocidos.
o Se conocen también como lenguajes aplicativos.
o Su mecanismo básico es la llamada de función, que involucra la transferencia de valores como
parámetros a las funciones y la obtención de valores resultantes como valores devueltos de las funciones.
o Este paradigma no involucra una idea de variable o asignación de variables.
o La programación funcional es en cierto sentido opuesta a POO, ya que, se concentra en los valores y las
funciones en vez de en localizaciones de memoria; además de que las operaciones repetitivas se expresan
mediante funciones recursivas y no mediante ciclos.
o Sus ventajas son que el lenguaje se hace más independiente del modelo de máquina y resulta más fácil extraer
conclusiones precisas con respecto a su comportamiento.
o Los lenguajes más importantes son LISP, Scheme y Haskell.

 Programación lógica:
o Basado en la lógica simbólica.
o El programa está formado por un conjunto de enunciados que describen lo que es verdad con respecto a un
resultado deseado, en oposición a dar una secuencia particular de enunciados que deben ser ejecutados en un
orden fijo para producir el resultado.
o No tienen necesidad de abstracciones de control como ciclos o selección.
o También se le conoce como programación declarativa o lenguajes de muy alto nivel. Las variables no
representan localizaciones de memoria sino que se comportan más como nombres para los resultados de las
computaciones parciales.
o Un lenguaje importante es Prolog.

 Es necesario resaltar, que aunque un LP pudiera tener la mayor parte de las propiedades de uno de los anteriores
paradigmas, contienen generalmente características de varios.

1.4. Definición de lenguaje


 Un LP necesita una descripción completa y precisa.
 Actualmente es habitual dar una definición formal sólo de fracciones de un LP.
 La importancia de una definición precisa para un LP debe resultar clara a partir de su uso para describir un cómputo.
 La mejor forma de lograr independencia de la máquina y independencia de la implementación es a través de la
estandarización, que requiere una definición del lenguaje independiente y precisa, así como universalmente aceptada.
 Los requisitos de una definición formal aportan disciplina durante el diseño de un lenguaje.
 La definición del lenguaje se puede dividir aproximadamente en dos partes:
o Sintaxis del elnguaje (su estructura):
 Es de la descripción de las maneras en que las diferentes partes del lenguaje pueden ser combinadas
para formar otras partes (equiparable a la gramática de un lenguaje natural):

… Un enunciado if está formado de la palabra "if" seguida de una


expresión entre paréntesis, …

 La sintaxis de casi todos los lenguajes está dada ahora utilizando gramáticas libres de contexto:

<enunciado if> ::= if(<expresión>)<enunciado>


[else <enunciado>]

 Un problema íntimamente relacionado con la sintaxis de un LP es su estructura léxica (equiparable a la


ortografía del lenguaje natural):
4 Longinos Recuero Bustos (http://longinox.blogspot.com)
o Es la estructura de las palabras del lenguaje que generalmente se conocen como tokens:

"if", "else", "+", "<=", ";", "."

o Semántica del lenguaje (su significado):


 Es mucho más compleja y difícil de describir con precisión.
 Al describir el significado de una fracción de código se involucra algún tipo de descripción de los
efectos de su ejecución, y no existe una manera estándar de hacer esto.
 Una descripción completa de su significado en todos los contextos puede llegar a ser extremadamente
compleja:

 A pesar de ello se han desarrollado varios sistemas de notación para definiciones formales: semántica
operacional, la semántica denotacional y la semántica axiomática.

1.5. Traducción del lenguaje


 Un traductor es un programa que acepta otros programas escritos en el lenguaje en cuestión y que, o los ejecuta
directamente (intérprete), o los transforma en una forma adecuada para su ejecución (compilador).
 La interpretación es un proceso que consta de un paso, en donde tanto el programa como la entrada le son dados al
intérprete y se obtiene una salida:

 La compilación es un proceso que consta de dos pasos:


o El programa original (programa fuente) es la entrada al compilador, y la salida del compilador es un nuevo
programa (programa objetivo) el cual puede ser entonces ejecutado (si está en lenguaje máquina), aunque lo
más común es que sea un lenguaje ensamblador.
o El programa objetivo debe ser traducido por un ensamblador en un programa objeto y posteriormente ligado
(linkado) con otros programas objetos y cargado en localizaciones de memoria apropiadas antes de que pueda
ser ejecutado.

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Los pseudointérpretes (traductores intermedios entre intérpretes y compiladores), ejecutan el programa sin producir un
programa objetivo.
 Tanto los compiladores como los intérpretes deben llevar a cabo:
o Primero, un analizador léxico (rastreador), debe convertir la representación textual del programa como una
secuencia de caracteres en una forma más fácil de procesar (agrupando caracteres en tokens).
o Acto seguido, el analizador sintáctico o gramatical debe determinar la estructura de la secuencia de los tokens
proporcionados por el rastreador.
o Finalmente, un analizador semántico debe determinar lo suficiente del significado de un programa como para
permitir la ejecución o la generación de un programa objetivo. Estas fases ocurren combinadas de diversas
maneras.
 Un traductor tiene que mantener un entorno de ejecución en el cual se asigna el espacio adecuado de memoria para los
datos del programa y registra el avance de la ejecución del mismo.
 Las propiedades de un LP que pueden ser determinadas antes de su ejecución se llaman propiedades estáticas,
mientras que las propiedades que solamente se pueden determinar durante la ejecución se conocen como propiedades
dinámicas.
 Un compilador puede utilizar sólo las propiedades estáticas de un lenguaje (su léxico y su estructura sintáctica).
 Un lenguaje que sea más dinámico es más adecuado para la interpretación.
 Un lenguaje que sólo tenga asignación estática puede utilizar un ambiente totalmente estático. Para lenguajes más
dinámicos debe utilizarse un ambiente más complejo totalmente dinámico. En un punto medio está el típico ambiente
basado en pilas (C y Ada).
 Los intérpretes son inherentemente menos eficientes que los compiladores.
 Una propiedad importante de un traductor de lenguaje es su respuesta a errores contenidos en un programa fuente.
 Un traductor debería emitir mensajes de error apropiados, siendo la recuperación de errores lo más apropiado es, que le
permite al traductor seguir adelante con la traducción, de manera que puedan describirse errores adicionales.
 Los errores léxicos ocurren durante el análisis léxico, generalmente están limitados al uso de caracteres ilegales.
 Los errores ortográficos los detectaría el analizador sintáctico (incluyen tokens faltantes o expresiones mal organizadas).
 Los errores semánticos pueden ser estáticos (tipos incompatibles o variables no declaradas) o dinámicos (subíndice
fuera de rango o la división entre cero).
 Los errores lógicos (los que comete el programador y hace que el programa se comporte de manera errónea), de
ninguna manera son errores desde el punto de vista de la traducción del lenguaje.

1.6. Diseño del lenguaje


 El reto del diseño del LP, es lograr la potencia, expresividad y comprensión que requiere la legibilidad del ser humano,
mientras que se conservan al mismo tiempo la precisión y simplicidad necesarias para la traducción de máquina.
 Un lenguaje exitoso de programación tiene utilerías para abstracción de datos y abstracción de control.
 La meta prevaleciente de la abstracción en el diseño de LP es el control de la complejidad.

6 Longinos Recuero Bustos (http://longinox.blogspot.com)


2. PRINCIPIOS DE DISEÑO DE LOS LENGUAJES ...................................................................................................................... 2

2.1. HISTORIA Y CRITERIOS DE DISEÑO.............................................................................................................................................. 2


2.2. EFICIENCIA ............................................................................................................................................................................ 3
o Eficiencia del código .......................................................................................................................................................... 3
o Eficiencia de Compilación .................................................................................................................................................. 3
o Capacidad de implementación .......................................................................................................................................... 3
o Eficiencia de la programación ........................................................................................................................................... 3
o Confiabilidad...................................................................................................................................................................... 3
o Legibilidad ......................................................................................................................................................................... 3
o Capacidad de mantenimiento ........................................................................................................................................... 3
2.3. REGULARIDAD ....................................................................................................................................................................... 3
o Generalidad ....................................................................................................................................................................... 3
o Ortogonalidad ................................................................................................................................................................... 4
o Uniformidad ...................................................................................................................................................................... 4
2.4. PRINCIPIOS ADICIONALES SOBRE DISEÑO DE LOS LENGUAJES .......................................................................................................... 4
 Simplicidad ........................................................................................................................................................................ 4
 Expresividad....................................................................................................................................................................... 4
 Extensibilidad .................................................................................................................................................................... 5
 Capacidad de restricción ................................................................................................................................................... 5
 Consistencia ante notaciones y reglas convencionales aceptadas .................................................................................... 5
 Precisión ............................................................................................................................................................................ 5
 Independencia de la máquina ........................................................................................................................................... 6
 Seguridad........................................................................................................................................................................... 6
2. Principios de diseño de los lenguajes
 El éxito o fracaso de un LP puede depender de las interacciones complejas entre los mecanismos del mismo.
 Toda aplicación de programación es distinta y requiere de abstracciones diferentes, por lo que, con el diseño del lenguaje,
debemos o bien proporcionar abstracciones adecuadas para situaciones particulares o herramientas de tipo general para la
creación sobre demandas de dichas abstracciones.
 Por lo tanto, el diseño del LP depende mucho del uso pretendido y los requisitos de este uso. En esto es similar a la
programación misma, se trata de una actividad orientada a metas.

2.1. Historia y criterios de diseño


 Al iniciarse los LP, existía un criterio de diseño primordial, eficiencia en la ejecución (las máquinas eran extremadamente
lentas). Este era el objetivo de algunos LP como FORTRAN.
 Naturalmente, se podía olvidar que la razón principal de la existencia de un LP de alto nivel era facilitar la escritura del
programa.
 Esta capacidad de escritura (la cualidad de un lenguaje que le permite a un programador su uso para expresar una
computación con claridad, corrección, de manera concisa y con rapidez) siempre quedó en segundo término en relación
con la eficiencia.
 Años 60:
o Tanto COBOL como Algol60 pueden considerarse como pasos hacia criterios más generales que la simple
eficiencia del código generado.
o Algol60:
 La estructura en bloques y la disponibilidad de la recursión hacía más adecuado para expresar
algoritmos en una forma lógicamente clara y concisa, promoviendo una mayor capacidad de escritura.
 Fue diseñado como un LP para comunicar algoritmos entre personas, por lo que su legibilidad (la
cualidad de un lenguaje que le permite a un programador comprender y entender de manera fácil y con
precisión la naturaleza de una computación) también era una meta importante en el diseño.
o Algol68:
 Intentó reducir la complejidad del lenguaje al hacerse totalmente general (muy pocas restricciones) y
ortogonal (se pudieran combinar sus características en cualquier forma significativa).
o COBOL:
 Intentaba mejorar una legibilidad de los programas haciendo que los programas se parecieran más al
inglés escrito ordinario.
o Simula67:
 Tenía como meta proveer mecanismos de abstracción más poderosos.
 Introducen el concepto de clase.
 Años 70 a 80:
o Llegó un mayor énfasis en la simplicidad y la abstracción, como PASCAL, C, Euclid, CLU, Modula-2 y Ada.
 Años 80 a 90:
o Continuó con el interés de mejorar la precisión lógica o matemática de los lenguajes, como Ml, Haskell y
Lisp/Scheme.
 Últimos 15 años:
o El desarrollo de mayor importancia ha sido el éxito práctico de los lenguajes orientados a objetos, como
Smalltalk, C++ y Java.
 Las metas de diseño de mayor éxito han sido:
o La correspondencia de los mecanismos de abstracción con las necesidades de las tareas de programación.
o El uso de bibliotecas para ampliar la resolución de tareas específicas.
o El uso de técnicas orientadas a objetos para aumentar la flexibilidad y reutilización del código.

2 Longinos Recuero Bustos (http://longinox.blogspot.com)


2.2. Eficiencia
 Este principio puede abarcar prácticamente todos los demás principios en varias formas:
o Eficiencia del código:
 El diseño del lenguaje debe ser tal que un compilador pueda generar un código ejecutable eficiente
(optimizabilidad).
o Eficiencia de Compilación:
 Compilación rápida, usando un compilador de tamaño reducido.
 La verificación de errores puede ser un problema grave de eficiencia, ya que verificar la existencia de
un error en tiempo de compilación puede hacer ineficiente al compilador, y la generación de código
para verificar el error durante la ejecución podría hacer que el código objetivo resulte ineficiente.
 Por otra parte, ignorar esa verificación, viola otro principio de diseño, la confiabilidad (el aseguramiento
de que un programa no se comportará en formas no esperadas durante su ejecución).
o Capacidad de implementación:
 Es la eficiencia con la que se puede escribir un compilador.
 El éxito de un lenguaje se puede obstaculizar simplemente porque es demasiado difícil escribir un
compilador.
o Eficiencia de la programación:
 Rapidez y facilidad en la escritura de programas en el lenguaje.
 Facilidad para expresar procesos y estructuras complejas (capacidad de expresión del lenguaje).
 Resumiendo, facilidad con la que se puede correlacionar el diseño en la cabeza del programador con el
código real.
 Lo conciso de la sintaxis y evitar detalles innecesarios, como son las declaraciones de variables, a
menudo también se consideran factores importantes en este tipo de eficiencia.
o Confiabilidad:
 Se puede considerar la confiabilidad como un problema de eficiencia en sí mismo.
 Un programa que no sea confiable genera muchos costos adicionales (modificaciones, pruebas, …).
 Este tipo de eficiencia es un problema de consumo de recursos en la ingeniería del software.
o Legibilidad:
 Facilidad con la que un lenguaje puede ser comprendido.
o Capacidad de mantenimiento:
 Facilidad con la cual se pueden localizar errores y corregirse, así como agregarse nuevas
características.

2.3. Regularidad
 Es una cualidad algo mal definida de un lenguaje que expresa lo bien que están integradas las características del mismo.
 Una mayor regularidad implica:
o Pocas restricciones en el uso de constructores.
o Menos interacciones raras entre dichos constructores.
o En general, menos sorpresas en la forma en que se comportan las características del lenguaje.

 La regularidad se puede dividir en tres conceptos:


o Generalidad:
 Un lenguaje logra tener generalidad eliminando casos especiales en la disponibilidad y uso de los
constructores y combinando constructores íntimamente relacionados en uno solo más general.
 Algunos ejemplos de violación de este concepto podrían ser la carencia de:
 Variables de procedimientos (Pascal).
 Anidación de funciones / procedimientos (C).
 Arreglos de longitud variable (Pascal).
 Extensión y creación de operadores (==, +, …) hacia nuevos tipos de datos (C) .

3 Longinos Recuero Bustos (http://longinox.blogspot.com)


o Ortogonalidad:
 Los constructores del lenguaje no deben comportarse de manera diferente en contextos diferentes, por
lo que las restricciones que dependen del contexto son no ortogonales, en tanto que las que se aplican
independientemente del contexto son no generales.
 Los constructores del lenguaje se pueden combinar en cualquier forma significativa y que la interacción
entre los constructores o el contexto del uso no debe generar restricción o comportamientos
inesperados.
 Los constructores no pueden comportarse de manera diferente en contextos diferentes.
 Algunos ejemplos de violación de este concepto podrían ser la carencia de:
 Las funciones solo devuelven valores escalares o apuntadores (Pascal).
 Las variable locales sólo pueden declararse al principio de un bloque (C).
 Paso de todos los parámetros por valor, excepto los arreglos (C).
 La ortogonalidad fue una meta principal de Algol68 y se mantiene como el mejor ejemplo de un
lenguaje donde los constructores pueden combinarse en todas las formas significativas.

o Uniformidad:
 Las cosas similares deben verse de forma similar y tener significados similares, y a la inversa, las
cosas diferentes se tienen que ver de forma diferente.
 Las no uniformidades son de dos tipos:
 Cosas similares no parecen ser o se no comportan de manera similar.
 Cosas no similares, parecen ser o se comportan de manera similar.
 Ejemplo de violación de este concepto puede ser:
 En C++ es necesario un punto y coma después de la definición de clase, pero está prohibido
después de la definición de una función.

class A { ... };
int f() { ... }

 El afán por hacer imponer una meta, como puedan ser la generalidad o la ortogonalidad en el diseño de un lenguaje,
puede resultar peligroso.
 Si una no regularidad no puede justificarse de forma razonable entonces se puede decir que es un error de diseño.

2.4. Principios adicionales sobre diseño de los lenguajes


 Simplicidad:
o La simplicidad parecía ser un principio fácil de lograr, pero en la práctica es asombrosamente difícil.
o No debe confundirse simplicidad con regularidad, Algol68 es uno de los lenguajes más regulares, pero no es
simple.
o Tampoco tener muy pocos constructores básicos es simplicidad, LISP y Prolog tienen unos cuantos
constructores básicos, pero dependen de un ambiente de ejecución complejo.
o Un LP demasiado simple puede hacer que la tarea de utilizarlo resulte más compleja. BASIC es un lenguaje
simple, pero la carencia de algunos constructores fundamentales (declaraciones, bloques, …) hace mucho más
difícil de programar aplicaciones grandes.
o “Todo debería hacerse tan simple como sea posible, pero no más simple”. Albert Einstein.
o La sobresimplicidad puede hacerse que el lenguaje sea difícil de utilizar, carente de expresividad, legibilidad o
seguridad y sujeto a demasiadas restricciones.
 Expresividad:
o Es la facilidad con que un lenguaje puede expresar procesos y estructuras complejas.
o Es una ayuda a la eficiencia de programación.
o Uno de los primeros adelantos en expresividad fue la adición de la recursión en los LP (LISP y Algol60)
o Puede entrar en conflicto con la simplicidad.

4 Longinos Recuero Bustos (http://longinox.blogspot.com)


o Es una de las razones del aumento de popularidad de los lenguajes orientados a objetos.
o En ocasiones la expresividad se considera como concisa, lo que puede, sin embargo, comprometer la
legibilidad, como por ejemplo en la siguiente expresión de C:

while(*s++ == *t++);
 Extensibilidad:
o Principio que indica que debería de haber algún mecanismo general que permita al usuario añadir nuevas
características al lenguaje.
o Podría significar simplemente el añadir:
 Nuevos tipos al lenguaje.
 Nuevas funciones a una biblioteca.
 Nuevas palabras claves.
o A lo largo de los últimos 10 años la extensibilidad ha pasado a ser de importancia primordial como una
propiedad de los lenguajes. En concreto la simplicidad sin extensibilidad, prácticamente tiene garantizado el
fracaso del lenguaje.

 Capacidad de restricción:
o Un diseño de lenguaje debería dar la posibilidad de que un programador pudiera programar de una forma útil
empleando un conocimiento mínimo del lenguaje, por lo que el diseño de un lenguaje debería promover la
capacidad de definir subconjuntos del lenguaje.
o Esto resulta útil de dos maneras:
 No es necesario que el programador aprenda todo el lenguaje para utilizarlo con efectividad.
 Un escritor de compilador podría elegir implementar únicamente un subconjunto en el caso de que la
implementación de todo lenguaje resultara demasiado costosa e innecesaria.

 Consistencia ante notaciones y reglas convencionales aceptadas:


o Un LP debería ser fácil de aprender por un programador experimentado.
o Por lo tanto un lenguaje debe incorporar tantas características y conceptos posibles que se hayan convertido en
estándar.
o Ley del mínimo asombro:
 Las cosas no deberían de parecer y comportarse de manera totalmente inesperadas.
 Por ejemplo, FORTRAN ignora los espacios en blanco y esto podría viola la ley del mínimo asombro:

DO 99 I = 1.10

Pudiera parecer que se quiere ejecutar 99 veces la asignación del valor 1.10 a la variable I, cuando
en realidad lo que se hace es asignar el valor 1.10 a la variable DO99I.

 Precisión:
o A veces conocida como claridad.
o Es la existencia de una definición precisa para un lenguaje, de tal manera que el comportamiento de los
programas pueda ser predecible.
o Pasos para lograrlo:
 Publicación de un manual o informe del lenguaje por parte del diseñador.
 Adopción de un estándar emitido por una organización nacional o internacional de estándares:
 ANSI.
 ISO.
 …

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Independencia de la máquina:
o El método primordial para lograr la independencia de la máquina es el uso de tipos de datos predefinidos que no
involucran detalles de asignación de memoria o de la arquitectura de máquina.
o Desafortunadamente, este tipo de datos no pueden nunca estar totalmente libres de problemas de la máquina.

 Seguridad:
o Este principio se basa en minimizar los errores de programación y permitir que dichos errores sean detectados e
informados.
o La seguridad está íntimamente relacionada con la confiabilidad y con la precisión.
o Este es el principio que condujo a los diseñadores del lenguaje a introducir los tipos, la verificación de tipos y las
declaraciones de variables en los lenguajes de programación.
o Con esto se puede comprometer tanto la expresividad como lo conciso del lenguaje.

6 Longinos Recuero Bustos (http://longinox.blogspot.com)


Tabla de contenido
3.1 PROGRAMAS COMO FUNCIONES ................................................................................................................................................ 2
3.2 EVALUACIÓN RETRASADA ......................................................................................................................................................... 3
3.3 HASKELL: UN LENGUAJE PEREZOSO COMPLETAMENTE CURRY CON SOBRECARGA ................................................................................. 4
3.4 LAS MATEMÁTICAS EN LA PROGRAMACIÓN FUNCIONAL I: FUNCIONES RECURSIVAS .............................................................................. 7
3. Programación funcional
 La programación funcional tiene varias ventajas sobre la programación imperativa.
 Incluyen la concepción de programas como funciones, el trato de las funciones como si fueran datos, la limitación de efectos
colaterales y el uso de administración automática de la memoria.
 Como resultado, un lenguaje de programación funcional tiene gran flexibilidad, es conciso en su notación y su semántica es
sencilla.
 El inconveniente ha sido la ineficiencia en la ejecución.
 Debido a su naturaleza dinámica, estos lenguajes han sido interpretados más que compilados, resultando en una pérdida
sustancial en velocidad de ejecución.
 Existen varias razones por las cuales nunca han llegado a tener la popularidad de otros lenguajes (imperativos u orientados a
objetos):
o Los programadores aprenden a programar con lenguajes imperativos o bien orientados a objetos.
o Los lenguajes orientados a objetos se construyen sobre conceptos imperativos tradicionales.

 Existe suficiente razón para estudiar la programación funcional incluso si uno no espera jamás escribir aplicaciones reales en
un lenguaje funcional, y que los métodos funcionales como la recursión, la abstracción funcional y las funciones de orden
superior han influido y/o se han convertido en parte de la mayoría de los lenguajes de programación y deberían formar parte
del conjunto de técnicas de todo programador profesional.

3.1 Programas como funciones


 Desde este paradigma, un programa es una función matemática:

 El conjunto es el dominio de , el conjunto se conoce como el rango de . La se conoce como variable


independiente y la como variable dependiente.
 Si f no está definida para toda x perteneciente a X se dice que f es una función parcial. En otro caso es total.
 Podemos referirnos a como ‘entrada’ y a como ‘salida’.
 El punto de vista funcional de la programación no hace ninguna distinción entres procedimiento, función o programa, sin
embargo, hace siempre distinción entre valores de entrada o de salida.
 En los lenguajes de programación debemos distinguir entre definición de función y aplicación de función:
o La primera se refiere a la forma en que debe de calcularse una función utilizando los parámetros formales.
o La segunda se refiere a la llamada a la función declarada utilizando parámetros reales.

 El punto de vista funcional de la programación elimina las variables (como apuntadores de memoria) y por lo tanto la
asignación, a esto se le define como programación funcional pura.
 Una consecuencia de la programación funcional pura, es que no pueden existir ciclos (bucles), esto es solventado gracias
a la recursividad.
 Otra consecuencia de la falta de variables es que no existe el concepto de estado interno de la función, por lo tanto, el
valor de cualquier función depende solo de los valores de sus argumentos.
 La propiedad de una función de que su valor sólo dependa de los valores de los argumentos (y de sus constantes no
locales) se conoce como transparencia referencial.
 Tomemos como ejemplo la función rand, que no depende sólo de los argumento, sino del estado de la máquina y de
las llamadas anteriores a sí misma, por lo tanto nunca podrá ser transparente referencialmente.
 Como consecuencia una función transparente referencialmente que no tenga argumentos siempre devuelve el mismo
valor, por lo que no se diferenciaría de una constante.
 La carencia de asignación y la transparencia referencial de la programación funcional hacen que los programas

2 Longinos Recuero Bustos (http://longinox.blogspot.com)


funcionales sean particularmente simples.
 El ambiente de ejecución asocia nombre sólo a valores no a localizaciones de memoria, una vez introducido un nombre
en el ambiente su valor nunca cambia. A esto se le denomina semántica de valor (para distinguirla de la semántica
de almacenamiento o de apuntadores).
 En la programación funcional debemos ser capaces de manipular las funciones en forma arbitraria, pero sin restricciones
arbitrarias. Las funciones mismas deben considerarse como valores que pueden ser calculados por otras funciones y que
también pueden ser parámetros de funciones.
 A esta generalidad de las funciones en programación funcional se dice que las funciones son valores de primera clase.
 Funciones que tienen parámetros que a su vez son funciones, o producen un resultado que es una función, o ambos, se
las denomina funciones de orden superior.
 Resumiendo las cualidades de los lenguajes de programación funcional y de programas funcionales:
o Todos los procedimientos son funciones y distinguen claramente los valores de entrada (parámetros) de los de
salida (resultados).
o No existen variables ni asignaciones. Las variables han sido reemplazadas por los parámetros.
o No existen ciclos. Estos han sido reemplazados por llamadas recursivas.
o El valor de una función depende solo del valor de sus parámetros y no del orden de evaluación o de la
trayectoria de ejecución que llevó a la llamada.
o Las funciones son valores de primera clase.

3.2 Evaluación retrasada


 Un problema importante que se presenta en el diseño y en el uso de los lenguajes funcionales es la distinción entre las
funciones “normales” y las formas especiales.
 En un lenguaje con una regla de evaluación de orden aplicativo, como son Scheme y ML todos los parámetros a las
funciones definidas por el usuario se evalúan en el momento de la llamada, aunque no sea necesario hacerlo.
 En el caso de la función and, la expresión Scheme (and a b) o la expresión infija a && b en un lenguaje como C o Java
no necesitan evaluar el parámetro b, si a se evalúa como falso.
 Esto se conoce como evaluación en cortocircuito de las expresiones booleanas y es un ejemplo de la utilidad de
evaluación retrasada.
 En el caso de la función if, no es solo un caso de simple utilidad, sino de necesidad: para que la expresión (if a b c)
en Scheme tenga la semántica apropiada, la evaluación de b y de c debe retrasarse hasta que se conozca el resultado de
a, y con base en lo anterior se evalúa b o c, pero nunca ambas.
 Scheme y ML deben distinguir entre dos clases de funciones predefinidas: aquellas que utilizan la evaluación estándar y
aquellas que no lo hacen (las formas especiales).
 Esto compromete la uniformidad y la extensión de estos lenguajes.
 Un posible argumento para restringir la evaluación de la función a la evaluación de orden aplicativo es que la
semántica (y la implementación) son más simples.
 Considerando el caso de una llamada a función en la que un parámetro puede tener un valor indefinido, como en la
expresión en C ( 1 == 0 ) && ( 1 == 1 / 0 ). En este caso la evaluación retrasada puede conducir a un resultado
bien definido.
 Las funciones con esta propiedad se conocen como no estrictas, y los lenguajes con funciones estrictas son más fáciles de
implementar.
 El no ser estricto puede ser una propiedad deseable.
 En un lenguaje que tenga valores de función es posible retrasar la evaluación de un parámetro colocándolo dentro de una
“cápsula”, es decir, una función sin parámetros. Como ejemplo tomemos la llamada p( 1, divByZero ) en el siguiente
fragmento de código en C:
typedef int (*IntProc)( void );

int divByZero( void ){ return( 1 / 0 ); }

int p( int x, IntProc y )


{
if( x ) return 1;
else return y();
}

3 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Algunas veces estos procedimientos de “cápsulas”, como divByZero se conocen como thunks de paso por nombre.
 Existen lenguajes de programación (Scheme), en donde se utiliza un proceso conocido como memoization, en donde el
valor de un objeto retrasado, es almacenado la primera vez que se aplica una función al objeto, y entonces, llamadas
subsecuentes a esa función, simplemente devuelven el valor anterior en vez de volver a efectuar el cálculo.
 Este tipo de evaluación de parámetros se conoce como paso por necesidad.
 La evaluación retrasada en el paso por nombre es útil porque permite que los parámetros se queden sin calcular si no es
necesario y permite que formas especiales como if sean definidas como funciones normales.
 Sin embargo, el paso por nombre no puede manejar situaciones más complejas donde dentro de un cálculo sólo se
necesita una parte de cada parámetro.
 El escenario que se ha descrito, junto con el proceso de memoization, se conoce como evaluación perezosa.
 Esta evaluación perezosa puede lograrse en un lenguaje funcional al requerir que el ambiente de ejecución evalúe
expresiones de acuerdo con las reglas que siguen:
o Todos los argumentos a las funciones definidas por el usuario, se retrasan.
o Todas las ligaduras de los nombres locales en expresiones let y letrec, se retrasan.
o Todos los argumentos a las funciones de constructor (como cons), se retrasan.
o Todos los argumentos a otras funciones predefinidas, como las funciones aritméticas +, * y demás, se obligan.
o Todos los argumentos valuados de función, se obligan.
o Todas las condiciones en las funciones de selección como if y cond, se obligan.

 Estas reglas permiten que las operaciones sobre listas largas calculen únicamente la porción de la lista que resulte
necesaria.
 También es posible incluir listas potencialmente infinitas en leguajes con evaluación perezosa ya que la parte “infinita”
jamás será calculada, sino solo lo necesario de la lista como se requiera para un cálculo en particular.
 Para expresar la naturaleza potencialmente infinita de este tipo de listas, aquella que obedecen a las reglas de evaluación
perezosa a menudo se llaman flujos.
 Un flujo puede considerarse como una lista parcialmente calculada donde los elementos restantes pueden seguir siendo
calculados hasta cualquier número deseado.
 Los flujos son un tema importante en la programación funcional.
 El ejemplo principal de un lenguaje funcional con evaluación perezosa es Haskell.
 La evaluación perezosa permite un estilo de programación funcional que nos da la libertad de separar un cómputo en
partes, formadas por procedimientos que generan flujos (generadores) y otros procedimientos que aceptan flujos (filtros), y
a este tipo de programación lo llamaremos programación de generadores y filtros.

3.3 Haskell: Un lenguaje perezoso completamente Curry con sobrecarga


 Haskell es un lenguaje funcional puro cuyo desarrollo se inicio a finales de los años 80.
 Elabora y amplia una serie de lenguajes perezosos puramente funcionales desarrollados a finales de los años 70 y 80 por
David A. Turner, y que culminaron en el leguaje de programación Miranda.
 Incluye características novedosas, incluyendo la sobrecarga de funciones y un mecanismo de nombre mónadas¸ para
ocuparse de efectos colaterales (por ejemplo la E/S).
 Reduce la sintaxis al mínimo utilizando indicios internos del programa, incluyendo la regla de disposición o regla de
diagramado, que utiliza la sangría y el formato de renglones para resolver ambigüedades.
 Por lo tanto, es raro que un programa Haskell requiera de puntos y comas, paréntesis, incluso no existe una sintaxis
especial para las funciones definidas y no es necesario utilizar barras verticales para las funciones definidas utilizando
emparejamiento de patrones.

fact 0 = 1
fact n = n * fact ( n - 1 )

 No permite la redefinición de funciones que ya hayan sido predefinidas.


 La creación de tipos se hace con el operador :: la creación de listas con el operador : y la concatenación de listas con el
operador ++.
 Haskell tiene un lenguaje completamente Curry:
o Las funciones de Haskell pueden tomar funciones como parámetros y devolver funciones como resultado.
Una función que hace ambas cosas o alguna de ellas se llama función de orden superior.
o La currificación (currying) consiste en simular funciones de varios argumentos mediante funciones de orden
superior de un argumento.

4 Longinos Recuero Bustos (http://longinox.blogspot.com)


Las funciones con múltiples argumentos, pueden manejarse de otra forma, un poco más compleja quizás,
para explotar la capacidad de retornar funciones como resultado o de tomar funciones como parámetros.
Si consideramos la siguiente definición del tipo de suma:

suma: Z → Z → Z

Esto se interpreta como, suma es una función que recibe un entero, seguido de otro entero y devuelve la
suma de ambos.
Esto se deduce de la notación utilizada para dar el tipo de la función Z → Z → Z.
Esta forma de denotar los tipos de las funciones se denomina currificada.
La notación currificada no es simplemente una notación, agrega poder de expresividad a los tipos de las
funciones.
El símbolo → (que se lee “implica”), asocia hacia la derecha, esto significa que las dos expresiones
siguientes son equivalentes:
suma: Z → Z → Z

suma: Z → (Z → Z)

Por lo tanto se puede decir también que suma es una función que recibe un entero y devuelve una función
que a su vez, recibe un entero y devuelve un entero.

Todos los operadores binarios predefinidos están en forma Curry y pueden ser aplicados parcialmente a
cualquier argumento utilizando paréntesis (esto se conoce en Haskell como una sección):

plus2 = (2 + )

> plus2 3
5

 Haskell tiene funciones anónimas (explesiones lambda), las cuales representan a “lambda” con una diagonal invertida:

> (\ x -> x * x ) 3
9

 El operador de composición de funciones en Haskell se identifica mediante el punto:

> ( ( * 3 ) . ( 2 + ) ) 5
21

 Haskell tiene listas y tuplas incorporadas, así como sinónimos de tipo y tipos polimórficos definidos por el usuario:

type ListFn a = [a] -> [a]


type Salary = FLoat

 Los nombres de tipos y de constructores deben escribirse en mayúsculas, en tanto que los nombres de funciones y de
valores deben aparecer en minúsculas.
 Haskell tiene un sistema de notación especial para operaciones aplicadas sobre listas, conocido como comprensión de
lista, mismo que está diseñado para tener una apariencia de definiciones de conjuntos matemáticos:

Square_list lis = [ x * x | x <- lis ]

> Square_list [2, 3]


[4, 9]

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Haskell es un lenguaje perezoso (con evaluación perezosa), esto también significa que las listas son idénticas a flujos, y son
capaces de ser potencialmente infinitas:
ones = 1 : ones
[n...]
[n, n + 1, ...]
[2, 4, ...]

 Para una lista potencialmente infinita, resultan útiles las funciones que calculan la lista en forma parcial:
o take: extrae los primeros n elementos.
o drop: descarta los primeros n elementos.

> take 5 ( drop 4 [ 2 ... ] )


[6, 7, 8, 9, 10]

 Cuando una función puede utilizarse con diferentes tipos de argumentos se dice que está sobrecargada. La función +, por
ejemplo, puede utilizarse para sumar enteros o para sumar reales. La resolución de la sobrecarga por parte del sistema
Haskell se basa en organizar los diferentes tipos en lo que se denominan clases de tipos (un conjunto de tipos que definen
ciertas funciones).
 Consideremos el operador de comparación ==. Existen muchos tipos cuyos elementos pueden ser comparables, sin
embargo, los elementos de otros tipos podrían no ser comparables. Por ejemplo, comparar la igualdad de dos funciones es
una tarea computacionalmente intratable, mientras que a menudo se desea comparar si dos listas son iguales.
 Las clases de tipos solucionan ese problema permitiendo declarar qué tipos son instancias de unas clases determinadas y
proporcionando definiciones de ciertas operaciones asociadas con cada clase de tipos:

class Eq a where
(==), (/=), :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)

 Naturalmente, existen clases de tipos adicionales que requieren o dependen de otras funciones. Esta dependencia de las
clases de tipo sobre otras se llama herencia de clase de tipo, y establece una jerarquía de éstas.
 Para establecer los requerimientos de herencia para cada clase de tipo, Haskell utiliza una notación similar a la calificación
de tipo.

class (Ea a, Show a) => Num a where ... class (Eq a) => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a

 Como ejemplo de lo anterior, tomemos una instantánea de la jerarquía de clases de tipos numéricos en Haskell:

6 Longinos Recuero Bustos (http://longinox.blogspot.com)


3.4 Las matemáticas en la Programación funcional I: Funciones recursivas
 Puede considerarse una función como un conjunto de pares de forma que o como un subconjunto
del producto cartesiano :

 Considerar una función como un conjunto tiene ciertas ventajas para el estudio de la definición de función en los
lenguajes de programación:

 Este método de definición de conjunto a veces se conoce como definición por extensión.
 Sin embargo, la definición más común de una función se hace mediante una fórmula o propiedad, conocida como
definición por comprensión:

 Importante, de aquí en adelante las definiciones dadas no son del texto base, principalmente debido a que en mi
opinión, este apartado del texto base confunde más que aporta.
 En la definición de una función se pueden usar las funciones estándar y las funciones definidas por el usuario. Pero
también se puede usar la propia función que se define en su definición. A tal definición se la llama definición recursiva.
 En Haskell no tenemos posibilidad de definir bucles. La forma de iterar es utilizando la recursividad.
 La recursividad se apoya en el principio de inducción. Este principio es ampliamente utilizado en matemáticas para
demostrar que se cumple una propiedad para cualquier valor del ámbito que se esté tratando.
 Principio de inducción:
o La afirmación es cierta para un valor inicial (caso base).
o será cierta para un valor , si es cierta para el valor anterior a , es decir, si es cierta para
entonces lo será para .
 Como ejemplo, podemos utilizar el principio de inducción para definir los números naturales:
o El número es natural.
o es natural si es natural.
 En Haskel:

natural 1 = True
natural n = natural (n-1)

> natural 5
True

Aunque aparentemente el principio de inducción funciona, no se ha tenido en cuenta que, el valor tiene que ser mayor
que el caso base. Por lo tanto si lo intentásemos con el valor de -1, la definición anterior nunca terminaría.

Añadiendo el caso anterior, tendríamos:

natural 1 = True
natural n
| n > 1 = natural (n-1)
| otherwise = False

> natural (-1)


False

 Recomendación importante: No seguiremos mentalmente la secuencia de llamadas recursivas para resolver los
problemas. Nos centraremos en definir bien el caso base (el primer elemento que cumple la propiedad) y la relación de un
valor con el anterior.

7 Longinos Recuero Bustos (http://longinox.blogspot.com)


4 PROGRAMACIÓN LÓGICA ................................................................................................................................................... 2

4.1 LÓGICA Y PROGRAMAS LÓGICOS ................................................................................................................................................ 2


4.2 CLÁUSULAS HORN .................................................................................................................................................................. 4
4.3 RESOLUCIÓN Y UNIFICACIÓN ..................................................................................................................................................... 4
4.4 EL LENGUAJE PROLOG ............................................................................................................................................................. 5
4.4.1 Notación y estructuras de datos ................................................................................................................................... 5
4.4.2 Ejecución en Prolog ....................................................................................................................................................... 6
4.4.3 Aritmética...................................................................................................................................................................... 6
4.4.4 Unificación .................................................................................................................................................................... 7
4.4.5 Estrategia de búsqueda en Prolog ................................................................................................................................ 8
4.4.6 Ciclos y estructuras de control....................................................................................................................................... 9
4.5 PROBLEMAS QUE SE PRESENTAN CON LA PROGRAMACIÓN LÓGICA .................................................................................................. 10
4.5.1 El problema de verificación de ocurrencias en la unificación ...................................................................................... 10
4.5.2 Negociación como fracaso .......................................................................................................................................... 10
4.5.3 Las cláusulas Horn no expresan toda la lógica ............................................................................................................ 11
4.5.4 Información de control en la programación lógica ..................................................................................................... 11
4 Programación lógica
 La lógica está relacionada con las computadoras y con los lenguajes de programación de varias maneras. En primer
lugar los circuitos de las computadoras está diseñados con ayuda del algebra booleana, y los datos y las ecuaciones
booleanas están siendo utilizados prácticamente de forma universal en los lenguajes de programación para el control de
sus acciones.
 Los enunciados lógicos, o por lo menos un forma restringida de ellos, pueden considerarse como un lenguaje de
programación y ejecutarse en una computadora, si existe un sistema de interpretación lo suficientemente sofisticado para
ello.
 Trabajando sobre este enunciado se llegó al lenguaje Prolog, el cual logró una fama instantánea cuando el gobierno
japonés lo utilizó para el proyecto de quinta generación, para el desarrollo de sistemas de cómputo basados en técnicas
de razonamiento y comprensión de lenguaje humano. Pero tras el abandono del proyecto, Prolog ha ido perdiendo
difusión a excepción del área de la comprensión del lenguaje natural y sistemas expertos.
 Actualmente Prolog sigue siendo el ejemplo más significativo de un lenguaje de programación lógico, aunque ha surgido
varias extensiones que se ha hecho populares: Programación lógica con restricciones y sistemas basados en ecuaciones
que utilizan ecuaciones en vez de la lógica para describir la computación.

4.1 Lógica y programas lógicos


 El tipo de lógica que se utiliza en la programación lógica es el cálculo de predicados de primer orden, que es una forma
de expresar de manera formal enunciado lógicos, esto es, enunciados que son verdaderos o falsos.

Enunciado Traducción
0 es un nº natural natural(0)
2 es un nº natural natural(2)
Para toda , si es nº natural, entonces también lo es el sucesor de Para todas , natural( ) natural(sucesor( ))
-1 es un nº natural natural(-1)

 Entre estos enunciados lógicos, el primer y tercer enunciado pueden considerarse como axiomas para los números
naturales: enunciados que se suponen verdaderos y a partir de los cuales todos los enunciados verdaderos con respecto
a los nos naturales pueden probarse.
 El cálculo de predicados de primer orden clasifica las distintas partes de estos enunciados de la siguiente forma:
1. Constantes (o átomos):
 Éstos son por lo general número o nombres.
 Ej.: 0.
2. Predicados:
 Éstos son nombres de funciones que son verdaderas o falsas. Pueden tomar varios argumentos.
 Ej.: natural(0).
3. Funciones:
 El cálculo de predicados distingue entre funciones que son verdaderas o falsa (predicados), y todas las
demás funciones.
 Ej.: sucesor( ).
4. Variables que representan cantidades todavía no especificadas:
 Ej.: .
5. Conectores:
 Éstos incluyen las operaciones , , , , .
6. Cuantificadores:
 Éstas son operaciones que introducen variables, cuantificadores universal y existencial.
 Ej.: Para todas …, Existe un …
7. Símbolos de puntuación:
 Éstos incluyen paréntesis izquierdo, derecho, la coma y el punto.

2 Longinos Recuero Bustos (http://longinox.blogspot.com)


 En el cálculo de predicados, los argumentos a los predicados y funciones sólo pueden ser términos, esto es,
combinaciones de variables, constantes y funciones.
 Los términos no pueden contener predicados, cuantificadores o conectores.
 Ej.:

Enunciado lógico Interpretación


Un caballo es un mamífero. mamífero(caballo).
Un ser humano es un mamífero. mamífero(humano).
Los mamíferos tienen cuatro patas y
para toda( ), mamífero( ) patas( , 4) y brazos( , 0) o patas ( , 2) y brazos( , 2).
ningún brazo, o dos patas y dos brazos.
Un caballo no tiene brazos. brazos(caballo, 0).
Un ser humano tiene brazos. no brazos(humano, 0) .
Un ser humano no tiene patas. patas(humano, 0).

 Aunando a las siete clases de símbolos que se han descrito, el cálculo de predicados de primer orden tiene reglas de
inferencia: formas de derivar o de probar nuevos enunciados a partir de un conjunto dado de enunciados.
Ej.: Dado los enunciados podemos derivar .

 Esta es la esencia de la programación lógica: una colección de enunciados que se supone son axiomas, y a partir de
ellos se deriva un hecho deseado mediante la aplicación de reglas de inferencia de alguna manera automatizada.
 Por lo tanto, un lenguaje de programación lógico es un sistema de notación para escribir enunciados lógicos junto con
algoritmos especificados para implementar las reglas de inferencia.
 El conjunto de enunciados lógicos que se toman como axiomas pueden construirse como el programa lógico, y el
enunciado o enunciados que vayan a derivarse pueden considerarse como la entrada que inicia el cómputo. Estas
entradas son proporcionadas también por el programador y se llaman consultas o metas.
 Por esta razón, en ocasiones los sistemas de programación lógica se conocen como bases de datos deductivas, bases
de datos formadas por un conjunto de enunciados y por un sistema de deducción que puede responder a las consultas.
 La secuencia de pasos que elige un sistema automático de deducción para derivar un enunciado es el problema de
control para un sistema de programación lógica.
 Los enunciados orinales representan la lógica del cómputo, en tanto que el sistema deductivo proporciona el control con
el cual se deriva un enunciado nuevo. Esta propiedad de los sistemas de programación lógica llevo a Kowalski a plantear
el paradigma de acuerdo con la pseudoecuación:

Lo que contrasta con la expresión dada por Niklaus Wirth de la programación imperativa:

 El principio de Kowalski pone de manifiesto una característica adicional de la programación lógica:


o Dado que los programas lógicos no expresan el control, las operaciones pueden llevarse a cabo en cualquier
orden o de manera simultánea. Por lo tanto, los lenguajes de programación lógica son candidatos naturales para
el paralelismo.
 Desafortunadamente, los sistemas automatizados de deducción tienen dificultades al manejar todo el cálculo de
predicados de primer orden. Existen demasiadas formas de expresar un mismo enunciado y existen demasiadas reglas
de inferencia. Como resultado, la mayoría de los sistemas de programación lógica se limitan o se restringen a un
subconjunto específico del cálculo de predicados, llamado cláusulas Horn.

3 Longinos Recuero Bustos (http://longinox.blogspot.com)


4.2 Cláusulas Horn
 Una cláusula Horn (en honor a su inventor Alfred Horn) es un enunciado de la forma:

 Donde a los sólo se les permite se enunciados simples sin involucrar conectores.
 A se le llama cabeza de la cláusula, y a el cuerpo de la cláusula.
 En la cláusula Horn, la cantidad de puede ser 0, en cuyo caso la cláusula Horn tiene esta forma:

 Una cláusula como esta significa que es siempre verdadero, esto es, que es un axioma y por lo general se escribe
sin el conector . En ocasiones a estas cláusulas se les llama hechos.
 Las cláusulas Horn pueden utilizarse para expresar la mayoría, pero no la totalidad, de los enunciados lógicos.
 La idea básica es eliminar conectores al describir cláusulas por separado, y tratar la carencia de cuantificadores
suponiendo que las variables que aparecen en la cabeza de una cláusula están universalmente cuantificadas, en tanto
que las variables que aparecen en el cuerpo de una cláusula están existencialmente cuantificadas.
Ej.: Enunciado lógico  traducción al cálculo de predicados  cláusulas Horn

para todas , si es un mamífero, entonces tiene dos o cuatro patas.

para todas , mamífero( ) patas( , 2) o patas( , 4).

mamífero( ) y no patas( ) patas( , 4).


mamífero( ) y no patas( ) patas( , 2).

 En general, mientras más conectores aparezcan a la derecha de un conector en un enunciado, más difícil será
traducirlo a un conjunto de cláusulas Horn.
 Las cláusulas Horn son de interés particular para los sistemas automáticos de deducción, por ejemplo, los sistemas de
programación lógica, ya que puede dárseles una interpretación procedimental.
 Si escribimos una cláusula Horn en orden inverso:

podemos considerar lo anterior como una definición de procedimiento b. Esto es muy similar a la forma en la que se
interpretan reglas gramaticales libre de contexto como definiciones de procedimientos en el análisis sintáctico recursivo
descendente.
 La mayoría de los sistemas de programación lógica no solamente escriben cláusulas Horn hacia atrás, sino también
cambia los conectores existentes en el cuerpo por comas.

4.3 Resolución y unificación


 La resolución es una regla de inferencia para cláusulas Horn.
 La resolución dice que si tenemos dos cláusulas Horn, y podemos parear la cabeza de la primera cláusula con uno de los
enunciados en el cuerpo de la segunda, entonces puede utilizarse la primera cláusula para reemplazar su cabeza en la
segunda utilizando su cuerpo.

4 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Ahora se puede ver la forma en la que un sistema de programación lógica puede tratar una meta o una lista de metas
como si fueran una cláusula Horn sin cabeza.
 El sistema intentará aplicar la resolución al parear una de las metas en el cuerpo de la cláusula sin cabeza con la cabeza
de una cláusula conocida. Acto seguido, reemplaza la meta pareada con el cuerpo de dicha cláusula, creando así una
nueva lista de metas, que sigue modificándose al utilizar el mismo método.
 Las nuevas metas se conocen como submetas.
 Si el sistema tiene éxito finalmente en eliminar todas las metas, derivando por lo tanto la cláusula Horn vacía, entonces
se ha comprobado el enunciado.
 Como se ha podido ver, para hacer coincidir enunciados que contienen variables, debemos establecer las variables
iguales a los términos de modo que los enunciados se hagan idénticos y puedan ser cancelados de ambos lados.
 Este proceso de pareamiento de patrones para hacer que los enunciados sean idénticos se llama unificación, y las
variables que se establecen iguales a patrones se dice que están instanciadas.
 Para implementar la resolución con efectividad se debe también proporcionar una algoritmo para la unificación ( ver
inferencia de tipos Hindley-Milner, tema 6).
 Para lograr una ejecución eficiente, un sistema de programación lógica debe aplicar un algoritmo fijo que especifique:
1. El orden en el cual el sistema intenta resolver una lista de metas.
2. El orden en el cual se usarán las cláusulas para resolver metas.

 Por tanto, el orden en el cual se utilizan las cláusulas también puede tener un efecto importante en el resultado de aplicar
la resolución (repasar problema del ancestro pág. 504).
 Los sistemas de programación lógica que usan cláusulas Horn y resolución con órdenes preespecificados para 1 y 2,
violan el principio básico que dichos sistemas se proponen lograr (un programador sólo debe preocuparse de la lógica
misma, pudiendo ignorar el control).

4.4 El lenguaje Prolog


 Es el lenguaje de programación lógica más utilizado.
 Utiliza cláusulas Horn en implementa la resolución utilizando un estrategia primero en profundidad estrictamente lineal.

4.4.1 Notación y estructuras de datos


 Prolog utiliza casi una notación idéntica a la desarrollada anteriormente para las cláusulas Horn, excepto que la flecha de
implicación ( ) se reemplaza por dos puntos y un guión medio (:-).

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Prolog distingue las variables de las constantes y los nombres de predicados y funciones utilizando mayúsculas para las
variables y minúsculas para las contantes y nombres.
 Prolog utiliza el (;) para el conector lógico . Sin embargo rara vez se utiliza, ya que no es una parte del estándar de la
lógica de las cláusulas Horn.
 Las estructuras básicas de datos son términos como menu(X, Y, Z) o bien entrada(paella).
 Prolog también incluye listas como estructura básica de datos:
o Se delimitan con llaves cuadradas [ ].
o Los elementos se separan con coma. Ej.: [1, 2, 3]
o Una lista puede dividirse en cabeza y cola mediante [H|T]. Ej.: H = 1 y T = [2, 3]
o Para procesar listas no hay ciclos, solo recursión.
o Una lista puede contener listas como elementos. Ej.: [1, [2, 3], 4]

 Prolog tiene varios predicados estándar que siempre están incorporados como not, =, call, append, …

4.4.2 Ejecución en Prolog


 Un programa en Prolog, incluye un juego de cláusulas Horn en la sintaxis de Prolog, que generalmente se toma de un
archivo y se almacena en una base de datos de cláusulas mantenida dinámicamente.
 Una vez introducido el juego de cláusulas en la base de datos, pueden introducirse las metas para empezar la ejecución.
 Iniciada la ejecución de un sistema Prolog, le mostrará al usuario una invitación (?-) para que haga una consulta:

 Como se puede observar, en la última consulta existen varias respuestas. La mayoría de los sistemas Prolog encontrarán
una respuesta y esperarán la indicación del usuario antes de imprimir más respuestas. Si el usuario suministra un punto y
coma ( lógico), entonces Prolog continuará encontrando más respuesta.

4.4.3 Aritmética
 Prolog tiene operaciones aritméticas incorporadas así como un evaluador aritmético.
 Los términos aritméticos pueden escribirse en notación infija usual o prefija +(3, 4).
 Para forzar la evaluación de un término aritmético se requiere de una nueva operación, con el predicado incorporado is:

 Como se observa en la consulta 3, dos términos aritméticos pueden no ser iguales como términos aunque tengan el
mismo valor, por esa razón el operador anterior fuerza la evaluación aritmética, obteniendo el resultado deseado, como
en la consulta 2.

6 Longinos Recuero Bustos (http://longinox.blogspot.com)


4.4.4 Unificación
 La unificación es el proceso mediante el cual se hace la instanciación de las variables o se les asigna memoria y valores
asignados, de manera que coincidan los patrones durante la resolución.
 La unificación es el proceso para hacer en cierto sentido que dos términos se conviertan en el mismo.
 La expresión básica, cuya semántica está determinada por la unificación, es la igualdad:
o En Prolog, la meta s = t intenta unificar los términos s y t.
o Tendrá éxito si la unificación tiene éxito.
o Se puede estudiar la unificación experimentando con el efecto de igualdad, obteniendo la formulación del
siguiente algoritmo de unificación para Prolog:
1. Una constante sólo se unifica con ella misma:

2. Una variable que no está instanciada se unifica con cualquier cosa y se hace instanciada a dicha cosa:

Como se puede observar, en la consulta 3, la unificación hace que las variables no instanciadas
compartan memoria, es decir, que se conviertan en alias una de la otra.

3. Un término estructurado, es decir, una función aplicada a argumentos, se unifica con otro término sólo
si se tiene el mismo número de argumentos y los argumentos pueden unificarse recursivamente:

 Se puede utilizar la unificación en Prolog para obtener expresiones muy cortas para muchas operaciones. Se tomará
como ejemplo la operación cons de LISP, escribiendo una clausula como sigue:

7 Longinos Recuero Bustos (http://longinox.blogspot.com)


Y utilizarla para calcular cabezas, colas y listas construidas, dependiendo de la forma en la que se instancien las
variables del lado izquierdo:

A tener en cuenta:
1. Se pueden utilizar variables en un término como parámetro de entrada o de salida y las clausulas
Prolog pueden ejecutarse hacia atrás igual que hacia delante ¡¡¡algo que la interpretación
procedimental de las cláusulas Horn nunca informó!!!
2. La presencia del patrón [X|Y] como tercera variable, automáticamente se unifica con una variable
utilizada en dicho lugar en una meta. Este proceso podría identificarse como invocación dirigida por
patrones.

4.4.5 Estrategia de búsqueda en Prolog


 Prolog aplica la resolución de una forma estrictamente lineal, reemplazando metas de izquierda a derecha y
considerando las cláusulas en la base de datos en orden descendente, de arriba abajo.
 Las submetas también son tomadas en consideración de forma inmediata en cuanto son establecidas.
 Esta estrategia, puede considerarse como una búsqueda en profundidad en un árbol de opciones posibles:

?- menu(paella, X, naranja)

1 5

entrada(paella) plato_fuerte(Y) 2 postre(naranja)

6
3

paella plato_de_carne(X) plato_de_pescado(X) naranja

5 8
4 7

plato_de_carne(filete) plato_de_carne(pollo) plato_de_pescado(besugo) plato_de_pescado(bacalo)

X = filete X = pollo X = besugo X = bacalao

8 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Los nodos de las hojas en el árbol ocurren ya sea cuando no hay coincidencia para la clausula mas a la izquierda o
cuando se han eliminado todas las cláusulas, indicando por tanto que se ha tenido éxito.
 Cuando se presenta un fracaso, o cuando el usuario indica que la búsqueda es continua mediante un punto y coma,
Prolog retrocede hacia arriba del árbol para localizar trayectorias adicionales hacia una hoja, emitiendo instanciaciones
de variables mientras lo hace.
 Esta estrategia es extremadamente eficiente, sin embargo, también quiere decir que pudieran no encontrarse soluciones
si el árbol de búsqueda tiene ramas de profundidad infinita.

4.4.6 Ciclos y estructuras de control


 Para que Prolog ejecute ciclos y búsquedas repetitivas se pude utilizar backtracking.
 Lo que se debe lograr es forzar el retroceso incluso cuando se haya encontrado una solución.
 Esto se hace con el predicado incorporado fail.
 fail es un predicado que siempre falla. Con esto se obliga a Prolog a buscar una nueva meta, evitando el tener que
poner un punto y coma, para pedir que se busque una nueva meta:

 Sin embargo, es posible que el uso de fail en determinadas circunstancias nos introduzca en ciclos infinitos.
 Por tanto, lo que se requiere es alguna forma de detener la búsqueda para que no continúe a través de todo el árbol.
 Prolog tiene para ello el operador cut, que generalmente se escribe con el signo de exclamación.
 cut congela la elección que se haga cuando ésta se encuentre. Si se llega a un cut en retroceso, cut poda el árbol de
búsqueda de todos los parientes hacia la derecha del nodo que contiene el cut:

 cut, también puede ser utilizado para imitar construcciones if then else en lenguajes imperativos y funcionales.
 Para escribir un cláusula como:

escribimos lo siguiente en Prolog:

 Un ejemplo de esto podría ser:

9 Longinos Recuero Bustos (http://longinox.blogspot.com)


4.5 Problemas que se presentan con la programación lógica
 La meta original de la programación lógica era hacer de la programación una actividad de especificación, permitiendo al
programador especificar sólo las propiedades de una solución y dejar que el sistema del lenguaje aporte el método real
para calcular la solución a partir de sus propiedades.
 Los lenguajes de programación lógica y Prolog en particular, sólo han llenado esta meta de forma parcial. En vez de ello,
la naturaleza de los algoritmos utilizados por los sistemas de programación lógica (resolución, unificación, …) han
introducido muchos escollos en lo especifico de escribir programas, mismo que un programador debe tener en cuenta a
fin de escribir programas eficientes o incluso correctos.

4.5.1 El problema de verificación de ocurrencias en la unificación


 El algoritmo de unificación utilizado por Prolog es erróneo. Al unificar una variable con un término, Prolog no verifica si la
variable misma está presente en el término en el cual está siendo instanciada.
 Este es el problema de verificación de ocurrencias (Occurs Check):

 Esto último es correcto, incluso en ausencia de cualquier otra clausula para f(X).
 Prolog no tiene una verificación para evitar estas ocurrencias por cuestiones de eficiencia.

4.5.2 Negociación como fracaso


 Todos los sistemas de programación lógica tienen la propiedad básica de que algo que no pueda ser probado como
verdadero se supone que es falso:

 Esto se conoce como la suposición del mundo cerrado (CWA: Closed World Assumption).
 La negación lógica () no está implementada en Prolog, en su lugar, define un operador not, el cual se asocia con la
falta de una afirmación, es decir, la meta not(X) tiene éxito siempre que la meta X fracase (negociación como fracaso):

10 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Un problema relacionado es que el fracaso hace que se liberen las instanciaciones de la variables mediante el regreso,
por lo que después del fracaso, una variable quizás ya no tenga un valor apropiado:

El segundo par de metas fracasa porque X está instanciada a 1 para hacer que X = 1 tenga éxito y entonces,
not(X=1) fracasa. Jamás se alcanza la meta X = 0.

4.5.3 Las cláusulas Horn no expresan toda la lógica


 No todo enunciado lógico puede ser convertido a una cláusula Horn. En particular, los enunciados que involucran
cuantificadores posiblemente no podrán expresarse en formato de cláusula Horn:

4.5.4 Información de control en la programación lógica


 Prolog, en su estrategia de búsqueda de primero en profundidad y de su procedimiento lineal de mentas y enunciados,
los programas Prolog contienen información implícita de control que fácilmente puede ser responsable de que fracasen
los programas:

 Podemos observar, como Prolog intenta instanciar la variable Z para que ancestor sea verdadero, creando un
descenso infinito en el árbol de búsqueda que usa la primera clausula.
 En el mejor de todos los mundos posibles, se desearía que un sistema de programación lógica acepte la definición
matemática de una propiedad y encuentre un algoritmo eficiente para calcularlo.
 Naturalmente en Prolog o en cualquier otro sistema de programación lógica no sólo suministramos especificaciones en
nuestro programas, sino que debemos también proveer la información de control algorítmica.

11 Longinos Recuero Bustos (http://longinox.blogspot.com)


5 SINTAXIS ............................................................................................................................................................................ 2

5.1 ESTRUCTURA LÉXICA DE LOS LP ................................................................................................................................................. 2


5.2 GRAMÁTICAS LIBRE DE CONTEXTO Y BNF .................................................................................................................................... 3
5.2.1 Reglas BNF como ecuaciones ........................................................................................................................................ 4
5.3 ÁRBOLES DE ANÁLISIS SINTÁCTICO Y ÁRBOLES DE SINTAXIS ABSTRACTA............................................................................................... 4
5.4 AMBIGÜEDAD, ASOCIATIVIDAD Y PRECEDENCIA ............................................................................................................................. 5
5.5 EBNF Y DIAGRAMAS SINTÁCTICOS ............................................................................................................................................. 7
5.6 TÉCNICAS Y HERRAMIENTAS DE ANÁLISIS SINTÁCTICO ..................................................................................................................... 8
5.7 LÉXICO COMPARADO CON LA SINTAXIS Y CON LA SEMÁNTICA ......................................................................................................... 11
5 Sintaxis
 La sintaxis es la estructura de un lenguaje.
 Uno de los más grandes adelantos en los LP es el desarrollo de un sistema formal para describir la sintaxis.
 En los años 50 Noam Chomsky desarrolló la idea de gramáticas libres de contexto y John Backus, con contribuciones de
Peter Naur, desarrolló un sistema notacional para la descripción de gramáticas y se utilizó por primera vez para describir la
sintaxis de Algol60.
 Estas formas Backus Naur (BNF), se han utilizado de manera subsecuente en la definición de muchos LP (Java, Ada, …).
 Estas BNF se representan con variaciones menores textuales en tres formas básicas:
o BNF original.
o EBNF (BNF extendido).
o Diagramas sintácticos.

5.1 Estructura léxica de los LP


 La estructura léxica de un lenguaje de programación es la estructura de sus palabras o tokens.
 La estructura léxica puede estudiarse por separado de la estructura sintáctica, pero está relacionada íntimamente.
 La fase de análisis léxico de un traductor reúne en forma de tokens secuencias de caracteres del programa de entrada,
los cuales posteriormente se procesan mediante una fase de análisis sintáctico, lo que determina la estructura sintáctica.
 Los tokens se clasifican en varias clases:
o Palabras reservadas (reserved words) o palabras clave:

if, while

o Literales o constantes:

42 (lit. numérico) o "hello" (lit. de cadena)

o Símbolos especiales:

";" o "<=" o "+"

o Identificadores:

x24 o monthly_balance

 Se llaman palabras reservadas porque un identificador no puede tener la misma cadena de caracteres que una palabra
reservada.
 A veces existe confusión en un lenguaje entre las palabras reservadas y los identificadores predefinidos (aquellos a los
cuales se les ha dado una interpretación inicial para todos los programas en el lenguaje, pero pueden ser capaces de redefinición,
como pueden ser integer o boolean).
 En algunos lenguajes los identificadores tienen un tamaño máximo fijo, en tanto que en la mayoría de los lenguajes los
identificadores pueden tener una longitud arbitraria.
 Los identificadores de longitud variable pueden presentar problemas con las palabras reservadas, esto es, el identificador
doif puede verse como un simple identificador o como dos palabras reservadas do e if. Por ello, se utiliza el principio de la
subcadena de mayor longitud o principio de trozo máximo.
 Este principio es una regla estándar en la determinación de tokens, en cada punto se reúne en un solo token la cadena
más larga posible de caracteres, requiriendo que ciertos tokens vengan separados por delimitadores de tokens o
espacios en blanco.
 Esto significa que doif sería tratado como un único token y por tanto un identificador.
 El formato de un programa puede afectar la forma en la que se recogen los tokens:
o Formato fijo:
 Los tokens deben aparecer en localizaciones preespecificadas sobre la página (FORTRAN).

o Formato libre (la mayoría de lenguajes modernos):

2 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Este formato no tiene efecto sobre la estructura de un programa, aparte de satisfacer el principio de la
subcadena de mayor longitud.
 Los tokens a menudo se describen en el idioma inglés, pero también pueden describirse formalmente mediante
expresiones regulares, que son descripciones de patrones de caracteres, descritos normalmente por tres operaciones
básicas:
o Concatenación:
 Poner en secuencia los elementos sin una operación explícita.
o Repetición:
 Se indica mediante el uso de un “*“ después del elemento a repetir.
 Indica cero o más repeticiones.
o Elección o selección:
 Se indica mediante el uso de una “|” entre los elementos de los cuales debe efectuarse una selección.
 Pueden incluirse paréntesis para permitir el agrupamiento de operaciones.

(a|b)*c

 A menudo, la notación de expresiones regulares se amplía mediante operaciones adicionales y caracteres especiales
como:
o [ - ]  Indican rango de caracteres.
o +  Indica una o más repeticiones.
o ?  Indica un elemento opcional, es decir, que la expresión a la que sigue, aparece como mucho una vez:

ob?curo  obscuro

 oscuro

o Los espacios no deben tenerse en cuenta.

[0-9]+(\.[0-9]+)?

5.2 Gramáticas libre de contexto y BNF


 Comenzamos con un ejemplo (una gramática para oraciones simples en inglés):

(1) oración  frase-sustantiva frase-verbal


(2) frase-sustantiva  artículo sustantivo
(3) artículo  a | the
(4) sustantivo  girl | dog
(5) frase-verbal  verbo frase-sustantiva
(6) verbo  sees | pets

 Una gramática libre de contexto consiste en un conjunto de reglas gramaticales, cada una de las cuales están formadas
de:
o Un lado izquierdo que es un solo nombre de estructura y a continuación el meta símbolo  (a veces se
reemplaza por “:=”).
o Un lado derecho formado por una secuencia de elementos que pueden ser símbolos u otros nombres de
estructuras.
 Las cursivas sirven para distinguir los nombres de las estructuras de las palabras reales, es decir, de los tokens que
pudieran aparecer en el lenguaje (sees).
 La barra vertical | es también un metasímbolo y significa “o”.
 Algunas veces un metasímbolo es un símbolo real en un lenguaje, en cuyo caso, es recomendable entrecomillar el
símbolo para distinguirlo del metasímbolo, o de lo contrario, el metasímbolo puede escribirse en un tipo de letra diferente.
 Algunas notaciones también se apoyan en metasímbolos (<, >), en cuyo caso también se reemplaza la flecha por el
metasímbolo (::=):

3 Longinos Recuero Bustos (http://longinox.blogspot.com)


<oración> ::= <frase-sustantiva> <frase-verbal>”.”

 Para indicar que una oración debe estar seguida por algún tipo de marcador final, se hace mediante el signo $:

entrada  oración $

 Los nombres de las estructuras se conocen como no terminales.


 A las palabras o símbolos de token se les denomina terminales.
 Las reglas gramaticales también son llamadas producciones.
 Las producciones se presentan en la forma de Backus-Naur si están dadas utilizando únicamente los metasímbolos “”
y “|” (y paréntesis algunas veces).
 Las reglas BNF son el conjunto de reglas utilizadas para expresar una gramática libre del contexto.
 Existe un no terminal denominado símbolo inicial. Este no terminal representa toda la estructura que se está definiendo y
es el símbolo a través del cual se inician todas las derivaciones.
 Una gramática libre de contexto define un lenguaje denominado lenguaje de la gramática.
 Este lenguaje es el conjunto de todas las cadenas de terminales para las cuales existe una derivación que empieza en el
símbolo inicial y acaba con la cadena de terminales.
 En general estos lenguajes son no finitos.
 En estas gramáticas no necesariamente suele haber tantas producciones como no terminales pero no siempre es así.
 Se dice que son libres del contexto porque los lados izquierdos de las reglas están compuestos por un solo no terminal.
 Esto significa que cada no terminal puede ser reemplazado por cualquier opción del lado derecho independientemente de
dónde pudiera aparecer el no terminal.
 Para darle sensibilidad de contexto habría que dar la posibilidad de que aparecieran cadenas de contexto en el lado
izquierdo.
 Se tomará como problema semántico y no sintáctico todo aquello que no pueda ser expresado con una gramática libre
de contexto.
 Cualquier frase válida de acuerdo con la gramática anterior puede construirse de la siguiente manera:
o Iniciamos con el símbolo oración (símbolo inicial) y seguimos reemplazando los lados izquierdos con los lados
derechos de las reglas anteriores. A este proceso se le denomina derivación.

5.2.1 Reglas BNF como ecuaciones


 Las reglas gramaticales se pueden expresar en forma de ecuaciones de conjunto.
 Ej.: Dada una regla gramatical como:

expr  expr + expr | número

Se puede expresar de la siguiente manera:

E=E+E N

 Las soluciones a ecuaciones recursivas aparecen frecuentemente en las descripciones formales en los lenguajes de
programación donde se conocen como puntos fijos mínimos.

5.3 Árboles de análisis sintáctico y árboles de sintaxis abstracta


 La sintaxis establece una estructura pero no un significado. Pero el significado de una oración ( o un programa) tiene que
estar relacionado con su sintaxis.
 El proceso de asignar la semántica de una construcción a su estructura sintáctica se conoce como semántica dirigida por
la sintaxis. Por lo que se deberá construir la sintaxis de manera que refleje lo mejor posible su semántica.
 Una forma de expresar la estructura sintáctica de un programa que determina su semántica es a través de lo que se
denomina árbol de análisis sintáctico o árbol de análisis gramatical.
 Este describe de manera gráfica el proceso de reemplazo dentro de una derivación.

4 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Un árbol de análisis gramatical (sintáctico) está identificado mediante no terminales en los nodos interiores y por
terminales en las hojas y su estructura está totalmente determinado por las reglas gramaticales del lenguaje y por una
derivación de una secuencia particular de terminales.
 No todas las terminales y no terminales pudieran ser necesarios para determinar totalmente la estructura sintáctica de
una expresión o de una oración.
 En estos casos los árboles suelen estar condensados y se conocen como árboles de sintaxis abstracta o árboles
sintácticos puesto que abstraen la estructura esencial del árbol de análisis sintáctico, como puede observarse en el
siguiente ejemplo, para la expresión 3 + 4 * 5:

expresión  expresión + expresión | expresión * expresión | (expresión) | número


número  número dígito | dígito
dígito 0|1|2|3|4|5|6|7|8|9

Árbol de análisis sintáctico Árbol de sintaxis abstracta

Expresión +

Expresión + Expresión 3 *

Número Expresión * Expresión 4 5

Dígito Número Número

Dígito Dígito
3

4 5

 Para el programador los árboles de sintaxis abstractas no son importantes (algunas veces la sintaxis ordinaria se distingue de
la sintaxis abstracta por el nombre de sintaxis concreta).
 No ocurre así con los diseñadores del lenguaje y para los autores de traductores, ya que es la sintaxis abstracta y no la
concreta la que expresa la estructura esencial del lenguaje.

5.4 Ambigüedad, asociatividad y precedencia


 Dos derivaciones diferentes pueden conducir al mismo árbol de análisis sintáctico:

Número  número digito Número  número


Número => número digito
digito Núm
 número 4  número
=> número digito
digito
digito
digito
 número digito 4  digito
=> digito digito
digito
digito
digito
 número 3 4 => 2digito
2 digito
digito
digito
 digito 3 4 => 23 digito
2 3 digito
234 => 2 3 4 234 =>

 Sin embargo, diferentes derivaciones también pueden conducir a diferentes árboles de análisis gramaticales:

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


exp exp

exp + exp exp * exp

3 exp * exp exp + exp 5

4 5 3 4
3+4*5 3+4*5

 Una gramática para la cual sean posibles dos árboles diferentes para un mismo análisis sintáctico se dice que es
ambigua.
 Ciertas derivaciones construidas en un orden especial corresponden al mismo árbol de análisis sintáctico.
 Una derivación que tienes esta propiedad es la derivación por la izquierda, que consiste en tomar como reemplazo el no
terminal restante más a la izquierda.
 Una forma de determinar si una gramática es ambigua es buscar dos derivaciones por la izquierda distintas de una
misma cadena.
 Una gramática para que sea útil no puede ser ambigua, por lo que si lo fuera habría que aplicarle alguna regla para
eliminar dicha ambigüedad.
 Para eliminar dicha ambigüedad se debe revisar la gramática o incluir nuevas reglas que eliminen la ambigüedad.
 La forma más habitual de revisar este tipo de gramáticas es escribiendo una nueva regla gramatical (llamada un
“término”) que establece una “cascada de precedencia”.
 Por ejemplo, la siguiente gramática es ambigua:

expresión  expresión + expresión | expresión * expresión | (expresión) | número


número  número dígito | dígito
dígito 0|1|2|3|4|5|6|7|8|9

 Las ambigüedades se producen en:


o expresión + expresión:
 8–4–2= 2
 8–4–2= 6
o expresión * expresión, visto en el ejemplo de 3 + 4 * 5.

 Para eliminar dicha ambigüedad modificamos la regla expresión por:


o expresión  expresión + término | término (recursiva por la izquierda) o,
o expresión  término + expresión | término (recursiva por la derecha)

Y definimos término como:


o término  término * término | (expresión) | número

 De esta forma eliminamos la ambigüedad de la gramática quedando finalmente como sigue:

expresión  expresión + término | término


término  término * factor | factor
factor  (expresión) | número
número  número dígito | dígito
dígito 0|1|2|3|4|5|6|7|8|9

 Algunas veces el proceso de volver a escribir una gramática para eliminar ambigüedad hace que sea extremadamente
compleja, y en estos casos preferimos enunciar una regla de no ambigüedad.
6 Longinos Recuero Bustos (http://longinox.blogspot.com)
5.5 EBNF y diagramas sintácticos
 EBNF (Backus-Naur extendida) surge para dotar al BNF de una notación especial para el tipo de reglas gramaticales que
expresan con mayor claridad la naturaleza repetitiva de su estructura:

expresión  expresión + término | término


 expresión + término + término
 expresión + término + término + término

 término + término + … + término

 En esta notación las llaves “{ }” representan “cero o más repeticiones de”:

número  dígito { dígito }

expr  término { + término }

 En este tipo de notación las llaves se han convertido en nuevos metasímbolos.


 Esta notación oculta su asociatividad por la izquierda la cual es generada por la recursividad por la izquierda.
 Las reglas recursivas por la derecha no se pueden representar mediante llaves por lo tanto, a través de EBNF no se
podrán representar directamente los árboles sintácticos o los árboles de análisis sintáctico, por lo cual, utilizaremos
siempre la notación BNF para escribir árboles de análisis sintáctico.
 Para indicar que una estructura tiene una parte opcional utilizaremos los corchetes “[ ]”.
 También los operadores asociativos por la derecha (binarios) pueden describirse utilizando estos nuevos metasímbolos:

expr  término @ expr | expr expr  término [ @ expr ]

donde @ es un operador asociativo por la derecha.


 Como ejemplo de uso, rescribiremos la gramática de ejemplo empleada en el apartado 4.4:

expresión  expresión { + término }


término  factor { * factor }
factor  (expresión) | número
número  número dígito | dígito
dígito 0|1|2|3|4|5|6|7|8|9

 Una representación gráfica a veces es útil para una regla gramatical es el diagrama sintáctico, el cual refleja la
secuencia de terminales y no terminales que se encuentran en el lado derecho de la regla.
 Estos diagramas:
o Utilizan círculos u óvalos para representar los terminales y rectángulos para representar los no terminales,
conectándolos entre sí mediante líneas y flechas con el fin de indicar la secuencia apropiada.
o Pueden condesar varias reglas gramaticales en un solo diagrama.
o Se escriben siempre partiendo de una notación EBNF, y nunca de una BNF.
o Son atractivos visualmente pero ocupan mucho espacio por lo que actualmente son poco utilizados a favor de
las notaciones EBNF y BNF.

7 Longinos Recuero Bustos (http://longinox.blogspot.com)


5.6 Técnicas y herramientas de análisis sintáctico
 Una gramática escrita en forma BNF, EBNF o diagrama sintáctico describe las cadenas de tokens que sintácticamente
son correctas en el lenguaje de programación. Por lo que de manera implícita quedan reflejadas las acciones que debe
realizar el analizador sintáctico para analizar de forma correcta una cadena de tokens.
 La forma más simple de un analizador sintáctico es un reconocedor, un programa que acepta o rechaza cadenas
dependiendo de si son o no aceptadas en el lenguaje.
 Según las distintas maneras en las que un analizador sintáctico interpreta una gramática en cualquiera de sus formas
tenemos distintos métodos de análisis:
o Analizadores sintácticos de abajo arriba:
 Intenta hacer coincidir una entrada con los lados derechos de las reglas gramaticales. Cuando se
encuentra con una coincidencia, el lado derecho es sustituido (reducido) por el no terminal de la
izquierda.
 Su denominación (abajo arriba) viene dada por el hecho de que construyen derivaciones y árboles
sintácticos de las hojas hacia la raíz (también llamados de desplazamiento-reducción).
o Analizadores sintácticos de arriba abajo:
 Los no terminales se expanden para coincidir con los tokens de entrada y construyen directamente una
derivación.

 El analizador de abajo arriba es más poderoso que el otro, por lo que es más utilizado generalmente por los
generadores de analizadores sintácticos (o compiladores de compiladores).
 Un generador ampliamente utilizado es el YACC o su versión libre Bison.
 Otro método más antiguo de general analizadores a partir de su gramática que resulta muy efectivo, es el análisis
sintáctico por descenso recursivo:

8 Longinos Recuero Bustos (http://longinox.blogspot.com)


o Básicamente opera convirtiendo los no terminales en procedimientos mutuamente recursivos, cuyas acciones
están basadas en los lados derechos de los BNF.
o En estos procedimientos los lados derechos se interpretan de la siguiente manera:
 Los tokens se hacen coincidir con los tokens de entrada según se van construyendo.
 Los no terminales se interpretan como llamadas a los procedimientos de los no terminales.
o Un ejemplo de análisis sintáctico por descenso recursivo es:

void oración( void ) void Frase-sustantiva( void ) void artículo( void )


{ { {
Frase-sustantiva(); artículo(); if( token == “a” ) coincidencia( “a” );
Frase-verbal(); sustantivo(); else if(token == “the”) coincidencia(“the”);
} } else error();
}

 Por otro lado, si tenemos una regla como:

expresión  expresión + término | término

se producen dos errores graves con el análisis por descenso recursivo:


o El primer error es para el procedimiento que representa a expresión + término, el cual produciría una llamada
recursiva antes de encontrar el signo +, por lo que entraría en un bucle infinito:

void Expresión( void )


{
Expresión() + Término();
}

o El segundo error es que no hay forma de determinar que opción tomar, si expresión + término o término, por lo
tato para seguir manteniendo la asociatividad por la izquierda y eliminar la recursividad, utilizamos la notación
EBNF en la que las llaves representan la eliminación de la recursión por la izquierda:

void Expresión( void )


{
Término();

expresión  término { + término } while( token == “+” )


{
Coincidencia( “+” );
Término();
}
}

 En el caso de la recursividad por la derecha no se presenta el problema señalado anteriormente para el análisis recursivo
descendente, sin embargo se da la situación de que el código para una regla de recursión por la derecha:

expr  término @ expr | término

se escribe en formato EBNF y se realiza una factorización por la izquierda:

9 Longinos Recuero Bustos (http://longinox.blogspot.com)


void Expresión( void )
{
Término();

expresión  término [ @ expr ] if( token == “@” )


{
Coincidencia( “@” );
Expresión();
}
}

 Por lo tanto en situaciones de recursión por la izquierda y en la factorización por la izquierda, las reglas EBNF o los
diagramas sintácticos corresponden con el código de un analizador sintáctico por descenso recursivo, siendo esta una de
las razones de amplia utilización.
 Un analizador sintáctico que basa su acción únicamente en el token disponible en el flujo de entrada, se conoce como
analizador sintáctico predictivo.
 Este uso de un solo token para dirigir la acción del analizador sintáctico se conoce con el nombre preanálisis de un
solo símbolo.
 Estos analizadores requieren que las gramáticas a analizar cumplan ciertas condiciones:
o La primera condición que requiere es la capacidad de escoger entre varias alternativas en una regla gramatical:

A | | |…|

Para decidir cual elegir, los tokens que inician cada tienen que ser distintos:

Primero( ) Primero( ) = para toda

donde Primero es la función que devuelve el conjunto de tokens que pueden presentarse al principio de cada .

o La segunda condición se presenta con las reglas gramaticales que contienen estructuras opcionales:

expr  término [ @ expr ]

Si @ se presenta como token en la entrada, pudiera ser el comienzo de una parte opcional, o pudiera ser un
token que aparece después de la expresión completa, por lo que para que esto no se de se tiene que cumplir:
Primero( ) Siguiente( ) =

donde Siguiente es el conjunto de tokens que pueden seguir a .

 El proceso de convertir reglas en gramaticales en un analizador sintáctico puede automatizarse, es decir puede
construirse un programa que traduzca un programa en un analizador sintáctico.
 Estos generadores de analizadores sintácticos, es decir, “compiladores de compiladores”, toman como entrada una
versión de las reglas BNF o EBNF y dan como salida un programa de analizador sintáctico en algún lenguaje.
 Dar una gramática a un generador del analizador sintáctico dará como resultado un reconocedor y para que un
analizador construya un árbol sintáctico o que lleve a cabo otras operaciones, debemos proporcionarles operaciones o
acciones a llevar a cabo asociadas a cada regla gramatical, esto es, un esquema dirigido por la sintaxis.
 Uno de estos generadores más conocidos es el YACC.

10 Longinos Recuero Bustos (http://longinox.blogspot.com)


5.7 Léxico comparado con la sintaxis y con la semántica
 Según el DRAE:
o Gramática:
 1. f. Ciencia que estudia los elementos de una lengua y sus combinaciones.
o Sintaxis:
 2. f. Inform. Conjunto de reglas que definen las secuencias correctas de los elementos de un lenguaje

de programación.
o Léxico:
 3. m. Vocabulario, conjunto de las palabras de un idioma, o de las que pertenecen al uso de una región,

a una actividad determinada, a un campo semántico dado, etc.


o Semántica:
 1. adj. Perteneciente o relativo a la significación de las palabras.

 Una gramática libre de contexto típicamente incluye una descripción de los tokens de un lenguaje al incluir en las reglas
gramaticales las cadenas de caracteres que forman los tokens.
 Algunas clases típicas de tokens, como las literales o constantes y los identificadores no son por sí mismos secuencias
fijas de caracteres, sino que se elaboran a partir de un conjunto fijo de caracteres, como los dígitos del 0 al 9.
 Estas clases de tokens pueden tener su estructura definida por la gramática, sin embargo, es posible e incluso deseable
utilizar un analizador léxico para reconocer estas estructuras, pues puede hacerlo mediante una operación repetitiva
simple.
 Los límites entre la sintaxis y el léxico no están claramente definidos, ya que si utilizamos la notación BNF, EBNF o
diagramas sintácticos, se pueden incluir las estructuras léxicas como parte de la descripción del lenguaje.
 Se ha definido la sintaxis como todo lo que se puede definir mediante una gramática libre de contexto, y semántica como
todo lo demás que no se puede definir así.
 Algunos autores incluyen como sintaxis, la declaración de variables antes de que sean utilizadas y la regla de que no se
pueden declarar identificadores dentro de un procedimiento. Estas son reglas sensibles al contexto y por lo tanto un
analizador sintáctico debería tener información sobre el contexto.
 Otro conflicto es cuando un leguaje requiere que ciertas cadenas sean identificadores predefinidos y no palabras
reservadas.
 La diferencia de ambos es que las palabras reservadas son cadenas fijas de caracteres, que son tokens ellas mismas y
que no pueden utilizarse como identificadores, mientras que identificadores predefinidos son cadenas fijas de caracteres
a las que se les ha dado un significado predefinido en el lenguaje, pero este significado puede ser redefinido.
 Por estas razones, el analizador sintáctico debería tener información del contexto sobre los identificadores disponibles a
fin de eliminar ambigüedades.

11 Longinos Recuero Bustos (http://longinox.blogspot.com)


6 SEMÁNTICA BÁSICA ........................................................................................................................................................... 2

6.1 ATRIBUTOS, LIGADURAS Y FUNCIONES SEMÁNTICAS ....................................................................................................................... 2


6.2 DECLARACIONES, BLOQUES Y ALCANCE ...................................................................................................................................... 4
6.3 TABLA DE SÍMBOLOS ............................................................................................................................................................... 6
6.4 RESOLUCIÓN Y SOBRECARGA DE NOMBRES ................................................................................................................................. 7
6.5 ASIGNACIÓN, TIEMPO DE VIDA Y EL ENTORNO ............................................................................................................................... 8
6.6 VARIABLES Y CONSTANTES ..................................................................................................................................................... 10
6.6.1 Variables ..................................................................................................................................................................... 10
6.6.2 Constantes................................................................................................................................................................... 11
6.7 ALIAS, REFERENCIAS PENDIENTES Y BASURA.............................................................................................................................. 12
6.7.1 Alias ............................................................................................................................................................................. 12
6.7.2 Referencias pendientes ............................................................................................................................................... 13
6.7.3 Basura ......................................................................................................................................................................... 14
6 Semántica básica
 Sintaxis:
o Es la forma en la que aparecen los constructores del lenguaje.
 Semántica:
o Es lo que los constructores del lenguaje hacen.

 La especificación de la semántica de un LP es una tarea más difícil que la especificación de su sintaxis, como podríamos
esperar cuando hablamos de significado en contraposición a forma o estructura.

 Existen varias formas de especificar la semántica mediante:


o Un manual de referencia de lenguaje:
 Éste es el método más común.
 Los inconvenientes son:
 La falta de precisión debido a las descripciones en lenguaje natural.
 Las omisiones o ambigüedades que puede tener.
o Un traductor definidor:
 Los inconvenientes son que:
 Las preguntas relacionadas con el comportamiento de un programa no pueden contestarse por
adelantado, debemos ejecutar el programa para saber lo que hace.
 Los errores y las dependencias con la máquina se convierten en parte de la semántica del
lenguaje.
o Una definición formal:
 Estos métodos matemáticos son precisos, complejos y abstractos.
 El mejor método es la semántica denotacional, que describe a la semántica mediante una serie de
funciones.

6.1 Atributos, ligaduras y funciones semánticas


 Un mecanismo fundamental de abstracción en un lenguaje de programación es el uso de nombres, es decir,
identificadores, para denotar entidades o constructores del lenguaje (variables, procedimientos, constantes, …).
 Un paso fundamental de la descripción de la semántica de un lenguaje consiste en describir las reglas convencionales
que determinan el significado de cada uno de los nombres utilizados en un programa.
 Además de los nombres para la descripción de la semántica se requiere de los conceptos de:
o Valor:
 Cualquier cantidad almacenable (enteros, reales, …).
o Localización:
 Lugares donde se pueden almacenar los valores.

 El significado de un nombre queda determinado por las propiedades o atributos asociados con el mismo:

const int n = 5; Asocia al nombre n el atributo de tipo de datos "constante entera" y el atributo de valor 5.
double f( int n ) Asocia el atributo “función” al nombre f y los siguientes atributos:
{
...
1. Cantidad de parámetros, nombres y tipos.
} 2. Tipo de datos del valor devuelto.
3. Cuerpo del código cuando se llama a la función.

 El proceso de asociación de atributo a un nombre se denomina ligadura.


 Se conoce como tiempo de ligadura de un atributo al tiempo en que se está calculando y vinculando con un
nombre durante el tiempo de compilación/ejecución y se puede clasificar en dos clases generales:

o Ligadura estática: Tiene lugar antes de la ejecución y genera atributos estáticos.


o Ligadura dinámica: Tiene lugar durante la ejecución y genera atributos dinámicos.
2 Longinos Recuero Bustos (http://longinox.blogspot.com)
 Los tiempos de ligadura pueden depender del compilador, es decir, para los intérpretes se crearán la mayoría de las
ligaduras de forma dinámicas, mientras que para los compilados se crearán la mayoría de las ligaduras de forma
estáticas.
 El tiempo de ligadura es el tiempo más corto que las reglas del lenguaje permiten que el atributo esté vinculado.
 Un atributo estático puede vincularse durante:

o El análisis gramatical o sintáctico.


o El análisis semántico (tiempo traducción o compilación).
o E l encadenamiento del programa con las bibliotecas (tiempo de ligado o linkaje).
o La carga del programa para su ejecución (tiempo de carga).

 Los nombres pueden vincularse con atributos aun antes del tiempo de compilación, como por ejemplo los
identificadores predefinidos como los tipos de datos boolean y char.

 Por lo anterior tenemos los siguientes tiempos de ligadura para los atributos de nombres:

- Tiempo de definición de lenguaje.


- Tiempo de implementación de lenguaje.
- Tiempo de traducción (“Tiempo de compilación”).
- Tiempo de linkaje.
- Tiempo de carga.
- Tiempo de ejecución (“Run Time”).

 Todos los tiempos anteriores representan ligaduras estáticas excepto el último que representa ligaduras dinámicas.
 El compilador debe conservar las ligaduras de tal manera que se den significados apropiados durante la compilación y
la ejecución.
 Esto se lleva a cabo mediante una estructura de datos que de una manera abstracta puede verse como una función que
expresa la ligadura de los atributos a los nombres.
 Esta función es parte fundamental de la semántica y se conoce como tabla de símbolos.
 Esta función cambiará durante el proceso de compilación y/o ejecución para reflejar adiciones o eliminaciones de
ligaduras.
 Existe una diferencia fundamental entre la forma en la que se mantiene una tabla de símbolos a través de un:
o Compilador:
 Puede, por definición, procesar únicamente atributos estáticos.
 La tabla de símbolos se ilustra como:
Tabla de símbolos
Nombres Atributos estáticos

 Durante la ejecución de un programa compilado, deben mantenerse ciertos atributos como por ejemplo
las localizaciones en memoria y los valores.
 Un compilador genera código que conserva estos atributos durante la ejecución.
 La parte de asignación de memoria en este proceso, es decir, la ligadura de nombres a localizaciones,
se denomina entorno:
Entorno
Nombres Localizaciones

 Las ligaduras de las localizaciones de almacenamiento junto con los valores se conocen como la
memoria, almacén o estado:
Memoria
Localizaciones Valores

o Intérprete:
 En un intérprete, se combinan la tabla de símbolos y el entorno, ya que durante la ejecución se
procesan atributos estáticos y dinámicos. Por lo general, en esta función también se incluye la
memoria:
Entorno Atributos (incluyendo
Nombres localizaciones y valores)

3 Longinos Recuero Bustos (http://longinox.blogspot.com)


6.2 Declaraciones, bloques y alcance
 Las ligaduras pueden determinarse mediante declaraciones, ya sea de manera explícita o implícita:
o int x; (Establece explícitamente el tipo de datos de x utilizando la palabra clave int, pero la localización exacta de x
durante la ejecución está únicamente vinculada de manera implícita)

 Aquellos lenguajes que tienen declaración implícita, generalmente tienen reglas convencionales de nombre para
establecer otros atributos:
o FORTRAN:
 Todas las variables no explícitamente declaradas se supone que son enteros, si sus nombres
empiezan con I, J, K, L, M, N.

 Las declaraciones que vinculan ciertos atributos se conocen como definiciones, mientras que aquellas que sólo
especifican parcialmente los atributos se conocen simplemente como declaraciones:

double f( int x ); Declaración. No especifica el código de la función f.


struct x; Declaración. Especifica un tipo incompleto.
char * name; Definición.
typedef int* IntPtr = 0; Declaración. Falta el atributo de valor.
struct rec; Declaración.
int gcd( int, int ); Declaración.
double y = 1.0; Definición.
extern int z; Declaración.

 Las declaraciones están asociadas tanto sintácticamente como semánticamente con ciertos constructores del lenguaje
como:
o Bloque:
 Consiste en una secuencia de declaraciones seguidas por una secuencia de enunciados, rodeado por
marcadores sintácticos como son las llaves o los pares begin-end.
 Las declaraciones que se encuentran dentro del bloque se conocen como locales y las que se
encuentran fuera de cualquier enunciado compuesto se llaman no locales (globales o externas).
/* Declaraciones no locales */
int x;
double y;

main()
{
/* Declaraciones locales */
int i, j;

}

o Tipos de datos estructurados:


 Todos los tipos de datos estructurados se definen utilizando declaraciones locales asociados con el
tipo.
struct A
{
int x; /* Local en A*/
double y;

struct
{
int* x; /* Declaraciones de miembros
char y; anidadas */
} z;
};

4 Longinos Recuero Bustos (http://longinox.blogspot.com)


o Clase:
 En lenguaje orientado a objetos, la clase es una fuente importante de declaraciones, de hecho, en
lenguajes OO puros como Java y Smalltalk, la clase es la única declaración que no necesita estar en el
interior de otra declaración de clase, siendo fuente primordial de declaraciones.

public class IntWithGcd


{
...

// Declaración local del método intValue


public int intValue()
{
return value;
}

// Declaración local del método gcd


public int gcd( int v )
{
...
}

// Declaración local del campo de valores


private int value;
}

o Agrupaciones grandes:
 Las declaraciones pueden reunirse en grupos más grandes como una manera de organizar los
programas y de obtener un comportamiento especial:
 Paquetes y tareas en Ada.
 Paquetes en Java.
 Los módulos en Haskell y ML.
 Los espacios de nombres en C++.
 …
 Las declaraciones vinculan varios atributos a los nombres, dependiendo del tipo de declaración.
 Cada una de estas ligaduras tiene por sí misma un atributo que queda determinado por la posición dentro de la
declaración en el programa.
 El alcance de un vínculo es la región del programa sobre la cual se conserva el vínculo.
 A veces nos referimos erróneamente al alcance de un nombre, pero esto no es cierto pues puede haber varias
declaraciones diferentes sobre el mismo nombre y cada una de ellas con un alcance distinto.
 En los lenguajes estructurados por bloques, se conoce como alcance léxico a aquel (alcance de vínculo) que queda
limitado al bloque dónde aparece su declaración asociada.
 C tiene la regla adicional conocida como declaración antes de uso, y define que:
o El alcance de una declaración se extiende desde el punto justo después de la misma hasta el final del bloque en
el que está localizado.
 Una característica de la estructura de bloques es que las declaraciones en los bloques anidados toman preferencia sobre
declaraciones anteriores:
int x;

void p( void )
{
char x;
x = 'a'; // x global no puede ser accedido desde p
}

main()
{
x = 2;
}

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


 La declaración de x se dice que tiene una apertura en el alcance dentro de p.
 Por esta razón a veces se hace distinción entre:
o Visibilidad:
 Hace referencia aquellas regiones del programa donde las ligaduras de una declaración son aplicables.
o Alcance:
 Incluye los agujeros en el alcance, dado que las ligaduras siguen existiendo, pero están ocultas a la
vista.
 En C++, el operador de resolución de alcance (::) puede utilizarse para tener acceso a estas declaraciones ocultas,
siempre que sean globales.
void p( void )
{
char x;
x = 'a';
::x = 42;
}

 En Ada, el comportamiento de C++, citado anteriormente se conoce como visibilidad por selección, utilizando una
notación de puntos similar al acceso de la escritura de registros:

B1: declare
a: integer;
begin
a := 2;
B2: declare
a: integer; -- a local oculta a 'a' en B1
begin
...
if y then a := B1.a; -- selecciona la a en B1
...
end B2;
end B1;

 En C, las declaraciones variables globales (no cualificadas con la palabra clave static) pueden ser accedidas a través de
archivos, utilizando la palabra clave extern.
 Las reglas de alcance necesitan también construirse de manera tal que las declaraciones recursivas o de referencia a sí
mismas sean posibles cuando tengan sentido.

int x = x + 1; // Expresión sin sentido

 Las vinculaciones establecidas por las declaraciones se conservan mediante tablas de símbolos.
 La forma en la que la tabla de símbolos procesa las declaraciones debe corresponder con el alcance de cada
declaración.
 Reglas de alcance diferentes requieren de un comportamiento diferente de la tabla de símbolos.

6.3 Tabla de símbolos


 Es como un diccionario variable, debe poder insertar, buscar y cancelar nombres con sus atributos asociados,
representando las vinculaciones en declaraciones.
 Un símbolo puede mantenerse con cualquier cantidad de estructuras de datos para permitir un acceso y mantenimiento
eficientes, sin embargo, el mantenimiento de información de alcance en un lenguaje con alcance léxico y estructura de
bloques requieren que las declaraciones sean procesadas en forma de pila.
 Sin embargo podemos considerar la tabla de símbolos como un conjunto de nombres, cada uno de los cuales tiene una
pila de declaraciones asociadas con ellos, de manera que la declaración en la parte superior de la pila es aquella cuyo
alcance actualmente está activo.

6 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Si la tabla de símbolos es manejada por un compilador y las ligaduras de las declaraciones son estáticas, entonces
procesará las declaraciones de manera estática (antes de su ejecución), dando como resultado una regla de alcance léxico
o estático.
Este proceso conserva la información apropiada de alcance incluyendo los agujeros de alcance.
 Si la tabla de símbolos está administrada dinámicamente (durante la ejecución), entonces las declaraciones se procesan
conforme se van encontrando a través del programa a lo largo de la trayectoria de ejecución, dando como resultado una
regla de alcance diferente, conocida como regla de alcance dinámico.
Notar que cada una de las llamadas a un mismo procedimiento a lo largo del programa puede tener una tabla de
símbolos diferentes a su entrada.
 Existen problemas importantes por los cuales se hace difícil la utilización del alcance dinámico, razón por la cual pocos
lenguajes la utilizan:
o Uno de ellos y más importante es que bajo el alcance dinámico, cuando un nombre no local es utilizado en una
expresión o en un enunciado, la declaración que se aplica a ese nombre no puede determinarse mediante la
simple lectura del programa.
En vez de ello, el programa deberá ser ejecutado, o en su ejecución rastreada a mano, para encontrar la
declaración aplicable (y diferentes ejecuciones pueden conducir a diferentes resultados).
Por lo tanto, la semántica de una función puede cambiar radicalmente conforme avanza la ejecución.
o Otro problema es, dado que las referencias a variables no locales no pueden predecirse antes de la ejecución,
tampoco pueden definirse los tipos de datos de estas variables.

Por este motivo la ligadura estática de los tipos de datos (tipificado estático) y el alcance dinámico son
inherentemente incompatibles.

 A pesar de todos esto, el alcance dinámico sigue siendo una opción posible para aquellos lenguajes muy dinámicos,
interpretados cuando no esperamos que los programas sean extremadamente grandes (APL, Snobol, Perl, Lisp).
 Independientemente de la cuestión del alcance léxico en contraste con el dinámico, existe una dificultad añadida con el
tratamiento de las estructuras.
 Cualquier estructura de alcance que pueda ser referenciada directamente en un lenguaje también debe tener su propia
tabla de símbolos.
Los ejemplos incluyen todos los alcances nombrados en Ada; las clases, las estructuras y los espacios de nombres en
C++; y las clases y los paquetes en Java. Por lo que una estructura más típica para la tabla de símbolos de un programa
en cualquiera de estos lenguajes es tener una tabla para cada uno de los alcances, que a su vez tienen que estar
anidados con sus propias tablas dentro de las tablas que las encierran.
Nuevamente éstos pueden mantenerse en un estilo basado en pila.
 Es muy aconsejable repasar todos los ejemplos de este apartado.

6.4 Resolución y sobrecarga de nombres


 La sobrecarga se refiere hasta donde un mismo nombre pude utilizarse para referirse a cosas distintas dentro de un
mismo programa y es una faceta importante con respecto a las declaraciones y a las operaciones con tabla de símbolos.
Por ejemplo, el símbolo + se refiere como mínimo a dos operaciones completamente distintas:
o La adicción de enteros.
o La adicción en coma flotante.
 Algunos lenguajes como C++ y Ada permiten una amplia sobrecarga tanto de operadores como de nombres de
funciones, Java sólo permite la sobrecarga en los nombres de funciones.
 Un compilador consigue determinar entre los distintos usos de un símbolo buscando en los tipos de datos de los
operandos.
 Para que esto sea posible hay que ampliar el mecanismo de búsqueda para que no sólo se limite a una búsqueda por
nombre sino que también distinga entre número de parámetro y el tipo de estos.
A este proceso se le conoce como resolución de sobrecarga.
 La tabla de símbolos puede determinar la función más apropiada para cada una de las llamadas de la información
contenida en cada llamada (contexto de llamada).
7 Longinos Recuero Bustos (http://longinox.blogspot.com)
 Se presentan problemas de ambigüedad cuando los lenguajes permiten la conversión automática de un tipo a otro,
generando llamadas ambiguas.
Por lo que las conversiones automáticas como en C++ y en Java complican de manera significativa el proceso de la
resolución de sobrecarga (menos en Java al tener una conversión más restrictiva, permitiendo sólo conversión cuando no hay
pérdida de información).
 Un problema adicional es si se permite información adicional en un contexto de llamada a parte del número de
parámetros y sus tipos, como pudiera ser el tipo del valor de retorno en una función.
En Ada por ejemplo se permiten el tipo de retorno e incluso los nombres de los parámetros en una definición.
 Tanto Ada como C++ (pero no Java) permiten la sobrecarga en los operadores incorporados.
En estos casos hay que respetar las propiedades sintácticas del operador; no se puede cambiar su asociatividad o su
precedencia.
 No existe diferencia semántica entre operadores y funciones, únicamente la diferencia sintáctica que los operadores
escriben en forma infija y las funciones en forma prefija.
 Se podría utilizar la sobrecarga para referirnos a cosas de tipos completamente diferentes con el mismo nombre, sin
embargo esto resultaría extremadamente confuso y los programas tienen poco que ganar utilizando esta sobrecarga, por
lo que no está permitido en la mayoría de los lenguajes.
 Sin embargo, este tipo de sobrecarga resulta útil como manera de limitar la cantidad de nombres que uno realmente tiene
que recordar en un programa.

/*
* Solo hay que recordar la estructura llamada A cuyos campos son:
* A.datos
* A.siguiente
*/

typedef struct A A;

struct A
{
int datos;
A* siguiente;
};

6.5 Asignación, tiempo de vida y el entorno


 El entorno se puede construir estáticamente, dinámicamente o como una mezcla de ambos (estático FORTRAN, dinámico
LISP y mezcla C, C++, Ada, Java…).
 No todos los nombres en un programa están vinculados con localizaciones. En un lenguaje compilado, los nombres de
las constantes y de los tipos de datos pueden representar puramente cantidades en tiempo de compilación, que no
tienen existencia en el tiempo de carga o en el de ejecución.

const int MAX = 10;

 Las declaraciones se utilizan para construir el entorno y la tabla de símbolos.


 En un compilador, las declaraciones son utilizadas para indicar el código de asignación que el compilador tiene que
generar conforme se procesa la declaración.
 En un intérprete, se combinan la tabla de símbolos y el entorno, por lo que la ligadura de atributos por una declaración en
un intérprete incluye la ligadura de las localizaciones.
 En un lenguaje con estructura de bloques:
o Las variables globales se asignan estáticamente.
o Las variables locales, se asignan dinámicamente cuando la ejecución llega al bloque en cuestión. Durante la
ejecución, cuando se entra a cada uno de los bloques, las variables declaradas al principio de cada bloque se
asignan, y cuando se sale de cada bloque, estas mismas variables se desasignan.
o Vincula las localizaciones a las variables locales en forma de pila.
 En los bloques de procedimiento o funciones:
8 Longinos Recuero Bustos (http://longinox.blogspot.com)
void p( void )
{
int x;
double y;

...
}

Cada vez que se llame a p, se asignarán nuevas variables locales.


Nos referimos a cada llamada a p como una activación de p y la región correspondiente de la memoria asignada como
un registro de activación.
 Si además el lenguaje tiene alcance léxico, se puede asociar el mismo nombre con varias localizaciones diferentes.
Debemos por tanto distinguir entre un nombre, una localización asignada y la declaración que hace que queden
vinculadas.
 Llamaremos a la localización asignada un objeto.
 Un objeto es un área de almacenamiento asignada en el entorno como resultado del procesamiento de una declaración.
 El tiempo de vida o extensión de un objeto es la duración de su asignación en el entorno. Las vidas de los objetos se
pueden extender más allá de la región de un programa donde pueden ser objeto de acceso.
 Un apuntador (puntero) es un objeto cuyo valor almacenado es una referencia a otro objeto.

int* x;
 Para permitir la inicialización de los apuntadores que no apuntan a un objeto asignado, C permite el uso del nombre
NULL a 0.
int* x = NULL;

 Para que x apunte a un objeto asignado, debemos asignarlo manualmente mediante el uso de una rutina de asignación.
C usa malloc, por lo que para asignar una nueva variable entera y al mismo tiempo asignar su localización en el valor
de x, se haría:
x = ( int* )malloc( sizeof( int ) );

 Se dice que la variable x se puede desreferenciar utilizando el operador *, entonces podemos asignar valores enteros a
*x y referirnos a esos valores como lo haríamos en una variable ordinaria:

*x = 2;

 *x también se puede desasignar haciendo una llamada al procedimiento free:

free( x );

 C++ simplifica la asignación dinámica de memoria incorporando los operadores new y delete como nombres
reservados:

int* x = new int;


*x = 2;
delete x;

 Para permitir la asignación arbitraria y la desasignación utilizando new y delete (o bien malloc y free), el entorno
debe tener un área en la memoria a partir de la cual se pueden asignar las localizaciones en respuesta a las llamadas de
new, y a la cual se pueden devolver las localizaciones en respuesta a las llamadas de delete.
Tradicionalmente esta área se conoce como un montículo o montón (aunque no tiene nada que ver con la estructura de datos
montículo).
La asignación en el montón se conoce como asignación dinámica.

9 Longinos Recuero Bustos (http://longinox.blogspot.com)


 En una implementación típica del entorno, la pila (para la asignación automática) y el montículo (para la asignación dinámica)
se mantienen en secciones diferentes de la memoria, y las variables globales también se asignan en un área por
separado, estática.

 Muchos lenguajes requieren que la desasignación del montón sea administrada de manera automática al igual que la
pila, excepto que la asignación debe conservarse bajo control del usuario.
 Todos los lenguajes funcionales requieren que el montón sea completamente automático, sin ninguna asignación o
desasignación bajo control del programador. Java, por otra parte, permite la asignación, pero no la desasignación.
 El motivo por el que los LP no permiten el control sobre la asignación y desasignación del montón, es que éstas son
operaciones poco seguras y pueden introducir un comportamiento erróneo en tiempo de ejecución.
 En un lenguaje con estructura de bloques con asignación de montones, existen tres tipos de asignación en el entorno:
o Estático, para variables globales.
o Automático, para variables locales
o Dinámicos, para asignación de montones.
Esas categorías también se conocen como clases de almacenamiento de la variable.

6.6 Variables y constantes


6.6.1 Variables
 Una variable es un objeto cuyo valor almacenado se puede cambiar durante la ejecución.
También se puede pensar en una variable como completamente especificada por sus atributos, que incluyen su
nombre, su localización, su valor y otros atributos como tipos de datos y tamaños.
 Una representación esquemática se puede dibujar como sigue:

 A la forma reducida se la conoce como diagrama cuadro y círculo:

10 Longinos Recuero Bustos (http://longinox.blogspot.com)


 La línea que une el nombre con el cuadro de localización se puede pensar que representa el vínculo del nombre con la
localización mediante el entorno.
 La forma principal en la que una variable cambia su valor es a través del enunciado de asignación.
 Dado que una variable tiene a la vez una localización y un valor almacenado en dicha localización, es importante
distinguir claramente entre ambos:
o Al valor almacenado en una localización se conoce como valor r (por valor del lado derecho).
o La localización de una variable se conoce como valor l (por valor del lado izquierdo).
 En C, además de la desreferenciación automática estándar de los valores l a valores r, existe un operador explícito
“dirección de” operador & que coloca una referencia y la convierte en un apuntador.
 La mezcla de apuntadores, valores r y valores l en C puede llevar a situaciones confusas y poco seguras.
Son sin embargo una consecuencia de la meta de diseño de C como lenguaje rápido y poco seguro para programación
de sistemas.
*( &x + 1 ) = 2;

 En algunos lenguajes se da un significado diferente a la asignación:


o Se copian localizaciones en vez de los valores, esto es, asignación por compartición.

o La asignación por clonación es otra alternativa para x = y, que consiste en asignar una nueva localización,
copiar el valor de y a la nueva localización y cambiar la localización de x por la nueva localización.
 En ambos casos esta interpretación de la asignación a veces se conoce como una semántica de apuntador a fin de
distinguirla de la semántica más usual, lo cual se conoce como semánticas de almacenamiento.

6.6.2 Constantes
 Es una identidad del lenguaje que tiene un valor fijo durante la duración de su existencia dentro de un programa, siendo
como una variable, excepto que no tiene atributo de localización.
 Una constante por lo tanto tiene una semántica de valor en vez de una semántica de almacenamiento como las
variables.
 Su valor no puede modificarse y su localización no puede ser referenciada de manera explícita a través de un programa.
 Esta idea de constante es simbólica, esto es, una constante esencialmente es el nombre de un valor.
 Hay que distinguir a los literales (representación de valores como 42 o "esto es un string") de las constantes.
 Las constantes pueden ser:
o Estáticas:
 Aquellas cuyo valor se pude calcular antes de la ejecución.
 A su vez pueden ser de dos tipos:
 Solo pueden ser calculadas en tiempo de traducción (tiempo de compilación).
 Solo son computables en tiempo de carga (como la localización estática de una variable
global) o al principio de la ejecución del programa.

11 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Esta distinción es importante ya que las primeras (tiempo de compilación) puede ser utilizada por el
compilador para mejorar la eficiencia de un programa y no necesita ocupar memoria.
Sin embargo las que se computan en tiempo de carga o dinámica tienen que ser computada ya
sea en el arranque o conforme avanza la ejecución y debe ser almacenada en memoria.
 Para evitar ambigüedades, nos referiremos a las constantes en tiempo de compilación como
constantes en tiempo de compilación y constante estática a las constantes en tiempo de carga.
o Dinámicas:
 Aquellas cuyo valor se puede computar únicamente durante la ejecución.

 También se puede hacer una distinción entre constantes de tipo general (todas las vistas anteriormente) y las constantes de
manifiesto (nombre de un literal).
 Considerando el siguiere ejemplo en C:
#include <stdio.h>
#include <time.h>

const int a = 2;
const int b = 27 + 2 * 2;
const int c = ( int )time( 0 );

int f( int x )
{
const int d = x + 1;
return b + c;
}
...

En este código a y b son constantes en tiempo de compilación (a es una constante de manifiesto), en tanto que c es una
constante estática (en tiempo de carga) y d es una constante dinámica.

En C, el atributo const se puede aplicar a cualquier variable en un programa que indique simplemente que el valor de
una variable no puede ser cambiado una vez establecido. Otros criterios determinan si una variable es o no estática (por
ejemplo el alcance global en el cual a, b , c se han definido más arriba).

6.7 Alias, referencias pendientes y basura


 Existen varios problemas con las reglas convencionales de nombramiento y asignación dinámica en los lenguajes
estructurados por bloques, y estos son: los Alias, las referencias pendientes y la basura.

6.7.1 Alias
o Un alias ocurre cuando el mismo objeto está vinculado a dos nombres diferentes al mismo tiempo.
o Esto puede ocurrir cuando:
 Durante la llamada de procedimiento (capítulo 8).
 A través del uso de variables de apuntador:
 El aliado debido a la asignación de apuntadores es difícil de controlar.
 Ejemplo:
1) int *X, *Y;
2) X = ( int* ) malloc( sizeof( int ) );
3) *X = 1
4) Y = X;
5) *Y = 2;

6) printf( "%d\n", *X );

12 Longinos Recuero Bustos (http://longinox.blogspot.com)


En este código, la línea 5 ha cambiado también el valor de X sin que en el código se haga
mención explícita de ello.

 A través de la asignación por compartición:


 Debido a que utiliza apuntadores implícitamente.
 Java es un ejemplo importante de este tipo de alisado.
 Ejemplo:
class ArrTest
{
public static void main(String[] arg)
{
int[] x = { 1, 2, 3 };
int[] y = x;

x[0] = 42;
System.out.println( y[0] );
}
}

El resultado final es la impresión de y[0] que ahora es 42 debido a la asignación por


compartición.
 Java tiene un mecanismo para clonar explícitamente cualquier objeto, de manera que los alias
no se creen por asignación:

int[] y = ( int )x.clone();

o Los alias presentan un problema en el hecho de que causan efectos colaterales potencialmente dañinos.
o Un efecto colateral de un enunciado es cualquier cambio en el valor de una variable que persiste más allá de la
ejecución de un enunciado.
o No todos los efectos colaterales son dañinos, puesto que en una asignación se pretende explícitamente que
cause uno.
o Sin embargo los efectos colaterales que son cambios a variables cuyos nombres no aparecen directamente en
el enunciado son potencialmente dañinos puesto que el efecto colateral no se puede determinar a partir del
código escrito.

6.7.2 Referencias pendientes


o Es una localización que ha sido desasignada del entorno, pero a la cual todavía tiene acceso el programa. Otra
manera de definirlas es que son objetos que pueden ser accedidos más allá de su tiempo de vida en el entorno.
o Un ejemplo simple en C es el de un apuntador que apunta a un apuntador desasignado:
int *x, *y;
...
x = ( int* )malloc( sizeof( int ) );
...
*x = 2;
...
y = x; /* y se inicializa con x, ahora esta como alias */
13 Longinos Recuero Bustos (http://longinox.blogspot.com)
free(x); /* Se libera x, quedando *y como una referencia pendiente */
...
printf( "%d\n", *y ); /* referencia ilegal */

o Otro ejemplo en C serian las referencias pendientes que resultan de la desasignación automática de las
variables locales cuando se sale del bloque de la declaración local (esto es debido, a que C tiene & “direccion de”
que permite asignar la localización de cualquier variable a una variable apuntador ):

{
int *x;

{
int y;
y = 2;
x = &y; /* x contiene la localización de y. *x es un alias de y */
}

/* *x es ahora una referencia pendiente */


}

o Java no permite referencias pendientes pues no existen apuntadores explícitos, ni operador &, ni operadores de
desasignación de memoria como free o delete.

6.7.3 Basura
o La basura es memoria que ha sido asignada en el entorno pero que se ha convertido en inaccesible para el
programa.
o En C, se genera basura si se omite la llamada a free antes de reasignar una variable de apuntador:
int *x;
...
x = ( int* )malloc( sizeof( int ) );
x = 0;

La localización asignada *x por la llamada a malloc es ahora basura, ya que ahora x contiene el apuntador
nulo y no existe ninguna manera de tener acceso al objeto anteriormente asignado.

o Otro ejemplo similar ocurre cuando la ejecución sale de la región del programa en el cual x misma está
asignada:
void p( void )
{
int *x;
x = ( int* )malloc( sizeof( int ) );
*x = 2;
}

Cuando se sale del procedimiento p, la variable x se desasigna y *x ya no es más accesible para el programa.
De manera similar ocurre con el anidamiento de bloques.

o La basura es un problema en la ejecución de un programa dado que se trata de memoria desperdiciada.


o Los programas que producen basura pueden tener menos errores serios, que los programas que contienen
referencias pendientes, porque estos programas aunque dejen de ejecutarse al quedarse sin memoria,
producen resultados correctos, pero un programa que tiene acceso a referencias pendientes, aunque se
ejecute, puede producir resultados incorrectos.
o Los sistemas de lenguajes que recuperan automáticamente la basura se dice que llevan a cabo la recolección
de basura.
o La administración de la memoria basada en pilas en un entorno de un lenguaje estructurado en bloques se
puede llamar un tipo de simple recolección de basura: cuando se sale del alcance de una declaración de

14 Longinos Recuero Bustos (http://longinox.blogspot.com)


variable automática, el entorno recupera la localización asignada a dicha variable al desapilar la memoria que se
le había asignado antes.
o Los sistemas funcionales fueron pioneros en recolección de basura como LISP.
o Los sistemas orientados a objetos como Smalltalk y Java también se apoyan en recolectores de basura para la
recuperación de memoria durante la ejecución del programa.

15 Longinos Recuero Bustos (http://longinox.blogspot.com)


7. TIPOS DE DATOS ................................................................................................................................................................ 2

7.1. TIPOS DE DATOS E INFORMACIÓN DE TIPOS ................................................................................................................... 2

7.2. TIPOS SIMPLES............................................................................................................................................................... 3

7.3. CONSTRUCTORES DE TIPO ............................................................................................................................................. 4

7.3.1. PRODUCTO CARTESIANO ............................................................................................................................................... 4

7.3.2. UNIÓN ............................................................................................................................................................................ 5

7.3.3. SUBCONJUNTOS ............................................................................................................................................................ 6

7.3.4. ARREGLOS Y FUNCIONES ............................................................................................................................................... 6

7.3.5. TIPOS APUNTADOR Y RECURSIVO .................................................................................................................................. 7

7.3.6. TIPOS DE DATOS Y EL ENTORNO .................................................................................................................................... 8

7.4. NOMENCLATURA DE TIPOS EN LENGUAJES DE EJEMPLO............................................................................................... 8

7.5. EQUIVALENCIA DE TIPOS................................................................................................................................................ 9

7.6. VERIFICACIÓN DE TIPOS ............................................................................................................................................... 10

7.6.1. COMPATIBILIDAD DE TIPOS .......................................................................................................................................... 11

7.6.2. TIPOS IMPLÍCITOS ........................................................................................................................................................ 12

7.6.3. TIPOS QUE SE SUPERPONEN Y VALORES DE TIPOS MÚLTIPLES ................................................................................... 12

7.6.4. OPERACIONES COMPARTIDAS ..................................................................................................................................... 12

7.7. CONVERSIÓN DE TIPOS ................................................................................................................................................ 12

7.8. VERIFICACIÓN DE TIPOS POLIMÓRFICOS ...................................................................................................................... 13

7.9. POLIMORFISMO EXPLÍCITO ........................................................................................................................................... 15


7. Tipos de datos
 Todo programa utiliza datos, ya sea de manera implícita o explícita, para llegar a un resultado.
 Los datos en un programa se reúnen en estructuras de datos, que se manipulan mediante estructuras de control que
representan algoritmos.
algoritmos + estructuras de datos = programas

 Los datos en su forma más primitiva en el interior de la computadora son simplemente una colección de bits.
 La mayoría de los lenguajes incluyen un conjunto de entidades simples de datos, como enteros, reales y booleanos, así
como mecanismos para construir nuevos tipos a partir de los mismos.
 Estas abstracciones contribuyen prácticamente a todas las metas del diseño de lenguajes como: legibilidad, capacidad de
escritura, confiabilidad e independencia de la máquina.
 Sin embargo estas abstracciones pueden conllevar una serie de problemas como son:
o Dependencia de la máquina:
 Un ejemplo de lo anterior es lo finito de todos los datos en una computadora, lo cual queda enmascarado
por las abstracciones.
o Precisión de los números y operaciones aritméticas con reales.
o La falta de consenso entre los diseñadores de lenguajes en relación al grado de información de tipos que
debe de hacerse explícita para verificar la corrección del programa antes de la ejecución.
o Existen muchas razones para tener alguna forma de verificación de tipos estática (en tiempo de traducción):
 La información de tipos estáticos permite a los compiladores asignar memoria con eficiencia y generar
código máquina que manipula los datos eficientemente, mejorando la eficiencia de la ejecución.
 Un compilador puede utilizar los tipos estáticos a fin de reducir la cantidad de código que necesita compilar,
mejorando la eficiencia de traducción.
 La verificación de tipos estáticos permite que muchos errores estándar de programación sean detectados
rápidamente, lo que mejora la capacidad de escritura.
 La verificación de tipos estáticos mejora la seguridad y la confiabilidad de un programa al reducir la cantidad
de errores de ejecución que pueden ocurrir.
 Los tipos explícitos mejoran la legibilidad al documentar el diseño de los datos.
 Pueden utilizarse los tipos explícitos para eliminar ambigüedades en los programas.
 Los programadores pueden utilizar los tipos explícitos combinados con la verificación de tipos estáticos
como una herramienta de diseño, de manera que las decisiones erróneas de diseño se pongan de
manifiesto en forma de errores en tiempo de traducción.
 La tipificación estática de interfaces mejora el desarrollo de los programas grandes al verificar la
consistencia y corrección de la interfaz.

7.1. Tipos de datos e información de tipos


 Los datos en los programas pueden clasificarse de acuerdo con sus tipos.
 Todo valor de datos expresable en un LP tiene implícito un tipo (por ejemplo, en C el valor -1 es de tipo entero, el 3,2334
es de tipo double...).
 Un tipo de datos es un conjunto de valores, junto con un conjunto de operaciones sobre dichos valores y con ciertas
propiedades.
 Un intérprete de lenguaje puede utilizar el conjunto de propiedades algebraicas de una variedad de maneras para
asegurarse de que los datos y las operaciones se utilizan de manera correcta en un programa.

z = x / y;

El interprete puede determinar si x e y tienen los mismos tipos y si dicho tipo tiene un operador de división
definido para sus valores.
 El proceso por el cual pasa un intérprete para determinar si la información de tipos en un programa es consistente se
conoce como verificación de tipos.
 El proceso de asignar tipos a expresiones (x / y) se conoce como inferencia de tipos.
 Este proceso puede considerarse como una operación por separado y llevarse a cabo durante la verificación de tipos o
considerarse como parte de la verificación de tipos misma.

2 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Todo lenguaje ofrece una diversidad de maneras para construir tipos más complejos, basándose en los tipos básicos
(int, char, double), estos mecanismos se conocen como constructores de tipo, y los tipos creados se conocen como
tipos definidos por el usuario.
 Los nuevos tipos creados con constructores de tipo no tienen automáticamente un nombre y se les denomina tipo
anónimo, sin embargo, los nombres son de extrema importancia (documentación de código, verificación de tipos, …).

int a[ 10 ]; // Crea en C una variable cuyo tipo "arreglo de int" anónimo

 Los nombres para los nuevos tipos se crean utilizandos una declaración de tipos (a veces definición de tipos).

// Definición de un nuevo tipo


typedef int Array_of_ten_integers[ 10 ];

// Asignación del nuevo tipo


Array_of_ten_integers a;

 Cada lenguaje con declaraciones de tipo tienen reglas para ello, y estas se conocen como algoritmos de equivalencia de
tipo.
 Los métodos utilizados para la construcción de tipos, el algoritmo de equivalencia de tipos y las reglas de inferencia y de
corrección de tipos, se conocen de manera colectiva como un sistema de tipos.
 Si en un lenguaje, todos los errores de tipo se detectan en tiempo de traducción, se dice que es un lenguaje fuertemente
tipificado.
 Un tipificado fuerte asegura que la mayoría de los programas peligrosos (programas con errores que corrompen datos) se
rechazarán en tiempo de traducción, y aquellos que no se rechacen, causarán un error antes de cualquier corrupción de
datos.
 Ada, ML, Haskell, Modula son lenguajes fuertemente tipificado. C se le conoce como un lenguaje débilmente tipificado.
 Los lenguajes sin sistemas de tipificación estática se conocen como lenguajes sin tipificación (o lenguajes con tipificación
dinámica). Estos lenguajes incluyen, Scheme y otros dialectos de Lisp, Smaltalk y la mayoría de los lenguajes de scripts,
como Perl.
 En un lenguaje sin tipos, toda la verificación de seguridad se lleva a cabo en tiempo de ejecución.

7.2. Tipos simples


 Todo lenguaje contiene un conjunto de tipos predefinidos (int, char, double).
 También a veces se predefinen variaciones sobre los tipos básicos (short, unsigned, …).
 Los tipos predefinidos son principalmente tipos simples: tipos que no tienen otra estructura que su aritmética inherente o
una estructura secuencial.
 Sin embargo existen tipos simples que no están predefinidos:
o Los tipos enumerados:
 Los tipos enumerados son conjuntos cuyos elementos se denominan y se listan de manera explícita.

enum Color { Red, Green, Blue };

 Las enumeraciones se definen en una declaración de tipo y son verdaderos nuevos tipos.
 En la mayoría de lenguajes, las enumeraciones están ordenadas, considerando que el orden en el cual
se listan los valores es importante, existiendo a menudo, una operación sucesora o predecesora para
todo tipo enumerado.

o Los tipos de subrango:


 Son subconjuntos contiguos de tipos simples especificados al dar por lo menos el elemento menor y el
más grande.

3 Longinos Recuero Bustos (http://longinox.blogspot.com)


type Digit_Type is range 0..9;

 Esto es útil si en un programa deseamos:


 Distinguir este tipo de otros tipos enteros.
 Minimizar almacenamiento.
 Generar verificaciones en tiempo de ejecución para asegurar que los valores siempre están en
el rango correcto.
 Estos tipos se conocen como tipos ordinales, porque existe un orden discreto en el conjunto.
 Todos los tipos enteros numéricos son tipos ordinales y entre otras disponen de operaciones de
sucesor y de predecesor. No así como los números reales, por lo que una operación de subrango es
usualmente incorrecta en la mayoría de lenguajes.

7.3. Constructores de tipo


 Dado que los tipos de datos son conjuntos, las operaciones de conjuntos pueden utilizarse para construir nuevos tipos a
partir de los existentes.
 Cuando se aplican estas operaciones de tipos a los tipos se les llama constructores de tipos.
 En un LP todos los tipos se construyen a partir de tipos predefinidos utilizando constructores de tipos.

7.3.1. Producto cartesiano


 Dados los conjuntos U y V, podemos formar el producto cartesiano formado por todos los pares ordenados de
elementos de U y V.
 En muchos lenguajes el constructor de tipos del producto cartesiano está disponible como la construcción de
estructuras o de registros.

struct IntCharReal
{
int i;
char c;
double r;
}

 Existe una diferencia entre un producto cartesiano y una estructura de registro:


o En una estructura los componentes tienen nombre, en tanto que en un producto cartesiano se hace
referencia a ellos por su posición.

 Las proyecciones en la estructura de registro están dadas por la operación de selector de componentes (o miembro
de la estructura):
o Si x es del tipo IntCharReal, entonces x.i es la proyección de x hacia enteros.

 La mayoría de los lenguajes de programación consideran los nombres componentes como parte del tipo definido por
un registro, por lo que la siguiente estructura puede considerarse diferente de la anterior aunque representen el
mismo producto cartesiano:

struct IntCharReal
{
int j;
char ch;
double d;
}

 Algunos lenguajes tienen una forma más pura del tipo estructura de registro, que es en esencia idéntica al producto
cartesiano, donde a menudo se les denomina tuplas. Por ejemplo en ML podemos definir IntCharReal como:
4 Longinos Recuero Bustos (http://longinox.blogspot.com)
Type IntCharReal = int * char * real;

 Un tipo de datos que se encuentra en los lenguajes orientados a objetos, que está relacionado con las estructuras,
es la clase.
 Un esquema típico de asignación para los tipos de producto cartesiano es la asignación secuencial, según el espacio
que requiere cada componente. Por ejemplo:

Type IntCharReal = int * char * real;

Requiere un espacio de 13 bytes, 4 para el int, 1 para el char y 8 para el real.

7.3.2. Unión
 Se forma tomando el conjunto de unión teórica de sus conjuntos de valores.
 Existen dos variedades de unión:
o Discriminada:
 Si se le agrega una etiqueta o discriminador para distinguir el tipo de elemento.
o Indiscriminadas:
 No tienen etiqueta y debe suponerse el tipo de cualquier valor en particular.

 En C y C++ el constructor de tipo union proporciona uniones indiscriminadas:

union IntOrReal
{
int i;
double r;
};

 Al igual que con struct, existen nombres para diferenciar los distintos componentes (i y r).
 Los nombres son necesarios porque comunican al intérprete el tipo con el que deben interpretarse los bits dentro de
la unión.
 Estos nombres no deben de confundirse con los discriminantes, que es un componente separado que indica el tipo
de datos que es realmente el valor, a diferencia del tipo que puede pensar que es:

Imitación de un discriminante Uso

enum Disc {IsInt, IsReal}; IntOrReal x;


struct IntOrReal x.which = IsReal;
{ x.val.r = 2.3;
enum Disc which;
union ...
{
int i; if(x.which == IsInt) printf("%d\n", s.val.i);
double r; else printf("%g\n", x.val.r);
} val;
};

 Las uniones pueden resultar útiles para reducir los requerimientos de asignación de memoria para las estructuras
cuando no se necesitan, simultáneamente, diferentes elementos de datos.
 Esto se debe a que a las uniones se les asigna un espacio de memoria equivalente al mayor necesario para cada
uno de sus componentes y los valores de cada componente se almacenan en regiones superpuestas de la memoria.
 Las uniones, sin embargo, no son necesarias en lenguajes orientados a objetos, ya que en un mejor diseño sería
utilizar la herencia para representar diferentes requerimientos de datos que no se superponen.

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


7.3.3. Subconjuntos
 En matemáticas se pueden definir subconjuntos al dar una regla para distinguir sus elementos, como:

 En los lenguajes de programación se puede hacer algo parecido para definir nuevos tipos que serán subconjuntos de
tipos conocidos.
 En ocasiones los subconjuntos heredan operaciones de sus tipos padres.
 Una perspectiva alternativa a la relación de subtipos es definirla en términos de operaciones compartidas. Esto es,
un tipo es subtipo de un tipo si y sólo si todas las operaciones de los valores de también pueden aplicarse a
valores del tipo .
 La herencia en los lenguajes orientados a objetos se puede considerar como un mecanismo de subtipo, en el mismo
sentido de compartir operaciones.

7.3.4. Arreglos y funciones


 El conjunto de todas las funciones:

puede dar lugar a un nuevo tipo de dos formas como un tipo:


o Arreglo.
o Función.

 Cuando es un ordinal, la función puede considerarse como un arreglo con un tipo de índice y tipo de
componente .
 En C, C++ y Java el conjunto de índices siempre es un rango de enteros positivos que comienzan por 0.
 Los arreglos pueden definirse con o sin tamaño, pero para definir una variable de tipo arreglo hay que asignarle un
tamaño ya que los arreglos son asignados estáticamente o en la pila.
 C puede definir arreglos y variables de arreglo de la siguiente manera:
const int Size = 5;

// Defienición de tipos
typedef int TenIntArray[ 10 ];
typedef int IntArray[];

// Definición de variables
TenIntArray x;
int y[ 5 ];
int z[] = { 1, 2, 3, 4, 5 };
IntArray w = { 1, 2 };

// IntArray w; /* Incorrecto!! */
// int x[ Size ]; /* Incorrecto en C!!, correctoe n C++ */

 C permite que arreglos sin tamaño especificado sean parámetros de funciones:

int array_max( int a[], int size )


{
int tem, i;
assert( size > 0 );
temp = a[ 0 ];
for( i = 1; i < size; i++ )
{
if( a[ i ] > temp) temp = a[ i ];
}
return temp;
};

6 Longinos Recuero Bustos (http://longinox.blogspot.com)


 En C y C++ el tamaño del arreglo no forma parte del mismo, por eso en el ejemplo anterior el tamaño del arreglo
tuvo que ser pasado como un parámetro adicional (int size).

 Java si puede asignar arreglos de forma dinámica (en el montón) y su tamaño puede especificarse en forma
totalmente dinámica.
 Dicho tamaño constituye una parte de la información almacenada cuando se asigna el arreglo (length).
 Los arreglos multidimensionales también son posibles, declarándolos como arreglos de arreglos.
 Los arreglos probablemente son los constructores más utilizados ya que su implementación puede hacerse en forma
muy eficiente.
 Los lenguajes funcionales por lo general no contienen un tipo arreglo ya que estos están pensados para la
programación imperativa.
 Usualmente los lenguajes funcionales utilizan listas en vez de arreglos.
 Algunos lenguajes pueden crear tipos generales de función y procedimiento.

// Definición de un tipo de función de enteros a enteros


typedef int( *IntFunction )( int );

// Este tipo puede definir variables o parámetros


int square( int x )
{
return x + x;
}

// Def. varaiable
IntFunction f = square;

// Def. parámetro
int evaluate( IntFunction g, int value )
{
return g( value );
}

 La mayoría de los lenguajes orientados a objetos, como Java y Smalltalk, no tienen variables o parámetros de
función, debido a que están enfocados a los objetos en vez de a funciones.

7.3.5. Tipos apuntador y recursivo


 Un constructor de tipos que no corresponde a una operación de conjuntos es constructor de referencias o
apuntadores, que construye el conjunto de todas las direcciones que se refieren a un tipo especificado.
 En C, una declaración que construye el tipo de todas las direcciones en que haya enteros almacenados sería:

typedef int* IntPtr;

 Los apuntadores están implícitos en lenguajes que tienen administración automática de memoria.
 Este es el caso de Java, para el cual todos los objetos son apuntadores implícitos que se asignan de forma explícita
(new) pero son desasignados automáticamente por un recolector de basura.
 A veces los lenguajes hacen distinción entre referencias y apuntadores, definiendo como referencia la dirección de
un objeto bajo el control del sistema, que no se puede utilizar como valor ni operar de forma alguna, no así con los
apuntadores.
 En este sentido los apuntadores en Java en realidad son referencias.
 Tal vez C++ es el único lenguaje donde coexisten apuntadores y referencias.
 En C++ los tipos de referencia se crean con un operador postfijo & (lo cual no debe confundirse con el operador
prefijo de dirección &, que devuelve un apuntador):

7 Longinos Recuero Bustos (http://longinox.blogspot.com)


double r = 2.3;
double& s = r; // s es una referencia a r, así comparten memoria
s += 1; // ahora tanto r como s valen 3.3

 Las referencias en C++ son en esencia apuntadores constantes que se desreferencian cada vez que se usan.
 En C y C++, los arreglos son implícitamente apuntadores constantes hacia su primer componente.

int a[] = { 1, 2, 3, 4, 5 };
int* p = a;

printf( "%d\n", *p ); // imprime 1 que es el valor de a[ 0 ]


printf( "%d\n", *( p + 2 ) ); // imprime 3 que es el valor de a[ 2 ]
printf( "%d\n", *( a + 2 ) ); // imprime 3 que es el valor de a[ 2 ]
printf( "%d\n", 2[ a ] ); // imprime 3 que es el valor de a[ 2 ]

 Los apuntadores son de gran utilidad en la creación de tipos recursivos (un tipo que se utiliza así mismo en su
declaración).
 Estos tipos tienen una gran importancia en las estructuras de datos y algoritmos, ya que corresponden naturalmente
a los algoritmos recursivos y representan datos cuya estructura y tamaño no se conocen de antemano. Dos ejemplos
típicos son las listas y los árboles.
 En C se permiten declaraciones recursivas indirectas por medio de apuntadores:
struct CharListNode
{
char data;
struct CharListNode* next;
};

typedef struct CharListNode* CharList;

CharList cl = ( CharList )malloc( sizeof( struct CharListNode ) );


( *cl ).data = 'a'; // expresión equivalente: cl->data = 'a';
( *cl ).next = 0; // expresión equivalente: cl->data = 0;

7.3.6. Tipos de datos y el entorno


 Véase capítulo 8.

7.4. Nomenclatura de tipos en lenguajes de


ejemplo
 C:
o Los tipos simples se conocen como tipos
básicos, y los que se construyen mediante
constructores de tipos se conocen como
derivados.
o Los tipos básicos incluyen el void (cuyo
conjunto de valores está vacío) y los tipos
numéricos: los tipos enteros (ordinales) y los
tipos flotantes.

 Java:
o Los tipos simples se llaman tipos primitivos, y los que se construyen utilizando constructores de tipos se llaman
tipos de referencia.
o Los tipos primitivos se dividen en el tipo boolean y tipos de punto flotante (cinco enteros y dos en coma flotante).

8 Longinos Recuero Bustos (http://longinox.blogspot.com)


o Sólo existen tres constructores de tipos: el arreglo, la clase y la interface.

 Ada:
o Los tipos simples se llaman tipos
escalares.
o Los tipos ordinales se llaman discretos,
los tipos numéricos comprenden los
tipos reales y enteros.
o Los tipos apuntador se llaman tipos
access.
o Los tipos de arreglo y de registro se
llaman tipos compuestos.

7.5. Equivalencia de tipos


 ¿En qué casos dos tipos son iguales? Una manera de responder a esta pregunta es comparar los conjuntos de valores
simplemente como conjuntos. Dos conjuntos son iguales si contienen los mismos valores.
 Dos de las formas más habituales de equivalencia de tipos en los lenguajes de programación actuales son:
o Equivalencia estructural:
 Dos tipos son iguales si tienen la misma estructura, es decir, están construidos de la misma forma a partir
de los mismos tipos simples y en el mismo orden y con los mismo nombres de variables internas.
 Es fácil de implementar y aporta toda la información necesaria para llevar a cabo la verificación de errores
y asignación de almacenamiento.
 Para verificar la equivalencia estructural, un intérprete puede representar los tipos como árboles y verificar
la equivalencia recursivamente en subárboles.
 A continuación se exponen dos equivalencias estructurales, siempre y cuando el tamaño del conjunto
índice no es parte del tipo arreglo:

typedef int A1[ 10 ];


typedef int A1[ 20 ];

 Un factor que complica las cosas es el uso de nombres de tipo en las declaraciones:
struct RecA struct RecB
{ {
char x; char x;
int y; int y;
}; };

9 Longinos Recuero Bustos (http://longinox.blogspot.com)


Ambas declaraciones deberían ser estructuralmente equivalentes, sin embargo no lo son, ya que las
variables de las diferentes estructuras tendrían que usar diferentes nombres para acceder a los datos de
los miembros.
 Para determinar la equivalencia estructural en presencia de nombres, basta con reemplazar en la
declaración cada nombre por la expresión asociada a su tipo, excepto en el caso de tipos recursivos, en
donde esta regla llevaría a un ciclo infinito.
 Las razones principales para incluir nombres de tipos es permitir la declaración de tipos recursivos.

o Equivalencia de nombres:
 Dos tipos son iguales sólo si tienen el mismo nombre y dos variables son equivalentes en tipo solo si sus
declaraciones usan exactamente el mismo nombre de tipo..
 La equivalencia de nombres en su estado más puro es incluso más fácil de implementar que la estructural,
siempre y cuando estemos obligados a dar nombre a todos los tipos.
 Ada es un lenguaje que ha implementado una equivalencia de nombres muy pura.
 C tiene una equivalencia que está entre la estructural y la de nombres y se puede decir que tiene una
equivalencia de nombre para struct, union y estructural para todo lo demás.
 En el siguiente fragmento de código en C:
struct RecA
{
char x;
int y;
};

// Definición de un nuevo tipo.


typedef struct RecA RecA;

struct RecA a;
RecA b;
struct RecA c;

struct
{
char x;
int y;
}d;

a, b, c y d son equivalentes estructuralmente, pero las únicas equivalentes en nombres son a y c.


 Java tiene un método relativamente simple para la equivalencia de tipos:
o Primero, no existe typedef, por lo que se minimizan los problemas con nombres.
o Segundo, las declaraciones class e interface crean implícitamente nuevos nombres de tipos
y para estos tipos se utiliza la equivalencia de nombres.
o La única complicación es que los arreglos emplean equivalencia estructural con reglas especiales
para establecer la equivalencia del tipo base.

7.6. Verificación de tipos


 La verificación de tipos es el proceso que sigue el interprete para verificar que todas la construcciones en un programa
tengan sentido en términos de los tipos de constantes, variables, procedimientos y otras entidades.
 Involucra la aplicación del algoritmo de verificación de tipos a expresiones y declaraciones, pudiendo este modificar el
uso del algoritmo de equivalencias para adecuarse al contexto.
 La verificación de datos puede dividirse en verificación:
o Dinámica:
 Si la información de tipos se conserva y se verifica en tiempos de ejecución.
 Por definición lo interpretes realizan verificación dinámica de los tipos.

10 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Los compiladores también pueden generar código que conserve los atributos del tipo durante el tiempo
de ejecución en una tabla o como etiquetas en un entorno.
 La verificación dinámica de los tipos es necesaria cuando los tipos sólo pueden determinarse en tiempo
de ejecución.

o Estática:
 Los tipos de expresiones y de objetos se extraen del texto del programa y el intérprete lleva a cabo la
verificación de tipos antes de la ejecución.

 Ejemplo 1: Los compiladores de C efectúan una verificación estática durante la traducción, pero realmente C no es un
lenguaje con tipificado fuerte y que muchas inconsistencias en los tipos no causan errores de compilación.
 Ejemplo 2: El dialecto Scheme de Lisp es un lenguaje con tipificado dinámico, pero los tipos se verifican en forma
rigurosa. Todos los errores de tipo provocan la terminación del programa.
 Ejemplo 3: Ada es un lenguaje con tipificado fuerte y todos los errores de tipo generan mensaje de error en la
compilación, pero sin embargo, incluso en Ada, ciertos errores, como los de rango en subíndice de arreglos, no pueden
detectarse antes de la ejecución.
 Una parte esencial en la verificación de tipos es la inferencia de tipos, en la cual los tipos de expresiones se infieren a
partir de los tipos de las subexpresiones que la componen.
 Las reglas de verificación de tipos y las reglas de inferencia a menudo están entremezcladas:
Por ejemplo, una expresión pueden determinarse como de tipo correcto, si y son del mismo tipo y este
tipo contiene una operación „ ‟ (verificación de tipos) y el tipo de la expresión resultante es del tipos de y (inferencia
de tipos).
 Las reglas de verificación y de inferencia de tipos tienen una estrecha relación con el algoritmo de equivalencia de tipos:

void p( struct{ int i; double r; } x )


{
...
}

La declaración de anterior es un error, según el algoritmo de equivalencia de tipos de C, ya que ningún parámetro real
puede tener el tipo del parámetro formal x.
 La inferencia de tipos y las reglas de corrección a menudo son las partes más complejas en la semántica de un lenguaje.

7.6.1. Compatibilidad de tipos


o A menudo resulta útil relajar las reglas de corrección de tipos de manera que los tipos de componentes no sean
precisamente iguales, según el algoritmo de equivalencia de tipos.
o Dos tipos diferentes que incluso pueden ser correctos cuando se combinan en ciertas formas se conocen como
tipos compatibles.
o Un término relacionado, la compatibilidad de asignación, a menudo se utiliza para la corrección de tipos de la
asignación .
o Inicialmente puede parecer que este enunciado es del tipos correcto cuando ambos componentes son del
mismo tipo, pero esto ignora una diferencia importante, el lado izquierdo debe ser un valor o referencia l, y el
lado derecho debe ser un valor r.
o Igual que con la compatibilidad ordinaria, la compatibilidad de asignación pude ampliarse para casos en los que
ambos lados son de diferente tipo.
o En C y en Java, todos los tipos numéricos son compatibles y las conversiones se llevan a cabo de forma que se
conserva tanta información como sea posible.

11 Longinos Recuero Bustos (http://longinox.blogspot.com)


7.6.2. Tipos implícitos
o Los tipos de las entidades básicas como las constantes o las variables pueden no establecerse explícitamente
en una declaración.
o En estos casos el intérprete debe de inferir el tipo, ya sea a través del contexto o a partir de alguna regla
estándar.
o Estos tipos se conocen como implícitos.
o En todos los lenguajes los literales son el ejemplo más claro de entidades tipificadas implícitamente.

7.6.3. Tipos que se superponen y valores de tipos múltiples


o Los tipos pueden superponerse cuando dos tipos contienen valores en común.
o Es preferible que los tipos sean conjuntos no conexos, evitando así ambigüedades al determinar el tipo de un
valor.
o Imponer esta restricción de forma arbitraria sería demasiado limitante, eliminando una de las características
principales de la POO, la capacidad de crear subtipos mediante la herencia que refina los tipos existentes, al
tiempo que conservan su pertenencia en el tipo más general.
o Sin embargo, en ocasiones los tipos que se superponen y los valores de tipos múltiples pueden representar un
beneficio directo para simplificar el código. Por ejemplo, en C, el literal 0 no solo es un valor de todos los tipos
enteros, sino también representa el apuntador nulo.

7.6.4. Operaciones compartidas


o Los tipos tienen operaciones asociadas que usualmente están definidas de forma implícita.
o A menudo estas operaciones son compartidas entre varios tipos, o tienen el mismo nombre que otras
operaciones que pueden ser diferentes.
o Se considera que estos operadores están sobrecargados ya que se utiliza el mismo nombre para operaciones
que en esencia son diferentes.
o En el caso de un operador sobrecargado, el intérprete debe decidir, a partir de los tipos de los operandos, a que
operación se refiere.

7.7. Conversión de tipos


 En todos los lenguajes de programación existe la necesidad, bajo ciertas circunstancias, de convertir un tipo a otro.
 Esta conversión de tipos puede incorporarse al sistema de tipos, de forma que las conversiones se lleven a cabo de
forma automática.

int x = 3;
...
x = 2.3 + x / 2;

En este ejemplo, el intérprete insertó dos conversiones automáticas o conversiones implícitas:


o La conversión del resultado int a double en la operación x / 2.
o La conversión del resultado double a int en la operación 2.3 + 1.0.
 Conversiones implícitas como esta se conocen como coacciones.
 La conversión de int a double es el resultado de una conversión de extensión.
 La conversión de double a int es el resultado de una conversión de restricción (puede involucrar perdida de datos).
 La conversión implícita puede debilitar la verificación de tipos de forma que no se detecten errores, poniendo en peligro el
tipificado fuerte y la confiabilidad del LP.
 Una alternativa a la conversión implícita es la conversión explícita, presentándose cuando las directrices de la
conversión se escriben directamente en el código, también llamada conversión forzada.
 Existen dos variantes de sintaxis para la conversión forzada:

12 Longinos Recuero Bustos (http://longinox.blogspot.com)


o La primera es utilizada en C y en Java:

x = ( int )( 2.3 + ( double )( x / 2 ) );

o La segunda es utilizad por C++ y Ada y se le conoce como sintaxis de llamado de funciones:

x = int( 2.3 + double( x / 2 ) );

o La ventaja de utilizar conversiones forzadas es la de documentan en forma precisa dentro del código, existiendo
menor probabilidad de comportamientos inesperados.
o Además, la eliminación de las conversiones implícitas facilita al intérprete resolver la sobrecarga:

double max( int, double );


double max( double, int );
...
double x = max( 2, 3 ); // ¿Qué función se ejecutará?

 Una alternativa a las conversiones forzadas es tener funciones predefinidas o de biblioteca, que lleven a cabo dichas
conversiones.
Como ejemplo, en Java la clase Integer en la biblioteca java.lang contiene las funciones de conversión
toString, que convierte un int en un String, y parseInt que convierte un String en un int.

String s = Integer.toString( 1234 );


int i = Integer.parseInt( "54321" );

 En algunos lenguajes está prohibida la conversión implícita a favor de la explícita, la cual favorece la documentación de
la conversión minimizando los riesgos de comportamientos extraños, facilitando la sobrecarga.
 Como ejemplo de estos lenguajes tenemos a Ada.
 Un paso intermedio es permitir la conversión implícita siempre que no involucre corrupción de los datos, en este sentido
Java sólo permite conversión por extensión.
 Los lenguajes orientados a objetos tienen requerimientos especiales para la conversión, ya que la herencia puede
interpretarse como un mecanismo de subtipificación y en algunos casos es necesario hacer conversiones de subtipos a
supertipos y viceversa.

7.8. Verificación de tipos polimórficos


 La mayoría de los lenguajes de tipificado estático exigen que en todos los nombres de cada declaración se dé
información sobre los tipos.
 Sin embargo, también es posible aplicar una forma de inferencia de tipos para determinar los tipos de los nombres en
una declaración en la que no se hayan dado dichos tipos.
 Esta forma es conocida como verificación de tipos Hindley-Milner, cuyos pasos son:
o Se comienza confeccionando el árbol sintáctico de la expresión.
o Se llenan los tipos de los nombres (los nodos de las hojas) a partir de las declaraciones.
o Si no se conocen los tipos, se asignan tipos simulados.
o A partir de las declaraciones, el verificador de tipos va ascendiendo por las ramas asignando y verificando que
los tipos sean correctos.

int i;
int a[] = { ... };
...
a[ i ] + i;

13 Longinos Recuero Bustos (http://longinox.blogspot.com)


i;
a[] = { ... };
...
a[ i ] + i;

 Dos observaciones sobre esta forma de verificación de tipos:


o Una vez que una variable de tipo es reemplazada por un tipo real (o una forma más especializada de tipo), todas
las instancias de la variable deben actualizarse para coincidir con el nuevo valor de la variable tipo. Este
proceso se conoce como instanciamiento de las variables tipo.
o Cuando se dispone de nueva información sobre los tipos, las expresiones de tipos para las variables pueden
cambiar de forma en diversas maneras. Este proceso se conoce como unificación.
 La unificación involucra tres casos:
o 1. Cualquier variable de tipo se unifica con cualquier expresión de tipos (y es instanciado según esa expresión)
o 2. Dos constantes de tipo cualquiera (como int o double) sólo se unifican si son del mismo tipo.
o 3. Dos construcciones de tipo cualquiera (como arreglo o struct) sólo se unifican si son aplicaciones del mismo
constructor de tipo y todos sus tipos componentes también se unifican.
 Por ejemplo:
o Al unificar la variable de tipo con la expresión de tipo “arreglo de ” se produce el caso 1, y es inicializado
por instancia a “arreglo de ”.
o Unificar int a int es un ejemplo del caso 2.
o Al unificar “arreglo de ” con “arreglo de ” se produce el caso 3.
 La verificación de tipos Hindley-Milner aporta una enorme ventaja en la verificación de tipos simples, ya que los tipos
pueden conservarse tan generales como sea posible, a la vez que se verifica su consistencia de forma estricta.
Considerando el ejemplo:
a[ i ] = b[ i ]

Esta expresión asigna el valor de b[ i ] a a[ i ].


La verificación de Hindley-Milner establecerá que i deberá ser un int, a deberá ser un “arreglo de ” y b debe ser un
“arreglo de ”, y luego, == .
 La verificación de tipos concluye considerando los tipos de a y b como restringidos a “arreglo de ”, pero sigue siendo
una variable irrestricta, que podría ser de cualquier tipo. Se dice que esta clase de expresiones son polimórficas y la
verificación de tipos Hindley-Milner implementa implícitamente la verificación de tipos polimórficos.

14 Longinos Recuero Bustos (http://longinox.blogspot.com)


 El tipo “arreglo de ” es en realidad un conjunto de tipos múltiples e infinitos, dependiendo de las posibles inicializaciones
por instancias de la variable de tipo .
 Esta clase de polimorfismo se conoce como polimorfismo paramétrico, ya que es un parámetro de tipo que puede ser
reemplazado por cualquier expresión de tipo.
 De esta manera podemos hablar de dos tipos de polimorfismo paramétrico:
o Implícito, los parámetros de tipo son introducidos implícitamente por el verificador de tipos.
o Explícito, los parámetros de tipo son introducidos explícitamente por el programador.
 Todo objeto de tipo polimórfico que se pasa a una función como parámetro debe tener una especialización fija a lo largo
de toda la función.
 Un intérprete lleva a cabo la traducción de código en el paso a una función con argumentos de tipos polimórficos de dos
formas:
o Expansión:
 Se examinan todas las llamadas a la función y se genera una copia del código para cada uno de los
tipos empleados usando el tamaño apropiado para cada tipo.
o Encuadrado y Etiquetado:
 Se fija un tamaño para todos los datos que pueden contener valores escalares, se agrega un campo de
bits que etiqueta el tipo como escalar o no, y un campo de tamaño si no son escalares. Todos los tipos
estructurados quedan representados por un apuntador y un tamaño, mientras que los tipos escalares
se copian directamente.

7.9. Polimorfismo explícito


 Deseamos definir una estructura de datos que pueda contener cualquier tipo de dato. Para ello, es imposible hacer que el
tipo polimórfico sea implícito; en vez de ello, debemos escribir en forma explícita mediante la sintaxis apropiada. A esto
se le conoce como polimorfismo paramétrico explícito.
 Los tipos de datos polimórficos con parámetros explícitos funcionan muy bien en lenguajes con verificación Hindley-
Milner, pero no son más que un mecanismo para crear constructores de tipos definidos por el ususario.
 C++ es un ejemplo de lenguaje con polimorfismo paramétrico explicito, pero sin tener asociada la verificación de tipos
Hindley-Milner.
 El mecanismo para lo anterior es la plantilla (template), que puede usarse ya sea con funciones o con construcciones
de tipos class o struct.
 Como ejemplo podemos tomar el siguiente código en C++ que define una pila utilizando un parámetro de tipo explícito:

template <typename T>


struct StackNode
{
T data;
StackNode< T >* next;
};

template <typename T>

struct Stack
{
StackNode< T >* theStack;
};

Stack< int > s;

s.theSatck = new StackNode< int >;


s.theStack->data = 3;
s.theStack->next = 0;

15 Longinos Recuero Bustos (http://longinox.blogspot.com)


8 CONTROL I – EXPRESIONES Y ENUNCIADOS ....................................................................................................................... 2

8.1 EXPRESIONES ......................................................................................................................................................................... 2


8.2 ENUNCIADOS Y GUARDIAS CONDICIONALES .................................................................................................................................. 4
8.2.1 Enunciados if ............................................................................................................................................................... 5
8.2.2 Enunciados case y switch .......................................................................................................................................... 6
8.3 CICLOS Y VARIACIONES SOBRE WHILE .......................................................................................................................................... 8
8.4 MANEJO DE EXCEPCIONES ...................................................................................................................................................... 10
8.4.1 Excepciones ................................................................................................................................................................. 11
8.4.2 Manejadores de excepciones ...................................................................................................................................... 11
8.4.3 Control ......................................................................................................................................................................... 12
8.4.4 Especificación de excepciones ..................................................................................................................................... 13
8 Control I – Expresiones y enunciados
 Las expresiones representan el mecanismo fundamental de cómputo de los programas.
 Una expresión:
o En su forma más pura devuelve un valor y no produce efectos colaterales, es decir, no hay cambios en la memoria
del programa.
o Son las partes del lenguaje de programación que más se parecen a las matemáticas.
o La forma en la que se evalúan expresiones, incluyendo el orden en el cual las subexpresiones se calculan, puede
tener un efecto importante sobre su significado, algo que en ningún caso es aplicable a las expresiones
matemáticas.
 Un enunciado:
o Se ejecuta por sus efectos colaterales y no devuelve ningún valor.

8.1 Expresiones
 Las expresiones básicas son los literales (constantes manifiestas) y los identificadores.
 Las expresiones más complejas se elaboran en forma recursiva a partir de las expresiones básicas mediante la
aplicación de operadores y funciones, lo que a veces involucra símbolos de agrupamiento como los paréntesis.
 Los operadores pueden tomar uno o más operandos (unarios, binarios, …).
 Los operadores pueden escribirse con notación infija, postfija y prefija.
 Esta notación corresponde con un recorrido en orden, postorden y preorden del árbol sintáctico de la expresión.
 Las formas prefijas y postfijas tienen la ventaja de no necesitar paréntesis para expresar el orden en que se aplican los
operadores.
 La asociatividad de operadores también queda implícita con las notaciones prefija y postfija sin la necesidad de reglas.
 Muchos lenguajes hacen distinción entre operadores y funciones.
 Los operadores si son binarios se escriben en forma infija con reglas de asociatividad y precedencia específicas.
 Las funciones que pueden ser predefinidas o definidas por el usuario, se escriben en forma prefija y los argumentos se
consideran como argumentos reales o parámetros para llamadas de las funciones:

3 + 4 * 5 add( 3, mul( 4, 5 ) )

 Todos los lenguajes tienen reglas para evaluar las expresiones.


 Una regla común es que todos los operandos son evaluados primero y después se les aplica los operadores. A esto se le
conoce como evaluación de orden aplicativo, o evaluación estricta.
 Corresponde a una evaluación de abajo hacia arriba de los valores en los nodos del árbol sintáctico.
 Por ejemplo la expresión ( 3 + 4 ) * ( 5 - 6 ) está representada por:

En la evaluación de orden aplicativo, primero se evalúan


los nodos + y - a 7 y -1, respectivamente, y después se
aplica el * para obtener -7.

 El orden natural en el que se evalúan las subexpresiones sería de izquierda a derecha, lo cual corresponde a un
recorrido de izquierda a derecha en el árbol sintáctico.
 Sin embargo muchos lenguajes establecen en forma explícita que no existe orden específico para la evaluación de
argumentos a funciones definidas por el usuario.
 Una de las razones para ello es que el intérprete puede reorganizar el orden del cálculo para que este sea más eficiente.
 Si la evaluación de una expresión no provoca efectos colaterales, éste dará el mismo resultado, independientemente del
orden en que se evalúan las subexpresiones.
 En presencia de efectos colaterales, sin embargo, el orden en el que se realice la evaluación puede provocar diferencias:

2 Longinos Recuero Bustos (http://longinox.blogspot.com)


int x = 1;

int f( void )
{
x += 1;
return x;
}

int p( int a, int b )


{
return ( a + b );
}

main()
{
printf( "%d\n", p( x, f() ) );
return 0;
}

Si los argumentos de la llamada p( x, f() ) son evaluados de izquierda a derecha, el resultado será 3, por el
contrario, si son evaluados de derecha a izquierda el resultado será 4.
Esto se debe a que una llamada a la función f tiene un efecto colateral (modifica el valor de la variable global x).
 Los programas que dependen del orden de evaluación para sus resultados, son incorrectos.
 C, así como otros lenguajes de expresiones, también tienen un operador de secuencia, que permite que se combinen
varias expresiones en una sola y que se evalúen secuencialmente. C especifica que el orden de evaluación es de
izquierda a derecha y que el valor de la expresión más a la derecha es el valor devuelto de toda la expresión:

int x = 1, y = 2;

x = ( y++, x += y, x + 1 );

// la expresión devuelve 5, x = 5, y = 3

 En un lenguaje de programación se puede especificar que las expresiones booleanas deben evaluarse en orden de
izquierda a derecha, hasta el punto en que se conoce el valor verdadero de toda la expresión, y en ese momento la
evaluación se detiene. Esta regla se conoce como evaluación en cortocircuito de las expresiones booleanas:

true || x; // true

x && false; // false

 Un beneficio de la evaluación en cortocircuito es la de protección frente a errores en tiempo de ejecución:

if( i <= lastIndex && a[ i ] >= x ) ...

if( x != 0 && y % x == 0 ) ...

3 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Las expresiones booleanas no son las únicas cuyas subexpresiones pueden no evaluarse totalmente. Muchos lenguajes
tienen expresiones que imitan enunciados de control, pero devuelven valores; dos ejemplos son la expresiones if y las
expresiones case:

e1 ? e2 : e3; // A diferencia del enunciado if, esta expresión debe tener parte else opcional

 Los operadores booleanos de corto circuito y de if son un caso especial de operadores que difieren la evaluación de sus
operandos. Esta situación de conoce como evaluación diferida, o como evaluación no estricta.
 En ausencia de efectos colaterales (cambios a las variables en memoria, en la entrada, en la salida), el orden de la evaluación de las
expresiones no tiene importancia en relación con el valor final de la expresión.
 En un lenguaje en el cual los efectos colaterales no existen, como los lenguajes funcionales, las expresiones en los
programas comparten una propiedad importante con las expresiones matemáticas, lo que se puede definir como la regla
de sustitución o transparencia referencial, es decir, dos expresiones cualesquiera en un programa, que tengan el mismo
valor, pueden ser sustituidas la una por la otra en cualquier parte del programa.
 La transparencia referencial permite que se utilice una forma muy sólida de la evaluación diferida, la cual tiene
importantes consecuencias teóricas y prácticas, conocida como evaluación de orden normal.
 Esta evaluación de una expresión significa que todas la operaciones o funciones comienzan a evaluarse antes de que
sus operandos sean evaluados y los operandos son evaluados sólo si son necesarios para el cálculo del valor de la
operación:

Evaluación de orden normal

cuadrado( doble(2) )  doble(2) * doble(2)  (2 + 2) * (2 + 2)


int doble( int x )
{ return x + x; }
 4 * (2 + 2)  4 * 4  16
int cuadrado( int x )
{ return x * x; } Evaluación de orden aplicativo

cuadrado( doble(2) )  cuadrado(2 + 2)  4 * 4  16

 La evaluación de orden normal se conoce como evaluación perezosa en Haskell.

8.2 Enunciados y guardias condicionales


 La forma más típica de control estructurado es la ejecución de un grupo de enunciados sólo bajo ciertas condiciones.
 Esto incluye llevar a cabo una prueba booleana o lógica antes de entrar a una secuencia de enunciados.
 El enunciado if es la forma más común de este tipo de construcciones.
 Una forma general de enunciado condicional que abarca todas las diversas construcciones condicionales es el if
cauteloso, utilizado por primera vez por E.W.Dijkstra:

if B1 -> S1
| B2 -> S2
. . .
| Bn -> Sn
fi>

 La semántica de este enunciado es como sigue:


o Los Bi son todas expresiones booleanas, conocidas como guardias.
o Las Si son secuencias de enunciados.
o Si uno de los Bi se evalúa como verdadero, entonces se ejecuta el Si correspondiente.

4 Longinos Recuero Bustos (http://longinox.blogspot.com)


o Si más de uno de los Bi es verdadero, entonces se selecciona una y solo una de las Si correspondientes para
su ejecución.
o Si ninguno de los Bi es verdadero, ocurre un error.
 Esta descripción incluye características interesantes:
o No dice que el primer Bi que se evalúe como cierto es aquel que se elige. Por tanto el if cauteloso introduce el
no determinismo en la programación.
o No especifica si se evalúan todas las guardias. Por lo cual, si la evaluación de un Bi tiene un efecto colateral, el
resultado de la ejecución del if cauteloso puede ser desconocido.

8.2.1 Enunciados if
 La forma básica del enunciado if es como aparece en la regla EBNF para el enunciado if en C, con una parte
else opcional:

en el cual puede ser ya sea un enunciado o una secuencia de enunciados encerrados entre corchetes.
 Esta forma del if presenta, sin embargo, un problema de ambigüedad sintáctica. El siguiente enunciado:

if( e1 ) if( e2 ) S1 else S2

tiene dos árboles de análisis gramaticales diferentes, de acurdo con la BNF:

 Esta ambigüedad se conoce como el problema del else ambiguo:


o La sintaxis no nos indica si un else después de dos enunciados if, debe asociarse con el primero o con el
segundo if.

 Este problema se resuelve mediante una regla para eliminar la ambigüedad:


o El else se asociará con el if más cercano que no tenga ya una parte else.

 A esta regla también se le conoce como la regla del anidamiento más cercano para los enunciados if.
 El problema del else ambiguo es de diseño del lenguaje y es discutible desde dos puntos de vista:
o Obliga a establecer una nueva regla.
o Dificulta al lector la interpretación de enunciado if.
 Esto viola el criterio de legibilidad del diseño.
 Una mejor forma para eliminar esta ambigüedad es empleando una palabra clave enmarcadora para el enunciado
if, como en la siguiente regla en ADA:

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Una extensión del enunciado if simplifica las cosas cuando hay muchas alternativas:

Sin extensión Con extensión

if e1 then S1 if e1 the S1
else if e2 then S2 elsif e2 then S2
else if e3 then S3 elsif e3 then S3
end if ; end if ; end if ; end if ;

 Una cuestión adicional con respecto al enunciado if es el tipo de expresión de control que debe ser:
o En Java, Ada y Pascal, la prueba siempre debe tener tipo booleano.
o C, no tienen tipo booleano y la expresión de control puede ser de tipo entero o de tipo puntero. El valor
resultante se compara con 0, siendo verdadero si la comparación es desigual y falso en caso contrario:

if( p != 0 ) ...  if( p ) ...

8.2.2 Enunciados case y switch


 El enunciado case (o enunciado switch en C/C++ y Java) fue inventado por C.A.R. Hoare como una variación
especial del if cauteloso, en el cual las guardias, en vez de ser expresiones booleanas, son valores ordinales
seleccionados por una expresión ordinal:

switch( x % 11 )
{
case 0:
y = 0;
break;
case 2:
case 3:
case 4:
z = 2;
break;
case 7:
case 9:
z = y = 1;
break;
default:
// NOP
break;
}

 La semántica de este enunciado es evaluar la expresión de control x % 5 y transferir el control al punto del
enunciado en donde está listado el valor.
 Los casos listados han de ser mutuamente excluyentes.
 Los valores de los casos pueden ser literales o expresiones constantes.
 Si el valor de la expresión de control no está listado en la etiqueta de un caso, entonces el control es transferido al
caso default, si existe. Si no, el control es transferido al enunciado que sigue al switch (el enunciado switch
fracasa).
 La estructura de este enunciado en C cuenta con una cantidad de características relativamente nuevas:
o Las etiquetas son manejadas sintácticamente como etiquetas ordinarias, lo que permite que ocurran
ubicaciones potencialmente extrañas:

6 Longinos Recuero Bustos (http://longinox.blogspot.com)


int x = 2;

switch(x) x++; switch(x) { x++; }

o Al no contar con un enunciado break, la ejecución fracasa y pasa al siguiente caso. Esto permite que los
casos sean listados juntos sin necesidad de repetir el código del siguiente caso; también tiene como
resultado una ejecución incorrecta, en caso de que no se incluya un enunciado break:

switch(x)
{
case 1: if( x > 2 )
case 2: x++;
default: break;
}

 ADA, tiene una versión algo más estándar, desde el punto de vista histórico:

case x % 11
when 0 when
y = 0;
when 2 .. 5 =>
z = 2;
when 7 | 9 C
z = y = 1;
when others =>
null;
end case;

Los valores case deben ser distintos y además deben ser completos, ya que si no existe un valor correcto listado y
no existe el caso por omisión (when others), ocurriría un error de compilación.
Esto implica que se debe conocer todo el conjunto de valores posibles de la expresión de control en tiempo de
compilación.
 El lenguaje de programación funcional ML, también cuenta con una construcción case, pero es una expresión que
devuelve un valor y no es un enunciado:

fun caseDemo x =
case x - 1 of
0 => 2 |
2 => 1 |
_ => 10
;

Los casos en una expresión case ML son sólo patrones a comparar, en vez de rangos o listas de valores.
El guión bajo es el patrón comodín, el cual funciona como el caso por omisión.

7 Longinos Recuero Bustos (http://longinox.blogspot.com)


8.3 Ciclos y variaciones sobre while
 Los ciclos y su aplicación para llevar a cabo operaciones repetitivas, han sido desde el comienzo una de las principales
características de la programación de computadoras.
 Una forma general de la construcción de un ciclo aparece en la estructura correspondiente al if cauteloso de Dijkstra, a
saber, el do cauteloso:

do B1 -> S1
| B2 -> S2
. . .
| Bn -> Sn
od

 Este enunciado se repite hasta que todos los Bi son falsos.


 En cada paso se selecciona en forma no determinística un Bi verdadero y se ejecuta el Si correspondiente.
 La forma más básica de la construcción de ciclo, que en esencia es un do cauteloso con solo una guardia (eliminando así
el no determinismo), es el ciclo while de C, C++ y Java:

while (e) S
o el ciclo while de Ada:

while e loop S1 ... Sn end loop;

 En los enunciados anteriores, la expresión de prueba e se evalúa primero. Si resulta verdadera, entonces se ejecuta el
enunciado[s] S[i]. Después, vuelve a evaluarse e y así sucesivamente.
 Si para empezar, e resultara falsa, entonces el código interior nunca es ejecutado.
 Por ello la mayoría de los lenguajes cuentan con un enunciado alternativo, que asegura que el código del ciclo se ejecute
al menos una vez:
do S while (e);

 Lo anterior es equivalente al siguiente código:

S;
while (e) S

así que el enunciado do es azúcar sintáctica (una construcción de leguaje que es completamente expresable en términos
de otras construcciones).
 Las construcciones while y do tienen la propiedad de que la terminación del ciclo se especifica al principio ( while) o al
final (do) del ciclo. Por esta razón, C, C++ y Java, incluye dos opciones:
o Puede utilizarse un enunciado break (exit en ADA) dentro de un ciclo para salir por completo del ciclo.
o Puede utilizarse un enunciado continue que salta el resto del cuerpo del ciclo y continúa la ejecución en la
siguiente evaluación de la expresión de control.
 Los puntos de terminación complican la semántica de los ciclos, así que varios lenguajes (Pascal entre ellos) los
prohíben.
 Un caso muy común de la construcción de ciclos es la construcción for-loop en C, C++ y Java:

for ( e1, e2, e3 ) S; e1;


while (e2)
{
S;
e3;
}

8 Longinos Recuero Bustos (http://longinox.blogspot.com)


 La expresión e1 es el inicializador del for-loop, e2 es la prueba y e3 es la actualización.
 C++, Java y el estándar C ISO1999 ofrece la característica adicional de que el inicializador de un for-loop puede
contener declaraciones, de forma que el índice de un ciclo puede definirse dentro del ciclo:

for( int i = 0; i < size; i++ )


{
sum += a[ i ];
}

 Muchos lenguajes restringen el formato para for-loop de forma que sólo pueda utilizarse en situaciones de indización.
 A menudo los lenguajes incluyen esta forma de ciclo porque puede optimizarse más eficientemente:
o La variable de control puede incluirse en los registros del microprocesador, lo cual permite operaciones
extrarápidas.
o Muchos procesadores tienen una sola instrucción según la cual pueden incrementar un registro, probarlo y
ramificarlo, de forma que el control y el incremento del ciclo pueden ocurrir con una sola instrucción
máquina.
 Para obtener esta eficiencia, deben imponerse muchas restricciones como:
o El valor de i no puede cambiarse dentro del cuerpo del ciclo.
o El valor de i es indefinido después de finalizar el ciclo.
o i no puede declararse en ciertas formas (parámetro de un procedimiento, campo de registro, etc).
 Algunas cuestiones adicionales sobre el comportamiento de los ciclos son las siguientes:
o ¿Se evalúan los límites solo una vez?
o Si el límite inferior es mayor al límite superior, ¿es ejecutado el ciclo?
o ¿Es indefinido el valor de la variable de control, aunque se utilice un enunciado exit o break para salir
del ciclo antes de terminarlo?
o ¿Qué verificaciones de intérpretes se llevan a cabo en las estructuras de ciclo?
 Algunos lenguajes, contienen una forma general de la construcción for-loop que incluye un nuevo objeto del
lenguaje, un iterador.
 En una definición abstracta, un iterador debe ocuparse de la definición de variables de control, aportar un esquema de
iteración para estas y un servicio utilitario para llevar a cabo pruebas de terminación.
 Por tanto, un iterador se convierte en algo semejante a una nueva declaración de tipo.
 Tomemos un ejemplo de esto en C++:

vector< char > v;

for( int i = 'A'; i <= 'Z'; i++ )


{
v.push_back( i );
}

vector<char>::iterator it; // it es un iterador de tipo char

for( it = v.begin(); it != v.end(); it++ )


{
cout << *it << endl;
}

return EXIT_SUCCESS;

9 Longinos Recuero Bustos (http://longinox.blogspot.com)


8.4 Manejo de excepciones
 El manejo de excepciones es el control de condiciones de error y otros eventos no usuales durante la ejecución de un
programa.
 El manejo de excepciones incluye la declaración tanto de las excepciones como de los manejadores de excepciones.
 Una excepción es cualquier evento inesperado o poco frecuente (división por cero, índices de arrays fuera de rango, etc).
Cuando esta ocurre, se dice que se pone de manifiesto o surge.
 En los lenguajes interpretados las excepciones también incluye errores estáticos, como los de sintaxis y de tipo.
 Un manejador de excepciones es un procedimiento o secuencia de código diseñado para ejecutarse cuando se pone de
manifiesto una excepción en particular (se dice que el manejador maneja o atrapa excepciones) y se supone que para hacer
posible que de alguna forma continúe la ejecución normal.
 El manejo de excepciones en particular se ha integrado con mucho éxito en los mecanismos orientados a objetos de
Java y C++ y en mecanismos funcionales de ML y Common Lisp.
 Las técnicas para el manejo de excepciones a menudo son ignoradas en la enseñanza de programación y merecen que
los programadores las utilicen más.
 El manejo de excepciones intenta imitar en el lenguaje de programación las características de una interrupción de
hardware o trampa de errores, mediante las cuales el procesador transfiere el control automáticamente a una ubicación
especificada, de acuerdo con el tipo de error o interrupción.
 No obstante, aun en un lenguaje con un buen mecanismo de excepciones, resulta irrazonable esperar que el programa
atrape o maneje todos los posibles errores que pueden ocurrir.
 La razón es que demasiadas fallas posibles pueden ocurrir en un nivel demasiado bajo (fallos hardware, memoria,
comunicación, etc) y que pueden llevarnos a situaciones en las que el S.O. se vea obligado a tomar medidas drásticas
para terminar el programa sin que éste pueda hacer algo al respecto.
 A esto errores se les conoce excepciones asíncronas, ya que pueden ocurrir en cualquier momento.
 Por su parte, los errores que el programa seguramente puede atrapar se conocen como excepciones síncronas.
 Las excepciones definidas por el usuario solo pueden ser síncronas, pero las predefinidas o de biblioteca pueden incluir
varias excepciones asíncronas, ya que el entorno de ejecución del programa puede cooperar con el S.O. para permitir al
programa atrapar algunos errores asíncronos.
 Resulta útil, al estudiar los mecanismo de manejo de excepciones, recordar como pueden tratarse las excepciones en
lenguajes que carezcan de estos mecanismos:

( 0 == y )? handleError("denominador in ratio is zero") : ratio = x / y;

enum ErrorKind { OutOfInput, BadChar, Normal };


typedef void ( *ErrorProc )( ErrorKind );
...
unsigned value;
...
void handler( ErrorKind error ){ .. }
...
unsigned getNumber( ErrorProc handler )
{
unsigned result;
int ch = fgetc( input );
if( EOF == ch ) handler( OutOfInput );
else if( !isDigit( ch ) ) handler( BadChar );

...
}

...

10 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Cuando se diseña un programa declarando las excepciones antes de que ocurran, especificando lo que el programa
debe hacer si ocurre una excepción, deben considerarse las siguientes puntos:
o Excepciones:
 ¿Cuáles son las predefinidas por el lenguaje?, ¿Pueden desactivarse?, ¿Pueden crearse excepciones
definidas por el usuario?, ¿Cuál es su alcance?
o Manejador de excepciones:
 ¿Cómo se definen?, ¿Cuál es su alcance?, ¿Cuáles son los manejadores por omisión?, ¿Pueden ser
reemplazados?
o Control:
 ¿Cómo se pasa el control a un manejador?, ¿A dónde se transfiere el control después de ejecutar el
manejador?, ¿Qué entorno de ejecución queda después de que se haya ejecutado un manejador de
excepciones?

8.4.1 Excepciones
 Típicamente el suceso de una excepción en un programa se representa con un objeto de datos, y ese objeto de datos
puede ser predefinido o definido por el usuario.
 Resulta extraño que en C++ no existe un tipo especial para las excepciones y por tanto, no hay palabra reservada para
declararlas. En vez de ello, cualquier tipo estructurado (class o struct) puede servir para representar una excepción:

struct Trouble
{
string error_message;
int wrong_value;
} trouble;

 La declaración de excepciones anterior por lo general obedece las mismas reglas de alcance que las demás
declaraciones del lenguaje.
 Ya que las excepciones ocurren en tiempo de ejecución, éstas, pueden hacer que la ejecución se salga del alcance de
una declaración de excepción en particular, sucediendo que en su manejo no pueda ser referenciada por nombre.
 Es deseable minimizar el problema que esto provoca declarando excepciones definidas por el usuario en forma global en
el programa, para evitar problemas de alcance. Sin embargo, bajo ciertas circunstancias, las excepciones locales pueden
tener sentido, con el fin de evitar la creación de gran cantidad de excepciones globales superfluas.
 En C++, según la práctica usual, no existen en el lenguaje excepciones predeterminadas. Sin embargo, mucho módulos
de biblioteca estándar aportan excepciones y mecanismos de excepción. Algunas de estas excepciones estándar son:
o std::bad_exception
o std::bad_alloc
o std::bad_array_new_length

8.4.2 Manejadores de excepciones


 En C++ los manejadores de excepciones están asociados con bloques try-catch, que pueden aparecer en cualquier
lugar donde puede haber un enunciado:

try
{
division = divendo / divisor;
}
catch( Trouble t )
{
// manejar el problema si es posible
printf( "\nt.error_message" );
}

11 Longinos Recuero Bustos (http://longinox.blogspot.com)


catch( Big_Troble b )
{
// manejar el problema si es posible
}
catch( ... )
{
// manejar todas las excepciones restantes
}

 El enunciado compuesto después de la palabra reservada try es obligatorio.


 Cualquier cantidad de bloques catch también necesita un enunciado completo y se escribe como una función de un
solo parámetro cuyo tipo es de excepción.
 Los manejadores predefinidos usualmente se limitan a imprimir un mensaje de error mínimo, que indica el tipo de
excepción y quizás cierta información sobre el lugar del programa donde ocurrió, y después termina el programa.
 C++, ofrece una manera de reemplazar el manejador predeterminado por un manejador definido por el usuario, usando el
módulo de biblioteca estándar <exception>. Esto se logra llamando a la función set_terminate, pasando un
argumento que consiste de una función void que no toma parámetros. Esta función entonces será llamada como
manejador predeterminado para todas las excepciones que no se atrapan explícitamente en el código del programa:

// set_terminate example
#include <iostream>
#include <exception>
#include <cstdlib>
using namespace std;

void myTerminate()
{
cerr << "terminate handler called\n";
abort(); // forces abnormal termination
}

int main( void )


{
set_terminate( myTerminate );
throw 0; // unhandled exception: calls terminate handler
return 0;
}

8.4.3 Control
 Una excepción predefinida o incorporada puede ser puesta de manifiesto, ya sea automáticamente por el sistema en
tiempo e ejecución o manualmente por el programa.
 C++ utiliza la palabra reservada throw y un objeto de excepción para poner de manifiesto una excepción:

if( /* algo está saliendo mal */ )


{
Trouble t;
t.error_message = "Malas noticias!!";
t.wrong_value = current_exception;
throw t;
}

12 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Cuando se pone de manifiesto una excepción, se abandona el cálculo que se está haciendo y el sistema en tiempo de
ejecución comienza a buscar un manejador.
 Esta búsqueda comienza en la sección para el manejador del bloque en el cual se atrapó la excepción. Si el programa no
encuentra en manejador, se consulta la sección del manejador siguiente así sucesivamente ( este proceso se conoce como
propagación de la excepción).
 Si se llega al último bloque de una función o procedimiento sin haber encontrado un manejador, la llamada se devuelve al
invocador y la excepción se vuelve a poner de manifiesto en éste como si la llamada misma hubiese puesto de manifiesto
la excepción.
 Este proceso continua hasta que se encuentra un manejador, o de lo contrario hasta que se sale del programa principal,
en cuyo caso se llama al manejador predeterminado.
 El proceso de salirse de vuelta a través de las llamadas de función a los invocadores, durante la búsqueda de un
manejador, se conoce como desenrollo de llamada o de pila, en donde la pila es de llamada.
 Una vez encontrado y ejecutado el manejador, todavía queda la cuestión respecto a donde se continuará la ejecución:
o Una opción es regresar al punto en el que por primera vez se puso de manifiesto la excepción y comenzar la
ejecución a partir de ese enunciado o expresión. Esto se conoce como el modelo de reanudación del manejo de
excepciones.
o Otra opción es continuar con la ejecución en el código inmediato siguiente al bloque o expresión en la que se
encontró el manejador que fue ejecutado. Esto se conoce como modelo de terminación de manejo de
excepciones.

while( 1 )
{
try
{
int* myarray = new int[10000];
break; // Éxito!!!
}
catch( bad_alloc& ba )
{
cerr << "bad_alloc caught: " << ba.what() << endl;

collect_garbage(); // Aún no es posible salir!!!

if( /* Aún no hay memoria suficiente */ )


{
// Se deja todo como está, evitando un ciclo infinito
throw bad_alloc;
}
}
}

 Casi todos los lenguajes modernos utilizan el modelo de terminación para el manejo de excepciones.
 El manejo de excepciones a menudo implica una carga general sustancial en tiempo de ejecución. Por esta razón y
porque las excepciones no representan una buena alternativa de control estructurado, es mejor evitar el uso excesivo de
excepciones para implementar situaciones de control ordinarias, que podrían ser reemplazadas por pruebas simples.

8.4.4 Especificación de excepciones


 Repasar por el texto base.

13 Longinos Recuero Bustos (http://longinox.blogspot.com)


9 CONTROL II - PROCEDIMIENTOS Y AMBIENTES .................................................................................................................. 2

9.1 DEFINICIÓN Y ACTIVACIÓN DE LOS PROCEDIMIENTOS...................................................................................................................... 2


9.2 SEMÁNTICA DE LOS PROCEDIMIENTOS ......................................................................................................................................... 2
9.3 MECANISMOS DE PASO DE PARÁMETROS ..................................................................................................................................... 4
9.3.1 Paso por valor ............................................................................................................................................................... 4
9.3.2 Paso por referencia ....................................................................................................................................................... 5
9.3.3 Paso por valor-resultado ............................................................................................................................................... 5
9.3.4 Paso por nombre y evaluación retardada ..................................................................................................................... 6
9.3.5 Mecanismo de paso de parámetros Vs. la especificación de los parámetros ............................................................... 6
9.3.6 Verificación de tipo de los parámetros .......................................................................................................................... 7
9.4 AMBIENTES, ACTIVACIÓN Y ASIGNACIÓN DE LOS PROCEDIMIENTOS ................................................................................................... 7
9.4.1 Ambientes totalmente estáticos ................................................................................................................................... 7
9.4.2 Ambientes de tiempo de ejecución basados en pilas .................................................................................................... 8
9.4.3 Procedimientos calculados dinámicamente y ambientes totalmente dinámicos ....................................................... 10
9 Control II - Procedimientos y ambientes
 La distinción entre procedimiento y funciones es, en esencia, lo mismo que la diferencia entre expresiones y declaraciones.
 Las llamadas a procedimientos son enunciados, en tanto que las llamadas a función son expresiones.
 De aquí en adelante no se hará una diferencia significativa entre procedimientos y funciones, puesto que, en la mayoría de
los lenguajes, sus propiedades semánticas son similares, aunque su sintaxis no lo sea.
 Los procedimientos surgieron cuando escaseaba la memoria, como una forma de dividir un programa en pequeñas partes
compiladas.
 Los procedimientos también pueden representar procesos recursivos no fácilmente representados por otras estructuras de
control y pueden representar procesos recursivos no fácilmente representables por otras estructuras de control.

9.1 Definición y activación de los procedimientos


 Un procedimiento es un mecanismo en un lenguaje de programación para abstraer un grupo de acciones o de
computaciones.
 El grupo de acciones se conoce como cuerpo del procedimiento, que está representado en su totalidad por el nombre del
procedimiento.
 Se define un procedimiento al proveer un especificación o interfaz y un cuerpo.
 La especificación le da nombre al procedimiento, una lista de los tipos y nombres de sus parámetros, así como el tipo de
su valor devuelto, si es que este existe:

// C++
void c( int& x, int& y ) // Especificación
{
int t = x; // Cuerpo
x = y; // Cuerpo
y = t; // Cuerpo
}

 En algunos lenguajes y en algunas situaciones, puede separarse una especificación de procedimiento de su cuerpo, en
el caso de que la especificación deba estar disponible por adelantado:

void intSwap( int&, int& )

Nótese como esta especificación no requiere que estén especificados los nombres de los parámetros.

 En C++ esta clase de especificación se conoce como una declaración, mientras que la definición completa se llama
definición.
 Se llama o activa un procedimiento al enunciar su nombre, junto con los argumentos de la llamada, que corresponden a
sus parámetros:

intSwap( a, b );

 Una llamada a un procedimiento transfiere el control al principio del procedimiento llamado (el llamado). Cuando la
ejecución llega al final del cuerpo (o algún enunciado de tipo return), el control es devuelto al llamador.
 Algunos lenguajes de programación pueden hacer la distinción entre procedimiento y funciones.
 Un procedimiento se comunica con el resto del programa a través de sus parámetros y también a través de sus
referencias no locales, esto es, referencias a variables declaradas fuera de su propio cuerpo.

9.2 Semántica de los procedimientos


 Un procedimiento es un bloque cuya declaración está separada de su ejecución.
 El ambiente determina la asignación de la memoria y mantiene el significado de los nombres durante la ejecución.

2 Longinos Recuero Bustos (http://longinox.blogspot.com)


 En un lenguaje estructurado en bloques, cuando se encuentra un bloque durante la ejecución, hace que ocurra la
asignación de variables locales y otros objetos correspondientes a las declaraciones del bloque.
 Esta memoria asignada para los objetos locales del bloque se conoce como registro de activación o marco de pila del
bloque, y se dice que el bloque está activado conforme se ejecuta bajo las limitaciones establecidas por su registro de
activación.

A:
{
int x, y;
... Registro de la activación de A
x = y * 10; x
y
B:
{
Registro de la activación de B
int i = x / 2;
i
}
}

 En el código anterior, el bloque B necesita tener acceso a la variable x declarada en el bloque A.


 Una referencia a x dentro de B es una referencia no local. Esto requiere que B retenga información con relación a la
activación que lo rodea.
 Sin embargo, existe una diferencia en la forma en que se resuelven las referencias no locales, si B fuese un
procedimiento llamado desde A.

int x;

void B( void )
{
int i = x / 2;
}
Entorno global
void A( void ) x
{
int x, y; Registro de la activación de A
... x
x = y * 10; y

B();
Registro de la activación de B
}
i
main()
{
A();
return 0;
}

 Bajo la regla de alcance léxica, la x en B es la x global del programa. Por lo tanto, la activación de B debe retener
información con respecto al ambiente global.
 Esto es debido a que el ambiente global es el ambiente definidor de B, en tanto que al registro de activación de A se le
conoce como ambiente invocador de B.
 Para bloques que no sean procedimientos, al ambiente definidor es igual al ambiente invocador.
 El método de comunicación de un procedimiento con su ambiente invocador es a través de sus parámetros.

3 Longinos Recuero Bustos (http://longinox.blogspot.com)


int max( int x, int y )
{
return ( x > y ) ? x : y;
}

 En la llamada a max, el parámetro x es reemplazado por el argumento a + b, y el parámetro y es reemplazado por el


argumento 10. A fin de enfatizar el hecho de que los parámetros no asumen valores, sino hasta que son reemplazados
por los argumentos, los parámetros a veces se conocen como parámetros formales, en tanto que los argumentos se
llaman parámetros actuales.
 Algunos procedimientos pueden depender solamente de parámetros y características fijas del lenguaje. A estos se dice
que están en una forma cerrada, ya que no contienen dependencias no locales. Como ejemplo la función max
anteriormente citada.
 Por otra parte, una definición polimórfica de la misma función en C++, usando plantillas:

template <typename T>


T max( T x, T y )
{
return ( x > y ) ? x : y;
}

no está en forma cerrada, ya que el operador > puede ser sobrecargado.

 Para escribir esta función en forma cerrada, tendríamos que incluir la operación en la lista de parámetros:

template <typename T>


T max( T x, T y, bool ( *gt )( T, T ) )
{
return gt( x, y ) ? x : y;
}

 Si optamos por no hacer esto, entonces la semántica de esta función solamente puede determinarse con relación al
ambiente que la rodea y el código de esta función junto con una representación de su ambiente de definición se llama
cerradura (también llamada clausura léxica o closures), porque puede ser utilizado para resolver todas las referencias no
locales excepcionales con relación al cuerpo de la función.

9.3 Mecanismos de paso de parámetros


 Algunos lenguajes ofrecen sólo una clase básica de mecanismo de paso de parámetros (C, Java, Lenguajes funcionales), en
tanto que otros pueden ofrecer dos (C++) o más.
 En lenguajes que solamente tienen un mecanismo, a menudo es posible también imitar otros, utilizando el direccionamiento
u otras características del lenguaje.

9.3.1 Paso por valor


 Este es, por mucho, el mecanismo más común para el paso de parámetros.
 En este mecanismo, los argumentos son expresiones que se evalúan en el momento de la llamada y sus valores se
convierten en los valores de los parámetros durante la ejecución del procedimiento. Esto significa que los parámetros de
valor se comportan como valores constantes durante la ejecución del procedimiento.
 Esta forma de paso por valor es usualmente el mecanismo de paso de parámetros de los lenguajes funcionales.

4 Longinos Recuero Bustos (http://longinox.blogspot.com)


 El paso por valor es también el mecanismo por omisión en C++ y en Pascal y es en esencia el único mecanismo de paso
por parámetros en C y en Java. Sin embargo, en estos lenguajes los parámetros se consideran como variables locales
del procedimiento, con valores iniciales dados por los argumentos en la llamada.
 Nótese que el paso por valor no implica que no puedan ocurrir cambios fuera del procedimiento mediante el uso de
parámetros. Si el parámetro tiene un tipo de apuntador o de referencia, entonces el valor es una dirección y puede
utilizarse para cambiar la memoria por fuera del procedimiento.
 Además, en algunos lenguajes ciertos valores son de manera implícita apuntadores o referencias:

void init_p_0( int p[] )


{
p[ 0 ] = 0;

p = ( int* ) malloc( sizeof( int ) ); // ERROR!! no tiene efecto


}

 Sin embargo, las asignaciones directas a los parámetros no cambia el argumento fuera del parámetro.

9.3.2 Paso por referencia


 Con este mecanismo un argumento debe ser en principio una variable con una dirección asignada.
 En vez de pasar el valor de la variable, el paso por referencia pasa la ubicación de la variable de modo que el parámetro
se convierte en un alias para el argumento y cualquier cambio que se le haga a éste lo sufre también el argumento.

C C++ Pascal

void inc( int* x ) void inc( int& x ) procedure inc( var x: integer );
{ { begin
( *x )++; x++; x:= x + 1;
} } end

Después de la llamada a inc el valor del argumento aumenta en 1, de manera que ha ocurrido un efecto colateral.
 Es posible el alias múltiple.
 Un problema adicional que debe ser resuelto en un lenguaje con paso por referencia es la respuesta del lenguaje a los
argumentos de referencia que no son variables.

9.3.3 Paso por valor-resultado


 Este mecanismo llega a un resultado similar al del paso por referencia, excepto que en este caso no se establece un
alias real.
 En el procedimiento, el valor el argumento es copiado y utilizado y después, cuando el procedimiento termina, el valor
final del parámetro se copia de regreso a la ubicación del argumento.
 Este método a veces es conocido como copia al entrar, copia al salir, o como copia-restauración.
 El paso por valor-resultado sólo se distingue del paso por referencia por la presencia del alias.

// El código que sigue está escrito en sintaxis C por comodidad

void p( int x, int y )


{
x++;
y++;
}

5 Longinos Recuero Bustos (http://longinox.blogspot.com)


main()
{
int a = 1;
p( a, a );
...
}

a tiene el valor 3 después de que p es llamado en el caso de que se utilice el paso por referencia, en tanto que a tiene el
valor 2 si se utiliza el paso por valor-resultado.
 Los problemas en este mecanismo, son el orden en el que los resultados se copian de regreso a los argumentos y si las
ubicaciones de los argumentos se calculan solamente a la entrada y se almacenan o son recalculados a la salida.

9.3.4 Paso por nombre y evaluación retardada


 La idea del paso por nombre es que no se evalúa el argumento hasta su uso real (como parámetro) en el procedimiento
llamado. Por lo que el nombre del argumento, o su representación textual en el punto de la llamada, reemplaza el nombre
del parámetro al cual corresponde.

int i;
int a[ 10 ];

void inc( int x )


{
i++;
x++;
}

main()
{
i = 1;
a[ 1 ] = 1;
a[ 2 ] = 2;
p( a[ i ] );
return 0;
}

 Este código tiene el resultado de establecer a a[ 2 ] en 3 y dejar a[ 1 ] sin modificación.


 El paso por nombre resulta problemático cuando se desean efectos colaterales.

9.3.5 Mecanismo de paso de parámetros Vs. la especificación de los parámetros


 Es posible criticar estas descripciones de mecanismos de paso de parámetros con base en que están íntimamente
ligados a la mecánica interna del código utilizado para su implementación.
 Todas las interrogantes con respecto a la interpretación que se ha analizado siguen presentándose en el código que
contiene efectos colaterales.
 Un lenguaje que intenta dar solución a este problema es Ada.
 Ada tiene dos conceptos de comunicación de parámetros que se pueden pasar a un procedimiento de tres formas
diferentes (http://es.wikibooks.org/wiki/Programaci%C3%B3n_en_Ada/Subprogramas):
o in: el parámetro formal es una constante y permite sólo la lectura del valor del parámetro real asociado.
o in out: el parámetro formal es una variable y permite la lectura y modificación del valor del parámetro real
asociado.
o out: el parámetro formal es una variable y permite únicamente la modificación del valor del parámetro real
asociado.
Ninguno de estos modos implica el uso de un paso de parámetro por valor o por referencia. El compilador es libre de
elegir el paso más adecuado de acuerdo al tamaño del parámetro y otras consideraciones.
6 Longinos Recuero Bustos (http://longinox.blogspot.com)
procedure Una_Prueba (A, B: in Integer; C: out Integer) is
begin
C:= A + B;
end Una_Prueba;

 Cuando se llame al procedimiento con la sentencia Una_Prueba(5 + P, 48, Q), se evalúan las expresiones 5 + P y 48
(sólo se permiten expresiones en el modo in), después se asignan a los parámetros formales A y B, que se comportan
como constantes. A continuación, se asigna el valor A + B a la variable formal C. Obsérvese que especificando el modo
out no se puede conocer el valor del parámetro real Q. En este caso, el parámetro formal C es una nueva variable cuyo
valor se asignará al parámetro real Q cuando finalice el procedimiento. Si se hubiera querido obtener el valor de Q,
además de poder modificarlo, se debería haber empleado C: in out Integer.

9.3.6 Verificación de tipo de los parámetros


 En lenguajes con tipificado fuerte, las llamadas a procedimientos deben verificarse de modo que los argumentos
concuerden en su tipo y número con los parámetros del procedimiento.
 En el caso del paso por valor referencia, los parámetros deben tener el mismo tipo, pero en el caso del paso por valor
esto puede relajarse hasta la compatibilidad de asignación y pueden permitirse conversiones, como se hace en C, C++ y
Java.

9.4 Ambientes, activación y asignación de los procedimientos


9.4.1 Ambientes totalmente estáticos
 En un lenguaje como Fortran77, toda la asignación de la memoria puede llevarse a cabo en tiempo de carga y las
localizaciones de todas las variables quedan fijas durante toda la ejecución del programa.
 Las definiciones de funciones y de procedimientos no pueden ser anidadas.
 No se permite recursión, por lo tanto, toda la información asociada con una función o con una subrutina puede asignarse
estáticamente.
 Cada procedimiento o función tiene un registro de activación fijo, que ha previsto espacio para las variables y parámetros
locales, así como posiblemente la dirección de retorno para el regreso correcto de las llamadas.
 Las variables globales se definen mediante declaraciones COMMON y se determinan utilizando apuntadores hacia un área
común.
 La forma general del ambiente en tiempo de ejecución para un programa FORTRAN con subrutinas S1…Sn es:

Área COMMON
Registro de activación
de programa principal
Registro de la
activación de S1
Registro de la
activación de S2

Además, cada registro de activación se subdivide en varias áreas:

Espacio para las variables locales


Espacio para los
parámetros pasados
Dirección de retorno
Espacio temporal para
evaluación de la expresión

7 Longinos Recuero Bustos (http://longinox.blogspot.com)


9.4.2 Ambientes de tiempo de ejecución basados en pilas
 En un lenguaje estructurado en bloques con recursión, las activaciones de los bloques de procediemento no pueden
asignarse estáticamente, y que el procedimiento pudiera volver a ser llamado antes de haber salido de su activación
anterior.
 Esto puede hacerse en una forma basada en pilas, con un nuevo registro de activación creado en la pila cada vez que
entra o se libera a la salida de un bloque.
 Para que la información que debe conservarse en el ambiente, para manejar un ambiente basado en pila, es necesario
asignar espacio en una activación para las variables locales, espacio temporal y un apuntador de retorno.
 Sin embargo se requerirá de información adicional:
o Debe mantenerse un apuntador a la activación presente. Este apuntador, debe conservarse en una ubicación
fija, por lo general un registro y se conoce como apuntador ambiente o ep (enviroment pointer).
o Debe conservase, además, un apuntador al registró de activación del bloque desde el que entró dicha
activación. Este apuntador, se conoce como enlace de control o enlace dinámico.
 Los campos en cada uno de los registros de activación deben contener:

enlace de
control
dirección
de retorno
parámetros
pasados
variables
locales
temporales

 Un ejemplo simple de lo anterior, sería:

void p( void ) registro de


{ activación de main ep
... enlace de
registro de activación de q
} control
enlace de
void q( void ) control
{ memoria
p(); libre
}

main()
{
q(); Dirección de crecimiento de la pila
...
}

 Las variables locales se ubican en el registro de activación presente, al cual ep apunta.


 Ya que las variables locales son asignadas como lo exigen las declaraciones del bloque y estas declaraciones son
estáticas, cada vez que entra el bloque se asignan las mismas clases de variables en el mismo orden. Por tanto, cada
variable puede ser ubicada en la misma posición en el registro de activación con relación al principio de mismo.
 Esta posición se conoce como desplazamiento de la variable local. Puede encontrarse cada variable local utilizando su
desplazamiento fijo a partir de la ubicación marcada por ep.
 En el siguiente ejemplo, cualquier registro de activación de p tendrá este formato:

8 Longinos Recuero Bustos (http://longinox.blogspot.com)


int x;

void p( int y )
{
int i = x;
char c;
...
}

void q( int a )
{
int x;
...
p( 1 );
}

main()
{
q( 2 );
return 0;
}

 En el caso de las referencias no locales, por ejemplo la referencia x a en p. Si el lenguaje no permite anidar
procedimientos (C, Fortran), todas las referencias no locales por fuera de un procedimiento son realmente globales y se
asignan estáticamente.
 Sin embargo, si el lenguaje permite anidar procedimientos (Pascal, Ada, Modula 2), las referencias no locales ahora
pueden ser variables locales en el alcance de procedimiento que lo rodea:

procedure q is
x: integer;

procedure p( y: integer ) is
i: integer := x;
begin
...
end p;

procedure r is
x: float;
begin
p( 1 );
...
end r;

begin
r;
end q;

 En el ejemplo anterior, para encontrar la referencia no local a la x de q desde el interior de p, se podría seguir el enlace
de control hasta el registro de activación de r, pero con ello encontraríamos la x local de r, logrando un alcance dinámico
en vez de un alcance léxico.
 Para lograr el alcance léxico, un procedimiento es que p mantenga un enlace a su ambiente léxico o de definición.
 A este enlace se le conoce como enlace de acceso o enlace estático. Ahora cada registro de activación necesita de un
nuevo campo, e campo del enlace de acceso.

9 Longinos Recuero Bustos (http://longinox.blogspot.com)


 Cuando los bloques están anidados profundamente, para encontrar una referencia no local, pudiera ser necesario seguir
varios enlaces de acceso en vez de uno solo.
 Este procedimiento se llama encadenamiento de accesos y la cantidad de enlaces de accesos que deberían seguirse
corresponde a la diferencia en niveles de anidamiento, o profundidad de anidamiento, entre el ambiente de acceso y el
ambiente definidor de la variable que se está accesando.
 Con este orden en el ambiente, la cerradura de un procedimiento (el código dl procedimiento, junto con un mecanismo para
resolver referencias no locales) se hace significativamente más complejo, ya que cada vez que un procedimiento es
llamado, debe incluirse como parte del registro de activación el ambiente definidor de dicho proceso.
 Entonces un leguaje como Ada o Pascal deben representarse no sólo por apuntador al código para el procedimiento, sino
también por una cerradura que consiste en un par de apuntadores:
o El apuntador del código o de la instrucción (ip).
o El apuntador de enlace de acceso o de ambiente de su ambiente definidor (ep).
 Esta clausura la escribiremos como <ep, ip>.

9.4.3 Procedimientos calculados dinámicamente y ambientes totalmente dinámicos


 Un ambiente basado en pilas tiene sus limitaciones.
 Por ejemplo, cualquier procedimiento que pueda devolver un apuntador a un objeto local, dará como resultado una
referencia pendiente al salir del procedimiento ya que el registro de activación del procedimiento habrá sido desasignado
de la pila.
 Ocurre una situación más severa si el diseñador del lenguaje desea extender la expresividad y la flexibilidad del lenguaje
al permitir que los procedimientos puedan ser creados dinámicamente.
 En un lenguaje de este tipo, los procedimientos se convierten en lo que se conoce como valores de primera clase.
 No obstante, se podría desear la capacidad de hacer cosas parecidas a las comentadas en los puntos anteriores y para
ello el tipo de ambiente debe ser totalmente dinámico, porque elimina los registros de activación sólo cuando ya no es
posible tener acceso a ellos desde el interior del programa en ejecución.
 Un ambiente de este tipo debe llevar a cabo alguna clase de recuperación automática de almacenamiento no accesible.
 Dos métodos estándar para ello son los conteos de referencia y la recolección de basura.
 Esta situación también quiere decir que la estructura de las activaciones se ramifica en vez de parecerse a una pila.
 Este es el modelo que ejecutan Scheme y otros lenguajes funcionales.

10 Longinos Recuero Bustos (http://longinox.blogspot.com)

También podría gustarte