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