SlideShare a Scribd company logo
1 of 174
Download to read offline
Capítulo 3. Nombres, Alcances y
Enlaces
Raúl José Palma Mendoza
Capítulo 3. Nombres, Alcances y
Enlaces
 Los lenguajes de programación de alto nivel
toman su nombre del relativo alto nivel o grado
de abstracción de las funciones que
proporcionan en comparación con las de los
lenguajes ensambladores.
 En este caso “abstracción”, desde un punto de
vista práctico, se refiere al grado de separación
que las funcionalidades del lenguaje tienen con
respecto a cualquier arquitectura particular de
hardware.
Capítulo 3. Nombres, Alcances y
Enlaces
 El desarrollo inicial de lenguajes como Fortran,
Algol, y Lisp fue conducido por un par de
objetivos complementarios:
 Independencia de la máquina.
 Facilidad de programación.
 Al abstraer el lenguaje del hardware, los
diseñadores no sólo hicieron posible escribir
programas que corriesen bien en una variedad
de máquinas, sino que también los hicieron
más fáciles de entender para los humanos.
Capítulo 3. Nombres, Alcances y
Enlaces
 Un “nombre” es una cadena mnemónica de
caracteres usado para representar algo más.
 Los nombres en la mayoría de los lenguajes
son tokens alfanuméricos, aunque otros
símbolos como: '+' o ':=' también pueden ser
nombres.
 Los nombres nos permiten referirnos a
variables, constantes, operaciones, tipos, etc.
de forma más fácil en vez de usar conceptos
de bajo nivel como las direcciones de memoria.
Capítulo 3. Nombres, Alcances y
Enlaces
 Los nombres también son esenciales en el
contexto del segundo significado práctico de la
palabra “abstracción”, vista como el proceso en
el cual el programador asigna un nombre a un
fragmento de código potencialmente complejo
que puede ser pensado en términos de su
propósito o función más que en términos de los
pasos para lograr dicho propósito.
3.1 Tiempo de Enlace
 Un enlace es una asociación entre dos
elementos: como el nombre y a lo que el
nombre se refiere.
 El tiempo de enlace es el momento en el cual
se crea un enlace, o de forma más general, el
tiempo en el cual se toma una decisión de
implementación acerca de algún asunto del
lenguaje (podemos verlo como el enlace entre
una pregunta y su respuesta).
3.1 Tiempo de Enlace
 Existen diferentes momentos en los que se
pueden dar enlaces:
 Tiempo de diseño del lenguaje: En la mayoría
de los lenguajes las construcciones para el
control de flujo, el conjunto fundamental de
tipos, los constructores disponible para crear
tipos más complejos, etc. Son elegidos en el
momento en que el lenguaje es diseñado.
3.1 Tiempo de Enlace
 Tiempo de implementación del lenguaje: La
mayoría de los manuales de lenguajes dejan
una variedad de asuntos a discreción del
implementador del lenguaje. Por ejemplo: la
precisión de tipos fundamentales, el
acoplamiento de la E/S con la noción de
ficheros del sistema operativo, la organización
y tamaños máximos de la pila y el heap, el
manejo de excepciones en tiempo de ejecución
como un desborde aritmético.
3.1 Tiempo de Enlace
 Tiempo de escritura del programa: Los
programadores por supuesto escogen
algoritmos, estructuras de datos y nombres,
entre otros elementos.
 Tiempo de compilación: Los compiladores
hacen el mapeo entre las contrucciones de alto
nivel y el código de máquina incluyendo la
disposición en memoria de los datos definidos
estáticamente.
3.1 Tiempo de Enlace
 Tiempo de enlace: (Aunque se usa el mismo nombre
este es distinto al tiempo de enlace del que trata esta
sección). Dado que la mayoría de los compiladores
soportan compilación separada y dependiente de la
disponibilidad de una librería estándar de subrutinas,
un programa no está completo hasta que varios
Módulos son unidos por un enlazador.
 El enlazador define la disposición de los módulos con
respecto a otros y resuelve la referencias entre ellos.
Cuando un nombre en un módulo hace referencia a
algo en otro, el enlace entre ambos se hace hasta el
tiempo de enlace (valga la redundancia).
3.1 Tiempo de Enlace
 Tiempo de carga: Se refiere al momento en el cual el
sistema operativo carga un programa en memoria de
forma que pueda ejecutarse. En los sistemas
opertivos más antiguos la elección de direcciones de
máquina para los objetos del programa no se
finalizaba hasta el tiempo de carga. En la actualidad,
la mayoría de los sistemas operativos distinguen entre
direcciones virtuales y direcciones físicas. Las
direcciones virtuales se selecciones en el tiempo de
enlace, las físicas pueden cambiar en tiempo de
ejecución. El hardware de traducción de memoria del
procesador hace la traducción entre direcciones
virtuales y físicas en tiempo de ejecución.
3.1 Tiempo de Enlace
 Tiempo de ejecución: es un término amplio que
abarca todo el tiempo que dura la ejecución del
programa. El enlace de los valores de las
variables usualmente ocurre en este momento,
así como otras decisiones que dependen del
lenguaje. El tiempo de ejecución incluye el
tiempo de arranque del programa, el tiempo de
entrada a un módulo, el tiempo de elaboración
(momento una declaración se ve por primera
vez), el tiempo de llamada a una subrutina, el
tiempo de entrada a un bloque y el tiempo de
ejecución de los enunciados.
3.1 Tiempo de Enlace
 Los tiempos de enlace son elementos de muy
importancia en el diseño e implementación de
los lenguajes de programación.
 En general, los tiempos de enlace tempranos
están relacionados con una mayor eficiencia y
los tardíos con mayor flexibilidad.
 Se usan los términos estático y dinámico para
referirse a los enlaces antes del tiempo de
ejecución y a los que se hacen en tiempo de
ejecución respectivamente.
3.1 Tiempo de Enlace
 Los lenguajes compilados tienden a hacer
enlaces tempranos y los interpretados tienden
a hacer enlaces tardíos. Por ejemplo un
compilador analiza la sintaxis y semántica de
las declaraciones de variables globales una
sola vez, luego decide la disposición de estas
variables en memoria y genera código eficiente
para acceder a las mismas desde cualquier
parte del programa. Por el contrario un
intérprete puro debe analizar estas
declaraciones cada vez que inicie la ejecución.
3.2 Tiempo de Vida y Manejo del
Almacenamiento
 Es importante distinguir entre los nombres y los
objetos a los que se refieren, e identificar los
siguientes eventos clave:
 Creación de un objeto.
 Creación de un enlace.
 Referencias a variables, subrutinas, tipos,
etc.
 Activación o desactivación de los enlaces.
 Destrucción de los enlaces.
 Destrucción de los objetos.
3.2 Tiempo de Vida y Manejo del
Almacenamiento
 Se conoce como tiempo de vida del enlace al
periodo de tiempo entre la creación y
destrucción de un enlace entre un nombre y un
objeto.
 Similarmente el tiempo de vida de un objeto es
el periodo transcurrido entre su creación y
destrucción.
 Este tiempos de vida no necesitan coincidir. En
particular un objeto puede retener su valor y
potencial de ser accedido, aún cuando no
exista un nombre para hacerlo.
3.2 Tiempo de Vida y Manejo del
Almacenamiento
 Ejemplo: Cuando se pasa por referencia una variable
a una subrutina, el enlace que se crea entre el nombre
del parámetro y la variable tiene un tiempo de vida
más corto que el de la variable en sí. También es
posible, y generalmente es signo de problema, que un
enlace de nombre a objeto tenga un tiempo de vida
más largo que el del objeto en sí mismo. Este podría
ocurrir por ejempo si un objeto creado en C++ con el
operador new es pasado por referencia y luego sea
desasignado (con la palabra reservada delete) antes
de que se retorne de la subrutina.
Un enlace a un objeto que ya “no vive” se conoce
como una referencia colgante.
3.2 Tiempo de Vida y Manejo del
Almacenamiento
 El tiempo de vida de los objetos corresponde a tres
mecanismos de manejo del almacenamiento:
 Almacenamiento estático, en éste los objetos
tienen una dirección absoluta que se mantiene
durante toda la ejecución del programa.
 Almacenamiento basado en pila, en éste los
objetos se colocan siguiendo el orden last-in first-
out usualmente en conjunción con las llamadas a
subrutinas y retornos de las mismas.
 Almacenamiento basado en el heap, aquí los
objetos se asignan y desasignan en tiempo
arbitrarios, se requiere un algoritmo más general y
costoso.
3.2.1 Almacenamiento Estático
 Las variables globales son el ejemplo más común de
objetos estáticos, pero no el único. Las instrucciones
que conforman el programa, también puede ser
consideradas objetos estáticos. Además existen
variables locales estáticas, que retienen sus valores
entre una invocación y otra. Las constantes literales
numéricas y cadenas también son asignadas
estáticamente, para enunciados como A = B/14.7 o
printf(”hola mundo!n”). (Las constantes que no
ocupan mucho espacio se almacenan comúnmente
dentro de la instrucción de la que forman parte, las
más grandes se les asigna una ubicación aparte)
3.2.1 Almacenamiento Estático
 Finalmente, la mayoría de los compiladores producen
una variedad de tablas que son usadas para rutinas
en tiempo de ejecución que soportan la depuración,
chequeo dinámico de tipos, recolección de basura,
manejo de excepciones y otros propósitos, todas las
anteriores también asignadas de forma estática.
 Los objetos asignados de forma estática cuyo valor no
debería de cambiar durante la ejecución de un
programa, usualmente son asignados a área de
memoria protegidas y de sólo lectura, de forma que
cualqueir intento inadvertido de modificarlos generará
una interrupción en el procesador, permitiendo al
sistema operativo anunciar el error.
3.2.1 Almacenamiento Estático
 Ejemplo: Las variables locales generalmente son
creadas cuando su subrutina es invocada y destruídas
cuando ésta retorna, pero no siempre es el caso, por
ejemplo en las versiones de Fortran que originalmente
no soportaban la recursión (se agregó hasta Fortran
90) nunca podria existir más de una invocación de
subrutina activa en un mismo instante de tiempo y por
esta razón un compilador podría escojer usar
almancenamiento estático para las variables locales
de modo que no tuviesen que ser creadas y
destruídas constantemente (simplemente
inicializadas)
3.2.1 Almacenamiento Estático
 En muchos lenguajes se requiere que las
constantes nombradas (no constantes literales)
tengan un valor que pueda ser determinado en
tiempo de compilación. A este tipo de
constantes junto con las constantes literales se
les llama constantes manifiestas o constantes
de tiempo de compilación.
 Las constantes manifiestas o evidentes pueden
ser siempre asignadas estáticamente, aún
cuando sean locales a un subrutina recursiva.
3.2.1 Almacenamiento Estático
 En otros lenguajes, las constantes son simples
variables que no pueden ser cambiadas
después del tiempo de elaboración. Sus
valores aunque no cambien pueden depender
de otros valores que no se conocen hasta el
tiempo de ejecución. Estas constantes de
tiempo de elaboración cuando son locales a
una subrutina recursiva no pueden ser
estáticas, deben almacenarse en la pila. C#,
por ejemplo provee ambas opciones con las
palabras reservadas const y readonly.
3.2.1 Almacenamiento Estático
 Además de las variables locales y las constantes de tiempo de
elaboración, el compilador típicamente almacena otra
información asociada a la subrutina incluyendo:
 Argumentos y valores de retorno. Los compiladores
modernos tratan de mantienerlos en registros del
procesador, pero en ocasiones se necesita memoria.
 Temporales. Son valores intermedios producidos por
cálculos complejos. Un buen compilador tratará de
mantenerlos en registros.
 Información de contabilidad. Podría incluir la dirección de
retorno, una referencia al marco de pila de la subrutina que
invocó a la actual (enlace dinámico), registros adicionales
en memoria, información de depuración, y otros valores.
3.2.2 Almacenamiento Basado en Pila
 Si un lenguaje permite la recursión, el almacenamiento
estático de los objetos locales no es una opción, pues
puede haber varias instancias de la misma variable en un
instante de tiempo.
El anidamiento natural de las llamadas de una subrutina
hace que sea fácil almacenar el espacio para variables
locales en una pila. A continuación mostramos una imagen
de un píla típica simplificada, cada instancia de una
subrutina en tiempo de ejecución tiene su propio marco
(también llamado registro de activación), que contiene
argumentos, valores de retorno, variables locales,
temporales e información de contabilidad entre otros.
3.2.2 Almacenamiento Basado en Pila
 Los argumentos que se pasarán a subrutinas
subsecuentes se dejan en el tope del marco, donde la
subrutina invocada puede hallarlos fácilmente. La
organización del resto de la información depende de
la implementación.
 En la figura siguiente observamos que en cualquier
momento el registro sp (stack pointer o puntero de
pila) apunta a la primera ubicación libre de la pila (o a
la última usada en otras máquinas) y el registros fp
(frame pointer o puntero de marco) apunta a una
ubicación conocida dentro del marco de la actual
subrutina.
3.2.2 Almacenamiento Basado en Pila
3.2.2 Almacenamiento Basado en Pila
 El mantenimiento de la pila es responsabilidad
de la “secuencia de llamada” que es el código
ejecutado por la subrutina que llama justo
antes de hacer la llamada y después de hacer
la llamada y también es responsabilidad del
prólogo (código ejecutado por la subrutina
llamada al inicio de su ejecución) y del epílogo
(código ejecutado por la subrutina llamada al
final). En ocasiones el término “secuencia de
llamada” es usado para referirse a las
operaciones combinadas de la subrutina que
llama, el prólogo y el epílogo.
3.2.2 Almacenamiento Basado en Pila
 Ejercicio:
¿Por qué en algunas implementaciones de
Fortran a pesar de que no se usa recursión, los
implementadores prefirieron usar
almacenamiento basado en pila?
3.2.3 Almacenamiento Basado en el
Heap
 El heap (o montículo) es una región de
almacenamiento en la que se pueden asignar y
desasignar subbloques en momentos arbitrarios. El
heap es requerido para asignar piezas de datos
dinámicas, como estructuras de datos enlazadas, y
objetos como algunas cadenas, listas, conjuntos cuyo
tamaño puede cambiar como resultado de una
operación de asignación o actualización.
 La asignación de objetos en el heap es realizada
cuando ocurren operaciones como: la instanciación de
un objeto, el agregar un elemento al fin de una lista,
asignar un valor muy grande a una cadena que antes
era corta, etc.
3.2.3 Almacenamiento Basado en el
Heap
 Hay varias estrategias posibles para manejar el
espacio en el heap, acá revisaremos las más
importantes. Las principales preocupaciones su
velocidad y espacio y como es usual hay
compensaciones entre ellas. En cuanto al tema
del espacio este se puede sudividir en:
 Fragmentación interna y
 Fragmentación externa.
3.2.3 Almacenamiento Basado en el
Heap
 La fragmentación interna ocurre cuando el
algoritmo de manejo del almacenamiento
asigna un bloque que es más grande de lo
requerido para guardar un objeto dado, el
espacio extra queda sin uso.
 La fragmentación externa ocurre cuando los
bloques asignados están dispersos en el heap
de forma que el espacio que queda sin usar
está compuesto por múltiples bloques: podría
haber mucho espacio libre, pero ningún bloque
podría ser suficientemente grande para cumplir
con una petición futura.
3.2.3 Almacenamiento Basado en el
Heap
3.2.3 Almacenamiento Basado en el
Heap
 Muchos algoritmos de manejo de almacenamiento
mantienen una única lista enlazada de bloques del
heap que no están en uso. Al inicio contiene un único
bloque que abarca todo el heap. En cada solicitud de
almacenamiento el algoritmo busca en la lista un
bloque de tamaño apropiado.
 Con un algoritmo de primer ajuste se selecciona el
primer bloque de la lista que tenga el tamaño
suficiente para satisfacer la petición.
 Con un algoritmo de mejor ajuste se busca en la
lista entera el bloque más pequeño con tamaño
suficiente para satisfacer la petición.
3.2.3 Almacenamiento Basado en el
Heap
 En cualquier caso, si el bloque seleccionado es
significativamente más grande que lo
solicitado, se divide en dos y se retorna la
porción vacía a la lista como un bloque más
pequeño. Si la porción que queda libre es más
pequeña que algún umbral, se podría dejar
como fragmentación interna. Cuando un bloque
es liberado se retorna a la lista, se revisa si uno
o los dos bloques físicamente adyacentes
están libres, si este es el caso, se combinan en
uno sólo.
3.2.3 Almacenamiento Basado en el
Heap
 Esperaríamos que un algoritmo de mejor ajuste
haga un mejor trabajo a la hora de reservar
bloques grandes para peticiones grandes. Al
mismo tiempo, ésto algoritmo tiene un costo de
asignación mayor que un algoritmo de primer
ajuste, porque tiene que buscar en toda la lista
y tiende a generar un mayor número de
bloques pequeños sin usar. Dependiendo de la
distribución de tamaños de las solicitudes
cualquiera de los dos algoritmos puede generar
mayor fragmentación externa.
3.2.3 Almacenamiento Basado en el
Heap
 En cualquier algoritmo de mejor ajuste que
mantenga una única lista de bloques libres el
costo de asignación es lineal al número de
bloques en la lista. Para reducir este costo a
uno constante, algunos algoritmos mantienen
varias listas para bloques de diferentes
tamaños. Cada solicitud es redondeada al
siguiente tamaño estándar y asignada de un
bloque de la lista apropiada.
 En efecto, el heap queda dividido en grupos de
tamaño estándar. Esta división puede ser
estática o dinámica.
3.2.3 Almacenamiento Basado en el
Heap
 Dos mecanismos comunes para hacer ajuste
dinámico de las listas se conocen como el
sistema de colegas (buddy system) y el heap
de Fibonacci. En el sistema de colegas, los
tamaños estándar de bloque son potencias de
dos. Si se ocupa un bloque de tamaño 2k
pero
no hay ninguno disponible un bloque de
tamaño 2k+1
se divide en dos, una de las
mitades es usada para satisfacer la petición y
la otra se coloca en la lista de los bloques de
tamaño 2k
. Cuando el bloque es liberado se
vuelve a unir con su colega si éste está libre.
3.2.3 Almacenamiento Basado en el
Heap
 Los heaps de Fibonacci son similares pero
usan números de Fibonacci como tamaños
estándar en vez de potencias de dos. El
algoritmo es un poco más complejo pero lleva a
menos fragmentación interna porque los
número de Fibonacci crecen más lento que las
potencias de 2.
3.2.3 Almacenamiento Basado en el
Heap
 El problema con la fragmentación externa es la
que la habilidad del heap para satisfacer las
peticiones se puede ir degradando en el
tiempo.
 El uso de múltiples listas puede ayudar, pero
no eliminan el problema. Siempre será posible
generar una secuencia de peticiones que no
puede ser satisfecha aún cuando el espacio
total requerido sea menor que el tamaño del
heap.
3.2.3 Almacenamiento Basado en el
Heap
 Si la memoria es particionada de forma estática
lo único que se necesita es exceder el número
máximo de solicitudes de un tamaño
específico.
 Si la memoria se reajusta de forma dinámica se
puede hacer al heap un gran cantidad de
solicitudes de bloques pequeños y luego
desasignar algunos tomando en cuenta su
ubicación física dejano un patrón alternante de
pequeños bloques asignados.
3.2.3 Almacenamiento Basado en el
Heap
 Para eliminar la fragmentación externa
debemos de estar preparados para compactar
el heap y mover bloques que ya están
asignados. Esta tarea es complicada por la
necesidad de encontrar y actualizar todas las
referencias a un bloque que se desea mover.
3.2.4 Recolección de Basura
 Así como la asignación de objetos en el heap ocurre
por algún evento explícito, la desasignación también
puede ser explícita en algunos lenguajes (ej.: C, C++,
y Pascal). Pero muchos lenguajes hacen la
desasignación de forma implícita cuando ya no es
posible acceder a ellos desde ninguna variable del
programa. La librería de tiempo de ejecución para
estos lenguajes debe entonces proveer un mecanismo
de recolección de basura para identificar y reclamar
éstos objetos inalcanzables. La mayoría de los
lenguajes funcionales y de scripting requieren un
recolector de basura así como muchos lenguajes
imperativos modernos como Modula-3, Java, y C#.
3.2.4 Recolección de Basura
 Los argumentos tradicionales en favor de la
desasignación explícita son la simplicidad de la
implementación y la velocidad de ejecución. Las
implementaciones más sencillas de recolección de
basura agregan un complejidad significante a un
lenguaje con un rico sistema de tipos e incluso los
recolectores de basura más sofisticados pueden
consumir un tiempo no trivial en ciertos programas. Si
el programador puede indentificar correctamente el fin
del tiempo de vida de un objeto si llevar mucha
contabilidad en tiempo de ejecución, el resultado
tendería ser una ejecución más rápida.
3.2.4 Recolección de Basura
 A través del tiempo los diseñadores y los
implementadores de lenguajes han ido considerando
más la recolección de basura como una característica
esencial en un lenguaje de programación.
 Los algoritmos de recolección de basura han
mejorado reduciendo su carga y además como las
implementaciones en general se han vuelto más
complejas, se ha reducido la complejidad relativa de la
recolección automática.
 Las aplicaciones más innovadores se han vuelto más
grandes y complejas, haciendo que los beneficios de
la recolección de basura sean muy convincentes.
3.3 Reglas de Alcance
 El alcance de un enlace es la región textual de
un programa donde éste está activo.
 En la mayoría de los lenguajes modernos este
alcance es determinado estáticamente, en
tiempo de compilación. En C, por ejemplo, se
introduce un nuevo alcance en la entrada a una
subrutina, se crean enlaces para objetos
locales y se desactivan enlaces para objetos
globales que son ocultados por objetos locales
con el mismo nombre. Al salir se reactivan los
enlaces de los objetos globales ocultos.
3.3 Reglas de Alcance
 Estan manipulaciones de los enlaces a primera vista
parecen ser operaciones en tiempo de ejecución, pero
no requiere ningún código especial, las porciones de
un programa en las que un enlace está activo se
determinan completamente en tiempo de compilación.
 Debido a que podemos observar un programa en C y
