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

174
Capítulo 3. Nombres, Alcances y Enlaces Raúl José Palma Mendoza

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

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

Capítulo 3. Nombres, Alcances y Enlaces

Raúl José Palma Mendoza

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

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.

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

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.

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

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.

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

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.

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

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

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

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.

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

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.

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

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.

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

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

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

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.

Page 12: nombres, alcances y enlaces (lenguajes de programació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.

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

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.

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

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.

Page 15: nombres, alcances y enlaces (lenguajes de programació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.

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

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.

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

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.

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

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.

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

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)

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

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.

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

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)

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

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.

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

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.

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

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.

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

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.

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

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.

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

3.2.2 Almacenamiento Basado en Pila

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

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.

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

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?

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

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.

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

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.

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

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.

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

3.2.3 Almacenamiento Basado en el Heap

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

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.

Page 35: nombres, alcances y enlaces (lenguajes de programació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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

Page 57: nombres, alcances y enlaces (lenguajes de programación)
Page 58: nombres, alcances y enlaces (lenguajes de programación)

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.

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

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.

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

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.

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

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?

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

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.

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

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.

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

3.3.2 Subrutinas Anidadas

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

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.

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

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.

Page 67: nombres, alcances y enlaces (lenguajes de programació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.

Page 68: nombres, alcances y enlaces (lenguajes de programació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.

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

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.

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

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.

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

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

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

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.

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

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

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

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.

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

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

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

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

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

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

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

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.

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

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.

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

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.

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

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.

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

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.

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

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

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

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

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

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.

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

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

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

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

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

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;

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

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;

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

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.

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

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.

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

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.

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

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.

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

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.

Page 95: nombres, alcances y enlaces (lenguajes de programació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;

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

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;

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

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

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

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.

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

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

...

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

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

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

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

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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?

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

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)

Page 113: nombres, alcances y enlaces (lenguajes de programació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.

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

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)

...

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

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.

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

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)

Page 117: nombres, alcances y enlaces (lenguajes de programació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.

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

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.

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

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.

Page 120: nombres, alcances y enlaces (lenguajes de programació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.

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

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

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

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.

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

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

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

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.

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

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.

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

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.

Page 127: nombres, alcances y enlaces (lenguajes de programació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.

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

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!

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

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

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

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.

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

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

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

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

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

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

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

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;

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

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.

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

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.

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

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

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

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.

Page 139: nombres, alcances y enlaces (lenguajes de programació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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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

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

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.

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

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:

Page 148: nombres, alcances y enlaces (lenguajes de programació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;

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

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

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

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

Page 151: nombres, alcances y enlaces (lenguajes de programació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 “<”.

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

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.

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

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.

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

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.

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

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.

Page 156: nombres, alcances y enlaces (lenguajes de programació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.

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

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.

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

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.

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

3.6 El Enlace de los Ambientes de Referencia

type person = record … age : integer …threshold : integerpeople : database

function older_than_threshold(p : person) : boolean return p.age ≥ threshold

procedure print_person(p : person) -- Usa la variable no local line_lenght ...

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

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 := 35print_selected_records(people, older_than_threshold, print_person)

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

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.

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

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.

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

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.

Page 164: nombres, alcances y enlaces (lenguajes de programació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”.)

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

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.

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

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.

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

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?

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

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.

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

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.

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

3.6.1 Cerraduras de Subrutinas

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

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.

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

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.

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

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.

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

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.