saber qué nombres se refieren a qué objetos en
cualquier punto del programa basándos únicamente
en reglas textuales es que podemos decir que C tiene
alcance estático o alcance léxico o lexicográfico.
3.3 Reglas de Alcance
 Lenguajes como APL, Snobol y los primeros dialectos
de Lisp tienen alcance dinámico, sus enlaces
dependen del flujo de instrucciones en tiempo de
ejecución.
 Además de hablar del “alcance de un enlace” también
se usa el término alcance por sí mismo sin un enlace
específico en mente. Informalmente, un alcance es
una región de un programa de tamaño máximo en que
los enlaces no cambian (o al menos ninguno es
destruído). Típicamente un alcance el es cuerpo de un
módulo, una clase, una subrutina o un enunciado de
control de flujo estructurado a veces llamado bloque.
3.3 Reglas de Alcance
 Algol 68 y Ada usando el término elaboración
para referirse al proceso en el cual las
declaraciones se activan cuando el control
entra a un alcance, la elaboración implica la
creación de enlaces, la asignación de espacio
en la pila para objetos locales y posiblemente
la asignación de valores iniciales.
3.3 Reglas de Alcance
 Llamamos ambiente de referencia al conjunto
de enlaces activos en un punto de la ejecución
del programa. Este conjunto es principalmente
determinado por reglas de alcance dinámicas o
estáticas.
 Un ambiente de referencia generalmente
corresponde a una secuencia de alcances que
pueden ser examinados (según un orden) para
encontrar el enlace actual para un nombre
dado.
3.3.1 Alcance Estático
 En un lenguaje con alcance estático (léxico),
típicamente el enlace actual para un nombre
dado es encontrado en la declaración
correspondiente cuyo bloque rodea de forma
más cercana un punto dado en el programa.
Aunque como veremos hay varias variantes de
esta regla básica.
3.3.1 Alcance Estático
 La regla más básica de alcance estático es
probablemente la de las primeras versiones de
Basic, en donde sólo existía un único alcance,
y éste era global. De hecho, son había unos
cientos de nombres posibles, cado uno
consistía en una letra seguida opcionalmente
por un dígito. No había declaraciones
explícitas, las variables eran declaradas de
forma implícita en virtud de su uso.
3.3.1 Alcance Estático
 Las reglas de alcance son un poco más
complejas en Fortran (pre-Fortran 90). Fortran
distingue entre variables locales y globales. El
alcance de una variable local está limitado a la
subrutina en la que aparece, y no es visible en
ningún lugar más. Las declaraciones de
variables son opcionales. Si una variable no
está declarada se asume que es local a la
subrutina actual y de tipo entero si su nombre
inicia con las letras entre la I y la N inclusive o
real de otra forma.
3.3.2 Subrutinas Anidadas
 La habilidad para anidar las subrutinas dentro
de otras, introducida en Algol 60, es una
característica de varios lenguajes modernos,
como ser Pascal, Ada, ML, Python, Scheme,
Common Lisp y Fortran 90 (con una extensión
limitada). Otros lenguajes incluyendo C y sus
descendientes permiten que se aniden clases y
otros alcances. Generalmente las constantes,
tipos, variables o subrutinas declaradas dentro
de un bloque no son visibles fuera de ese
bloque en lenguajes tipo Algol.
3.3.2 Subrutinas Anidadas
 Formalmente, el anidamiento estilo Algol da pie
a la regla de alcance anidado más cercano
para enlace de nombres a objetos que implica
que un nombre que es introducido en una
declaración es conocido en el alcance en el
cual es declarado y en cada alcance anidado
de éste a menos que sea oculto por otra
declaración del mismo nombre en uno o más
alcances anidados.
3.3.2 Subrutinas Anidadas
 Para encontrar el objeto que corresponde a un
nombre dado, se busca un declaración con ese
nombre en el alcance actual más interno. Si
esta existe, esta define el enlace activo para el
nombre. Si no existe, se una busca una
declaracion en el alcance que rodea
inmediatamente al actual, y sino se encuentra
acá se continua en sucesivos alcances
circudantes hasta que se llegue al nivel más
exterior, donde se declaran los objetos
globales. Si no se encuentra en este nivel, se
anuncia un error.
3.3.2 Subrutinas Anidadas
 Muchos lenguajes proveen una colección de objetos
predefinidos como rutinas de E/S, funciones
matemáticas y tipos como enteros y caracteres. Es
comun considerar que éstos están declarados en un
alcance extra, invisible y más externo que rodea al
alcance donde se declaran los objetos globales. Por
tanto la búsqueda de enlaces en el párrafo anterior
terminaría en este alcance extra más externo. Esta
convención permite al programador definir un objeto
global cuyo nombre sea el mismo de algún objeto
predefinido, ocultándolo y haciéndolo inusable.
3.3.2 Subrutinas Anidadas
 De un enlace de nombre a objeto que ha sido
oculto por una declaración anidada se dice que
tiene un agujero en su alcance.
 En la mayoría de los lenguajes el objeto cuyo
nombre ha sido oculto es innaccesible en el
alcance interno (a menos que tenga más de un
nombre). Algunos lenguajes permiten al
programador acceder al significado externo de
un nombre al aplicar un cualificador o un
operador de resolución de alcance.
3.3.2 Subrutinas Anidadas
 En Ada por ejemplo, un nombre puede ir
predecido por el nombre del alcance en el cual
fue declarado, usando una sintaxis similar a la
del acceso a los campos de un registro. Por ej.:
mi_proc.X, se refiere a la declaración de X en
la subrutina mi_proc, aún cuando haya otra
declaración de X más interna.
3.3.2 Subrutinas Anidadas
 El compilador organizará al registro puntero de
marco de forma que siempre apunte al marco
de la subrutina que se esté ejecutando. Usando
este registro como base para el
direccionamiento (registro más un offset), el
código meta puede acceder a objetos dentro
del actual marco.
 Pero ¿qué se hace para acceder a objetos en
subrutinas que rodean lexicamente a la actual?
3.3.2 Subrutinas Anidadas
 Para acceder a estos objetos necesitamos una forma
de encontrar los marcos que correspondan a estos
alcances en tiempo de ejecución.
 Como un subrutina anidada puede llamar a otra que
esté en un alcance externo, el orden de los marcos de
la pila en tiempo de ejecución no corresponde
necesariamente al orden del anidamiento léxico. Sin
embargo podemos estar seguros que el marco para el
alcance circundante está en la pila pues la actual
subrutina no podría haber sido llamada a menos que
fuese visible y sólo puede ser visible si el alcance
circundante está activo.
3.3.2 Subrutinas Anidadas
 Entonces, la foma más fácil de encontrar los marcos
de los alcances circundantes es manteniendo un
enlace estático en cada marco que apunte al marco
“padre”, el marco de la invocación más reciente de la
subrutina circundante. Si una subrutina es declarada
en el alcance más externo entonces su marco tendrá
un enlace estático nulo. Si una subrutina está anidada
en k niveles, entonces el enlace estático de su marco,
y los de su padre y abuelo y todos los antecesores
formarán una cadena estática de longitud k en tiempo
de ejecución. Para encontrar una variable o parámetro
declarado j alcances hacia afuera, el código meta en
tiempo de ejecución puede desreferenciar la cadena
estática j veces y luego agregar el offset apropiado.
3.3.2 Subrutinas Anidadas
3.3.3 Orden de las Declaraciones
 En nuestra discusión hasta este punto hemos pasado
por lato un asunto importante: suponga que un objeto
x está declarado en algún lugar dentro de un bloque
B. La pregunta que surge es la siguiente ¿el alcance
de x incluye la porción de B antes de la declaración y
por tanto x puede realmente ser usada en esta
porción de código?
 Varios de los primeros lenguajes de alto nivel
incluyendo Algol 60 y Lisp requerían que todas las
declaraciones apareciesen al inicio del alcance. Al
inicio podríamos pensar que esta regla evitaría la
pregunta que nos planteamos anteriormente, pero no
porque las declaraciones se pueden referir unas a
otras.
3.3.3 Orden de las Declaraciones
 Ejemplo: En un intento aparente de simplificar
la implementación del compilador, Pascal
estableció que nombres deben ser declarados
antes de ser usados (con mecanismos
especiales para acomodar tipos recursivos y
subrutinas). Al mismo tiempo retuvo la noción
de que el alcance de una declaración es el
bloque circundante entero. Estas dos reglas
pueden interactuar de formas sorprendentes
como lo muestra el código a continuación.
3.3.3 Orden de las Declaraciones
const N = 10;
...
procedure foo;
const
M = N; (* static semantic error! *)
...
N = 20; (* local const declaration; hides the outer N *)
En este caso, Pascal dice que la segunda declaración
de N cubre todo el procedimiento foo, así que el
analizador semántico indica en la linea “M = N;” que N
está siendo usado antes de ser declarado, cuando
probablemente el programador se intentaba referir a la
primera N.
3.3.3 Orden de las Declaraciones
 Ejemplo: Algunos compiladores de Algol 60
procesaban las declaraciones de un alcance en
el orden en que estaban escritas. Esta
estrategia tenía el desafortunado efecto de
prohibir de forma implícita los tipos y subrutinas
mutuamente recursivas, algo que iba en contra
de la intención de los diseñadores del lenguaje.
3.3.3 Orden de las Declaraciones
 Para determinar la validez de cualquier declaración
que aparente usar el nombre de un alcance
circundante un compilador de Pascal debe escanear
el resto de las declaraciones de un alcance para ver si
el nombre no ha sido oculto. Para evitar esta
complicación la mayoría de los sucesores de Pascal
(incluyendo algunos dialectos del mismo) especifican
que el alcance de un identificador no es el boque
entero en el cual está declarado (excluyendo sus
agujeros) sino la porción de ese bloque desde la
declaración hasta el fin del mismo (nuevamente
excluyendo los agujeros). Si el fragmento de código
anterior hubiese sido escrito en C, C++ o Java no se
hubiese reportado ningún error semántico.
3.3.3 Orden de las Declaraciones
 Ejemplo: C++ y Java permiten que se rompa de
la regla de “definir antes de usar” en muchos
casos. En ambos lenguajes los miembros de
una clase (incluyendo aquellos que no son
definidos sino hasta más adelante en el texto
del programa) son visibles en todos los
métodos de la clase. En Java, las clases en sí
mismas pueden ser declaradas en cualquier
orden.
3.3.3 Orden de las Declaraciones
 Ejemplo: De forma interesante en C# aunque, al igual
que en Java, se requiere la declaración antes de usar
variables locales (pero no clases o miembros), se
toma la noción de Pascal del alcance en todo el
bloque, de forma que el siguiente código es inválido
en C#:
class A {
const int N = 10;
void foo() {
const int M = N; // Se usa N antes de su declaración
const int N = 20;
...
3.3.3 Orden de las Declaraciones
 Ejemplo: Quizá el enfoque más simple en lo
que respecta al orden de las declaraciones
desde un punto de vista conceptual, es el de
Modula-3, que dice que el alcance de una
declaración es el bloque entero donde aparece
(menos los agujeros) y que el orden de las
declaraciones no importa. La principal objeción
a este enfoque es que los programadores
podría encontrar contraintuitivo el uso de una
variable local antes de que sea declarada.
3.3.3 Orden de las Declaraciones
 Ejemplo: Python lleva la regla de alcance de “todo el
bloque” un paso más adelante, dispensando las
declaraciones de variables. En vez de éstas adopta la
convención inusual de que las variables locales de la
subrutina S son precisamente aquellas variables que
son modificadas por algún enunciado en el cuerpo
(estático) de S. Si S está anidada dentro de T, y el
nombre x aparece en el lado izquierdo de enunciados
de asignación tanto en S como en T, entonces hay
dos x distintas: una en S y otra en T. Las variables no
locales son de sólo lectura a menos que se les
importe de forma explícita (usando el enunciado
global de Python).
3.3.3 Orden de las Declaraciones
 Ejemplo: Los tipos y subrutinas recursivas introducen
un problemas para los lenguajes que requieren
“declaración ante de uso”: ¿cómo pueden dos
declaraciones aparecer ambas antes que la otra? C y
C++ manejan el problema distinguiendo entre la
declaración de un objeto y su definición. Una
declaración introduce un nombre e indica el alcance,
pero puede omitir ciertos detalles de implementación.
Una definción describe el objeto con suficiente detalle
para el compilador como para que éste determine su
implementación. Si una declaración no está
suficientemente detallada para ser una definición,
entonces se necesita que aparezca una definición
detallada en algún lugar del alcance.
3.3.3 Orden de las Declaraciones
En C podemos escribir:
struct manager; /* declaration only */
struct employee {
struct manager *boss;
struct employee *next_employee;
...
};
struct manager { /* definition */
struct employee *first_employee;
...
};
3.3.3 Orden de las Declaraciones
void list_tail(follow_set fs); /* declaration only */
void list(follow_set fs)
{
switch (input_token) {
case id : match(id); list_tail(fs);
...
}
void list_tail(follow_set fs) /* definition */
{
switch (input_token) {
case comma : match(comma); list(fs);
...
}
3.3.3 Orden de las Declaraciones
 Ejemplo: En muchos lenguajes incluyendo Algol 60,
C89, y Ada, las variables locales deben ser
declaradas no sólo al inicio de inicio de una subrutina
sino en el tope de cualquier bloque. Otros lenguajes
incluyendo Algol 68, C99, y todos los descendientes
de C, son aún más flexibles, permitiendo
declaraciones donde sea que éstas aparezcan. En la
mayoría de los lenguajes una declaración anidada
oculta cualquier declaración externa con el mismo
nombre (Java y C# lanzan un error semántico estático
si la declaración externa es local a la subrutina
actual).
3.3.3 Orden de las Declaraciones
 Ejemplo: Las variables declaradas en bloques
anidados pueden ser muy útiles, como por ejemplo en
el siguiente código en C:
{
int temp = a;
a = b;
b = temp;
}
El mantener la declaración de temp adyacente al
código que la usa hace que el programa sea más fácil
de leer y elimina la posibilidad que este código
interfiera con otra variable llamada temp.
3.3.3 Orden de las Declaraciones
No se necesita trabajo en tiempo de ejecución para
asignar o desasignar variables declaradas en bloques
anidados, su espacio puede ser incluído en el espacio
total para variables locales asignadas en el prólogo de
la subrutina y desasignadas en el epílogo de la
misma.
3.3.4 Módulos
 Un gran reto en la construcción de cualquier
software grande es determinar cómo dividir el
esfuerzo entre varios programadores de forma
tal que el trabajo pueda realizarse en varios
elementos del programa de forma simultánea.
 Hacer esto es hacer un esfuerzo de
modularización que depende sobretodo en el
concepto de “ocultamiento de la información”,
que implica que objetos y algoritmos sean
invisibles dentro de lo posible a porciones del
sistema que no los necesitan.
3.3.4 Módulos
 Un código modularizado correctamente reduce
la “carga cognitiva” del programador al
minimizar la cantidad de información necesaria
para entender cualquier parte del sistema.
 Un programa bien diseñado trata de que las
interfaces entre sus módulos sean lo más
simples posible tratando de que los cambios en
el diseño queden ocultos en un sólo módulo,
este último punto es crucial para hacer
mantenimiento del programa.
3.3.4 Módulos
 Además de reducir la “carga cognitiva” el
ocultamiento de la información reduce el riesgo
de que halla conflictos de nombres, pues
reduce la cantidad de nombre visibles, y
también proteje la integridad de las
abstracciones de datos: cualquier intento de
acceder a un objeto fuera de la subrutina a la
que pertenecen generará un error. Finalmente
también ayuda a compartimentalizar los errores
en tiempo de ejecución: si una variable toma un
valor incorrecto sabemos que fue modificada
por el código dentro de su alcance.
3.3.4 Módulos
 Desafortunadamente el ocultamiento de la información
proporcionada por la subrutinas anidadas está
limitado a objetos con un tiempo de vida igual al de la
instancia de una subrutina. Una solución parcial a esto
son las variables estáticas (own en Algol 60, static en
C y Java).
 Podemos decir que las variables estáticas
proporcionan un forma de construir abstracciones con
una única subrutina, pero no más de una. Por ejemplo
se deseasemos construir una abstracción pila, nos
gustaría ocultar su estructura interna al resto del
programa pero que se pueda acceder a ella a través
de las subrutinas push (mete) y pop (saca).
3.3.4 Módulos
 Para poder construir abstracciones con varias
subrutinas en su interface, muchos lenguajes de
programación proporcionan un construcción módulo.
 Un módulo permite que una colección de objetos
(subrutinas, variables, tipos, etc.) sean encapsulados
de forma que:
 Los objetos del interior son visibles entre sí.
 Los objetos del interior no son visibles al exterior a
menos que sean exportados explícitamente.
 Los objetos del exterior no son visibles en el interior
a menos que sean importados explícitamente (se
cumple en varios lenguajes).
3.3.4 Módulos
 Note que las reglas planteadas afectan sólo la
visibilidad de los objetos y no su tiempo de vida.
 Los módulos aparecen en la mayoría de los lenguajes
modernos con diferentes nombres, por ejemplo en
Modula 1 al 3 se les llama módulos, en Ada, Java y
Perl se les llama paquetes, en C++, C# y PHP se
llaman espacios de nombre (namespace). En C se
pueden emular hasta cierto grado usando las
capacidades de compilación separada que presta el
lenguaje.
3.3.4 Módulos
 Ejemplo: A continuación vemos una abstracción
pila en Modula-2. Como los modulos no afectan
el tiempo de vida de los enlaces, éstos se
vuelven inactivos al salir del módulo pero no se
destruyen. En el ejemplo a continuación el
tiempo de vida de las variables “s” y “top”
hubiese sido el mismo sino estuviesen
declaradas dentro del módulo, claro ahora
dentro del mismo sólo son visibles desde el
código de las subrutinas: “pop” y “push”.
3.3.4 Módulos
CONST stack_size = ...
TYPE element = ...
...
MODULE stack;
IMPORT element, stack_size;
EXPORT push, pop;
TYPE
stack_index = [1..stack_size];
VAR
s : ARRAY stack_index OF element;
top : stack_index; (* first unused slot *)
3.3.4 Módulos
PROCEDURE error; ...
PROCEDURE push(elem : element);
BEGIN
IF top = stack_size THEN
error;
ELSE
s[top] := elem;
top := top + 1;
END;
END push;
3.3.4 Módulos
PROCEDURE pop() :
element;
BEGIN
IF top = 1 THEN
error;
ELSE
top := top - 1;
RETURN s[top];
END;
END pop;
(* Código de inicialización *)
BEGIN
top := 1;
END stack;
(* Uso de la pila *)
VAR x, y : element;
...
push(x);
...
y := pop;
3.3.4 Módulos
 La mayoría de los lenguajes basados en
módulos permiten al programador especificar
que ciertos nombres exportados sean usados
sólamente de formas restringidas, por ejemplo
las variables podrían ser exportadas como de
sólo lectura, los tipos podrían ser exportados
de forma opaca de modo que las variables de
ese tipo puedan ser declaradas, pasadas como
argumentos a las subrutinas del módulo y
posiblemente comparadas o asignadas entre
ellas mismas, pero no puedan ser manipuladas
de otra forma.
3.3.4 Módulos
 Podemos clasificar los módulos según su
apertura hacia los nombres en alcances
exteriores de la siguiente forma:
 Los módulos en los que los nombres deben
ser importados de forma explícita para poder
ser usados se conocen como alcances
cerrados, ejemplo: Modula (1, 2 y 3) y
Haskell.
 Por extensión los módulos que no requieren
la importación explícita se conocen como
alcances abiertos.
3.3.4 Módulos
 Una opción cada vez más común, que se da en
lenguajes como Ada, Java, C# y Phyton, es la de
los módulos selectivamente abiertos, en estos
módulos un nombre como “foo” exportado desde un
módulo A es automáticamente visible en un módulo
B como “A.foo”; además puede ser visible
simplemente como “foo” si B lo importa
explícitamente.
 La importaciones sirve para documentar el programa,
incrementan la modularidad al requerir que un módulo
especifique la forma en que depende del resto del
programa. Además reducen los conflictos de nombres
al no importar nada que no se ocupe.
3.3.4 Módulos
 A diferecia de los módulos, las subrutinas son usualmente
alcances abiertos en la mayoría de los lenguajes de la
familia de Algol, excepciones importantes a esta reglas
son Euclid en el cual tanto módulos como subrutinas son
cerrados y Turing, Modula-1 y Perl en los que las
subrutinas son opcionalmente cerradas (si se hacen
imporatciones explícitas ningún otro nombre no local será
visible) y Clu que prohibe el uso de variables no locales
completamente.
 Como en el caso de los módulos las listas de importación
en subrutinas sirven para documentar. De forma que la
mayoría de los diseñadores de lenguajes han decidido
que la documentación no vale la incoveniencia.
3.3.4 Módulos
 Módulos como manejadores
Los módulos facilitan la construcción de abstracciones
permitiendo que los datos se hagan privados a las
subrutinas que los usan. Pero cuando son usados como el
ejemplo de la pila que anteriormente vimos, cada módulo
define una única abstracción. Si quisiésemos tener varias
pilas, tendremos en general que convertir el módulo en un
manejador de instancias de tipo pila, y exportar dicho tipo
desde el módulo, como se muestra en la figura a
continuación. El hacer esto implica también que se creen
rutinas adicionales para crear/inicializar la pila y
posiblemente para destruir instancias de la misma y
requiere que cada subrutina tome un extra parámetro para
especificar la pila en cuestión.
3.3.4 Módulos
CONST stack_size = ...
TYPE element = ...
MODULE stack_manager;
IMPORT element, stack_size;
EXPORT stack, init_stack, push, pop;
TYPE
stack_index = [1..stack_size];
stack = RECORD
s : ARRAY stack_index OF element;
top : stack_index; (* first unused slot *)
END;
3.3.4 Módulos
PROCEDURE
init_stack(VAR stk : stack);
BEGIN
stk.top := 1;
END init_stack;
PROCEDURE push(VAR
stk : stack; elem : element);
BEGIN
IF stk.top = stack_size
THEN
error;
ELSE
stk.s[stk.top] := elem;
stk.top := stk.top + 1;
END;
END push;
3.3.4 Módulos
PROCEDURE pop(VAR stk :
stack) : element;
BEGIN
IF stk.top = 1 THEN
error;
ELSE
stk.top := stk.top - 1;
RETURN stk.s[stk.top];
END;
END pop;
END stack_manager;
var A, B : stack;
var x, y : element;
...
init_stack(A);
init_stack(B);
...
push(A, x);
...
y := pop(B);
3.3.5 Tipos Módulos y Clases
 Una solución alternativa al problema de las múltiples
instancias puede ser encontrada en Simula, Euclid, y (en
un sentido un poco diferente) ML, que tratan los módulos
como tipos en vez de contrucciones de encapsulación.
Dado un tipo módulo, el programador puede declarar un
arbitrario número de objetos módulo similares. A
continuación se muestra el esquema de una pila en
Euclid, que como vemos permita al programador
proporcionar código de inicialización que se ejecuta cada
vez que una pila es creada, además Euclid permite definir
código de finalización que se ejecuta al final del tiempo de
vida del módulo, características que es necesaria cuando
se almacenan elementos en el heap y necesitan ser
removidos.
3.3.5 Tipos Módulos y Clases
const stack_size := ...
type element : ...
type stack = module
imports (element,
stack_size)
exports (push, pop)
type
stack_index =
1..stack_size
var
s : array stack_index of
element
top : stack_index
procedure push(elem :
element) = ...
function pop returns
element = ...
...
3.3.5 Tipos Módulos y Clases
initially
top := 1
end stack
var A, B : stack
var x, y : element
...
A.push(x)
...
y := B.pop
3.3.5 Tipos Módulos y Clases
 La diferencia entre las aproximaciones de
módulos como manejadores y los tipos
módulos consiste en que con los tipos módulos
el programador puede pensar en la subrutinas
como “pertenecientes” a la pila en cuestión
(A.push(x)) en vez de como entidades externas
a las que la pila es pasada como argumento
(push(A,x)).
3.3.5 Tipos Módulos y Clases
 En el caso de los tipos módulos,
conceptualmente se ve a las subrutinas como
operaciones separadas pero en la práctica no
es necesario tener copias separadas del mismo
código, de modo que todas las pilas comparten
las mismas operaciones. El compilador logra
esto haciendo que se envíe un puntero a la pila
en cuestión como un parámetro extra y oculto a
las subrutinas. De modo que la implementación
termina siendo muy similar a la de los módulos
como manejadores, pero el programador no
necesita pensarlo de esa forma.
3.3.5 Tipos Módulos y Clases
 Compilación separada
Uno de los distintivos de una buena
abstracción es que es útil en varios contextos.
Para facilitar la reutilización de código muchos
lenguajes hacen de los módulos la base para la
compilación por separado.
Los módulos en muchos lenguajes (por
ejemplo Modula-2 y Oberon) pueden ser
dividos en una parte de declaración (header) y
una de implemetación (body), cada una una en
archivos separados.
3.3.5 Tipos Módulos y Clases
El código que usa las exportaciones de un
módulo dado puede ser compilado con sólo
que exista el “header”, no depende del “body”.
En particular, el trabajo en los cuerpos de
módulos cooperantes puede proceder
paralelamente una vez que los headers
existan.
3.3.5 Tipos Módulos y Clases
 Orientación a Objetos
Como una extensión de la aproximación de
tipos módulos a la abstracción de datos,
muchos lenguajes proveen una construcción
clase para la programación orientada a objetos.
De entrada, podemos pensar a la clases como
tipos módulos aumentados con un mecanismo
de herencia.
3.3.5 Tipos Módulos y Clases
La herencia permite definir nuevas clases como
extensiones o especializaciones de clases
existentes, permitiendo un estilo de
programación en el que todas o la mayoría de
las operaciones se piensan como pernecientes
a los objetos.
Las clases tiene su origen en Simula-67 y son
la innovación central en los lenguajes
orientados a objetos como Smalltalk, Eiffel, C+
+, Java y C#, también son fundamentales en
lenguajes de scripting como Phyton y Ruby.
3.3.5 Tipos Módulos y Clases
 Módulos que contienen clases
A pesar de que existe un claro avance desde
los módulos a los tipos módulos y a las clases,
esto no significa que las clases son un
reemplazo adecuado para los módulos en
todos los casos. Por ejemplo: suponga que se
está desarrollando un complejo videojuego.
Una jerarquía de clases será justo lo que se
necesita para representar a los personajes,
objetos que se puedan adquirir, edificios,
metas, etc.
3.3.5 Tipos Módulos y Clases
Pero al mismo tiempo, especialmente en un proyecto con
un amplio equipo de programadores se querrá dividir la
funcionalidad del juego entre subsistemas de gran tamaño
como la parte de gráficos y renderizado, física, estrategia,
etc. Estos subsistemas no son en realidad abstracciones y
probablemente no se desee crear múltiples instancias de
los mismos. Éstos son naturalmente realizables a través
de módulos.
Muchas aplicaciones tienen una necesidad similar tanto
de abstracciones con múltiples instancias como de la
subdivisión funcional, es por esto que muchos lenguajes
incluyendo C++, Java, C#, Python, y Ruby, proveen
mecanismos tanto para las clases como para los módulos.
3.3.6 Alcance Dinámico
 En un lenguaje con alcance dinámico, los enlaces
entre nombres y objetos dependen del flujo de control
en tiempo de ejecución y en particular del orden en
que las subrutinas sean llamadas. Las reglas de
alcance son más simples que en el alcance estático
pues el enlace para un nombre no local se determina
buscando en los alcances encontrados más
recientemente en la ejecución que aún no hayan
finalizado su ejecución
 Algunos ejemplos de lenguajes con alcance dinámico
son APL, Snobol, TEX, Perl y los primeros dialectos
de Lisp.
3.3.6 Alcance Dinámico
 Debido a que el flujo del control no puede en general
ser predecido, los enlaces entre los nombres y objetos
en un lenguaje con enlace dinámico no pueden ser
determinados por un compilador. Como resultado,
muchas reglas semántica pasan de ser semántica
estática a semántica dinámica. La revisión de tipos en
expresiones y revisión de argumentos en llamadas a
subrutinas, por ejemplo, deben en general ser
realizadas hasta el tiempo de ejecución. Para
acomodar todas estas revisiones, los lenguajes con
alcance dinámico tienden a ser interpretados más que
compilados.
3.3.6 Alcance Dinámico
 Ejercicio: Dado el siguiente programa ¿cuál sería su
salida en la caso de que el lenguaje tuviese un
alcance estático y cuál sería esta salida si el alcance
fuese dinámico?
3.3.6 Alcance Dinámico
n : integer -- global
procedure first
n := 1 -- local
procedure second
n : integer
first()
procedure main
n := 2
if read_integer() > 0
second()
else
first()
write_integer(n)
3.3.6 Alcance Dinámico
 Ejemplo: Usando alcance dinámico es posible que no se
detecten errores asociados con el ambiente de referencia
hasta el tiempo de ejecución, en el código a continuación,
la variable local max_score en el prodecimiento foo
accidentalmente redefine una variable global usada por la
función scaled_score, que es luego llamada por foo. Como
el max_score global es un entero y el local es un flotante,
las revisión de semántica dinámica en algunos lenguajes
generará en un error en la conversión de tipos en tiempo
de ejecución.
Además si la variable local también fuese un entero, no se
detectaría ningún error, pero el programa posiblemente
produciría resultados no deseados, que es un error más
difícil de encontrar.
3.3.6 Alcance Dinámico
max_score : integer -- maximum possible score
function scaled_score(raw_score : integer) : real
return raw_score / max_score * 100
procedure foo
max_score : real := 0
…
foreach student in class
student.percent := scaled_score(student.points)
...
3.3.6 Alcance Dinámico
 Ejemplo: El principal argumento a favor del
alcance dinámico es que facilita la
personalización de las subrutinas. Suponga
que se tiene una rutina de librería print_integer
que imprime su argumento en varias bases
(decimal, binaria, hexadecimal, etc.) y además
suponga que la mayor parte de las veces se
desea que se use la notación decimal, así que
no se desea especificar una base en cada
llamada individual.
3.3.6 Alcance Dinámico
Esto se puede lograr con el alcance dinámico al hacer
que print_integer obtenga su base de una variable no
local print_base, se puede establecer un valor por
defecto al declarar una variable print_base igual a 10
en un alcance al inicio de la ejecución y luego cada
vez que se quiera cambiar la base temporalmente se
puede escribir:
begin -- bloque anidado
print_base : integer := 16 -- base hexadecimal
print_integer(n)
3.3.6 Alcance Dinámico
Lo anterior se podría lograr en un lenguaje con alcace
estático usando dos procedimientos, por ejemplo
print_integer y print_with_base, o sobrecargando el
mismo con varios parámetros o también sino se desea
tener varios procedimientos se podría utilizar una
variable global definiéndola con un valor de 10 y luego
asignarle un valor distinto antes de hacer cada
llamado, claro sin olvidar restablecer el valor original
después de la llamada, finalmente sino se desea la
declaración global se podría declarar una variable
estática encapsulada con print_integer en un módulo.
3.4 Implementando el Alcance
 Para darle seguimiento a los nombres en un
alcance estático el compilador utiliza una
abstraccion de datos llamada tabla de
símbolos, que en esencia es un diccionario,
pues mapea los nombres con la información
que el compilador tiene sobre los mismos. La
operaciones más básicas son la inserción de
un nuevo elemento (enlace de nombre a
objeto) o la de buscar la información de un
objeto dado un nombre.
3.4 Implementando el Alcance
 Las reglas de alcance estático agregan complejidad
pues permiten que un mismo nombre corresponda a
diferentes objetos in diferentes partes del programa.
La mayoría de estas variaciones son manejadas
aumentando el estilo básico de diccionario de la tabla
de símbolos con las operaciones enter_scope y
leave_scope para darle seguimiento a la visibilidad.
 Nada se borra de la tabla, la estructura se mantiene
durante la compilación e incluso se puede incluir en el
código objeto para ser usada por depuradores y
mecanismos de reflexión en tiempo de ejecución.
3.5 El Significado de los Nombres
dentro de un Alcance
 Hasta el momento al hablar de enlaces nombre a objeto,
en su mayor parte hemos asumido que hay una
correspondencia uno a uno entre los nombres y los
objetos visibles en un punto dado del programa, pero esto
no necesariamente es así siempre:
 Dos o más nombres que refieren al mismo objeto en el
mismo punto del programa, se dice que son un “alias” o
pseudónimos el uno del otro.
 Un nombre que puede referirse a más de un objeto en
el mismo punto de programa se dice que está
sobrecargado.
3.5.1 Aliases
 Ejemplo: Los “aliases” surgen naturalmente en los
lenguajes que soportan el uso de estructuras basadas en
apuntadores, otra forma de crear aliases en muchos
lenguajes es pasando un parámetro por referencia a una
subrutina que accede a esa variable directamente, como
en el ejemplo en C++ a continuación:
double sum, sum_of_squares;
void accumulate(double& x){
sum += x;
sum_of_squares += x * x;
}
accumulate(sum);
3.5.1 Aliases
En el ejemplo anterior vemos que sum es pasado como
argumento a la función accumulate, pero como x y sum se
convierten en aliases uno del otro la primera línea de
código de esta función no solamente modificaría a sum
sino también a x y por tanto la siguiente línea de código
puede que no tenga los resultados esperados.
Este tipo de errores fue una de las principales
motivaciones para hacer que las subrutinas en Turing y
Euclid fuesen alcances cerrados, pues al exigir listas de
importación el compilador es capaz de determinar y
prohibir la creación de un alias dentro de la subrutina.
3.5.1 Aliases
 Ejemplo: Como una regla general, los aliases tienden a
hacer los programas más confusos y más difíciles para
que el compilador pueda hacer importantes
optimizaciones en el código. Considere el siguiente código
en C:
int a, b, *p, *q;
...
a = *p; // Se copia en a el valor al que apunta p
*q = 3;
b = *p; // Se copia en b el valor al que apunta p
3.5.1 Aliases
La asignación inicial en la mayoría de las máquinas,
requiere que *p está cargado en un registro. Debido a que
acceder a la memoria principal tiene su costo, el
compilador desaría mantener este valor de *p en el
registro para usarlo en la tercera asignación (b = *p). Pero
no podrá hacer esto, a menos que pueda verificar que p y
q no son aliases. Aunque esta verificación es posible de
hacer en muchos casos, en general es incomputable.
3.5.1 Aliases
 Punteros en C y en Fortran
La tendencia de los punteros a introducir aliases es una
de la razones por las cuales los compiladores de Fortran
han tendido históricamente a producir código más rápido
que los compiladores de C, pues los punteros han sido
abundatemente usados en C y no han existido en Fortran
77 y sus predecesores. Ha sido en años recientes que
sofisticados algoritmos de análisis de alias han permitido a
los compiladores de C competir con Fortran en velocidad
del código generado. Este análisis de los punteros ha sido
tan importante que los diseñadores del estándar C99
decidieron agregar una nueva palabra clave al lenguaje:
restrict.
3.5.1 Aliases
El cualificador restrict cuando se agrega a una declaración
de un puntero, es una aseveración de parte del
programador indicando que el objeto al que el puntero se
refiere no tiene un alias en el alcance actual. Es
responsabilidad del programador hacer cierta esta
aseveración, pues el compilador no necesita comprabarla.
C99 también introduce el concepto de aliasing estricto,
que permite al compilador asumir que los punteros de
distintos tipos nunca harán referencia a la misma
ubicación en memoria. La mayoría de los compiladores
proveen una opción en línea de comandos que deshabilita
las optimizaciones que explotan esta regla, pues de otro
modo algunos programas antiguos y pobremente escritos
podrían comportarse de forma incorrecta al compilarse a
altos niveles de optimización.
3.5.2 Sobrecarga
 La mayoría de los lenguajes de programación proveen al
menos una forma limitada de hacer sobrecarga. En C, por
ejemplo, el signo más “+” es usado para nombrar
diferentes funciones, incluyendo la suma de enteros con
signos, sin signo y números flotantes. A la mayoría de los
programadores no le interesa la distinción entre estas
funciones pero éstas toman argumentos de diferentes
tipos y realizan operaciones muy distintas a nivel de bits.
Una forma un poco más compleja de sobrecarga aparece
en Ada a nivel de las enumeraciones. En el siguiente
ejemplo de código en Ada vemos como las constantes
“oct” y “dec” pueden referirse a meses o bases numéricas
dependiendo del contexto en que aparezcan.
3.5.2 Sobrecarga
declare
type month is (jan, feb, mar, apr, may, jun,
jul, aug, sep, oct, nov, dec);
type print_base is (dec, bin, oct, hex);
mo : month;
pb : print_base;
begin
mo := dec;
pb := oct;
print(oct); -- error!
3.5.2 Sobrecarga
 Dentro de la tabla de símbolos de un compilador, la
sobrecarga se maneja haciendo que la rutina de búsqueda
retorne una lista de los posibles significados de un nombre
buscado. El analizador semántico debe entonces escojer
de la lista de elementos basado en el contexto en que
aparece el nombre. Cuando el contexto no es suficiente
para decidir como en la llamada a print en el ejemplo
anterior, entonces se debe anunciar un error.
 La mayoría de los lenguajes que permiten constantes de
enumeración sobrecargadas permiten al programador
proveer un contexto apropiado de forma explícita. En Ada
por ejemplo se puede escribir:
print(month'(oct));
3.5.2 Sobrecarga
 En Modula-3 y C#, cada uso de una constante de
enumeración debe estar precedido por un nombre de tipo,
por lo cual no hay espacio para la ambigüedad:
mo := month.dec;
pb := print_base.oct;
 En C, C++ y Pascal estándar, no es posible sobrecargar
las constantes de enumeración, cada constante visible en
un alcance debe ser distinta.
3.5.2 Sobrecarga
 Ejemplo: En Ada y C++, entre otros lenguajes, es posible
sobrecargar los nombres de las subrutinas, es decir, un
nombre dado puede referirse a un arbitrario número de
subrutinas en el mismo alcance, con tal que éstas difieran
en sus argumentos y sea en número o tipo. En el siguiente
código vemos un ejemplo de sobrecarga en C++:
struct complex {
double real, imaginary;
};
enum base {dec, bin, oct, hex};
3.5.2 Sobrecarga
int i;
complex x;
void print_num(int n) { ...
void print_num(int n, base b) { ...
void print_num(complex c) { ...
print_num(i);
print_num(i, hex);
print_num(x);
3.5.2 Sobrecarga
 Ada, C++, C#, Fortran 90, y Haskell, entre
otros, también permiten que los operadores
predefinidos (+, -, *, etc.) sean sobrecargados
con funciones definidas por el usuario. Ada, C+
+ y C# hacen esto definiendo formas prefijas
alternativas para cada operador y dejando las
formas infijas usales como abreviaciones de las
prefijas. Por ejemplo en Ada A + B es una
abreviación de “+”(A,B).
En el siguiente código vemos un ejemplo de
sobrecarga del operador “+” en C++.
3.5.2 Sobrecarga
class complex {
double real, imaginary;
...
public:
complex operator+(complex other) {
return complex(real + other.real, imaginary +
other.imaginary);
}
...
};
complex A, B, C;
...
C = A + B;
3.5.2 Sobrecarga
Con respecto a este estilo de abreviación de
operadores basado en clases, se podría estar
tentado a pensar en que no hay una
sobrecarga real de los mismos, pues la
abreviación se expande a un nombre no
ambiguo (es el operador + de la clase A,
A.operator+) y de hecho este es el caso en el
lenguaje Clu, pero en C++ y C# puede haber
más de una definición de A.operator+,
permitiendo que el segundo argumento sea de
varios tipos.
3.5.3 Polimorfismo y Conceptos
Relacionados
 En el caso de los nombres de subrutinas es
importante diferenciar claramente la
sobrecarga de los conceptos relacionados
como son el polimorfismo y la coerción. La
confusión puede surgir pues los tres pueden
ser usados para pasar argumentos de múltiples
tipos a un nombre de subrutina dado o para
retornar múltiples tipos de un nombre de
subrutina dado.
Esta similitud sintáctica, esconde importantes
diferencias semánticas y pragmáticas.
3.5.3 Polimorfismo y Conceptos
Relacionados
 Ejemplo: Supongamos que se desea el mínimo
de dos valores sean flotantes o enteros, en Ada
se podría lograr esto usando dos funciones
sobrecargadas:
function min(a, b : integer) return integer is ...
function min(x, y : real) return real is ...
En C, sin embardo, podríamos hacer el mismo
trabajo sólo con la función:
double min(double x, double y) { ...
3.5.3 Polimorfismo y Conceptos
Relacionados
Si la función en C es llamada en un contexto que espera
un entero como resultado (ej.: int i = min(j, k)), el
compilador automáticamente convertirá los argumentos
enteros a números flotantes (double), llamará a la función
min y luego convertirá el resultado de nuevo en un entero
vía truncado (eliminando la parte decimal). Así que
mientras las variables de tipo flotante tengan capacidad
para almacenar la misma cantidad de bits significativos
que un entero (cosa que sí ocurre en el caso de los
enteros de 32-bit y los double de 64-bit), el resultado será
numéricamente correcto, con sólo usar una función.
3.5.3 Polimorfismo y Conceptos
Relacionados
 La coerción es el proceso en que un compilador
automáticamente convierte un valor de un tipo en un valor
de otro tipo que el segundo tipo es requerido por el
contexto.
 La coerción es un tema controversial en los lenguajes de
programación, pues algunos evitan al máximo utilizarla,
Ada solo coerciona las constantes explícitas, subrangos y
en ciertos casos arreglos de con el mismo tipo de
elementos. Pascal coerciona enteros a flotantes en
expresiones y asignaciones. Fortran también coerciona
flotantes a enteros con una potencial pérdida de precisión.
C hace también coerciones en los argumentos a
funciones. La mayoría de los lenguajes de scripting
proveen un rico conjunto de coerciones predefinidas.
3.5.3 Polimorfismo y Conceptos
Relacionados
 C++ además permite extender su conjunto predefinido de
coerciones con coerciones definidas por el usuario.
 Volviendo al ejemplo anterior, la sobrecarga en Ada
permite al compilador escojer entre dos diferentes
opciones de la función “min”, la coerción en C permite al
compilador modificar los argumentos de la función “min”.
 El polimorfismo provee otra opción: permite que una sola
subrutina acepte argumentos de múltiples tipos sin ser
convertidos. El término “polimórfico” viene del griego y
significa “tener muchas formas”. Es aplicado al código
(sean estructuras de datos o subrutinas) que pueden
trabajar con valores de múltiples tipos.
3.5.3 Polimorfismo y Conceptos
Relacionados
 Coercion vrs Sobrecarga
Además de sus diferencias semánticas, la coerción y
la sobrecarga puede tener costos muy diferentes.
Invocar una versión específica para enteros de los
función “min” es mucho más eficiente que llamar una
función “min” para números flotantes con argumentos
enteros, pues en el primer caso se usaría aritmética
de enteros para hacer la comparación (que podría ser
más eficiente) y se evitaría hacer tres operaciones de
conversión (los dos argumentos y el resultado). Un
argumento en contra de la coerción es que tiende a
imponer costos ocultos.
3.5.3 Polimorfismo y Conceptos
Relacionados
 Para que el concepto de polimorfismo tenga sentido, los
tipos deben generalmente tener ciertas características en
común y el código no debe depender nada más que de
éstas. Las características comunes son generalmente
tomadas de dos formas principales:
 El polimorfismo paramétrico, en este caso el código
toma un tipo (o un conjunto de tipos) como parámetro
ya sea de forma explícita o implícita.
 El polimorfismo de subtipo, acá el código es diseñado
para trabajar con valores de un tipo específico T, pero
el programador puede definir tipos adicionales que
sean extensiones de T (herencia en programación
orientada a objetos) y el código polimórfico funcionará
además de con T con todos sus subtipos.
3.5.3 Polimorfismo y Conceptos
Relacionados
 El polimorfismo paramétrico explícito es también conocido
como genericidad. Existen varios lenguajes como Ada, C+
+, Clu, Eiffel, Modula-3, Java, y C# que dan soporte a la
genericidad, por ejemplo en C++ la genericidad se
soportar a través de las plantillas (templates).
 El polimorfismo paramétrico implícito aparece en la familia
de lenguajes de Lisp y ML y en varios lenguajes de
scripting.
 El polimorfismo de subtipo es fundamental en los
lenguajes orientados a objetos en los que se dice que los
subtipos (clases) heredan los métodos de sus tipos
padres.
3.5.3 Polimorfismo y Conceptos
Relacionados
 La genericidad es usualmente (no siempre)
implementada mediante la creación de
múltiples copias del código polimórfico, cada
una especializada para cada tipo concreto, el
polimorfismo de subtipo es casi siempre
implementado mediante la creación de una
sola copia del código y a través de la inclusión
en la representación de objetos de suficientes
“metadatos” de modo que el código pueda
saber cuándo tratarlos de forma diferente.
3.5.3 Polimorfismo y Conceptos
Relacionados
 El polimorfismo paramétrico implícito puede ser
implementado de ambas formas. La mayoría de las
implementaciones de Lisp usan una única copia del
código y dejan todos las revisiones semánticas hasta
el tiempo de ejecución. ML y sus descendientes
realizan toda la revisión de tipos en tiempo de
compilación, típicamente se genera una única copia
del código cuando es posible (ej.: cuando todos los
tipos en cuestión son registros que tienen una
representación similar) y múltiples copias cuando es
necesario (ej.: cuando la aritmética polimórfica debe
operar tanto en números enteros como flotantes).
3.5.3 Polimorfismo y Conceptos
Relacionados
 Los lenguajes orientados a objetos que realizan
la revisión de tipos en la compilación como C+
+, Eiffel, Java, y C# generalmente proveen
soporte tanto para la genericidad como para el
polimorfismo de subtipo.
 Smalltalk, Objective-C, Python, y Ruby usan un
único mecanismo (con revisión en tiempo de
ejecución) para proveer tanto el polimorfismo
paramétrico como el de subtipo.
3.5.3 Polimorfismo y Conceptos
Relacionados
 Ejemplo: Como un ejemplo concreto de
genericidad, considere las funciones “min”
sobrecargadas en el ejemplo anterior. El código
fuente para las versiones para enteros y
flotantes es similiar, se podría aprovechar esta
similaridad para definir una única versión que
funcione no sólo para enteros y reales sino
para cualquier tipo cuyos valores estén
totalmente ordenados. Una forma de hacer
esto en Ada se muestra en el código a
continuación:
3.5.3 Polimorfismo y Conceptos
Relacionados
generic
type T is private;
with function "<"(x, y : T) return Boolean;
function min(x, y : T) return T;
function min(x, y : T) return T is
begin
if x < y then return x;
else return y;
end if;
end min;
3.5.3 Polimorfismo y Conceptos
Relacionados
function string_min is new min(string, "<");
function date_min is new min(date, date_precedes);
En este código se observa una declaración inicial de “min”
sin cuerpo (implemetación) que está precedida por una
cláusula genérica que especifica que dos cosas son
necesarias para crear una instancia concreta de una
función “min”: un tipo T y una rutina de comparación.
Luego esta declaración está seguida por el código actual
de “min”, por ejemplo dadas las declaraciones apropiadas
para los tipos string y date y sus rutinas de comparación,
se pueden crear funciones “min” que funcionen con estos
tipos como se ve en la últimas dos líneas (observe que el
operador “<” que se envía a string_min probablemente
esté sobrecargado).
3.5.3 Polimorfismo y Conceptos
Relacionados
 Ejemplo: Con el polimorfismo paramétrico implícito de
Lisp, ML y sus descendientes, el programador no tiene
que especificar un tipo para los parámetros, por ejemplo
en Scheme la definición de “min” sería:
(define min (lambda (a b) (if (< a b) a b)))
La implementación típica de Scheme utiliza un intérprete
que examina los argumentos que recibe “min” y determina
en tiempo de ejecución si soportan el operador “<”. (Como
todos los dialectos de Lisp, Scheme coloca los nombres
de las funciones dentro de los paréntesis justo antes de
los parámetros y la palabra clave “lambda” se usa se usa
para introducir la lista de parámetros y el cuerpo de una
función).
3.5.3 Polimorfismo y Conceptos
Relacionados
Para el caso de función “min” anterior, la
expresión (min 123 456) retorna 123; (min
3.14159 2.71828) retorna 2.71828 y (min "abc"
"def") produce un error en tiempo de ejecución
porque el operador de comparación de
cadenas es “<?” no “<”.
3.5.3 Polimorfismo y Conceptos
Relacionados
 Ejemplo:
En Haskell la versión de “min” sería similar a la
de Scheme:
min a b = if a < b then a else b
Esta versión funciona para valores de cualquier
tipo totalmente ordenado, incluyendo las
cadenas, pero la revisión de tipos se hace en
tiempo de compilación usando un sofisticado
sistema de inferencia de tipos.
3.5.3 Polimorfismo y Conceptos
Relacionados
 En conclusión, la diferencia entre las versiones
sobrecargadas de “min” y la versión genérica radica en la
generalidad del código. Con la sobrecarga el programador
debe escribir una copia separada del código a mano para
cada tipo con el que se desea que funcione “min”, en
cambio con la genericidad, es el compilador (en la
implementaciones típicas), que crea automáticamente una
copia del código para cada tipo. La similitud de la sintaxis
con que se invocan las subrutinas y de el código que se
genera, ha llevado a algunos autores a referirse a la
sobrecarga como caso especial de polimorfismo.
3.5.3 Polimorfismo y Conceptos
Relacionados
 Sin embargo, no hay una razón particular para
que el programador piense en la genericidad
en términos de múltiples copias, desde un
punto de vista semántico (conceptual), las
subrutinas sobrecargadas usan un sólo nombre
para varios elementos y una subrutina
polimórfica es un sólo elemento.
3.6 El Enlace de los Ambientes de
Referencia
 Ya hemos visto cómo las reglas de alcance
determinan el ambiente de referencia de un
enunciado dado en el programa. La reglas de
alcance estático especifican que el ambiente
de referencia depende del anidado léxico de
los bloques del programa donde los nombres
son declarados. Las reglas de alcance
dinámico especifican que el ambiente de
referencia depende del orden en que las
declaraciones son encontradas en el tiempo de
ejecución.
3.6 El Enlace de los Ambientes de
Referencia
 Pero hay un elemento que no hemos
considerado, que surge en lenguajes que
permiten crear referencias a subrutinas, por
ejemplo, pasándolas como parámetro ¿Cuándo
debería ser aplicadas las reglas de alcance a
esa subrutina? ¿Cuando se crea por primera
vez o cuando es invocada? La respuesta es
importante tanto en lenguajes con alcance
dinámico como estático.
3.6 El Enlace de los Ambientes de
Referencia
 Ejemplo: en el código a continuación aparece
un ejemplo de alcance dinámico, el
procedimiento “print_selected_records” es una
rutina de propósito general que sabe como
recorrer los registros de una base de datos, sin
importar si representan personas, herramientas
o ensaladas. Toma como parámetros la base
de datos, un predicado para saber cuándo
imprimirá un registro o no y una subrutina que
sabe cómo formatear los datos en los registros
de esta base de datos particular.
3.6 El Enlace de los Ambientes de
Referencia
Además se define una función “print_person” que usa el
valor de una variable no local “line_length” para calcular el
número y ancho de las columnas en la salida. En un
lenguaje con alcance dinámico, es natural que el
procedimiento “print_selected_records” declare e inicialice
esta variable localmente, sabiendo que el código en
“print_routine” tomará su valor si es necesario. Pero para
que esta técnica funcione, el ambiente de referencia de
“print_routine” no debe ser creado si hasta que la rutina
sea invocada por “print_selected_records”. Este enlace
tardío del ambiente de referencia de una subrutina que ha
sido pasada como parámetro es conocido como enlace
superficial, y es el enlace por defecto en los lenguajes con
alcance dinámico.
3.6 El Enlace de los Ambientes de
Referencia
type person = record
…
age : integer
…
threshold : integer
people : database
function older_than_threshold(p : person) : boolean
return p.age ≥ threshold
procedure print_person(p : person)
-- Usa la variable no local line_lenght
...
3.6 El Enlace de los Ambientes de
Referencia
procedure print_selected_records(db : database;
predicate, print_routine : procedure)
line_length : integer
if device_type(stdout) = terminal
line_length := 80
else
line_length := 132
foreach record r in db
if predicate(r)
print_routine(r)
-- main program
…
threshold := 35
print_selected_records(people, older_than_threshold,
print_person)
3.6 El Enlace de los Ambientes de
Referencia
 En contraste y siempre haciendo referencia al código
anterior, para la función “older_than_threshold” el enlace
superficial podría no funcionar muy bien. Si por ejemplo, el
procedimiento “print_selected_records” tuviese una
variable local “threshold”, entoces la variable establecida
por el programa principal para influenciar el
comportamiento de “older_than_threshold” no será visible
cuando la función sea finalmente llamada y
probablemente la función no genere los resultados
esperados. En una situación como esta, el código que
originalmente pasa la función como parámetro tiene un
ambiente de referencia particular, y no desea que la
subrutina que pasa se llamada con otro ambiente distinto.
3.6 El Enlace de los Ambientes de
Referencia
Así que tiene sentido también enlazar el
ambiente en el momento en que la rutina es
pasada por primera vez como parámetro y
luego restaurar este ambiente cuando sea
finalmente invocada. Este enlace temprano del
ambiente de referencia es conocido como
enlace profundo. La necesidad del enlace
profundo es en ocasiones referido como el
problema funarg (function argument) en Lisp.
3.6.1 Cerraduras de Subrutinas
 El enlace profundo es implementado mediante la creación
de una representación explícita de un ambiente de
referencia (generalmente el ambiente en que la rutina se
ejecutaría si fuese llamada en el tiempo presente) que se
grupa junto con la referencia a la subrutina. Este conjunto
se conoce como cerradura. Usualmente una subrutina por
sí misma puede ser representada dentro de la cerradura
mediante un puntero a su código. En un lenguaje con
alcance dinámico, la representación del ambiente de
referencia depende de si la implementación del lenguaje
usa una lista de asociación a una tabla central de
referencias para hacer la búsqueda de los nombres en
tiempo de ejecución.
3.6.1 Cerraduras de Subrutinas
 Aunque el enlace superficial es usualmente la opción por
defecto en lenguajes con alcance dinámico, el enlace
profundo puede estar disponible como opción. En los
dialectos más antinguos de Lisp, por ejemplo, existe una
primitiva llamada “function” que toma una función como
argumento y retorna una cerradura cuyo ambiente de
referencia es el que la función tendría si fuese ejecutada
en el momento actual. Esta cerradura puede ser pasada
como parámetro a otra función, de modo que cuando sea
invocada se ejecute en el ambiente guardado. (Las
cerradurs funcionan un poco distinto de las simples
funciones en la mayoría de los dialectos de Lisp: deben se
llamadas pasándolas como argumentos a primitivas
predefinidas como “funcall” o “apply”.)
3.6.1 Cerraduras de Subrutinas
 El enlace profundo es generalmente la opción por defecto
en lenguajes con alcance estático. De entrada se podría
pensar que el tiempo de enlace de los ambiente de
referecia en un lenguaje con alcance estático no debería
de importar. De todos modos, el significado de un nombre
en un lenguaje con alcance estático depende de su
anidado léxico, no del flujo de la ejecución y este anidado
es el mismo no importando si es capturado al momento en
que la subrutina es pasada como parámetro o después
cuando es invocada. El detalle es que un programa en
ejecución puede tener más de una instancia de un objeto
que está declarado dentro de una subrutina recursiva.
3.6.1 Cerraduras de Subrutinas
 Una cerradura en un lenguaje con alcance estático
captura la instancia actual de cada objeto en el momento
es creada. Cuando la subrutina sea llamada, encontrará
estas instancias capturadas, aún cuando hayan sido
creadas nuevas instancias por llamados recursivos.
 Es posible imaginar la combinación de alcance estático
con el enlace superficial, pero la combinación no parece
tener mucho sentido y al parecer no ha sido implementada
en ningún lenguaje.
3.6.1 Cerraduras de Subrutinas
 Ejercicio: En el siguiente código se muestra un programa
en Pascal que ilustra el impacto de las reglas de enlace en
la presencia de un alcance estático. ¿Cuál será su salida
en el caso del enlace profundo y cuál será en el caso del
enlace superficial?
3.6.1 Cerraduras de Subrutinas
program binding_example
procedure A(I : integer;
procedure P);
procedure B;
begin
writeln(I);
end;
begin (* A *)
if I > 1 then
P
else
A(2, B);
end;
procedure C; begin end;
begin (* main *)
A(1, C);
end.
3.6.1 Cerraduras de Subrutinas
 Solución: En la siguiente figura se muestra una vista
conceptual de la pila en tiempo de ejecución, los
ambientes de referencia capturados en cerraduras se
muestran como cajas punteadas y flechas, Cuando B es
llamado a través del parámetro P, dos instancias de I
existen. Como la cerradura para P fue creada en la
invocación inicial a A, el enlace estático de B apunta al
marco de esa invocación previa. B usa la instacia de I de
dicha invocación y por tanto la salida provocada por
writeln es 1.
3.6.1 Cerraduras de Subrutinas
3.6.1 Cerraduras de Subrutinas
 Debe también notarse que las reglas de enlace
con alcance estático importan sólo cuando los
objetos que se acceden no son ni locales ni
globales sino que están definidos en un nivel
intermedio de anidamiento. Si un objeto es
local a la subrutina que se ejecuta actualmente,
entonces no importa si la subrutina fue
invocada directamente o a través de una
cerradura. Si un objeto es global, nunca habrá
más de una instancia, pues la subrutina
principal de un programa no es recursiva.
3.6.1 Cerraduras de Subrutinas
 Por tanto las reglas de enlace son irrelevantes
en lenguajes como C que no tiene subrutinas
anidadas or Modula-2, que sólo permite a las
subrutinas más externas ser pasadas como
parámetros, asegurándose de este modo que
cualquier variable definida fuera de la subrutina
sea global.
Además las reglas de enlace son irrelevantes
en lenguajes como PL/I y Ada 83, que no
permiten que ninguna subrutina sea pasada
como parámetro.
3.6.1 Cerraduras de Subrutinas
 Suponiendo que se tenga un lenguaje con
alcance estático en el que las subrutinas
anidadas pueden ser pasadas como
parámetros con enlace profundo. Para
representar una cerradura para la subrutina S,
simplemente se puede almacenar un puntero al
código de S junto con el enlace estático que S
usaría si fuese llamado en el tiempo actual, en
el ambiente actual.
3.6.1 Cerraduras de Subrutinas
Cuando S finalmente es invocada, se restaura
temporalmente el enlace estático guardado en
vez de crear uno nuevo. Cuando S sigue la
cadena estática para acceder al objeto no local
que busca, encontrará la instancia del objeto
correcta, la que era actual en el momento que
la cerradura fue creada. Esta instancia podría
no tener el valor que tenía cuando la cerradura
fue creada, pero al menos su identidad será la
que esperaba el creador de la cerradura.

More Related Content

What's hot

Lenguajes de interfaz
Lenguajes de interfazLenguajes de interfaz
Lenguajes de interfaz
Xavi Flores
 
Introduccion a la administracion de los procesos y el procesador (S.O)
Introduccion a la administracion de los procesos y el procesador (S.O)Introduccion a la administracion de los procesos y el procesador (S.O)
Introduccion a la administracion de los procesos y el procesador (S.O)
Javier Alvarez
 
Estructura de un compilador 2
Estructura de un compilador 2Estructura de un compilador 2
Estructura de un compilador 2
perlallamas
 
SISTEMAS OPERATIVOS MULTIMEDIA
SISTEMAS OPERATIVOS MULTIMEDIASISTEMAS OPERATIVOS MULTIMEDIA
SISTEMAS OPERATIVOS MULTIMEDIA
Mari Ng
 
Ingeniería de requisitos
Ingeniería de requisitosIngeniería de requisitos
Ingeniería de requisitos
Zuleima
 
Apunte Algoritmos Geneticos
Apunte Algoritmos GeneticosApunte Algoritmos Geneticos
Apunte Algoritmos Geneticos
ESCOM
 
Herramientas para-el-analisis-de-flujo-de-datos
Herramientas para-el-analisis-de-flujo-de-datosHerramientas para-el-analisis-de-flujo-de-datos
Herramientas para-el-analisis-de-flujo-de-datos
Danitortas
 
PROGRAMACION CONCURRENTE
PROGRAMACION CONCURRENTEPROGRAMACION CONCURRENTE
PROGRAMACION CONCURRENTE
gladysmamani
 
Organización y estructura interna del cpu
Organización y estructura interna del cpuOrganización y estructura interna del cpu
Organización y estructura interna del cpu
Isaí Beto Matz Mijes
 

What's hot (20)

Lenguajes de interfaz
Lenguajes de interfazLenguajes de interfaz
Lenguajes de interfaz
 
Procesos
ProcesosProcesos
Procesos
 
Introduccion a la administracion de los procesos y el procesador (S.O)
Introduccion a la administracion de los procesos y el procesador (S.O)Introduccion a la administracion de los procesos y el procesador (S.O)
Introduccion a la administracion de los procesos y el procesador (S.O)
 
Memoria Estatica
Memoria EstaticaMemoria Estatica
Memoria Estatica
 
Tipos de datos abstractos
Tipos de datos abstractosTipos de datos abstractos
Tipos de datos abstractos
 
Estructura de un compilador 2
Estructura de un compilador 2Estructura de un compilador 2
Estructura de un compilador 2
 
SISTEMAS OPERATIVOS MULTIMEDIA
SISTEMAS OPERATIVOS MULTIMEDIASISTEMAS OPERATIVOS MULTIMEDIA
SISTEMAS OPERATIVOS MULTIMEDIA
 
arquitecturas-SISD%SIMD%MISD%MIMD
arquitecturas-SISD%SIMD%MISD%MIMDarquitecturas-SISD%SIMD%MISD%MIMD
arquitecturas-SISD%SIMD%MISD%MIMD
 
Ingeniería de requisitos
Ingeniería de requisitosIngeniería de requisitos
Ingeniería de requisitos
 
GRUPO 12 Ámbito: variables locales y globales
GRUPO 12  Ámbito: variables locales y globales GRUPO 12  Ámbito: variables locales y globales
GRUPO 12 Ámbito: variables locales y globales
 
Apunte Algoritmos Geneticos
Apunte Algoritmos GeneticosApunte Algoritmos Geneticos
Apunte Algoritmos Geneticos
 
Función Hash: metodos de división y de medio Cuadrado.
Función Hash: metodos de división y de medio Cuadrado.Función Hash: metodos de división y de medio Cuadrado.
Función Hash: metodos de división y de medio Cuadrado.
 
Recursividad
RecursividadRecursividad
Recursividad
 
Herramientas para-el-analisis-de-flujo-de-datos
Herramientas para-el-analisis-de-flujo-de-datosHerramientas para-el-analisis-de-flujo-de-datos
Herramientas para-el-analisis-de-flujo-de-datos
 
Memoria virtual
Memoria virtualMemoria virtual
Memoria virtual
 
Funciones Internas
Funciones Internas Funciones Internas
Funciones Internas
 
PROGRAMACION CONCURRENTE
PROGRAMACION CONCURRENTEPROGRAMACION CONCURRENTE
PROGRAMACION CONCURRENTE
 
control de concurrencia
control de concurrenciacontrol de concurrencia
control de concurrencia
 
Algoritmos Paralelos
Algoritmos ParalelosAlgoritmos Paralelos
Algoritmos Paralelos
 
Organización y estructura interna del cpu
Organización y estructura interna del cpuOrganización y estructura interna del cpu
Organización y estructura interna del cpu
 

Similar to nombres, alcances y enlaces (lenguajes de programación)

Proyecto De Tecnica De Programacioin I I
Proyecto De Tecnica De Programacioin  I IProyecto De Tecnica De Programacioin  I I
Proyecto De Tecnica De Programacioin I I
AmistadLealtad
 
Framework .NET 3.5 04 El common language runtime
Framework .NET 3.5 04 El common language runtimeFramework .NET 3.5 04 El common language runtime
Framework .NET 3.5 04 El common language runtime
Antonio Palomares Sender
 

Similar to nombres, alcances y enlaces (lenguajes de programación) (20)

TEORIA_DE_LOS_LENGUAJES.pdf
TEORIA_DE_LOS_LENGUAJES.pdfTEORIA_DE_LOS_LENGUAJES.pdf
TEORIA_DE_LOS_LENGUAJES.pdf
 
Resumen actividades
Resumen actividadesResumen actividades
Resumen actividades
 
Int a la computacion
Int a la computacionInt a la computacion
Int a la computacion
 
Lenguaje de programacion de c++
Lenguaje de programacion de c++Lenguaje de programacion de c++
Lenguaje de programacion de c++
 
Computacion alejandro
Computacion alejandroComputacion alejandro
Computacion alejandro
 
Glosario de terminos
Glosario de terminosGlosario de terminos
Glosario de terminos
 
Taller
TallerTaller
Taller
 
Proyecto De Tecnica De Programacioin I I
Proyecto De Tecnica De Programacioin  I IProyecto De Tecnica De Programacioin  I I
Proyecto De Tecnica De Programacioin I I
 
Lenguajes lógicos definicion y funcion
Lenguajes lógicos definicion y funcionLenguajes lógicos definicion y funcion
Lenguajes lógicos definicion y funcion
 
Tutorial de visual c++
Tutorial de visual c++Tutorial de visual c++
Tutorial de visual c++
 
Tutorial de visual C++
Tutorial de visual C++Tutorial de visual C++
Tutorial de visual C++
 
Tutorial de visual_c_
Tutorial de visual_c_Tutorial de visual_c_
Tutorial de visual_c_
 
Tutorial de visual c++
Tutorial de visual c++Tutorial de visual c++
Tutorial de visual c++
 
Unidad2
Unidad2Unidad2
Unidad2
 
Programacion Basica
Programacion Basica Programacion Basica
Programacion Basica
 
Presentación de programacion
Presentación  de programacionPresentación  de programacion
Presentación de programacion
 
Sesion 2
Sesion 2Sesion 2
Sesion 2
 
Hilo de ejecución
Hilo de ejecuciónHilo de ejecución
Hilo de ejecución
 
Framework .NET 3.5 04 El common language runtime
Framework .NET 3.5 04 El common language runtimeFramework .NET 3.5 04 El common language runtime
Framework .NET 3.5 04 El common language runtime
 
Posix
PosixPosix
Posix
 

Recently uploaded

6.-Como-Atraer-El-Amor-01-Lain-Garcia-Calvo.pdf
6.-Como-Atraer-El-Amor-01-Lain-Garcia-Calvo.pdf6.-Como-Atraer-El-Amor-01-Lain-Garcia-Calvo.pdf
6.-Como-Atraer-El-Amor-01-Lain-Garcia-Calvo.pdf
MiNeyi1
 
2 REGLAMENTO RM 0912-2024 DE MODALIDADES DE GRADUACIÓN_.pptx
2 REGLAMENTO RM 0912-2024 DE MODALIDADES DE GRADUACIÓN_.pptx2 REGLAMENTO RM 0912-2024 DE MODALIDADES DE GRADUACIÓN_.pptx
2 REGLAMENTO RM 0912-2024 DE MODALIDADES DE GRADUACIÓN_.pptx
RigoTito
 
FORTI-MAYO 2024.pdf.CIENCIA,EDUCACION,CULTURA
FORTI-MAYO 2024.pdf.CIENCIA,EDUCACION,CULTURAFORTI-MAYO 2024.pdf.CIENCIA,EDUCACION,CULTURA
FORTI-MAYO 2024.pdf.CIENCIA,EDUCACION,CULTURA
El Fortí
 
Proyecto de aprendizaje dia de la madre MINT.pdf
Proyecto de aprendizaje dia de la madre MINT.pdfProyecto de aprendizaje dia de la madre MINT.pdf
Proyecto de aprendizaje dia de la madre MINT.pdf
patriciaines1993
 
Concepto y definición de tipos de Datos Abstractos en c++.pptx
Concepto y definición de tipos de Datos Abstractos en c++.pptxConcepto y definición de tipos de Datos Abstractos en c++.pptx
Concepto y definición de tipos de Datos Abstractos en c++.pptx
Fernando Solis
 

Recently uploaded (20)

ACERTIJO DE POSICIÓN DE CORREDORES EN LA OLIMPIADA. Por JAVIER SOLIS NOYOLA
ACERTIJO DE POSICIÓN DE CORREDORES EN LA OLIMPIADA. Por JAVIER SOLIS NOYOLAACERTIJO DE POSICIÓN DE CORREDORES EN LA OLIMPIADA. Por JAVIER SOLIS NOYOLA
ACERTIJO DE POSICIÓN DE CORREDORES EN LA OLIMPIADA. Por JAVIER SOLIS NOYOLA
 
origen y desarrollo del ensayo literario
origen y desarrollo del ensayo literarioorigen y desarrollo del ensayo literario
origen y desarrollo del ensayo literario
 
Estrategia de prompts, primeras ideas para su construcción
Estrategia de prompts, primeras ideas para su construcciónEstrategia de prompts, primeras ideas para su construcción
Estrategia de prompts, primeras ideas para su construcción
 
ACRÓNIMO DE PARÍS PARA SU OLIMPIADA 2024. Por JAVIER SOLIS NOYOLA
ACRÓNIMO DE PARÍS PARA SU OLIMPIADA 2024. Por JAVIER SOLIS NOYOLAACRÓNIMO DE PARÍS PARA SU OLIMPIADA 2024. Por JAVIER SOLIS NOYOLA
ACRÓNIMO DE PARÍS PARA SU OLIMPIADA 2024. Por JAVIER SOLIS NOYOLA
 
Tema 10. Dinámica y funciones de la Atmosfera 2024
Tema 10. Dinámica y funciones de la Atmosfera 2024Tema 10. Dinámica y funciones de la Atmosfera 2024
Tema 10. Dinámica y funciones de la Atmosfera 2024
 
Lecciones 05 Esc. Sabática. Fe contra todo pronóstico.
Lecciones 05 Esc. Sabática. Fe contra todo pronóstico.Lecciones 05 Esc. Sabática. Fe contra todo pronóstico.
Lecciones 05 Esc. Sabática. Fe contra todo pronóstico.
 
6.-Como-Atraer-El-Amor-01-Lain-Garcia-Calvo.pdf
6.-Como-Atraer-El-Amor-01-Lain-Garcia-Calvo.pdf6.-Como-Atraer-El-Amor-01-Lain-Garcia-Calvo.pdf
6.-Como-Atraer-El-Amor-01-Lain-Garcia-Calvo.pdf
 
BIOMETANO SÍ, PERO NO ASÍ. LA NUEVA BURBUJA ENERGÉTICA
BIOMETANO SÍ, PERO NO ASÍ. LA NUEVA BURBUJA ENERGÉTICABIOMETANO SÍ, PERO NO ASÍ. LA NUEVA BURBUJA ENERGÉTICA
BIOMETANO SÍ, PERO NO ASÍ. LA NUEVA BURBUJA ENERGÉTICA
 
Tema 8.- PROTECCION DE LOS SISTEMAS DE INFORMACIÓN.pdf
Tema 8.- PROTECCION DE LOS SISTEMAS DE INFORMACIÓN.pdfTema 8.- PROTECCION DE LOS SISTEMAS DE INFORMACIÓN.pdf
Tema 8.- PROTECCION DE LOS SISTEMAS DE INFORMACIÓN.pdf
 
2 REGLAMENTO RM 0912-2024 DE MODALIDADES DE GRADUACIÓN_.pptx
2 REGLAMENTO RM 0912-2024 DE MODALIDADES DE GRADUACIÓN_.pptx2 REGLAMENTO RM 0912-2024 DE MODALIDADES DE GRADUACIÓN_.pptx
2 REGLAMENTO RM 0912-2024 DE MODALIDADES DE GRADUACIÓN_.pptx
 
Supuestos_prácticos_funciones.docx
Supuestos_prácticos_funciones.docxSupuestos_prácticos_funciones.docx
Supuestos_prácticos_funciones.docx
 
FORTI-MAYO 2024.pdf.CIENCIA,EDUCACION,CULTURA
FORTI-MAYO 2024.pdf.CIENCIA,EDUCACION,CULTURAFORTI-MAYO 2024.pdf.CIENCIA,EDUCACION,CULTURA
FORTI-MAYO 2024.pdf.CIENCIA,EDUCACION,CULTURA
 
Proyecto de aprendizaje dia de la madre MINT.pdf
Proyecto de aprendizaje dia de la madre MINT.pdfProyecto de aprendizaje dia de la madre MINT.pdf
Proyecto de aprendizaje dia de la madre MINT.pdf
 
2024 KIT DE HABILIDADES SOCIOEMOCIONALES.pdf
2024 KIT DE HABILIDADES SOCIOEMOCIONALES.pdf2024 KIT DE HABILIDADES SOCIOEMOCIONALES.pdf
2024 KIT DE HABILIDADES SOCIOEMOCIONALES.pdf
 
Prueba de evaluación Geografía e Historia Comunidad de Madrid 4ºESO
Prueba de evaluación Geografía e Historia Comunidad de Madrid 4ºESOPrueba de evaluación Geografía e Historia Comunidad de Madrid 4ºESO
Prueba de evaluación Geografía e Historia Comunidad de Madrid 4ºESO
 
Tema 17. Biología de los microorganismos 2024
Tema 17. Biología de los microorganismos 2024Tema 17. Biología de los microorganismos 2024
Tema 17. Biología de los microorganismos 2024
 
Qué es la Inteligencia artificial generativa
Qué es la Inteligencia artificial generativaQué es la Inteligencia artificial generativa
Qué es la Inteligencia artificial generativa
 
Concepto y definición de tipos de Datos Abstractos en c++.pptx
Concepto y definición de tipos de Datos Abstractos en c++.pptxConcepto y definición de tipos de Datos Abstractos en c++.pptx
Concepto y definición de tipos de Datos Abstractos en c++.pptx
 
SELECCIÓN DE LA MUESTRA Y MUESTREO EN INVESTIGACIÓN CUALITATIVA.pdf
SELECCIÓN DE LA MUESTRA Y MUESTREO EN INVESTIGACIÓN CUALITATIVA.pdfSELECCIÓN DE LA MUESTRA Y MUESTREO EN INVESTIGACIÓN CUALITATIVA.pdf
SELECCIÓN DE LA MUESTRA Y MUESTREO EN INVESTIGACIÓN CUALITATIVA.pdf
 
SEPTIMO SEGUNDO PERIODO EMPRENDIMIENTO VS
SEPTIMO SEGUNDO PERIODO EMPRENDIMIENTO VSSEPTIMO SEGUNDO PERIODO EMPRENDIMIENTO VS
SEPTIMO SEGUNDO PERIODO EMPRENDIMIENTO VS
 

nombres, alcances y enlaces (lenguajes de programación)

  • 1. Capítulo 3. Nombres, Alcances y Enlaces Raúl José Palma Mendoza
  • 2. Capítulo 3. Nombres, Alcances y Enlaces  Los lenguajes de programación de alto nivel toman su nombre del relativo alto nivel o grado de abstracción de las funciones que proporcionan en comparación con las de los lenguajes ensambladores.  En este caso “abstracción”, desde un punto de vista práctico, se refiere al grado de separación que las funcionalidades del lenguaje tienen con respecto a cualquier arquitectura particular de hardware.
  • 3. Capítulo 3. Nombres, Alcances y Enlaces  El desarrollo inicial de lenguajes como Fortran, Algol, y Lisp fue conducido por un par de objetivos complementarios:  Independencia de la máquina.  Facilidad de programación.  Al abstraer el lenguaje del hardware, los diseñadores no sólo hicieron posible escribir programas que corriesen bien en una variedad de máquinas, sino que también los hicieron más fáciles de entender para los humanos.
  • 4. Capítulo 3. Nombres, Alcances y Enlaces  Un “nombre” es una cadena mnemónica de caracteres usado para representar algo más.  Los nombres en la mayoría de los lenguajes son tokens alfanuméricos, aunque otros símbolos como: '+' o ':=' también pueden ser nombres.  Los nombres nos permiten referirnos a variables, constantes, operaciones, tipos, etc. de forma más fácil en vez de usar conceptos de bajo nivel como las direcciones de memoria.
  • 5. Capítulo 3. Nombres, Alcances y Enlaces  Los nombres también son esenciales en el contexto del segundo significado práctico de la palabra “abstracción”, vista como el proceso en el cual el programador asigna un nombre a un fragmento de código potencialmente complejo que puede ser pensado en términos de su propósito o función más que en términos de los pasos para lograr dicho propósito.
  • 6. 3.1 Tiempo de Enlace  Un enlace es una asociación entre dos elementos: como el nombre y a lo que el nombre se refiere.  El tiempo de enlace es el momento en el cual se crea un enlace, o de forma más general, el tiempo en el cual se toma una decisión de implementación acerca de algún asunto del lenguaje (podemos verlo como el enlace entre una pregunta y su respuesta).
  • 7. 3.1 Tiempo de Enlace  Existen diferentes momentos en los que se pueden dar enlaces:  Tiempo de diseño del lenguaje: En la mayoría de los lenguajes las construcciones para el control de flujo, el conjunto fundamental de tipos, los constructores disponible para crear tipos más complejos, etc. Son elegidos en el momento en que el lenguaje es diseñado.
  • 8. 3.1 Tiempo de Enlace  Tiempo de implementación del lenguaje: La mayoría de los manuales de lenguajes dejan una variedad de asuntos a discreción del implementador del lenguaje. Por ejemplo: la precisión de tipos fundamentales, el acoplamiento de la E/S con la noción de ficheros del sistema operativo, la organización y tamaños máximos de la pila y el heap, el manejo de excepciones en tiempo de ejecución como un desborde aritmético.
  • 9. 3.1 Tiempo de Enlace  Tiempo de escritura del programa: Los programadores por supuesto escogen algoritmos, estructuras de datos y nombres, entre otros elementos.  Tiempo de compilación: Los compiladores hacen el mapeo entre las contrucciones de alto nivel y el código de máquina incluyendo la disposición en memoria de los datos definidos estáticamente.
  • 10. 3.1 Tiempo de Enlace  Tiempo de enlace: (Aunque se usa el mismo nombre este es distinto al tiempo de enlace del que trata esta sección). Dado que la mayoría de los compiladores soportan compilación separada y dependiente de la disponibilidad de una librería estándar de subrutinas, un programa no está completo hasta que varios Módulos son unidos por un enlazador.  El enlazador define la disposición de los módulos con respecto a otros y resuelve la referencias entre ellos. Cuando un nombre en un módulo hace referencia a algo en otro, el enlace entre ambos se hace hasta el tiempo de enlace (valga la redundancia).
  • 11. 3.1 Tiempo de Enlace  Tiempo de carga: Se refiere al momento en el cual el sistema operativo carga un programa en memoria de forma que pueda ejecutarse. En los sistemas opertivos más antiguos la elección de direcciones de máquina para los objetos del programa no se finalizaba hasta el tiempo de carga. En la actualidad, la mayoría de los sistemas operativos distinguen entre direcciones virtuales y direcciones físicas. Las direcciones virtuales se selecciones en el tiempo de enlace, las físicas pueden cambiar en tiempo de ejecución. El hardware de traducción de memoria del procesador hace la traducción entre direcciones virtuales y físicas en tiempo de ejecución.
  • 12. 3.1 Tiempo de Enlace  Tiempo de ejecución: es un término amplio que abarca todo el tiempo que dura la ejecución del programa. El enlace de los valores de las variables usualmente ocurre en este momento, así como otras decisiones que dependen del lenguaje. El tiempo de ejecución incluye el tiempo de arranque del programa, el tiempo de entrada a un módulo, el tiempo de elaboración (momento una declaración se ve por primera vez), el tiempo de llamada a una subrutina, el tiempo de entrada a un bloque y el tiempo de ejecución de los enunciados.
  • 13. 3.1 Tiempo de Enlace  Los tiempos de enlace son elementos de muy importancia en el diseño e implementación de los lenguajes de programación.  En general, los tiempos de enlace tempranos están relacionados con una mayor eficiencia y los tardíos con mayor flexibilidad.  Se usan los términos estático y dinámico para referirse a los enlaces antes del tiempo de ejecución y a los que se hacen en tiempo de ejecución respectivamente.
  • 14. 3.1 Tiempo de Enlace  Los lenguajes compilados tienden a hacer enlaces tempranos y los interpretados tienden a hacer enlaces tardíos. Por ejemplo un compilador analiza la sintaxis y semántica de las declaraciones de variables globales una sola vez, luego decide la disposición de estas variables en memoria y genera código eficiente para acceder a las mismas desde cualquier parte del programa. Por el contrario un intérprete puro debe analizar estas declaraciones cada vez que inicie la ejecución.
  • 15. 3.2 Tiempo de Vida y Manejo del Almacenamiento  Es importante distinguir entre los nombres y los objetos a los que se refieren, e identificar los siguientes eventos clave:  Creación de un objeto.  Creación de un enlace.  Referencias a variables, subrutinas, tipos, etc.  Activación o desactivación de los enlaces.  Destrucción de los enlaces.  Destrucción de los objetos.
  • 16. 3.2 Tiempo de Vida y Manejo del Almacenamiento  Se conoce como tiempo de vida del enlace al periodo de tiempo entre la creación y destrucción de un enlace entre un nombre y un objeto.  Similarmente el tiempo de vida de un objeto es el periodo transcurrido entre su creación y destrucción.  Este tiempos de vida no necesitan coincidir. En particular un objeto puede retener su valor y potencial de ser accedido, aún cuando no exista un nombre para hacerlo.
  • 17. 3.2 Tiempo de Vida y Manejo del Almacenamiento  Ejemplo: Cuando se pasa por referencia una variable a una subrutina, el enlace que se crea entre el nombre del parámetro y la variable tiene un tiempo de vida más corto que el de la variable en sí. También es posible, y generalmente es signo de problema, que un enlace de nombre a objeto tenga un tiempo de vida más largo que el del objeto en sí mismo. Este podría ocurrir por ejempo si un objeto creado en C++ con el operador new es pasado por referencia y luego sea desasignado (con la palabra reservada delete) antes de que se retorne de la subrutina. Un enlace a un objeto que ya “no vive” se conoce como una referencia colgante.
  • 18. 3.2 Tiempo de Vida y Manejo del Almacenamiento  El tiempo de vida de los objetos corresponde a tres mecanismos de manejo del almacenamiento:  Almacenamiento estático, en éste los objetos tienen una dirección absoluta que se mantiene durante toda la ejecución del programa.  Almacenamiento basado en pila, en éste los objetos se colocan siguiendo el orden last-in first- out usualmente en conjunción con las llamadas a subrutinas y retornos de las mismas.  Almacenamiento basado en el heap, aquí los objetos se asignan y desasignan en tiempo arbitrarios, se requiere un algoritmo más general y costoso.
  • 19. 3.2.1 Almacenamiento Estático  Las variables globales son el ejemplo más común de objetos estáticos, pero no el único. Las instrucciones que conforman el programa, también puede ser consideradas objetos estáticos. Además existen variables locales estáticas, que retienen sus valores entre una invocación y otra. Las constantes literales numéricas y cadenas también son asignadas estáticamente, para enunciados como A = B/14.7 o printf(”hola mundo!n”). (Las constantes que no ocupan mucho espacio se almacenan comúnmente dentro de la instrucción de la que forman parte, las más grandes se les asigna una ubicación aparte)
  • 20. 3.2.1 Almacenamiento Estático  Finalmente, la mayoría de los compiladores producen una variedad de tablas que son usadas para rutinas en tiempo de ejecución que soportan la depuración, chequeo dinámico de tipos, recolección de basura, manejo de excepciones y otros propósitos, todas las anteriores también asignadas de forma estática.  Los objetos asignados de forma estática cuyo valor no debería de cambiar durante la ejecución de un programa, usualmente son asignados a área de memoria protegidas y de sólo lectura, de forma que cualqueir intento inadvertido de modificarlos generará una interrupción en el procesador, permitiendo al sistema operativo anunciar el error.
  • 21. 3.2.1 Almacenamiento Estático  Ejemplo: Las variables locales generalmente son creadas cuando su subrutina es invocada y destruídas cuando ésta retorna, pero no siempre es el caso, por ejemplo en las versiones de Fortran que originalmente no soportaban la recursión (se agregó hasta Fortran 90) nunca podria existir más de una invocación de subrutina activa en un mismo instante de tiempo y por esta razón un compilador podría escojer usar almancenamiento estático para las variables locales de modo que no tuviesen que ser creadas y destruídas constantemente (simplemente inicializadas)
  • 22. 3.2.1 Almacenamiento Estático  En muchos lenguajes se requiere que las constantes nombradas (no constantes literales) tengan un valor que pueda ser determinado en tiempo de compilación. A este tipo de constantes junto con las constantes literales se les llama constantes manifiestas o constantes de tiempo de compilación.  Las constantes manifiestas o evidentes pueden ser siempre asignadas estáticamente, aún cuando sean locales a un subrutina recursiva.
  • 23. 3.2.1 Almacenamiento Estático  En otros lenguajes, las constantes son simples variables que no pueden ser cambiadas después del tiempo de elaboración. Sus valores aunque no cambien pueden depender de otros valores que no se conocen hasta el tiempo de ejecución. Estas constantes de tiempo de elaboración cuando son locales a una subrutina recursiva no pueden ser estáticas, deben almacenarse en la pila. C#, por ejemplo provee ambas opciones con las palabras reservadas const y readonly.
  • 24. 3.2.1 Almacenamiento Estático  Además de las variables locales y las constantes de tiempo de elaboración, el compilador típicamente almacena otra información asociada a la subrutina incluyendo:  Argumentos y valores de retorno. Los compiladores modernos tratan de mantienerlos en registros del procesador, pero en ocasiones se necesita memoria.  Temporales. Son valores intermedios producidos por cálculos complejos. Un buen compilador tratará de mantenerlos en registros.  Información de contabilidad. Podría incluir la dirección de retorno, una referencia al marco de pila de la subrutina que invocó a la actual (enlace dinámico), registros adicionales en memoria, información de depuración, y otros valores.
  • 25. 3.2.2 Almacenamiento Basado en Pila  Si un lenguaje permite la recursión, el almacenamiento estático de los objetos locales no es una opción, pues puede haber varias instancias de la misma variable en un instante de tiempo. El anidamiento natural de las llamadas de una subrutina hace que sea fácil almacenar el espacio para variables locales en una pila. A continuación mostramos una imagen de un píla típica simplificada, cada instancia de una subrutina en tiempo de ejecución tiene su propio marco (también llamado registro de activación), que contiene argumentos, valores de retorno, variables locales, temporales e información de contabilidad entre otros.
  • 26. 3.2.2 Almacenamiento Basado en Pila  Los argumentos que se pasarán a subrutinas subsecuentes se dejan en el tope del marco, donde la subrutina invocada puede hallarlos fácilmente. La organización del resto de la información depende de la implementación.  En la figura siguiente observamos que en cualquier momento el registro sp (stack pointer o puntero de pila) apunta a la primera ubicación libre de la pila (o a la última usada en otras máquinas) y el registros fp (frame pointer o puntero de marco) apunta a una ubicación conocida dentro del marco de la actual subrutina.
  • 28. 3.2.2 Almacenamiento Basado en Pila  El mantenimiento de la pila es responsabilidad de la “secuencia de llamada” que es el código ejecutado por la subrutina que llama justo antes de hacer la llamada y después de hacer la llamada y también es responsabilidad del prólogo (código ejecutado por la subrutina llamada al inicio de su ejecución) y del epílogo (código ejecutado por la subrutina llamada al final). En ocasiones el término “secuencia de llamada” es usado para referirse a las operaciones combinadas de la subrutina que llama, el prólogo y el epílogo.
  • 29. 3.2.2 Almacenamiento Basado en Pila  Ejercicio: ¿Por qué en algunas implementaciones de Fortran a pesar de que no se usa recursión, los implementadores prefirieron usar almacenamiento basado en pila?
  • 30. 3.2.3 Almacenamiento Basado en el Heap  El heap (o montículo) es una región de almacenamiento en la que se pueden asignar y desasignar subbloques en momentos arbitrarios. El heap es requerido para asignar piezas de datos dinámicas, como estructuras de datos enlazadas, y objetos como algunas cadenas, listas, conjuntos cuyo tamaño puede cambiar como resultado de una operación de asignación o actualización.  La asignación de objetos en el heap es realizada cuando ocurren operaciones como: la instanciación de un objeto, el agregar un elemento al fin de una lista, asignar un valor muy grande a una cadena que antes era corta, etc.
  • 31. 3.2.3 Almacenamiento Basado en el Heap  Hay varias estrategias posibles para manejar el espacio en el heap, acá revisaremos las más importantes. Las principales preocupaciones su velocidad y espacio y como es usual hay compensaciones entre ellas. En cuanto al tema del espacio este se puede sudividir en:  Fragmentación interna y  Fragmentación externa.
  • 32. 3.2.3 Almacenamiento Basado en el Heap  La fragmentación interna ocurre cuando el algoritmo de manejo del almacenamiento asigna un bloque que es más grande de lo requerido para guardar un objeto dado, el espacio extra queda sin uso.  La fragmentación externa ocurre cuando los bloques asignados están dispersos en el heap de forma que el espacio que queda sin usar está compuesto por múltiples bloques: podría haber mucho espacio libre, pero ningún bloque podría ser suficientemente grande para cumplir con una petición futura.
  • 34. 3.2.3 Almacenamiento Basado en el Heap  Muchos algoritmos de manejo de almacenamiento mantienen una única lista enlazada de bloques del heap que no están en uso. Al inicio contiene un único bloque que abarca todo el heap. En cada solicitud de almacenamiento el algoritmo busca en la lista un bloque de tamaño apropiado.  Con un algoritmo de primer ajuste se selecciona el primer bloque de la lista que tenga el tamaño suficiente para satisfacer la petición.  Con un algoritmo de mejor ajuste se busca en la lista entera el bloque más pequeño con tamaño suficiente para satisfacer la petición.
  • 35. 3.2.3 Almacenamiento Basado en el Heap  En cualquier caso, si el bloque seleccionado es significativamente más grande que lo solicitado, se divide en dos y se retorna la porción vacía a la lista como un bloque más pequeño. Si la porción que queda libre es más pequeña que algún umbral, se podría dejar como fragmentación interna. Cuando un bloque es liberado se retorna a la lista, se revisa si uno o los dos bloques físicamente adyacentes están libres, si este es el caso, se combinan en uno sólo.
  • 36. 3.2.3 Almacenamiento Basado en el Heap  Esperaríamos que un algoritmo de mejor ajuste haga un mejor trabajo a la hora de reservar bloques grandes para peticiones grandes. Al mismo tiempo, ésto algoritmo tiene un costo de asignación mayor que un algoritmo de primer ajuste, porque tiene que buscar en toda la lista y tiende a generar un mayor número de bloques pequeños sin usar. Dependiendo de la distribución de tamaños de las solicitudes cualquiera de los dos algoritmos puede generar mayor fragmentación externa.
  • 37. 3.2.3 Almacenamiento Basado en el Heap  En cualquier algoritmo de mejor ajuste que mantenga una única lista de bloques libres el costo de asignación es lineal al número de bloques en la lista. Para reducir este costo a uno constante, algunos algoritmos mantienen varias listas para bloques de diferentes tamaños. Cada solicitud es redondeada al siguiente tamaño estándar y asignada de un bloque de la lista apropiada.  En efecto, el heap queda dividido en grupos de tamaño estándar. Esta división puede ser estática o dinámica.
  • 38. 3.2.3 Almacenamiento Basado en el Heap  Dos mecanismos comunes para hacer ajuste dinámico de las listas se conocen como el sistema de colegas (buddy system) y el heap de Fibonacci. En el sistema de colegas, los tamaños estándar de bloque son potencias de dos. Si se ocupa un bloque de tamaño 2k pero no hay ninguno disponible un bloque de tamaño 2k+1 se divide en dos, una de las mitades es usada para satisfacer la petición y la otra se coloca en la lista de los bloques de tamaño 2k . Cuando el bloque es liberado se vuelve a unir con su colega si éste está libre.
  • 39. 3.2.3 Almacenamiento Basado en el Heap  Los heaps de Fibonacci son similares pero usan números de Fibonacci como tamaños estándar en vez de potencias de dos. El algoritmo es un poco más complejo pero lleva a menos fragmentación interna porque los número de Fibonacci crecen más lento que las potencias de 2.
  • 40. 3.2.3 Almacenamiento Basado en el Heap  El problema con la fragmentación externa es la que la habilidad del heap para satisfacer las peticiones se puede ir degradando en el tiempo.  El uso de múltiples listas puede ayudar, pero no eliminan el problema. Siempre será posible generar una secuencia de peticiones que no puede ser satisfecha aún cuando el espacio total requerido sea menor que el tamaño del heap.
  • 41. 3.2.3 Almacenamiento Basado en el Heap  Si la memoria es particionada de forma estática lo único que se necesita es exceder el número máximo de solicitudes de un tamaño específico.  Si la memoria se reajusta de forma dinámica se puede hacer al heap un gran cantidad de solicitudes de bloques pequeños y luego desasignar algunos tomando en cuenta su ubicación física dejano un patrón alternante de pequeños bloques asignados.
  • 42. 3.2.3 Almacenamiento Basado en el Heap  Para eliminar la fragmentación externa debemos de estar preparados para compactar el heap y mover bloques que ya están asignados. Esta tarea es complicada por la necesidad de encontrar y actualizar todas las referencias a un bloque que se desea mover.
  • 43. 3.2.4 Recolección de Basura  Así como la asignación de objetos en el heap ocurre por algún evento explícito, la desasignación también puede ser explícita en algunos lenguajes (ej.: C, C++, y Pascal). Pero muchos lenguajes hacen la desasignación de forma implícita cuando ya no es posible acceder a ellos desde ninguna variable del programa. La librería de tiempo de ejecución para estos lenguajes debe entonces proveer un mecanismo de recolección de basura para identificar y reclamar éstos objetos inalcanzables. La mayoría de los lenguajes funcionales y de scripting requieren un recolector de basura así como muchos lenguajes imperativos modernos como Modula-3, Java, y C#.
  • 44. 3.2.4 Recolección de Basura  Los argumentos tradicionales en favor de la desasignación explícita son la simplicidad de la implementación y la velocidad de ejecución. Las implementaciones más sencillas de recolección de basura agregan un complejidad significante a un lenguaje con un rico sistema de tipos e incluso los recolectores de basura más sofisticados pueden consumir un tiempo no trivial en ciertos programas. Si el programador puede indentificar correctamente el fin del tiempo de vida de un objeto si llevar mucha contabilidad en tiempo de ejecución, el resultado tendería ser una ejecución más rápida.
  • 45. 3.2.4 Recolección de Basura  A través del tiempo los diseñadores y los implementadores de lenguajes han ido considerando más la recolección de basura como una característica esencial en un lenguaje de programación.  Los algoritmos de recolección de basura han mejorado reduciendo su carga y además como las implementaciones en general se han vuelto más complejas, se ha reducido la complejidad relativa de la recolección automática.  Las aplicaciones más innovadores se han vuelto más grandes y complejas, haciendo que los beneficios de la recolección de basura sean muy convincentes.
  • 46. 3.3 Reglas de Alcance  El alcance de un enlace es la región textual de un programa donde éste está activo.  En la mayoría de los lenguajes modernos este alcance es determinado estáticamente, en tiempo de compilación. En C, por ejemplo, se introduce un nuevo alcance en la entrada a una subrutina, se crean enlaces para objetos locales y se desactivan enlaces para objetos globales que son ocultados por objetos locales con el mismo nombre. Al salir se reactivan los enlaces de los objetos globales ocultos.
  • 47. 3.3 Reglas de Alcance  Estan manipulaciones de los enlaces a primera vista parecen ser operaciones en tiempo de ejecución, pero no requiere ningún código especial, las porciones de un programa en las que un enlace está activo se determinan completamente en tiempo de compilación.  Debido a que podemos observar un programa en C y saber qué nombres se refieren a qué objetos en cualquier punto del programa basándos únicamente en reglas textuales es que podemos decir que C tiene alcance estático o alcance léxico o lexicográfico.
  • 48. 3.3 Reglas de Alcance  Lenguajes como APL, Snobol y los primeros dialectos de Lisp tienen alcance dinámico, sus enlaces dependen del flujo de instrucciones en tiempo de ejecución.  Además de hablar del “alcance de un enlace” también se usa el término alcance por sí mismo sin un enlace específico en mente. Informalmente, un alcance es una región de un programa de tamaño máximo en que los enlaces no cambian (o al menos ninguno es destruído). Típicamente un alcance el es cuerpo de un módulo, una clase, una subrutina o un enunciado de control de flujo estructurado a veces llamado bloque.
  • 49. 3.3 Reglas de Alcance  Algol 68 y Ada usando el término elaboración para referirse al proceso en el cual las declaraciones se activan cuando el control entra a un alcance, la elaboración implica la creación de enlaces, la asignación de espacio en la pila para objetos locales y posiblemente la asignación de valores iniciales.
  • 50. 3.3 Reglas de Alcance  Llamamos ambiente de referencia al conjunto de enlaces activos en un punto de la ejecución del programa. Este conjunto es principalmente determinado por reglas de alcance dinámicas o estáticas.  Un ambiente de referencia generalmente corresponde a una secuencia de alcances que pueden ser examinados (según un orden) para encontrar el enlace actual para un nombre dado.
  • 51. 3.3.1 Alcance Estático  En un lenguaje con alcance estático (léxico), típicamente el enlace actual para un nombre dado es encontrado en la declaración correspondiente cuyo bloque rodea de forma más cercana un punto dado en el programa. Aunque como veremos hay varias variantes de esta regla básica.
  • 52. 3.3.1 Alcance Estático  La regla más básica de alcance estático es probablemente la de las primeras versiones de Basic, en donde sólo existía un único alcance, y éste era global. De hecho, son había unos cientos de nombres posibles, cado uno consistía en una letra seguida opcionalmente por un dígito. No había declaraciones explícitas, las variables eran declaradas de forma implícita en virtud de su uso.
  • 53. 3.3.1 Alcance Estático  Las reglas de alcance son un poco más complejas en Fortran (pre-Fortran 90). Fortran distingue entre variables locales y globales. El alcance de una variable local está limitado a la subrutina en la que aparece, y no es visible en ningún lugar más. Las declaraciones de variables son opcionales. Si una variable no está declarada se asume que es local a la subrutina actual y de tipo entero si su nombre inicia con las letras entre la I y la N inclusive o real de otra forma.
  • 54. 3.3.2 Subrutinas Anidadas  La habilidad para anidar las subrutinas dentro de otras, introducida en Algol 60, es una característica de varios lenguajes modernos, como ser Pascal, Ada, ML, Python, Scheme, Common Lisp y Fortran 90 (con una extensión limitada). Otros lenguajes incluyendo C y sus descendientes permiten que se aniden clases y otros alcances. Generalmente las constantes, tipos, variables o subrutinas declaradas dentro de un bloque no son visibles fuera de ese bloque en lenguajes tipo Algol.
  • 55. 3.3.2 Subrutinas Anidadas  Formalmente, el anidamiento estilo Algol da pie a la regla de alcance anidado más cercano para enlace de nombres a objetos que implica que un nombre que es introducido en una declaración es conocido en el alcance en el cual es declarado y en cada alcance anidado de éste a menos que sea oculto por otra declaración del mismo nombre en uno o más alcances anidados.
  • 56. 3.3.2 Subrutinas Anidadas  Para encontrar el objeto que corresponde a un nombre dado, se busca un declaración con ese nombre en el alcance actual más interno. Si esta existe, esta define el enlace activo para el nombre. Si no existe, se una busca una declaracion en el alcance que rodea inmediatamente al actual, y sino se encuentra acá se continua en sucesivos alcances circudantes hasta que se llegue al nivel más exterior, donde se declaran los objetos globales. Si no se encuentra en este nivel, se anuncia un error.
  • 57.
  • 58. 3.3.2 Subrutinas Anidadas  Muchos lenguajes proveen una colección de objetos predefinidos como rutinas de E/S, funciones matemáticas y tipos como enteros y caracteres. Es comun considerar que éstos están declarados en un alcance extra, invisible y más externo que rodea al alcance donde se declaran los objetos globales. Por tanto la búsqueda de enlaces en el párrafo anterior terminaría en este alcance extra más externo. Esta convención permite al programador definir un objeto global cuyo nombre sea el mismo de algún objeto predefinido, ocultándolo y haciéndolo inusable.
  • 59. 3.3.2 Subrutinas Anidadas  De un enlace de nombre a objeto que ha sido oculto por una declaración anidada se dice que tiene un agujero en su alcance.  En la mayoría de los lenguajes el objeto cuyo nombre ha sido oculto es innaccesible en el alcance interno (a menos que tenga más de un nombre). Algunos lenguajes permiten al programador acceder al significado externo de un nombre al aplicar un cualificador o un operador de resolución de alcance.
  • 60. 3.3.2 Subrutinas Anidadas  En Ada por ejemplo, un nombre puede ir predecido por el nombre del alcance en el cual fue declarado, usando una sintaxis similar a la del acceso a los campos de un registro. Por ej.: mi_proc.X, se refiere a la declaración de X en la subrutina mi_proc, aún cuando haya otra declaración de X más interna.
  • 61. 3.3.2 Subrutinas Anidadas  El compilador organizará al registro puntero de marco de forma que siempre apunte al marco de la subrutina que se esté ejecutando. Usando este registro como base para el direccionamiento (registro más un offset), el código meta puede acceder a objetos dentro del actual marco.  Pero ¿qué se hace para acceder a objetos en subrutinas que rodean lexicamente a la actual?
  • 62. 3.3.2 Subrutinas Anidadas  Para acceder a estos objetos necesitamos una forma de encontrar los marcos que correspondan a estos alcances en tiempo de ejecución.  Como un subrutina anidada puede llamar a otra que esté en un alcance externo, el orden de los marcos de la pila en tiempo de ejecución no corresponde necesariamente al orden del anidamiento léxico. Sin embargo podemos estar seguros que el marco para el alcance circundante está en la pila pues la actual subrutina no podría haber sido llamada a menos que fuese visible y sólo puede ser visible si el alcance circundante está activo.
  • 63. 3.3.2 Subrutinas Anidadas  Entonces, la foma más fácil de encontrar los marcos de los alcances circundantes es manteniendo un enlace estático en cada marco que apunte al marco “padre”, el marco de la invocación más reciente de la subrutina circundante. Si una subrutina es declarada en el alcance más externo entonces su marco tendrá un enlace estático nulo. Si una subrutina está anidada en k niveles, entonces el enlace estático de su marco, y los de su padre y abuelo y todos los antecesores formarán una cadena estática de longitud k en tiempo de ejecución. Para encontrar una variable o parámetro declarado j alcances hacia afuera, el código meta en tiempo de ejecución puede desreferenciar la cadena estática j veces y luego agregar el offset apropiado.
  • 65. 3.3.3 Orden de las Declaraciones  En nuestra discusión hasta este punto hemos pasado por lato un asunto importante: suponga que un objeto x está declarado en algún lugar dentro de un bloque B. La pregunta que surge es la siguiente ¿el alcance de x incluye la porción de B antes de la declaración y por tanto x puede realmente ser usada en esta porción de código?  Varios de los primeros lenguajes de alto nivel incluyendo Algol 60 y Lisp requerían que todas las declaraciones apareciesen al inicio del alcance. Al inicio podríamos pensar que esta regla evitaría la pregunta que nos planteamos anteriormente, pero no porque las declaraciones se pueden referir unas a otras.
  • 66. 3.3.3 Orden de las Declaraciones  Ejemplo: En un intento aparente de simplificar la implementación del compilador, Pascal estableció que nombres deben ser declarados antes de ser usados (con mecanismos especiales para acomodar tipos recursivos y subrutinas). Al mismo tiempo retuvo la noción de que el alcance de una declaración es el bloque circundante entero. Estas dos reglas pueden interactuar de formas sorprendentes como lo muestra el código a continuación.
  • 67. 3.3.3 Orden de las Declaraciones const N = 10; ... procedure foo; const M = N; (* static semantic error! *) ... N = 20; (* local const declaration; hides the outer N *) En este caso, Pascal dice que la segunda declaración de N cubre todo el procedimiento foo, así que el analizador semántico indica en la linea “M = N;” que N está siendo usado antes de ser declarado, cuando probablemente el programador se intentaba referir a la primera N.
  • 68. 3.3.3 Orden de las Declaraciones  Ejemplo: Algunos compiladores de Algol 60 procesaban las declaraciones de un alcance en el orden en que estaban escritas. Esta estrategia tenía el desafortunado efecto de prohibir de forma implícita los tipos y subrutinas mutuamente recursivas, algo que iba en contra de la intención de los diseñadores del lenguaje.
  • 69. 3.3.3 Orden de las Declaraciones  Para determinar la validez de cualquier declaración que aparente usar el nombre de un alcance circundante un compilador de Pascal debe escanear el resto de las declaraciones de un alcance para ver si el nombre no ha sido oculto. Para evitar esta complicación la mayoría de los sucesores de Pascal (incluyendo algunos dialectos del mismo) especifican que el alcance de un identificador no es el boque entero en el cual está declarado (excluyendo sus agujeros) sino la porción de ese bloque desde la declaración hasta el fin del mismo (nuevamente excluyendo los agujeros). Si el fragmento de código anterior hubiese sido escrito en C, C++ o Java no se hubiese reportado ningún error semántico.
  • 70. 3.3.3 Orden de las Declaraciones  Ejemplo: C++ y Java permiten que se rompa de la regla de “definir antes de usar” en muchos casos. En ambos lenguajes los miembros de una clase (incluyendo aquellos que no son definidos sino hasta más adelante en el texto del programa) son visibles en todos los métodos de la clase. En Java, las clases en sí mismas pueden ser declaradas en cualquier orden.
  • 71. 3.3.3 Orden de las Declaraciones  Ejemplo: De forma interesante en C# aunque, al igual que en Java, se requiere la declaración antes de usar variables locales (pero no clases o miembros), se toma la noción de Pascal del alcance en todo el bloque, de forma que el siguiente código es inválido en C#: class A { const int N = 10; void foo() { const int M = N; // Se usa N antes de su declaración const int N = 20; ...
  • 72. 3.3.3 Orden de las Declaraciones  Ejemplo: Quizá el enfoque más simple en lo que respecta al orden de las declaraciones desde un punto de vista conceptual, es el de Modula-3, que dice que el alcance de una declaración es el bloque entero donde aparece (menos los agujeros) y que el orden de las declaraciones no importa. La principal objeción a este enfoque es que los programadores podría encontrar contraintuitivo el uso de una variable local antes de que sea declarada.
  • 73. 3.3.3 Orden de las Declaraciones  Ejemplo: Python lleva la regla de alcance de “todo el bloque” un paso más adelante, dispensando las declaraciones de variables. En vez de éstas adopta la convención inusual de que las variables locales de la subrutina S son precisamente aquellas variables que son modificadas por algún enunciado en el cuerpo (estático) de S. Si S está anidada dentro de T, y el nombre x aparece en el lado izquierdo de enunciados de asignación tanto en S como en T, entonces hay dos x distintas: una en S y otra en T. Las variables no locales son de sólo lectura a menos que se les importe de forma explícita (usando el enunciado global de Python).
  • 74. 3.3.3 Orden de las Declaraciones  Ejemplo: Los tipos y subrutinas recursivas introducen un problemas para los lenguajes que requieren “declaración ante de uso”: ¿cómo pueden dos declaraciones aparecer ambas antes que la otra? C y C++ manejan el problema distinguiendo entre la declaración de un objeto y su definición. Una declaración introduce un nombre e indica el alcance, pero puede omitir ciertos detalles de implementación. Una definción describe el objeto con suficiente detalle para el compilador como para que éste determine su implementación. Si una declaración no está suficientemente detallada para ser una definición, entonces se necesita que aparezca una definición detallada en algún lugar del alcance.
  • 75. 3.3.3 Orden de las Declaraciones En C podemos escribir: struct manager; /* declaration only */ struct employee { struct manager *boss; struct employee *next_employee; ... }; struct manager { /* definition */ struct employee *first_employee; ... };
  • 76. 3.3.3 Orden de las Declaraciones void list_tail(follow_set fs); /* declaration only */ void list(follow_set fs) { switch (input_token) { case id : match(id); list_tail(fs); ... } void list_tail(follow_set fs) /* definition */ { switch (input_token) { case comma : match(comma); list(fs); ... }
  • 77. 3.3.3 Orden de las Declaraciones  Ejemplo: En muchos lenguajes incluyendo Algol 60, C89, y Ada, las variables locales deben ser declaradas no sólo al inicio de inicio de una subrutina sino en el tope de cualquier bloque. Otros lenguajes incluyendo Algol 68, C99, y todos los descendientes de C, son aún más flexibles, permitiendo declaraciones donde sea que éstas aparezcan. En la mayoría de los lenguajes una declaración anidada oculta cualquier declaración externa con el mismo nombre (Java y C# lanzan un error semántico estático si la declaración externa es local a la subrutina actual).
  • 78. 3.3.3 Orden de las Declaraciones  Ejemplo: Las variables declaradas en bloques anidados pueden ser muy útiles, como por ejemplo en el siguiente código en C: { int temp = a; a = b; b = temp; } El mantener la declaración de temp adyacente al código que la usa hace que el programa sea más fácil de leer y elimina la posibilidad que este código interfiera con otra variable llamada temp.
  • 79. 3.3.3 Orden de las Declaraciones No se necesita trabajo en tiempo de ejecución para asignar o desasignar variables declaradas en bloques anidados, su espacio puede ser incluído en el espacio total para variables locales asignadas en el prólogo de la subrutina y desasignadas en el epílogo de la misma.
  • 80. 3.3.4 Módulos  Un gran reto en la construcción de cualquier software grande es determinar cómo dividir el esfuerzo entre varios programadores de forma tal que el trabajo pueda realizarse en varios elementos del programa de forma simultánea.  Hacer esto es hacer un esfuerzo de modularización que depende sobretodo en el concepto de “ocultamiento de la información”, que implica que objetos y algoritmos sean invisibles dentro de lo posible a porciones del sistema que no los necesitan.
  • 81. 3.3.4 Módulos  Un código modularizado correctamente reduce la “carga cognitiva” del programador al minimizar la cantidad de información necesaria para entender cualquier parte del sistema.  Un programa bien diseñado trata de que las interfaces entre sus módulos sean lo más simples posible tratando de que los cambios en el diseño queden ocultos en un sólo módulo, este último punto es crucial para hacer mantenimiento del programa.
  • 82. 3.3.4 Módulos  Además de reducir la “carga cognitiva” el ocultamiento de la información reduce el riesgo de que halla conflictos de nombres, pues reduce la cantidad de nombre visibles, y también proteje la integridad de las abstracciones de datos: cualquier intento de acceder a un objeto fuera de la subrutina a la que pertenecen generará un error. Finalmente también ayuda a compartimentalizar los errores en tiempo de ejecución: si una variable toma un valor incorrecto sabemos que fue modificada por el código dentro de su alcance.
  • 83. 3.3.4 Módulos  Desafortunadamente el ocultamiento de la información proporcionada por la subrutinas anidadas está limitado a objetos con un tiempo de vida igual al de la instancia de una subrutina. Una solución parcial a esto son las variables estáticas (own en Algol 60, static en C y Java).  Podemos decir que las variables estáticas proporcionan un forma de construir abstracciones con una única subrutina, pero no más de una. Por ejemplo se deseasemos construir una abstracción pila, nos gustaría ocultar su estructura interna al resto del programa pero que se pueda acceder a ella a través de las subrutinas push (mete) y pop (saca).
  • 84. 3.3.4 Módulos  Para poder construir abstracciones con varias subrutinas en su interface, muchos lenguajes de programación proporcionan un construcción módulo.  Un módulo permite que una colección de objetos (subrutinas, variables, tipos, etc.) sean encapsulados de forma que:  Los objetos del interior son visibles entre sí.  Los objetos del interior no son visibles al exterior a menos que sean exportados explícitamente.  Los objetos del exterior no son visibles en el interior a menos que sean importados explícitamente (se cumple en varios lenguajes).
  • 85. 3.3.4 Módulos  Note que las reglas planteadas afectan sólo la visibilidad de los objetos y no su tiempo de vida.  Los módulos aparecen en la mayoría de los lenguajes modernos con diferentes nombres, por ejemplo en Modula 1 al 3 se les llama módulos, en Ada, Java y Perl se les llama paquetes, en C++, C# y PHP se llaman espacios de nombre (namespace). En C se pueden emular hasta cierto grado usando las capacidades de compilación separada que presta el lenguaje.
  • 86. 3.3.4 Módulos  Ejemplo: A continuación vemos una abstracción pila en Modula-2. Como los modulos no afectan el tiempo de vida de los enlaces, éstos se vuelven inactivos al salir del módulo pero no se destruyen. En el ejemplo a continuación el tiempo de vida de las variables “s” y “top” hubiese sido el mismo sino estuviesen declaradas dentro del módulo, claro ahora dentro del mismo sólo son visibles desde el código de las subrutinas: “pop” y “push”.
  • 87. 3.3.4 Módulos CONST stack_size = ... TYPE element = ... ... MODULE stack; IMPORT element, stack_size; EXPORT push, pop; TYPE stack_index = [1..stack_size]; VAR s : ARRAY stack_index OF element; top : stack_index; (* first unused slot *)
  • 88. 3.3.4 Módulos PROCEDURE error; ... PROCEDURE push(elem : element); BEGIN IF top = stack_size THEN error; ELSE s[top] := elem; top := top + 1; END; END push;
  • 89. 3.3.4 Módulos PROCEDURE pop() : element; BEGIN IF top = 1 THEN error; ELSE top := top - 1; RETURN s[top]; END; END pop; (* Código de inicialización *) BEGIN top := 1; END stack; (* Uso de la pila *) VAR x, y : element; ... push(x); ... y := pop;
  • 90. 3.3.4 Módulos  La mayoría de los lenguajes basados en módulos permiten al programador especificar que ciertos nombres exportados sean usados sólamente de formas restringidas, por ejemplo las variables podrían ser exportadas como de sólo lectura, los tipos podrían ser exportados de forma opaca de modo que las variables de ese tipo puedan ser declaradas, pasadas como argumentos a las subrutinas del módulo y posiblemente comparadas o asignadas entre ellas mismas, pero no puedan ser manipuladas de otra forma.
  • 91. 3.3.4 Módulos  Podemos clasificar los módulos según su apertura hacia los nombres en alcances exteriores de la siguiente forma:  Los módulos en los que los nombres deben ser importados de forma explícita para poder ser usados se conocen como alcances cerrados, ejemplo: Modula (1, 2 y 3) y Haskell.  Por extensión los módulos que no requieren la importación explícita se conocen como alcances abiertos.
  • 92. 3.3.4 Módulos  Una opción cada vez más común, que se da en lenguajes como Ada, Java, C# y Phyton, es la de los módulos selectivamente abiertos, en estos módulos un nombre como “foo” exportado desde un módulo A es automáticamente visible en un módulo B como “A.foo”; además puede ser visible simplemente como “foo” si B lo importa explícitamente.  La importaciones sirve para documentar el programa, incrementan la modularidad al requerir que un módulo especifique la forma en que depende del resto del programa. Además reducen los conflictos de nombres al no importar nada que no se ocupe.
  • 93. 3.3.4 Módulos  A diferecia de los módulos, las subrutinas son usualmente alcances abiertos en la mayoría de los lenguajes de la familia de Algol, excepciones importantes a esta reglas son Euclid en el cual tanto módulos como subrutinas son cerrados y Turing, Modula-1 y Perl en los que las subrutinas son opcionalmente cerradas (si se hacen imporatciones explícitas ningún otro nombre no local será visible) y Clu que prohibe el uso de variables no locales completamente.  Como en el caso de los módulos las listas de importación en subrutinas sirven para documentar. De forma que la mayoría de los diseñadores de lenguajes han decidido que la documentación no vale la incoveniencia.
  • 94. 3.3.4 Módulos  Módulos como manejadores Los módulos facilitan la construcción de abstracciones permitiendo que los datos se hagan privados a las subrutinas que los usan. Pero cuando son usados como el ejemplo de la pila que anteriormente vimos, cada módulo define una única abstracción. Si quisiésemos tener varias pilas, tendremos en general que convertir el módulo en un manejador de instancias de tipo pila, y exportar dicho tipo desde el módulo, como se muestra en la figura a continuación. El hacer esto implica también que se creen rutinas adicionales para crear/inicializar la pila y posiblemente para destruir instancias de la misma y requiere que cada subrutina tome un extra parámetro para especificar la pila en cuestión.
  • 95. 3.3.4 Módulos CONST stack_size = ... TYPE element = ... MODULE stack_manager; IMPORT element, stack_size; EXPORT stack, init_stack, push, pop; TYPE stack_index = [1..stack_size]; stack = RECORD s : ARRAY stack_index OF element; top : stack_index; (* first unused slot *) END;
  • 96. 3.3.4 Módulos PROCEDURE init_stack(VAR stk : stack); BEGIN stk.top := 1; END init_stack; PROCEDURE push(VAR stk : stack; elem : element); BEGIN IF stk.top = stack_size THEN error; ELSE stk.s[stk.top] := elem; stk.top := stk.top + 1; END; END push;
  • 97. 3.3.4 Módulos PROCEDURE pop(VAR stk : stack) : element; BEGIN IF stk.top = 1 THEN error; ELSE stk.top := stk.top - 1; RETURN stk.s[stk.top]; END; END pop; END stack_manager; var A, B : stack; var x, y : element; ... init_stack(A); init_stack(B); ... push(A, x); ... y := pop(B);
  • 98. 3.3.5 Tipos Módulos y Clases  Una solución alternativa al problema de las múltiples instancias puede ser encontrada en Simula, Euclid, y (en un sentido un poco diferente) ML, que tratan los módulos como tipos en vez de contrucciones de encapsulación. Dado un tipo módulo, el programador puede declarar un arbitrario número de objetos módulo similares. A continuación se muestra el esquema de una pila en Euclid, que como vemos permita al programador proporcionar código de inicialización que se ejecuta cada vez que una pila es creada, además Euclid permite definir código de finalización que se ejecuta al final del tiempo de vida del módulo, características que es necesaria cuando se almacenan elementos en el heap y necesitan ser removidos.
  • 99. 3.3.5 Tipos Módulos y Clases const stack_size := ... type element : ... type stack = module imports (element, stack_size) exports (push, pop) type stack_index = 1..stack_size var s : array stack_index of element top : stack_index procedure push(elem : element) = ... function pop returns element = ... ...
  • 100. 3.3.5 Tipos Módulos y Clases initially top := 1 end stack var A, B : stack var x, y : element ... A.push(x) ... y := B.pop
  • 101. 3.3.5 Tipos Módulos y Clases  La diferencia entre las aproximaciones de módulos como manejadores y los tipos módulos consiste en que con los tipos módulos el programador puede pensar en la subrutinas como “pertenecientes” a la pila en cuestión (A.push(x)) en vez de como entidades externas a las que la pila es pasada como argumento (push(A,x)).
  • 102. 3.3.5 Tipos Módulos y Clases  En el caso de los tipos módulos, conceptualmente se ve a las subrutinas como operaciones separadas pero en la práctica no es necesario tener copias separadas del mismo código, de modo que todas las pilas comparten las mismas operaciones. El compilador logra esto haciendo que se envíe un puntero a la pila en cuestión como un parámetro extra y oculto a las subrutinas. De modo que la implementación termina siendo muy similar a la de los módulos como manejadores, pero el programador no necesita pensarlo de esa forma.
  • 103. 3.3.5 Tipos Módulos y Clases  Compilación separada Uno de los distintivos de una buena abstracción es que es útil en varios contextos. Para facilitar la reutilización de código muchos lenguajes hacen de los módulos la base para la compilación por separado. Los módulos en muchos lenguajes (por ejemplo Modula-2 y Oberon) pueden ser dividos en una parte de declaración (header) y una de implemetación (body), cada una una en archivos separados.
  • 104. 3.3.5 Tipos Módulos y Clases El código que usa las exportaciones de un módulo dado puede ser compilado con sólo que exista el “header”, no depende del “body”. En particular, el trabajo en los cuerpos de módulos cooperantes puede proceder paralelamente una vez que los headers existan.
  • 105. 3.3.5 Tipos Módulos y Clases  Orientación a Objetos Como una extensión de la aproximación de tipos módulos a la abstracción de datos, muchos lenguajes proveen una construcción clase para la programación orientada a objetos. De entrada, podemos pensar a la clases como tipos módulos aumentados con un mecanismo de herencia.
  • 106. 3.3.5 Tipos Módulos y Clases La herencia permite definir nuevas clases como extensiones o especializaciones de clases existentes, permitiendo un estilo de programación en el que todas o la mayoría de las operaciones se piensan como pernecientes a los objetos. Las clases tiene su origen en Simula-67 y son la innovación central en los lenguajes orientados a objetos como Smalltalk, Eiffel, C+ +, Java y C#, también son fundamentales en lenguajes de scripting como Phyton y Ruby.
  • 107. 3.3.5 Tipos Módulos y Clases  Módulos que contienen clases A pesar de que existe un claro avance desde los módulos a los tipos módulos y a las clases, esto no significa que las clases son un reemplazo adecuado para los módulos en todos los casos. Por ejemplo: suponga que se está desarrollando un complejo videojuego. Una jerarquía de clases será justo lo que se necesita para representar a los personajes, objetos que se puedan adquirir, edificios, metas, etc.
  • 108. 3.3.5 Tipos Módulos y Clases Pero al mismo tiempo, especialmente en un proyecto con un amplio equipo de programadores se querrá dividir la funcionalidad del juego entre subsistemas de gran tamaño como la parte de gráficos y renderizado, física, estrategia, etc. Estos subsistemas no son en realidad abstracciones y probablemente no se desee crear múltiples instancias de los mismos. Éstos son naturalmente realizables a través de módulos. Muchas aplicaciones tienen una necesidad similar tanto de abstracciones con múltiples instancias como de la subdivisión funcional, es por esto que muchos lenguajes incluyendo C++, Java, C#, Python, y Ruby, proveen mecanismos tanto para las clases como para los módulos.
  • 109. 3.3.6 Alcance Dinámico  En un lenguaje con alcance dinámico, los enlaces entre nombres y objetos dependen del flujo de control en tiempo de ejecución y en particular del orden en que las subrutinas sean llamadas. Las reglas de alcance son más simples que en el alcance estático pues el enlace para un nombre no local se determina buscando en los alcances encontrados más recientemente en la ejecución que aún no hayan finalizado su ejecución  Algunos ejemplos de lenguajes con alcance dinámico son APL, Snobol, TEX, Perl y los primeros dialectos de Lisp.
  • 110. 3.3.6 Alcance Dinámico  Debido a que el flujo del control no puede en general ser predecido, los enlaces entre los nombres y objetos en un lenguaje con enlace dinámico no pueden ser determinados por un compilador. Como resultado, muchas reglas semántica pasan de ser semántica estática a semántica dinámica. La revisión de tipos en expresiones y revisión de argumentos en llamadas a subrutinas, por ejemplo, deben en general ser realizadas hasta el tiempo de ejecución. Para acomodar todas estas revisiones, los lenguajes con alcance dinámico tienden a ser interpretados más que compilados.
  • 111. 3.3.6 Alcance Dinámico  Ejercicio: Dado el siguiente programa ¿cuál sería su salida en la caso de que el lenguaje tuviese un alcance estático y cuál sería esta salida si el alcance fuese dinámico?
  • 112. 3.3.6 Alcance Dinámico n : integer -- global procedure first n := 1 -- local procedure second n : integer first() procedure main n := 2 if read_integer() > 0 second() else first() write_integer(n)
  • 113. 3.3.6 Alcance Dinámico  Ejemplo: Usando alcance dinámico es posible que no se detecten errores asociados con el ambiente de referencia hasta el tiempo de ejecución, en el código a continuación, la variable local max_score en el prodecimiento foo accidentalmente redefine una variable global usada por la función scaled_score, que es luego llamada por foo. Como el max_score global es un entero y el local es un flotante, las revisión de semántica dinámica en algunos lenguajes generará en un error en la conversión de tipos en tiempo de ejecución. Además si la variable local también fuese un entero, no se detectaría ningún error, pero el programa posiblemente produciría resultados no deseados, que es un error más difícil de encontrar.
  • 114. 3.3.6 Alcance Dinámico max_score : integer -- maximum possible score function scaled_score(raw_score : integer) : real return raw_score / max_score * 100 procedure foo max_score : real := 0 … foreach student in class student.percent := scaled_score(student.points) ...
  • 115. 3.3.6 Alcance Dinámico  Ejemplo: El principal argumento a favor del alcance dinámico es que facilita la personalización de las subrutinas. Suponga que se tiene una rutina de librería print_integer que imprime su argumento en varias bases (decimal, binaria, hexadecimal, etc.) y además suponga que la mayor parte de las veces se desea que se use la notación decimal, así que no se desea especificar una base en cada llamada individual.
  • 116. 3.3.6 Alcance Dinámico Esto se puede lograr con el alcance dinámico al hacer que print_integer obtenga su base de una variable no local print_base, se puede establecer un valor por defecto al declarar una variable print_base igual a 10 en un alcance al inicio de la ejecución y luego cada vez que se quiera cambiar la base temporalmente se puede escribir: begin -- bloque anidado print_base : integer := 16 -- base hexadecimal print_integer(n)
  • 117. 3.3.6 Alcance Dinámico Lo anterior se podría lograr en un lenguaje con alcace estático usando dos procedimientos, por ejemplo print_integer y print_with_base, o sobrecargando el mismo con varios parámetros o también sino se desea tener varios procedimientos se podría utilizar una variable global definiéndola con un valor de 10 y luego asignarle un valor distinto antes de hacer cada llamado, claro sin olvidar restablecer el valor original después de la llamada, finalmente sino se desea la declaración global se podría declarar una variable estática encapsulada con print_integer en un módulo.
  • 118. 3.4 Implementando el Alcance  Para darle seguimiento a los nombres en un alcance estático el compilador utiliza una abstraccion de datos llamada tabla de símbolos, que en esencia es un diccionario, pues mapea los nombres con la información que el compilador tiene sobre los mismos. La operaciones más básicas son la inserción de un nuevo elemento (enlace de nombre a objeto) o la de buscar la información de un objeto dado un nombre.
  • 119. 3.4 Implementando el Alcance  Las reglas de alcance estático agregan complejidad pues permiten que un mismo nombre corresponda a diferentes objetos in diferentes partes del programa. La mayoría de estas variaciones son manejadas aumentando el estilo básico de diccionario de la tabla de símbolos con las operaciones enter_scope y leave_scope para darle seguimiento a la visibilidad.  Nada se borra de la tabla, la estructura se mantiene durante la compilación e incluso se puede incluir en el código objeto para ser usada por depuradores y mecanismos de reflexión en tiempo de ejecución.
  • 120. 3.5 El Significado de los Nombres dentro de un Alcance  Hasta el momento al hablar de enlaces nombre a objeto, en su mayor parte hemos asumido que hay una correspondencia uno a uno entre los nombres y los objetos visibles en un punto dado del programa, pero esto no necesariamente es así siempre:  Dos o más nombres que refieren al mismo objeto en el mismo punto del programa, se dice que son un “alias” o pseudónimos el uno del otro.  Un nombre que puede referirse a más de un objeto en el mismo punto de programa se dice que está sobrecargado.
  • 121. 3.5.1 Aliases  Ejemplo: Los “aliases” surgen naturalmente en los lenguajes que soportan el uso de estructuras basadas en apuntadores, otra forma de crear aliases en muchos lenguajes es pasando un parámetro por referencia a una subrutina que accede a esa variable directamente, como en el ejemplo en C++ a continuación: double sum, sum_of_squares; void accumulate(double& x){ sum += x; sum_of_squares += x * x; } accumulate(sum);
  • 122. 3.5.1 Aliases En el ejemplo anterior vemos que sum es pasado como argumento a la función accumulate, pero como x y sum se convierten en aliases uno del otro la primera línea de código de esta función no solamente modificaría a sum sino también a x y por tanto la siguiente línea de código puede que no tenga los resultados esperados. Este tipo de errores fue una de las principales motivaciones para hacer que las subrutinas en Turing y Euclid fuesen alcances cerrados, pues al exigir listas de importación el compilador es capaz de determinar y prohibir la creación de un alias dentro de la subrutina.
  • 123. 3.5.1 Aliases  Ejemplo: Como una regla general, los aliases tienden a hacer los programas más confusos y más difíciles para que el compilador pueda hacer importantes optimizaciones en el código. Considere el siguiente código en C: int a, b, *p, *q; ... a = *p; // Se copia en a el valor al que apunta p *q = 3; b = *p; // Se copia en b el valor al que apunta p
  • 124. 3.5.1 Aliases La asignación inicial en la mayoría de las máquinas, requiere que *p está cargado en un registro. Debido a que acceder a la memoria principal tiene su costo, el compilador desaría mantener este valor de *p en el registro para usarlo en la tercera asignación (b = *p). Pero no podrá hacer esto, a menos que pueda verificar que p y q no son aliases. Aunque esta verificación es posible de hacer en muchos casos, en general es incomputable.
  • 125. 3.5.1 Aliases  Punteros en C y en Fortran La tendencia de los punteros a introducir aliases es una de la razones por las cuales los compiladores de Fortran han tendido históricamente a producir código más rápido que los compiladores de C, pues los punteros han sido abundatemente usados en C y no han existido en Fortran 77 y sus predecesores. Ha sido en años recientes que sofisticados algoritmos de análisis de alias han permitido a los compiladores de C competir con Fortran en velocidad del código generado. Este análisis de los punteros ha sido tan importante que los diseñadores del estándar C99 decidieron agregar una nueva palabra clave al lenguaje: restrict.
  • 126. 3.5.1 Aliases El cualificador restrict cuando se agrega a una declaración de un puntero, es una aseveración de parte del programador indicando que el objeto al que el puntero se refiere no tiene un alias en el alcance actual. Es responsabilidad del programador hacer cierta esta aseveración, pues el compilador no necesita comprabarla. C99 también introduce el concepto de aliasing estricto, que permite al compilador asumir que los punteros de distintos tipos nunca harán referencia a la misma ubicación en memoria. La mayoría de los compiladores proveen una opción en línea de comandos que deshabilita las optimizaciones que explotan esta regla, pues de otro modo algunos programas antiguos y pobremente escritos podrían comportarse de forma incorrecta al compilarse a altos niveles de optimización.
  • 127. 3.5.2 Sobrecarga  La mayoría de los lenguajes de programación proveen al menos una forma limitada de hacer sobrecarga. En C, por ejemplo, el signo más “+” es usado para nombrar diferentes funciones, incluyendo la suma de enteros con signos, sin signo y números flotantes. A la mayoría de los programadores no le interesa la distinción entre estas funciones pero éstas toman argumentos de diferentes tipos y realizan operaciones muy distintas a nivel de bits. Una forma un poco más compleja de sobrecarga aparece en Ada a nivel de las enumeraciones. En el siguiente ejemplo de código en Ada vemos como las constantes “oct” y “dec” pueden referirse a meses o bases numéricas dependiendo del contexto en que aparezcan.
  • 128. 3.5.2 Sobrecarga declare type month is (jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec); type print_base is (dec, bin, oct, hex); mo : month; pb : print_base; begin mo := dec; pb := oct; print(oct); -- error!
  • 129. 3.5.2 Sobrecarga  Dentro de la tabla de símbolos de un compilador, la sobrecarga se maneja haciendo que la rutina de búsqueda retorne una lista de los posibles significados de un nombre buscado. El analizador semántico debe entonces escojer de la lista de elementos basado en el contexto en que aparece el nombre. Cuando el contexto no es suficiente para decidir como en la llamada a print en el ejemplo anterior, entonces se debe anunciar un error.  La mayoría de los lenguajes que permiten constantes de enumeración sobrecargadas permiten al programador proveer un contexto apropiado de forma explícita. En Ada por ejemplo se puede escribir: print(month'(oct));
  • 130. 3.5.2 Sobrecarga  En Modula-3 y C#, cada uso de una constante de enumeración debe estar precedido por un nombre de tipo, por lo cual no hay espacio para la ambigüedad: mo := month.dec; pb := print_base.oct;  En C, C++ y Pascal estándar, no es posible sobrecargar las constantes de enumeración, cada constante visible en un alcance debe ser distinta.
  • 131. 3.5.2 Sobrecarga  Ejemplo: En Ada y C++, entre otros lenguajes, es posible sobrecargar los nombres de las subrutinas, es decir, un nombre dado puede referirse a un arbitrario número de subrutinas en el mismo alcance, con tal que éstas difieran en sus argumentos y sea en número o tipo. En el siguiente código vemos un ejemplo de sobrecarga en C++: struct complex { double real, imaginary; }; enum base {dec, bin, oct, hex};
  • 132. 3.5.2 Sobrecarga int i; complex x; void print_num(int n) { ... void print_num(int n, base b) { ... void print_num(complex c) { ... print_num(i); print_num(i, hex); print_num(x);
  • 133. 3.5.2 Sobrecarga  Ada, C++, C#, Fortran 90, y Haskell, entre otros, también permiten que los operadores predefinidos (+, -, *, etc.) sean sobrecargados con funciones definidas por el usuario. Ada, C+ + y C# hacen esto definiendo formas prefijas alternativas para cada operador y dejando las formas infijas usales como abreviaciones de las prefijas. Por ejemplo en Ada A + B es una abreviación de “+”(A,B). En el siguiente código vemos un ejemplo de sobrecarga del operador “+” en C++.
  • 134. 3.5.2 Sobrecarga class complex { double real, imaginary; ... public: complex operator+(complex other) { return complex(real + other.real, imaginary + other.imaginary); } ... }; complex A, B, C; ... C = A + B;
  • 135. 3.5.2 Sobrecarga Con respecto a este estilo de abreviación de operadores basado en clases, se podría estar tentado a pensar en que no hay una sobrecarga real de los mismos, pues la abreviación se expande a un nombre no ambiguo (es el operador + de la clase A, A.operator+) y de hecho este es el caso en el lenguaje Clu, pero en C++ y C# puede haber más de una definición de A.operator+, permitiendo que el segundo argumento sea de varios tipos.
  • 136. 3.5.3 Polimorfismo y Conceptos Relacionados  En el caso de los nombres de subrutinas es importante diferenciar claramente la sobrecarga de los conceptos relacionados como son el polimorfismo y la coerción. La confusión puede surgir pues los tres pueden ser usados para pasar argumentos de múltiples tipos a un nombre de subrutina dado o para retornar múltiples tipos de un nombre de subrutina dado. Esta similitud sintáctica, esconde importantes diferencias semánticas y pragmáticas.
  • 137. 3.5.3 Polimorfismo y Conceptos Relacionados  Ejemplo: Supongamos que se desea el mínimo de dos valores sean flotantes o enteros, en Ada se podría lograr esto usando dos funciones sobrecargadas: function min(a, b : integer) return integer is ... function min(x, y : real) return real is ... En C, sin embardo, podríamos hacer el mismo trabajo sólo con la función: double min(double x, double y) { ...
  • 138. 3.5.3 Polimorfismo y Conceptos Relacionados Si la función en C es llamada en un contexto que espera un entero como resultado (ej.: int i = min(j, k)), el compilador automáticamente convertirá los argumentos enteros a números flotantes (double), llamará a la función min y luego convertirá el resultado de nuevo en un entero vía truncado (eliminando la parte decimal). Así que mientras las variables de tipo flotante tengan capacidad para almacenar la misma cantidad de bits significativos que un entero (cosa que sí ocurre en el caso de los enteros de 32-bit y los double de 64-bit), el resultado será numéricamente correcto, con sólo usar una función.
  • 139. 3.5.3 Polimorfismo y Conceptos Relacionados  La coerción es el proceso en que un compilador automáticamente convierte un valor de un tipo en un valor de otro tipo que el segundo tipo es requerido por el contexto.  La coerción es un tema controversial en los lenguajes de programación, pues algunos evitan al máximo utilizarla, Ada solo coerciona las constantes explícitas, subrangos y en ciertos casos arreglos de con el mismo tipo de elementos. Pascal coerciona enteros a flotantes en expresiones y asignaciones. Fortran también coerciona flotantes a enteros con una potencial pérdida de precisión. C hace también coerciones en los argumentos a funciones. La mayoría de los lenguajes de scripting proveen un rico conjunto de coerciones predefinidas.
  • 140. 3.5.3 Polimorfismo y Conceptos Relacionados  C++ además permite extender su conjunto predefinido de coerciones con coerciones definidas por el usuario.  Volviendo al ejemplo anterior, la sobrecarga en Ada permite al compilador escojer entre dos diferentes opciones de la función “min”, la coerción en C permite al compilador modificar los argumentos de la función “min”.  El polimorfismo provee otra opción: permite que una sola subrutina acepte argumentos de múltiples tipos sin ser convertidos. El término “polimórfico” viene del griego y significa “tener muchas formas”. Es aplicado al código (sean estructuras de datos o subrutinas) que pueden trabajar con valores de múltiples tipos.
  • 141. 3.5.3 Polimorfismo y Conceptos Relacionados  Coercion vrs Sobrecarga Además de sus diferencias semánticas, la coerción y la sobrecarga puede tener costos muy diferentes. Invocar una versión específica para enteros de los función “min” es mucho más eficiente que llamar una función “min” para números flotantes con argumentos enteros, pues en el primer caso se usaría aritmética de enteros para hacer la comparación (que podría ser más eficiente) y se evitaría hacer tres operaciones de conversión (los dos argumentos y el resultado). Un argumento en contra de la coerción es que tiende a imponer costos ocultos.
  • 142. 3.5.3 Polimorfismo y Conceptos Relacionados  Para que el concepto de polimorfismo tenga sentido, los tipos deben generalmente tener ciertas características en común y el código no debe depender nada más que de éstas. Las características comunes son generalmente tomadas de dos formas principales:  El polimorfismo paramétrico, en este caso el código toma un tipo (o un conjunto de tipos) como parámetro ya sea de forma explícita o implícita.  El polimorfismo de subtipo, acá el código es diseñado para trabajar con valores de un tipo específico T, pero el programador puede definir tipos adicionales que sean extensiones de T (herencia en programación orientada a objetos) y el código polimórfico funcionará además de con T con todos sus subtipos.
  • 143. 3.5.3 Polimorfismo y Conceptos Relacionados  El polimorfismo paramétrico explícito es también conocido como genericidad. Existen varios lenguajes como Ada, C+ +, Clu, Eiffel, Modula-3, Java, y C# que dan soporte a la genericidad, por ejemplo en C++ la genericidad se soportar a través de las plantillas (templates).  El polimorfismo paramétrico implícito aparece en la familia de lenguajes de Lisp y ML y en varios lenguajes de scripting.  El polimorfismo de subtipo es fundamental en los lenguajes orientados a objetos en los que se dice que los subtipos (clases) heredan los métodos de sus tipos padres.
  • 144. 3.5.3 Polimorfismo y Conceptos Relacionados  La genericidad es usualmente (no siempre) implementada mediante la creación de múltiples copias del código polimórfico, cada una especializada para cada tipo concreto, el polimorfismo de subtipo es casi siempre implementado mediante la creación de una sola copia del código y a través de la inclusión en la representación de objetos de suficientes “metadatos” de modo que el código pueda saber cuándo tratarlos de forma diferente.
  • 145. 3.5.3 Polimorfismo y Conceptos Relacionados  El polimorfismo paramétrico implícito puede ser implementado de ambas formas. La mayoría de las implementaciones de Lisp usan una única copia del código y dejan todos las revisiones semánticas hasta el tiempo de ejecución. ML y sus descendientes realizan toda la revisión de tipos en tiempo de compilación, típicamente se genera una única copia del código cuando es posible (ej.: cuando todos los tipos en cuestión son registros que tienen una representación similar) y múltiples copias cuando es necesario (ej.: cuando la aritmética polimórfica debe operar tanto en números enteros como flotantes).
  • 146. 3.5.3 Polimorfismo y Conceptos Relacionados  Los lenguajes orientados a objetos que realizan la revisión de tipos en la compilación como C+ +, Eiffel, Java, y C# generalmente proveen soporte tanto para la genericidad como para el polimorfismo de subtipo.  Smalltalk, Objective-C, Python, y Ruby usan un único mecanismo (con revisión en tiempo de ejecución) para proveer tanto el polimorfismo paramétrico como el de subtipo.
  • 147. 3.5.3 Polimorfismo y Conceptos Relacionados  Ejemplo: Como un ejemplo concreto de genericidad, considere las funciones “min” sobrecargadas en el ejemplo anterior. El código fuente para las versiones para enteros y flotantes es similiar, se podría aprovechar esta similaridad para definir una única versión que funcione no sólo para enteros y reales sino para cualquier tipo cuyos valores estén totalmente ordenados. Una forma de hacer esto en Ada se muestra en el código a continuación:
  • 148. 3.5.3 Polimorfismo y Conceptos Relacionados generic type T is private; with function "<"(x, y : T) return Boolean; function min(x, y : T) return T; function min(x, y : T) return T is begin if x < y then return x; else return y; end if; end min;
  • 149. 3.5.3 Polimorfismo y Conceptos Relacionados function string_min is new min(string, "<"); function date_min is new min(date, date_precedes); En este código se observa una declaración inicial de “min” sin cuerpo (implemetación) que está precedida por una cláusula genérica que especifica que dos cosas son necesarias para crear una instancia concreta de una función “min”: un tipo T y una rutina de comparación. Luego esta declaración está seguida por el código actual de “min”, por ejemplo dadas las declaraciones apropiadas para los tipos string y date y sus rutinas de comparación, se pueden crear funciones “min” que funcionen con estos tipos como se ve en la últimas dos líneas (observe que el operador “<” que se envía a string_min probablemente esté sobrecargado).
  • 150. 3.5.3 Polimorfismo y Conceptos Relacionados  Ejemplo: Con el polimorfismo paramétrico implícito de Lisp, ML y sus descendientes, el programador no tiene que especificar un tipo para los parámetros, por ejemplo en Scheme la definición de “min” sería: (define min (lambda (a b) (if (< a b) a b))) La implementación típica de Scheme utiliza un intérprete que examina los argumentos que recibe “min” y determina en tiempo de ejecución si soportan el operador “<”. (Como todos los dialectos de Lisp, Scheme coloca los nombres de las funciones dentro de los paréntesis justo antes de los parámetros y la palabra clave “lambda” se usa se usa para introducir la lista de parámetros y el cuerpo de una función).
  • 151. 3.5.3 Polimorfismo y Conceptos Relacionados Para el caso de función “min” anterior, la expresión (min 123 456) retorna 123; (min 3.14159 2.71828) retorna 2.71828 y (min "abc" "def") produce un error en tiempo de ejecución porque el operador de comparación de cadenas es “<?” no “<”.
  • 152. 3.5.3 Polimorfismo y Conceptos Relacionados  Ejemplo: En Haskell la versión de “min” sería similar a la de Scheme: min a b = if a < b then a else b Esta versión funciona para valores de cualquier tipo totalmente ordenado, incluyendo las cadenas, pero la revisión de tipos se hace en tiempo de compilación usando un sofisticado sistema de inferencia de tipos.
  • 153. 3.5.3 Polimorfismo y Conceptos Relacionados  En conclusión, la diferencia entre las versiones sobrecargadas de “min” y la versión genérica radica en la generalidad del código. Con la sobrecarga el programador debe escribir una copia separada del código a mano para cada tipo con el que se desea que funcione “min”, en cambio con la genericidad, es el compilador (en la implementaciones típicas), que crea automáticamente una copia del código para cada tipo. La similitud de la sintaxis con que se invocan las subrutinas y de el código que se genera, ha llevado a algunos autores a referirse a la sobrecarga como caso especial de polimorfismo.
  • 154. 3.5.3 Polimorfismo y Conceptos Relacionados  Sin embargo, no hay una razón particular para que el programador piense en la genericidad en términos de múltiples copias, desde un punto de vista semántico (conceptual), las subrutinas sobrecargadas usan un sólo nombre para varios elementos y una subrutina polimórfica es un sólo elemento.
  • 155. 3.6 El Enlace de los Ambientes de Referencia  Ya hemos visto cómo las reglas de alcance determinan el ambiente de referencia de un enunciado dado en el programa. La reglas de alcance estático especifican que el ambiente de referencia depende del anidado léxico de los bloques del programa donde los nombres son declarados. Las reglas de alcance dinámico especifican que el ambiente de referencia depende del orden en que las declaraciones son encontradas en el tiempo de ejecución.
  • 156. 3.6 El Enlace de los Ambientes de Referencia  Pero hay un elemento que no hemos considerado, que surge en lenguajes que permiten crear referencias a subrutinas, por ejemplo, pasándolas como parámetro ¿Cuándo debería ser aplicadas las reglas de alcance a esa subrutina? ¿Cuando se crea por primera vez o cuando es invocada? La respuesta es importante tanto en lenguajes con alcance dinámico como estático.
  • 157. 3.6 El Enlace de los Ambientes de Referencia  Ejemplo: en el código a continuación aparece un ejemplo de alcance dinámico, el procedimiento “print_selected_records” es una rutina de propósito general que sabe como recorrer los registros de una base de datos, sin importar si representan personas, herramientas o ensaladas. Toma como parámetros la base de datos, un predicado para saber cuándo imprimirá un registro o no y una subrutina que sabe cómo formatear los datos en los registros de esta base de datos particular.
  • 158. 3.6 El Enlace de los Ambientes de Referencia Además se define una función “print_person” que usa el valor de una variable no local “line_length” para calcular el número y ancho de las columnas en la salida. En un lenguaje con alcance dinámico, es natural que el procedimiento “print_selected_records” declare e inicialice esta variable localmente, sabiendo que el código en “print_routine” tomará su valor si es necesario. Pero para que esta técnica funcione, el ambiente de referencia de “print_routine” no debe ser creado si hasta que la rutina sea invocada por “print_selected_records”. Este enlace tardío del ambiente de referencia de una subrutina que ha sido pasada como parámetro es conocido como enlace superficial, y es el enlace por defecto en los lenguajes con alcance dinámico.
  • 159. 3.6 El Enlace de los Ambientes de Referencia type person = record … age : integer … threshold : integer people : database function older_than_threshold(p : person) : boolean return p.age ≥ threshold procedure print_person(p : person) -- Usa la variable no local line_lenght ...
  • 160. 3.6 El Enlace de los Ambientes de Referencia procedure print_selected_records(db : database; predicate, print_routine : procedure) line_length : integer if device_type(stdout) = terminal line_length := 80 else line_length := 132 foreach record r in db if predicate(r) print_routine(r) -- main program … threshold := 35 print_selected_records(people, older_than_threshold, print_person)
  • 161. 3.6 El Enlace de los Ambientes de Referencia  En contraste y siempre haciendo referencia al código anterior, para la función “older_than_threshold” el enlace superficial podría no funcionar muy bien. Si por ejemplo, el procedimiento “print_selected_records” tuviese una variable local “threshold”, entoces la variable establecida por el programa principal para influenciar el comportamiento de “older_than_threshold” no será visible cuando la función sea finalmente llamada y probablemente la función no genere los resultados esperados. En una situación como esta, el código que originalmente pasa la función como parámetro tiene un ambiente de referencia particular, y no desea que la subrutina que pasa se llamada con otro ambiente distinto.
  • 162. 3.6 El Enlace de los Ambientes de Referencia Así que tiene sentido también enlazar el ambiente en el momento en que la rutina es pasada por primera vez como parámetro y luego restaurar este ambiente cuando sea finalmente invocada. Este enlace temprano del ambiente de referencia es conocido como enlace profundo. La necesidad del enlace profundo es en ocasiones referido como el problema funarg (function argument) en Lisp.
  • 163. 3.6.1 Cerraduras de Subrutinas  El enlace profundo es implementado mediante la creación de una representación explícita de un ambiente de referencia (generalmente el ambiente en que la rutina se ejecutaría si fuese llamada en el tiempo presente) que se grupa junto con la referencia a la subrutina. Este conjunto se conoce como cerradura. Usualmente una subrutina por sí misma puede ser representada dentro de la cerradura mediante un puntero a su código. En un lenguaje con alcance dinámico, la representación del ambiente de referencia depende de si la implementación del lenguaje usa una lista de asociación a una tabla central de referencias para hacer la búsqueda de los nombres en tiempo de ejecución.
  • 164. 3.6.1 Cerraduras de Subrutinas  Aunque el enlace superficial es usualmente la opción por defecto en lenguajes con alcance dinámico, el enlace profundo puede estar disponible como opción. En los dialectos más antinguos de Lisp, por ejemplo, existe una primitiva llamada “function” que toma una función como argumento y retorna una cerradura cuyo ambiente de referencia es el que la función tendría si fuese ejecutada en el momento actual. Esta cerradura puede ser pasada como parámetro a otra función, de modo que cuando sea invocada se ejecute en el ambiente guardado. (Las cerradurs funcionan un poco distinto de las simples funciones en la mayoría de los dialectos de Lisp: deben se llamadas pasándolas como argumentos a primitivas predefinidas como “funcall” o “apply”.)
  • 165. 3.6.1 Cerraduras de Subrutinas  El enlace profundo es generalmente la opción por defecto en lenguajes con alcance estático. De entrada se podría pensar que el tiempo de enlace de los ambiente de referecia en un lenguaje con alcance estático no debería de importar. De todos modos, el significado de un nombre en un lenguaje con alcance estático depende de su anidado léxico, no del flujo de la ejecución y este anidado es el mismo no importando si es capturado al momento en que la subrutina es pasada como parámetro o después cuando es invocada. El detalle es que un programa en ejecución puede tener más de una instancia de un objeto que está declarado dentro de una subrutina recursiva.
  • 166. 3.6.1 Cerraduras de Subrutinas  Una cerradura en un lenguaje con alcance estático captura la instancia actual de cada objeto en el momento es creada. Cuando la subrutina sea llamada, encontrará estas instancias capturadas, aún cuando hayan sido creadas nuevas instancias por llamados recursivos.  Es posible imaginar la combinación de alcance estático con el enlace superficial, pero la combinación no parece tener mucho sentido y al parecer no ha sido implementada en ningún lenguaje.
  • 167. 3.6.1 Cerraduras de Subrutinas  Ejercicio: En el siguiente código se muestra un programa en Pascal que ilustra el impacto de las reglas de enlace en la presencia de un alcance estático. ¿Cuál será su salida en el caso del enlace profundo y cuál será en el caso del enlace superficial?
  • 168. 3.6.1 Cerraduras de Subrutinas program binding_example procedure A(I : integer; procedure P); procedure B; begin writeln(I); end; begin (* A *) if I > 1 then P else A(2, B); end; procedure C; begin end; begin (* main *) A(1, C); end.
  • 169. 3.6.1 Cerraduras de Subrutinas  Solución: En la siguiente figura se muestra una vista conceptual de la pila en tiempo de ejecución, los ambientes de referencia capturados en cerraduras se muestran como cajas punteadas y flechas, Cuando B es llamado a través del parámetro P, dos instancias de I existen. Como la cerradura para P fue creada en la invocación inicial a A, el enlace estático de B apunta al marco de esa invocación previa. B usa la instacia de I de dicha invocación y por tanto la salida provocada por writeln es 1.
  • 170. 3.6.1 Cerraduras de Subrutinas
  • 171. 3.6.1 Cerraduras de Subrutinas  Debe también notarse que las reglas de enlace con alcance estático importan sólo cuando los objetos que se acceden no son ni locales ni globales sino que están definidos en un nivel intermedio de anidamiento. Si un objeto es local a la subrutina que se ejecuta actualmente, entonces no importa si la subrutina fue invocada directamente o a través de una cerradura. Si un objeto es global, nunca habrá más de una instancia, pues la subrutina principal de un programa no es recursiva.
  • 172. 3.6.1 Cerraduras de Subrutinas  Por tanto las reglas de enlace son irrelevantes en lenguajes como C que no tiene subrutinas anidadas or Modula-2, que sólo permite a las subrutinas más externas ser pasadas como parámetros, asegurándose de este modo que cualquier variable definida fuera de la subrutina sea global. Además las reglas de enlace son irrelevantes en lenguajes como PL/I y Ada 83, que no permiten que ninguna subrutina sea pasada como parámetro.
  • 173. 3.6.1 Cerraduras de Subrutinas  Suponiendo que se tenga un lenguaje con alcance estático en el que las subrutinas anidadas pueden ser pasadas como parámetros con enlace profundo. Para representar una cerradura para la subrutina S, simplemente se puede almacenar un puntero al código de S junto con el enlace estático que S usaría si fuese llamado en el tiempo actual, en el ambiente actual.
  • 174. 3.6.1 Cerraduras de Subrutinas Cuando S finalmente es invocada, se restaura temporalmente el enlace estático guardado en vez de crear uno nuevo. Cuando S sigue la cadena estática para acceder al objeto no local que busca, encontrará la instancia del objeto correcta, la que era actual en el momento que la cerradura fue creada. Esta instancia podría no tener el valor que tenía cuando la cerradura fue creada, pero al menos su identidad será la que esperaba el creador de la cerradura.