0% encontró este documento útil (0 votos)
162 vistas78 páginas

Introducción a Lenguajes de Programación

Este documento habla sobre los lenguajes de programación. Explica que un lenguaje de programación permite describir computaciones de forma legible para humanos y máquinas. Describe las abstracciones de datos y control en los lenguajes, así como paradigmas como la programación imperativa, orientada a objetos, funcional y lógica. También define la sintaxis, semántica y procesos de traducción como la interpretación y compilación de los lenguajes.
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
162 vistas78 páginas

Introducción a Lenguajes de Programación

Este documento habla sobre los lenguajes de programación. Explica que un lenguaje de programación permite describir computaciones de forma legible para humanos y máquinas. Describe las abstracciones de datos y control en los lenguajes, así como paradigmas como la programación imperativa, orientada a objetos, funcional y lógica. También define la sintaxis, semántica y procesos de traducción como la interpretación y compilación de los lenguajes.
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

LENGUAJES DE PROGRAMACIÓN

Página 1 de 78
Autor: informatica_uned@[Link]

1 Introducción
1.1 ¿Qué es un lenguaje de programación?

Un lenguaje de programación es un sistema notacional para describir computaciones en una


forma legible tanto para la máquina como para el ser humano.
La computación incluye todo tipo de operaciones de computadora, incluyendo la manipulación de
datos, el procesamiento de texto y el almacenamiento y recuperación de la información.
Para que un lenguaje de programación sea legible por parte de los seres humanos requiere que
proporcione abstracciones de las acciones de las computadoras fáciles de comprender. Los
lenguajes de programación tienen tendencia a parecerse a los lenguajes naturales (inglés).

1.2 Abstracciones en los lenguajes de programación

Las abstracciones de los lenguajes de programación se agrupan en dos clases generales:


abstracción de datos y abstracción de control. Éstas a su vez, se agrupan en niveles: básicas,
estructuradas y unitarias.

1.2.1 Abstracciones de datos

Básicas: resumen la representación interna de valores de datos comunes en una computadora.


Ejemplos: variables y tipo (var x:integer)

Estructuradas: abstracción de colecciones de valores de datos relacionados entre sí. Ej:


registros; arreglos, tipos estructurados.

Unitarias: incluyen al encapsulado de datos y la ocultación de la información. Ej: paquete, clase.


Una propiedad adicional de la abstracción de datos unitarios es su reutilización. Típicamente estas
abstracciones representan componentes o contenedores; y se convierten en la base de
mecanismos de bibliotecas de lenguaje.

1.2.2 Abstracciones de control.

Básicas: son las sentencias de un lenguaje que combinan unas cuantas instrucciones de máquina
en una sentencia abstracta más comprensible. Ej: enunciado de asignación; goto.

Estructuradas: dividen un programa en grupos de instrucciones que están anidadas. Ej:


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

Unitarias: consiste en efectuar abstracciones de control con la finalidad de incluir una colección
de procedimientos que proporcionan servicios relacionados lógicamente con otras partes del
programa y que forman una parte unitaria, o independiente, del programa. Un tipo de
abstracción de control que resulta difícil de clasificar son los mecanismos de programación en
paralelo, por ejemplo, las tareas (task) de ADA son una abstracción unitaria, pero los hilos o
hebras de Java se consideran estructuradas.

1
LENGUAJES DE PROGRAMACIÓN
Página 2 de 78
Autor: informatica_uned@[Link]

1.3 Paradigmas de computación

Programación imperativa, tienen las siguientes propiedades: La ejecución secuencial de


instrucciones, el uso de variables en representación de localizaciones de memoria y el uso de la
asignación para cambiar el valor de las variables.
El requisito de que la computación sea descrita como una secuencia de instrucciones se le llama
cuello de botella de Von Neumann dado que limita la capacidad de un lenguaje de indicar
computación en paralelo.

Programación orientada a objetos. Un objeto puede describirse como una colección de


localizaciones de memoria. En muchos lenguajes, éstos se agrupan en clases que representan
todos los objetos con las mismas propiedades. Entonces, se crean los objetos como instancias
(ejemplos particulares) de una clase. Los métodos son las funciones u operaciones que contienen
esas clases. En una clase, primero se define un constructor, el cual asigna memoria y proporciona
los valores iniciales para los datos de un objeto. Los lenguajes orientados a objetos más
importantes son Java, C++ y Smalltalk. Permiten a los programadores escribir código reutilizable
y ampliable.

Programación funcional. Basa la descripción de las computaciones en la evaluación de


funciones o en la aplicación de funciones a valores conocidos. Se conocen también como
lenguajes aplicativos. Su mecanismo básico es la llamada de función, que involucra la
transferencia de valores como parámetros a las funciones y la obtención de valores resultantes
como valores devueltos de las funciones.
La programación funcional es en cierto sentido opuesta a la programación orientada a objetos, ya
que, se concentra en los valores y las funciones en vez de en localizaciones de memoria; además
de que las operaciones repetitivas se expresan mediante funciones recursivas y no mediante
ciclos.
Sus ventajas es que el lenguaje se hace más independiente del modelo de máquina y resulta más
fácil extraer conclusiones precisas con respecto a su comportamiento.
Ej: Ada, LISP, Scheme y Haskell.

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

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

1.4 Definición de lenguaje

La definición del lenguaje se puede dividir aproximadamente en dos partes: sintaxis (estructura) y
semántica (significado).

Sintaxis del lenguaje. Es de muchas maneras como la gramática de un lenguaje natural. La


sintaxis de casi todos los lenguajes está dada ahora utilizando gramáticas libres de contexto.

2
LENGUAJES DE PROGRAMACIÓN
Página 3 de 78
Autor: informatica_uned@[Link]

Ej enunciado if de C:
<enunciado if>::= if (<expresión>)<enunciado>
[else<enunciado>]
o utilizando caracteres de formato especial:
enunciado if  if (expresión) enunciado
[else enunciado]
Un problema íntimamente relacionado con la sintaxis de un lenguaje de programación es su
estructura léxica, la cual es similar a la ortografía del lenguaje natural. Es la estructura de las
palabras del lenguaje que generalmente se conocen como tokens.

Semántica del lenguaje. Una descripción completa de su significado en todos los contextos
puede llegar a ser extremadamente compleja. A pesar de ello se han desarrollado varios sistemas
de notación para definiciones formales: semántica operacional, la semántica denotacional y la
semántica axiomática.

1.5 Traducción del lenguaje

Un traductor es un programa que acepta otros programas escritos en el lenguaje en cuestión y


que, o los ejecuta directamente (intérprete), o los transforma en una forma adecuada para su
ejecución (compilador).

La interpretación es un proceso que consta de un paso, en donde tanto el programa como la


entrada le son dados al intérprete y se obtiene una salida.

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

Los pseudointérpretes (traductores intermedios entre intérpretes y compiladores), ejecutan el


programa sin producir un programa objetivo.

Tanto los compiladores como los intérpretes deben llevar a cabo: Primero, un analizador léxico,
es decir un rastreador, debe convertir la representación textual del programa como una secuencia
de caracteres en una forma más fácil de procesar (agrupando caracteres en tokens); acto
seguido, el analizador sintáctico o gramatical debe determinar la estructura de la secuencia de
los tokens proporcionados por el rastreador; finalmente, un analizador semántico debe
determinar lo suficiente del significado de un programa como para permitir la ejecución o la
generación de un programa objetivo. Estas fases ocurren combinadas de diversas maneras.

Un traductor tiene que mantener un entorno de ejecución en el cual se asigna el espacio


adecuado de memoria para los datos del programa y registra el avance de la ejecución del mismo.
Las propiedades de un lenguaje de programación que pueden ser determinadas antes de su
ejecución se llaman propiedades estáticas, mientras que las propiedades que solamente se
pueden determinar durante la ejecución se conocen como propiedades dinámicas.

Un compilador puede utilizar sólo las propiedades estáticas de un lenguaje (su léxico y su
estructura sintáctica). En algunos lenguajes como C o Ada algunas propiedades semánticas
también son estáticas (tipos de datos de las variables).
Un lenguaje que sea más dinámico es más adecuado para la interpretación.

3
LENGUAJES DE PROGRAMACIÓN
Página 4 de 78
Autor: informatica_uned@[Link]

Un lenguaje que sólo tenga asignación estática puede utilizar un ambiente totalmente estático.
Para lenguajes más dinámicos debe utilizarse un ambiente más complejo totalmente dinámico. En
un punto medio está el típico ambiente basado en pilas (C y Ada).

Los intérpretes son inherentemente menos eficientes que los compiladores.

Una propiedad importante de un traductor de lenguaje es su respuesta a errores contenidos en


un programa fuente. Un traductor debería emitir mensajes de error apropiados. Lo más apropiado
es la recuperación de errores que le permite al traductor seguir adelante con la traducción, de
manera que puedan describirse errores adicionales.

Los errores léxicos ocurren durante el análisis léxico, generalmente están limitados al uso de
caracteres ilegales. Los errores ortográficos los detectaría el analizador sintáctico (incluyen
tokens faltantes o expresiones mal organizadas). Los errores semánticos pueden ser estáticos
(tipos incompatibles o variables no declaradas) o dinámicos (subíndice fuera de rango o la división
entre cero). Los errores lógicos de ninguna manera son errores desde el punto de vista de la
traducción del lenguaje.

1.6 Diseño del lenguaje

La legibilidad de máquina y del ser humano son los requisitos que prevalecen en el diseño.
El reto del diseño del lenguaje de programación, es lograr la potencia, expresividad y comprensión
que requiere la legibilidad del ser humano, mientras que se conservan al mismo tiempo la
precisión y simplicidad necesarias para la traducción de máquina.
En lenguaje exitoso de programación tiene utilerías para abstracción de datos y abstracción de
control.
La meta prevaleciente de la abstracción en el diseño de lenguajes de programación es el control
de la complejidad.
Los errores se pueden clasificar de acuerdo con la etapa de traducción en la que ocurren.
Errores ortográficos como “whillle” en vez de “while”, los detectara el analizador sintáctico.
Los errores semánticos pueden ser estáticos (tipos incompatibles, variables no declaradas).
Errores lógicos: son aquellos que comete el programador, hacen que el programa se comporte de
una manera errónea.

4
LENGUAJES DE PROGRAMACIÓN
Página 5 de 78
Autor: informatica_uned@[Link]

3 Principios de diseño de los lenguajes


3.1 Historia y criterios de diseño

- Eficiencia en la ejecución. Objetivo de FORTRAM.


- Capacidad de escritura. Objetivo de COBOL Y Algol60.
- Legibilidad. COBOL.
- Lenguaje general y ortogonal (regular): Algol60.
- Años 70 a 80, búsqueda de simplicidad y abstracción: PASCAL, C y Modula-2.
- Años 80 a 90, mejorar precisión matemática y lógica, introducir la lógica: ML o
Haskell.
- Últimos 15 años, lenguajes orientados a objetos: Smaltk, C++ y Java.

3.2 Eficiencia

Esta puede aplicarse en muchas formas diferentes:

Optimizabilidad o eficiencia del código: El diseño del lenguaje debe ser tal que un traductor
pueda generar un código ejecutable eficiente.
Como ejemplo: las variables con tipos estáticos permiten generar código que las asignan y las
referencias con eficiencia.

Eficiencia de la traducción: ¿Permite el diseño del lenguaje que el código fuente se traduzca
con eficiencia, esto es, con rapidez y con un traductor de tamaño razonable?, ¿Permite el diseño
de un lenguaje que se escriba un compilador de una sola pasada? En Pascal y en C la variables
deben ser declaradas antes de que se utilicen, sin embargo en C++ esta restricción se ha liberado
por lo que se necesita una segunda pasada de compilación.
En ocasiones los lenguajes incluyen reglas extremadamente difíciles de verificar en tiempo de
traducción, o incluso en tiempo de ejecución.

En general la verificación de errores puede ser un problema grave de eficiencia, sin embargo el
ignorar la verificación de los errores viola otro principio de diseño, la confiabilidad (el
aseguramiento de que un programa no se comportará de forma inesperada ni desastrosa en
tiempo de ejecución).

La capacidad de implementación es la eficiencia con la que se puede escribir un traductor. Está


relacionado con la eficiencia de la traducción pero también es una función de la complejidad de la
definición del lenguaje.
Ocasionalmente puede darse el caso de que un lenguaje tenga un requisito que sea imposible de
traducir.

Eficiencia de la programación: ¿Cómo de rápido y fácilmente se puede escribir programas en


el lenguaje?, una de las cualidades involucradas en lo anterior es la capacidad de expresión
del lenguaje: ¿con qué facilidad se pueden expresar procesos y estructuras complejas?.

Lo conciso de la sintaxis así como evitar detalles innecesarios como la declaración de variables
también se consideran factores importantes en este tipo de eficiencia. Desde este punto de vista
Lisp y Prolog son lenguajes ideales.
Esto puede comprometer otros principios del lenguaje como son la legibilidad, la eficiencia de
ejecución y la confiabilidad.

5
LENGUAJES DE PROGRAMACIÓN
Página 6 de 78
Autor: informatica_uned@[Link]

Un programa que no sea confiable genera muchos costos adicionales ocasionados por la
necesidad de aislar o eliminar el comportamiento erróneo, tiempo adicional de pruebas etc... En el
sentido de la ingeniería del software la eficiencia con la que se puede crear software depende
de la legibilidad y la capacidad de darle mantenimiento.
Los ingenieros del software estiman que ocupa mucho más tiempo en eliminar errores y en el
mantenimiento que en la codificación original, por la que la legibilidad y la capacidad de
mantenimiento pueden ser en último término de los mayores problemas de eficiencia.

3.3 Regularidad

Expresa lo bien que están integradas las características del mismo. Una mayor regularidad implica
pocas restricciones no usuales en el uso de constructores particulares, menos interacciones raras
entre dichos constructores y, en general, menos sorpresas en la forma en que se comportan las
características del lenguaje.

La regularidad se puede dividir en tres conceptos: Generalidad (eliminación de casos especiales


en la disponibilidad y uso de los constructores y combinando constructores íntimamente
relacionados en uno sólo más general), Ortogonalidad (Los constructores del lenguaje se
pueden combinar en cualquier forma significativa y que la interacción entre los constructores o el
contexto del uso no debe generar restricción o comportamientos inesperados) y uniformidad
(Las cosas similares deben verse de forma similar y tener significados similares y a la inversa, las
cosas diferentes se tienen que ver de forma diferente).

Algunos ejemplos que nos puede aclarar estos conceptos:

Generalidad:
- Pascal tiene funciones y procedimientos anidados y estos se pueden pasar como
parámetros a otros procedimientos, pero sin embargo estos no pueden ser asignados a
variables por lo que los procedimientos carecen de generalidad. C carece de la definición
de procedimientos anidados por lo que no tienen generalidad. Scheme y ML tienes un
constructor de procedimientos y funciones totalmente general.
- Pascal no tiene arreglos de longitud variable por lo que dichos arreglos carecen de
generalidad. C y Ada los tienen.
- En C no se pueden comparar estructuras o arreglos con el operador “==” por lo que el
operador carece de generalidad. En Ada esta restricción ha sido eliminada. Algunos
lenguajes como Haskell permiten la definición de nuevos operadores por lo que se puede
decir que los operadores han alcanzado la generalidad completa.
- En Fortran las constantes nombradas no existen, en Pascal no puede ser expresiones y
en Módula-2 las expresiones de constantes no puede incluir llamadas a funciones. Ada
tiene una herramienta de aclaración de constantes completamente general.

Ortogonalidad:
Esto consiste en que los constructores no pueden comportarse de manera diferente en
contextos diferentes, por lo que las restricciones que dependen del contexto son no
ortogonales:
- En Pascal, las funciones sólo pueden devolver tipos de datos escalares o
apuntadores.
- En C, las variables locales sólo pueden ser declaradas al principio de un bloque.
- En C existe una no ortogonalidad en el paso de parámetros a las funciones, pasa
todos los parámetros por valor excepto los de tipo arreglo.
Algo68 tuvo como meta la ortogonalidad, sus constructores pueden combinarse de todas
las formas significativas.

6
LENGUAJES DE PROGRAMACIÓN
Página 7 de 78
Autor: informatica_uned@[Link]

Uniformidad: Son de dos tipos: Cosas similares no parecen ser o se comportan de


manera similar y cosas distintas se comportan de manera similar cuando no deberían:
- En C++ se coloca un punto y coma después de la definición de clase pero está
prohibida después de una función.
- En C los operadores “&” y “&&”, el primero es “and bit a bit” y el segundo es “and
logical”.
- Los valores devueltos por las funciones en Pascal se parecen a las asignaciones.

Hay que hacer notar que el afán por hacer imponer una meta como puedan ser la
generalidad o la ortogonalidad en el diseño puede resultar ser un error. Como ejemplo
tenemos Algo68, que si bien cumple con los criterios de generalidad y ortogonalidad esto
mismo hizo que el lenguaje tuviera una cierta oscuridad y complejidad.
La legibilidad y la confiabilidad pueden verse seriamente comprometidas sin la existencia
de restricciones en el uso de ciertas características.
Si una no regularidad no puede justificarse de forma razonable entonces se puede decir
que es un error de diseño.

3.4 Principios adicionales sobre diseño de los lenguajes.

Simplicidad.
La simplicidad fue el primer objetivo de Pascal y por lo cual tuvo tanto éxito. En principio la
simplicidad pude parecer fácil de lograr pero en la práctica es algo bastante complicado. No hay
que confundir simplicidad con regularidad, Algo68 es uno de los lenguajes más regulares, pero sin
embargo no es simple. Tener pocos constructores básicos no es simplicidad pero ayuda. Por otro
parte, un lenguaje muy simple puede, de hecho, hacer que su uso sea más complejo.
La sobresimplicidad de Pascal ha hecho que se utilicen más C, C++ y Java, por lo que C podría
considerarse como un intento de mayor éxito hacia la simplicidad.
La sobresimplicidad puede hacer que un lenguaje sea difícil de utilizar, carente de expresividad,
legibilidad o seguridad y sujeto a demasiadas restricciones.

Expresividad.
Es la facilidad con la cual un lenguaje puede expresar procesos y estructuras complejas. Uno de
los adelantos en la expresividad fue la introducción de la recursión en los lenguajes de
programación.
La expresividad pude entrar en conflicto con la simplicidad: Lisp, Prolog y Algo68 son lenguajes de
gran expresividad pero no simples (en parte como resultado de su expresividad).
El existo de la programación orientada a objetos viene determinada en gran parte por su gran
expresividad. Estas características (las de la programación OOP), ayudan sobre manera a los
programadores a escribir sus códigos imitando sus diseños.
A veces la expresividad puede comprometer la legibilidad. El lenguaje C es expresivo pero muchas
de sus expresiones puede resultar difíciles de comprender.

Extensibilidad.
Principio que indica que debería de haber algún mecanismo general que permita al usuario añadir
nuevas características al lenguaje. Podría significar simplemente el añadir nuevos tipos al
lenguaje, añadir nuevas funciones a una biblioteca o también poder añadir nuevas palabras claves
y constructores al traductor mismo. Esto en lenguajes funcionales como LISP, no resulta difícil.
Sin embargo en lenguajes imperativos esto resulta bastante más difícil.
A lo largo de los últimos 10 años la extensibilidad ha pasado a ser de importancia primordial como
una propiedad de los lenguajes. En concreto la simplicidad sin extensibilidad, prácticamente tiene
garantizado el fracaso del lenguaje.

7
LENGUAJES DE PROGRAMACIÓN
Página 8 de 78
Autor: informatica_uned@[Link]

Capacidad de restricción.
Un diseño del lenguaje debería dar la posibilidad de que un programador pudiera programar de
una forma útil empleando un conocimiento mínimo del leguaje y un número mínimo de
constructores. El lenguaje debería de tener la capacidad de definir subconjuntos del lenguaje.
Esto produce dos beneficios: No es necesario que un programador aprenda todo el lenguaje para
empezar a utilizarlo con efectividad y segundo el traductor podría elegir implementar un
subconjunto. Un aspecto de la capacidad de restricción es la eficiencia; un programa en C++ que
no utiliza el manejo de excepciones no debe ejecutarse más lentamente que un programa
equivalente en C por el simple hecho de que C++ pueda manejar excepciones.

Consistencia ante notaciones y reglas convencionales aceptadas.


Un lenguaje de programación debería ser fácil de aprender por un programador experimentado.
Dicho de otra manera, un lenguaje debería de tener tantas características y conceptos posibles
que se hayan convertido en estándares.
Ley del mínimo asombro: las cosas no deberían de parecer y comportarse de manera totalmente
inesperadas.

Precisión.
Es la existencia de una definición precisa para el lenguaje, de tal manera que su comportamiento
de los programas pueda ser predecible. Un paso para lograr la precisión es la publicación de un
manual o informe del lenguaje por parte del diseñador.

Independencia de la máquina.
El método primordial para lograr la independencia de la máquina es el uso de datos predefinidos
que no involucran detalles de asignación de memoria o de la arquitectura de máquina.
Desafortunadamente, este tipo de datos no pueden nunca estar totalmente libres de problemas
de la máquina. Las constantes definidas por la implementación en las bibliotecas estándares de C
son un ejemplo de una manera útil de aislar las dependencias de la máquina.

Seguridad.
Este principio se basa en minimizar los errores de programación y permitir que dichos errores
sean detectados e informados. La seguridad está íntimamente relacionada con la confiabilidad y
con la precisión. Este es el principio que condujo a los diseñadores del lenguaje a introducir los
tipos, la verificación de tipos y las declaraciones de variables en los lenguajes de programación.
Con esto se puede comprometer tanto la expresividad como lo conciso del lenguaje. Carga al
programador con la tarea de poner tantas cosas cómo sea posible en el código real. Por otra
parte, en aplicaciones industriales, comerciales y de defensa, regularmente se presenta una
demanda de incluso mayor seguridad.
El problema real es la forma en el que un lenguaje debe generarse para que sea seguro y aún así
permitan un máximo de expresividad y generalidad.

8
LENGUAJES DE PROGRAMACIÓN
Página 9 de 78
Autor: informatica_uned@[Link]

4 Sintaxis
La sintaxis es la estructura de un lenguaje. Aunque la semántica sigue describiéndose en ingles,
uno de los más grandes adelantos en los lenguajes de programación es el desarrollo de un
sistema formal para describir la sintaxis. Las formas Backus Naur – BNF- se están utilizando de
manera frecuente en la definición de muchos lenguajes de programación incluyendo Java y Ada.
Existen 3 formas de representar BNF: BNF original, BNF extendido (EBNF) y los diagramas
sintácticos.

4.1 Estructura léxica de los lenguajes de programación.

La estructura léxica de un lenguaje de programación es la estructura de sus palabras o


tokens.
La estructura léxica puede estudiarse por separado de la estructura sintáctica, pero está
relacionada íntimamente. La fase de análisis léxico de un traductor reúne en forma de token
secuencias de caracteres del programa de entrada, los cuales posteriormente se procesan
mediante una fase de análisis sintáctico, lo que determina la estructura sintáctica.

Los tokens se clasifican en varias clases:


• Palabras reservadas: if while.
• Literales o constantes: como 42 o “hello”.
• Símbolos especiales: “;” “+”.
• Identificadores: x24, monthly_balance o putchar.

Se llaman palabras reservadas porque un identificador no puede tener la misma cadena de


caracteres que una palabra reservada.
Los identificadores predefinidos son aquellos a los que se les ha dado una interpretación
inicial para todos los programas en el lenguaje, pero son capaces de redefinición. Ej. integer
Para resolver las posible ambigüedades que se puede presentar (doif…) se considera el uso del
principio de la subcadena de mayor longitud como una regla estándar en la determinación
de tokens. Este consiste que en cada punto se reúne en un solo tokens la cadena más larga
posible de caracteres. Este principio requiere que ciertos tokens vengan separados por
delimitadores o espacios en blanco.
Se puede utilizar la sangría para determinar la estructura pero lo más normal es los lenguajes
sean de formato libre, por lo que el formato no tiene efecto sobre la estructura del programa.
Son muy escasos los lenguajes de formato fijo donde los tokens deben aparecer en posiciones
preespecíficas de la página.
Los tokens a menudo son descritos en formato ingles, pero también pueden describirse
formalmente mediante expresiones regulares las cuales son descripciones de patrones
regulares descritos normalmente por las tres operaciones:

1. Concatenación: al poner en secuencia elementos sin una operación explicita.


2. Repetición: mediante el uso de un asterisco “*”.
3. Selección o elección: mediante la barra vertical “|”.

A menudo, la notación de expresiones regulares se amplía mediante operaciones


adicionales y caracteres especiales como:
[ - ] .- Indican rango de caracteres. [0 – 9]
+ .- Indica una o más repeticiones.
? .- Indica un elemento opcional
Los espacios no deben tenerse en cuenta.

9
LENGUAJES DE PROGRAMACIÓN
Página 10 de 78
Autor: informatica_uned@[Link]

4.2 Gramáticas libres del contexto.

Consiste en un conjunto de reglas gramaticales, las cuales están formadas de un lado izquierdo
(un solo nombre de estructura) y a continuación el meta símbolo “” (a veces se reemplaza
por “::=”), seguido de un lado derecho formado por una secuencia de elementos que pueden ser
símbolos u otros nombres de estructuras. Las cursivas sirven para distinguir los nombres de las
estructuras de las palabras reales, es decir, de los tokens que pudieran aparecer en el lenguaje.
La barra vertical “|” es también un metasímbolo y significa “o”. Algunas veces un metasímbolo es
un símbolo real en un lenguaje, en cuyo caso, es recomendable entrecomillar el símbolo para
distinguirlo del metasímbolo. Para indicar que una oración debe estar seguida por algún tipo de
marcador final, se hace mediante el signo $.

Los nombres de las estructuras se conocen como no terminales (pueden subdividirse en


estructuras adicionales). A las palabras o símbolos de token se les denomina terminales (nunca
se subdividen). Las reglas gramaticales también son llamadas producciones. Las producciones
se presentan en la forma de Backus-Naur si están como dadas utilizando únicamente los
metasímbolos “” y “|” (y paréntesis algunas veces). Existe un no terminal denominado símbolo
inicial. Este no terminal representa toda la estructura que se está definiendo y es el símbolo a
través del cual se inician todas las derivaciones.

Una gramática libre de contexto define un lenguaje denominado lenguaje de la gramática.


Este lenguaje es el conjunto de todas las cadenas de terminales para las cuales existe una
derivación que empieza en el símbolo inicial y acaba con la cadena de terminales. En general
estos lenguajes son no finitos.
En estas gramáticas no necesariamente suele haber tantas producciones como no terminales pero
no siempre es así (sustantivo -> girl | dog; sustantivo -> girl ; sustantivo -> dog).
Se dice que son libres del contexto por que los lados izquierdos de las reglas están compuestos
por un solo no terminal. Esto significa que cada no terminal puede ser reemplazado por cualquier
opción del lado derecho independientemente de dónde pudiera aparecer el no terminal. Para darle
sensibilidad de contexto habría que dar la posibilidad de que aparecieran “cadenas de contexto”
en el lado izquierdo.

Se tomará como problema semántico y no sintáctico todo aquello que no pueda ser expresado
con una gramática libre de contexto.

Ejemplo:
(1) oración -> frase-sustantiva frase-verbal
(2) frase-sustantiva -> artículo sustantivo
(3) artículo -> a | the
(4) sustantivo -> girl | dog
(5) frase-verbal -> verbo frase-sustantiva
(6) verbo -> sees | pets

Cualquier frase valida de acuerdo con la gramática anterior puede construirse de la


siguiente manera: Iniciamos con el símbolo oración (símbolo inicial) y seguimos
reemplazando los lados izquierdos con los lados derechos de las reglas anteriores. A este
proceso se le denomina derivación.

10
LENGUAJES DE PROGRAMACIÓN
Página 11 de 78
Autor: informatica_uned@[Link]

4.2.1 Reglas BNF como ecuaciones.

Las reglas gramaticales se pueden expresar en forma de ecuaciones de conjunto. Como


ejemplo la siguiente ecuación:
expr -> expr + expr | número
Se puede expresar de la siguiente manera:
E=E+EUN
Las soluciones a ecuaciones recursivas aparecen frecuentemente en las descripciones formales en
los lenguajes de programación donde se conocen como puntos fijos mínimos.

4.3 Árboles de análisis sintáctico y árboles de sintaxis abstracta.

La sintaxis establece una estructura pero no un significado. Pero el significado de una oración (o
un programa) tiene que estar relacionado con su sintaxis.
El proceso de asignar la semántica de una construcción a su estructura sintáctica se conoce como
semántica dirigida por la sintaxis. Por lo que se deberá construir la sintaxis de manera que
refleje lo mejor posible su semántica.

Una forma de expresar la estructura sintáctica de un programa que determina su semántica es a


través de lo que se denomina árbol de análisis sintáctico. Este describe de manera gráfica el
proceso de reemplazo dentro de una derivación.
Un árbol de análisis gramatical (sintáctico) está identificado mediante no terminales en los nodos
interiores y por terminales en las hojas y su estructura está totalmente determinado por las reglas
gramaticales del lenguaje y por una derivación de una secuencia en particular.

No todas las terminales y no terminales pudieran ser necesarios para determinar totalmente la
estructura sintáctica de una expresión o de una oración. Es estos casos los árboles suelen estar
condensados y se conocen como árboles de sintaxis abstracta o árboles sintácticos puesto
que abstraen la estructura esencial del árbol de análisis sintáctico. (Ver ejemplo Pág. 82).

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

4.4 Ambigüedad, asociatividad y precedencia.

Dos derivaciones diferentes pueden conducir al mismo árbol sintáctico. Sin embargo diferentes
derivaciones pueden producir diferentes árboles de análisis gramatical y sus subsecuentes árboles
abstractos. Una gramática para la cual sean posibles dos árboles diferentes para un mismo
análisis sintáctico se dice que es ambigua.

Ejem.
expr -> expr + expr | expr * expr | (expr) | número
número -> número dígito | dígito
dígito -> 0 | 1 | 2 | 3 | …..

Ciertas derivaciones construidas en un orden específico corresponden a árboles sintácticos únicos.


Una derivación que tienes esta propiedad es la derivación por la izquierda, que consiste en
tomar como reemplazo el no terminal restante más a la izquierda.
Una forma de determinar si una gramática es ambigua es buscar dos derivaciones por la izquierda
distintas de una misma cadena.

11
LENGUAJES DE PROGRAMACIÓN
Página 12 de 78
Autor: informatica_uned@[Link]

Una gramática para que sea útil no puede ser ambigua, por lo que si lo fuera habría que aplicarle
alguna regla para eliminar dicha ambigüedad.
La forma más habitual de revisar este tipo de gramáticas es escribiendo una nueva regla
gramatical (llamada un “término”) que establece una cascada de precedencia. Para el caso de la
gramática anterior:

expr -> expr + expr | término


término -> término * término | (expr) | número

Ej. Gramática revisada para expresiones aritméticas de enteros simples:


expr -> expr + término | término
término -> término * factor | factor
factor -> (expr) | número
número -> número dígito | dígito
dígito -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

4.5 EBNF y diagramas sintácticos.

EBNF surge para dotar al BNF de una notación especial para el tipo de reglas gramaticales que
expresan con mayor claridad la naturaleza repetitiva de su estructura.

En esta notación las llaves “{}” indican cero o más repeticiones de algo:
Número -> dígito {dígito}
Expr -> término {+término}
En este tipo de notación (Backus-Naur extendida) las llaves se han convertido en nuevos meta
símbolos. Esta notación oculta su asociatividad por la izquierda la cual es generada por la
recursividad por la izquierda.

Las reglas recursivas por la derecha no se pueden representar mediante llaves por lo que no será
por lo que a través de EBNF no se podrán representar directamente los árboles sintácticos o los
árboles de análisis sintáctico, por lo que utilizaremos siempre la notación BNF para escribir árboles
de análisis sintáctico.
Para indicar que una estructura tiene una parte opcional utilizaremos los corchetes “[]”:
Como ejemplo:

Enunciado if -> if (expresión) enunciado | if (expresión) enunciado else enunciado


Tendremos:
Enunciado if -> if (expresión) enunciado [else enunciado]

También los operadores asociativos por la derecha (binarios) pueden describirse utilizando
estos nuevos metasímbolos:

Expr -> término @ expr | expr


Como
Expr -> término [@ expr]

expr -> término {+ término}


término -> factor {* factor}
factor -> (expr) | número
número -> dígito { dígito}
dígito -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Ej. Gramática EBNF para expresiones gramaticales enteros simples.

12
LENGUAJES DE PROGRAMACIÓN
Página 13 de 78
Autor: informatica_uned@[Link]

El diagrama sintáctico es una representación de una regla gramatical, el cual refleja la


secuencia de terminales y no terminales que se encuentran en el lado derecho de la regla. Estos
utilizan círculos u óvalos para representar los terminales y rectángulos para representar los no
terminales, conectándolos entre sí mediante líneas y flechas con el fin de indicar la secuencia
apropiada. Estos también pueden condesar varias reglas gramaticales en un solo diagrama. (Ver
ejemplos en el libro. Pagina 89-91).
Estos diagramas se escriben siempre partiendo de una notación EBNF, y nunca de una BNF.
Estos diagramas son atractivos visualmente pero ocupan mucho espacio por lo que actualmente
son poco utilizados a favor de las notaciones EBNF y BNF.

4.6 Técnicas y herramientas de análisis sintáctico.

Una gramática escrita en forma BNF, EBNF o diagrama sintáctico describe las cadenas de tokens
que sintácticamente son correctas en el lenguaje de programación. Por lo que de manera implícita
quedan reflejadas las acciones que debe realizar el analizador sintáctico para analizar de forma
correcta una cadena de tokens.

La forma más simple de un analizador sintáctico es un reconocedor, programa que acepta o


rechaza cadenas dependiendo de si son o no aceptadas en el lenguaje.
Según las distintas maneras en las que un analizador sintáctico interpreta una gramática en
cualquiera de sus formas tenemos distintos métodos de análisis:

- Analizadores sintácticos de abajo arriba: Intenta hacer coincidir una entrada con los
lados derechos de las reglas gramaticales, Cuando se encuentra con una coincidencia el
lado derecho es sustituido (reducido) por el no Terminal de la izquierda. Su denominación
(abajo arriba) viene dada por el hecho de que construyen derivaciones y árboles
sintácticos de las hojas hacia la raíz (también llamados de desplazamiento-reducción).

- Analizadores sintácticos de arriba abajo: Los no terminales se expanden para coincidir


con los tokens de entrada y construyen directamente una derivación.

El analizador de abajo arriba es más poderoso que el otro por lo que es más utilizado
generalmente por los generadores de analizadores sintácticos (o compiladores de
compiladores). Un generador ampliamente utilizado es el YACC o su versión libre Birson.

- Otro método más antiguo de general analizadores a partir de su gramática, que resulta
muy efectivo es el análisis sintáctico por descenso recursivo. Básicamente opera
convirtiendo los no terminales en procedimientos mutuamente recursivos, cuyas acciones
están basadas en los lados derechos de los BNF. En estos procedimientos los lados
derechos se interpretan de la siguiente manera: los tokens se hacen coincidir con los
tokens de entrada según se van construyendo y los no terminales se asocian con llamadas
al procedimiento que los representa.

Este tipo de analizadores presenta un problema con las reglas recursivas por la izquierda como la
que corresponde con una expresión:

Expr -> expr + término | término

13
LENGUAJES DE PROGRAMACIÓN
Página 14 de 78
Autor: informatica_uned@[Link]

Si intentamos escribir esta regla como un proceso recursivo descendente se presentan dos
problemas graves:

1- La primera opción de esta regla haría que el proceso se llamara a sí mismo de manera
recursiva y de forma infinita.
2- El segundo problema estriba en que no se sabrá que opción es la correcta hasta que se
viese o no un (+) mucho más adelante en la entrada.

Estos problemas están motivados por la recursividad por la izquierda ya que con la
recursividad por la derecha

Expr -> término + expr | término

Se puede resolver el problema de la siguiente manera:

Void expr (void)


{
Term ();
If (token == “+”)
{
Match (“+”);
Expr ();
}
}

Pero desafortunadamente esta regla gramatical convierta al + en un operador asociativo por la


derecha. La solución está implícita en la notación EBNF de la regla que expresa la recursión como
un ciclo:

Expr -> término {+ término}

En EBNF las llaves representan la eliminación de la recursión por la izquierda, mediante el uso de
un ciclo (ver. 93) y aunque hay mecanismo más generales para resolver la recursión por la
izquierda, en al práctica el simple uso de EBNF es suficiente.

En le caso de la recursividad por la derecha no se presenta el problema señalado anteriormente


para el análisis recursivo descendente, sin embargo se da la situación de que el código para una
regla de recursión por la derecha:

Expr -> término @ expr | término


Se escribe en formato EBNF:
Expr -> término [@ expr]
Proceso que se conoce como factorización por la izquierda

Por lo tanto en situaciones de recursión por la izquierda y en la factorización por la izquierda, las
reglas EBNF o los diagramas sintácticos corresponden con el código de un analizador sintáctico
por descenso recursivo, siendo esta una de las razones de amplia utilización.

14
LENGUAJES DE PROGRAMACIÓN
Página 15 de 78
Autor: informatica_uned@[Link]

- Un analizador sintáctico predictivo es aquel que basa su acción únicamente en el token


disponible en el flujo de entrada. Este uso de un solo token para dirigir la acción del analizador
sintáctico se conoce con el nombre preanálisis de un solo símbolo. Estos analizadores
requieren que las gramáticas a analizar cumplan ciertas condiciones:

* La primera condición que requiere es capacidad de escoger entre varias alternativas


en una regla gramatical

A -> α1 | α2 | α3 | … | αn

Para decidir cual elegir, los tokens que inician cada αi tienen que ser distintos. ∩ Primero
(αi) = Ө, donde primero es la función que devuelve el conjunto de tokens que pueden
presentarse al principio de cada αi.

* La segunda condición se presenta con las reglas gramaticales que contienen


estructuras opcionales:

Expr -> término [ @ expr]

Si @ se presenta como token en la entrada, pudiera ser el comienzo de una parte


opcional, o pudiera ser un token que aparece después de la expresión completa. Por lo
que para que esto no se de se tiene que cumplir:
Primero (α) ∩ Siguiente (α) = Ө, donde siguiente es el conjunto de tokens que pueden
seguir a α

El proceso de convertir reglas en gramaticales en un analizador sintáctico puede automatizarse, es


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

4.7 Léxico comparado con la sintaxis y con la semántica.

Una gramática libre de contexto típicamente incluye una descripción de los tokens de un lenguaje
al incluir en las reglas gramaticales las cadenas de caracteres que forman los tokens.

Algunas clases típicas de tokens, como las literales o constantes y los identificadores no son por sí
mismos secuencias fijas de caracteres, sino que se elaboran a partir de un conjunto fijo de
caracteres, como los dígitos del 0 al 9.

Estas clases de tokens pueden tener su estructura definida por la gramática, sin embargo, es
posible e incluso deseable utilizar un analizador léxico para reconocer estas estructuras, pues
puede hacerlo mediante una operación repetitiva simple.

Un conflicto entre sintaxis y semántica se presenta cuando los lenguajes requieren que ciertas
cadenas sean identificadores predefinidos (pueden ser modificados) en lugar de palabras
reservadas (cadenas fijas de caracteres no pueden utilizarse como identificadores).

15
LENGUAJES DE PROGRAMACIÓN
Página 16 de 78
Autor: informatica_uned@[Link]

5 Semántica básica.
La especificación de la semántica de un lenguaje de programación es una tarea más difícil que la
especificación de su sintaxis. Como ya se vio en el capítulo 1 existen varias formas de especificar
la semántica de un lenguaje de programación:

- Mediante un manual de referencia del lenguaje. Este es el método más


común.
- Mediante un traductor definidor:
- Mediante una definición formal: Estos métodos matemáticos son precisos
pero también son complejos y requieren de estudio para su comprensión.

5.1 Atributos, ligaduras y funciones semánticas.

Un mecanismo fundamental de abstracción en un lenguaje de programación es el uso de


nombres, es decir, identificadores, para denotar entidades o constructores del lenguaje. Un
paso fundamental de la descripción de la semántica de un lenguaje consiste en describir las reglas
convencionales que determinan el significado de cada uno de los nombres utilizados en un
programa.

Además de los nombres para la descripción de la semántica se requiere de los conceptos de


valor: Cualquier cantidad almacenable como los enteros. Y localización: Dónde se pueden
almacenar estos valores. Se puede pensar en ellas de una manera más abstracta que las
direcciones de una computadora en particular.

El significado de un nombre queda determinado por sus atributos asociados. Tanto las
declaraciones de constantes como las de funciones como las asignaciones tienen sus atributos
asociados.
Ejemplos:

- const int n = 5; Asocia al nombre n el atributo de tipo de datos “Constante entera” y el


atributo de valor 5
- int X; Asocia el atributo “Variable” y el tipo de datos “entero”.
- double f(int n)
{
…..
}

En este caso asocia el atributo “función” al nombre f y los siguientes atributos adicionales:
1.- La cantidad, nombre y tipos de sus parámetros.
2.- El tipo de datos del valor devuelto.
3.- El cuerpo del código a ejecutarse cuando se llama a f.
- X = 2; Asocia el atributo “valor 2” a la variable X.

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


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

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

- Ligadura dinámica: Tiene lugar durante la ejecución y genera atributos dinámicos.

16
LENGUAJES DE PROGRAMACIÓN
Página 17 de 78
Autor: informatica_uned@[Link]

Los tiempos de ligadura pueden depender del traductor, es decir, para los interpretes se crearán
la mayoría de las ligaduras de forma dinámicas, mientras que para los compilados se crearán la
mayoría de las ligaduras de forma estáticas.
Un atributo estático puede vincularse durante el análisis gramatical o durante el análisis semántico
(tiempo traducción), durante el encadenamiento del programa (tiempo de ligado) o durante la
carga del programa para su ejecución (tiempo de carta).

Los tiempos de ligadura se pueden clasificar de la manera siguiente:

- Tiempo de definición de lenguaje.


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

Todos los tiempos anteriores representan ligaduras estáticas excepto el último que representa
ligaduras dinámicas.
El traductor debe conservar las ligaduras de tal manera que se den significados apropiados
durante la traducción y la ejecución. Esto se lleva a cabo mediante una estructura de datos que
visto de una manera abstracta se puede ver como una función que expresa la ligadura de los
atributos a los nombres. Esta función es parte fundamental de la semántica y se conoce como
tabla de símbolos. Esta función cambiará durante el proceso de traducción y/o ejecución para
reflejar adiciones o eliminaciones de ligaduras.

Para un compilador la función quedará de la siguiente manera ya que procesa básicamente


atributos estáticos:
Tabla de Símbolos
Nombres ---------------------------------> Atributos estáticos

En un programa compilado la parte de asignación de memoria por parte de la ligadura de los


nombres a localizaciones de almacenamiento se conoce como el entorno:

Entorno
Nombres ---------------------------------> Localizaciones

Mientras que las ligaduras de las localizaciones de almacenamiento con los valores se conoce
como la memoria:
Memoria
Localizaciones ---------------------------> Valores

En un intérprete se combinan tabla de símbolos con entorno y memoria, ya que durante la


ejecución se procesan ambos atributos estáticos y dinámicos y la función tiene la siguiente
imagen:

Entorno
Nombres ---------------------------------> Atributos (incluyendo localizaciones y valores)

17
LENGUAJES DE PROGRAMACIÓN
Página 18 de 78
Autor: informatica_uned@[Link]

5.2 Declaraciones, bloques y alcance.

A través de la declaraciones puede determinarse las ligaduras ya sea de manera implícita como
explícita.

Int x; con esta declaración se establece de manera explícita el tipo de datos para X, pero su
localización exacta durante la ejecución se vincula de forma implícita.

Las declaraciones también pueden realizarse de manera implícita o explícita. Aquellos lenguajes
que tienen declaración implícita, generalmente tienen reglas convencionales de nombre para
establecer otros atributos.
Las declaraciones que vinculan ciertos atributos se conocen como definiciones, mientras que
aquellas que sólo especifican parcialmente los atributos se conocen simplemente como
declaraciones.
Las declaraciones están asociadas tanto sintácticamente como semánticamente con ciertos
constructores del lenguaje como son el Bloque, los Tipos de datos estructurados y las
clases:

Bloque: Consiste en una secuencia de declaraciones seguidas por una secuencia de enunciados,
y rodeado por marcadores sintácticos como son las llaves o los pares begin-end.
Las declaraciones hechas dentro de un bloque se conocen como locales mientras que las hechas
por fuera se conocen como no locales.

Tipos de datos estructurados: Otro tipo de constructor que establece declaraciones de


variables de ámbito local.

Las clases: También son un fuente de declaración de variables.

Para terminar las clases también pude reunirse en grupos más grandes como una manera de
organizar los programas: Los paquetes, las tareas de ADA, los paquete de Java, los módulos,
etc.…

Las declaraciones vinculan varios atributos a los nombres, dependiendo del tipo de declaración.
Cada una de estas ligaduras tiene por si misma un atributo que queda determinado por la
posición dentro de la declaración en el programa.

El alcance de un vínculo es la región del programa sobre la cuál se conserva el vínculo. A veces
nos referimos erróneamente al alcance un nombre, pero esto no es cierto pues puede haber
varias declaraciones diferentes sobre el mismo nombre y cada una de ellas con un alcance
distinto.

Se conoce como alcance léxico aquel que comprende el bloque dónde aparece su declaración
asociada. Esta es una regla estándar de alcance de la mayoría de los lenguajes.
Otra regla de alcance (en C) es la conocida como declaración antes de uso, y define el alcance de
una declaración desde el punto justo después de la misma hasta el final del bloque en el que está
localizado.

En el caso de bloques anidados las declaraciones en los bloques anidados toman preferencia
sobre declaraciones anteriores, este caso la declaración global se dice que tiene una apertura en
el alcance dentro del bloque anidado donde se encuentra la otra declaración. En base a esto se
hace una diferenciación entre visibilidad y alcance: la visibilidad hace referencia a aquellas
regiones del programa donde las ligaduras de una declaración son aplicables, en tanto que el
alcance incluye los agujeros en el alcance (dado que las ligaduras siguen existiendo). Se suele

18
LENGUAJES DE PROGRAMACIÓN
Página 19 de 78
Autor: informatica_uned@[Link]

utilizar el operador de resolución de alcance para tener acceso a estas declaraciones ocultas.
(Ejemplo Páginas 124-125).
Una situación especial de alcance sucede con las declaraciones de clases en lenguajes orientados
a objetos, pues este tipo de declaraciones tienen un alcance que se extiende hacia atrás a fin de
incluir toda la clase (suspendiendo la regla de declaración antes de uso).

Las vinculaciones establecidas por las declaraciones se conservan mediante la tabla de símbolos.
La forma en que quedan procesadas las declaraciones en la tabla de símbolos corresponde con el
alcance de cada declaración. Reglas de alcance diferente requieren un comportamiento diferente
en la tabla de símbolos e incluso el uso de estructuras diferentes dentro de la misma.

5.3 La tabla de símbolos.

La tabla de símbolos es como un diccionario variable: debe permitir la inserción, búsqueda y


cancelación de nombres con sus atributos asociados, representando las vinculaciones en
declaraciones.
El mantenimiento de información de alcance en lenguaje con alcance léxico y estructura de
bloques requiere que las declaraciones sean procesadas en forma de pila: a la entrada de un
bloque todas las declaraciones de dicho bloque se procesan y se agregan la vinculaciones que
procedan a la tabla de símbolos, y a la salida del bloque se eliminan las ligaduras perteneciente a
dicho bloque, restaurando cualquier vínculo anterior que pudiera existir.

Podemos considerar esquemáticamente la tabla de símbolos como un conjunto de nombres, cada


uno de los cuales tiene una pila de declaraciones asociadas con ellos, de manera que la
declaración en la cima de la pila será aquella cuyo alcance sea el actualmente activo.

Ver ejemplos páginas 127-130. Hay que observar que este proceso conserva la información
apropiada de alcance incluyendo los agujeros de alcance. Esta representación de la tabla de
símbolos supone que la tabla procesa las declaraciones de manera estática (antes de su
ejecución) o sea manejada por un compilador, y que las ligaduras de las declaraciones sean todas
estáticas. Pero si estuviera administrada de esta misma forma pero dinámicamente, es decir,
durante la ejecución, entonces las declaraciones se procesan conforme se van encontrando a
través del programa a lo largo de la trayectoria de ejecución. Esto determina una regla de alcance
diferente denominada alcance dinámico. A la regla de alcance léxico anterior se le denomina
alcance estático. Ver ejemplo de alcance dinámico páginas 131-132.
Hay que notar que cada una de las llamadas a un mismo procedimiento a lo largo del programa
puede tener una tabla de símbolos diferentes a su entrada.

Existen varios problemas importantes por los cuales se hace difícil la utilización del alcance
dinámico por la mayoría de los lenguajes:

 El principal problema y más importante es que bajo el alcance dinámico cuando se utiliza
un nombre no local en una sentencia, la declaración que se aplica a este nombre no puede
determinarse mediante la simple lectura del programa. Por lo que diferentes ejecuciones
del programa pueden conducir a diferentes resultados, por lo que la semántica de una
función puede cambiar considerablemente conforme avanza la ejecución del programa.
 Otro problema serio es que dado que las referencias variables no locales no pueden
predecirse antes de la ejecución tampoco, pueden definirse los tipos de estas variables.

Por este motivo la ligadura estática de los tipos de datos (tipificado estático) y el alcance dinámico
son inherentemente incompatibles.
A pesar de todos esto, el alcance dinámico sigue siendo una opción posible para aquellos
lenguajes muy dinámicos, interpretados cuando no esperamos que los programas sean

19
LENGUAJES DE PROGRAMACIÓN
Página 20 de 78
Autor: informatica_uned@[Link]

extremadamente grandes (APL, Snobol , Perl y Lisp).


Independientemente de la cuestión del alcance léxico nos encontramos con un dificultad añadida
con el tratamiento de las estructuras.
Cada una de las declaraciones “struct” debe de contener declaraciones adicionales de los campos
de datos dentro de cada “struct” y estas deben de ser accesibles siempre que las mismas estén
en alcance. (Ejemplo Página 136). Esto quiere decir dos cosas:

(1) Una declaración “struct” realmente contiene una tabla de símbolos locales que es en sí
un atributo.
(2) Esta tabla de símbolos no puede eliminarse hasta que la variable “struct” que la
contenga sea eliminada de la tabla de símbolos global.

Cualquier estructura de alcance que puede ser referenciada directamente en un programa tiene
que tener su propia tabla de símbolos. Esto incluye los alcances nombrados en Ada, las clases, las
estructuras, las clases y los paquetes de Java, etc.… Por lo que una estructura de símbolos más
típica en cualquiera de estos lenguajes es tener una tabla para cada uno de los alcances, que a su
vez tienen que estar anidados con sus propias tablas dentro de las tablas que las encierran. De
nuevo estos pueden tener un estilo basado en pilas. (Ejemplo Página 138).

5.4 Resolución y sobrecarga de nombres.

La sobrecarga se refiere hasta donde un mismo nombre pude utilizarse para referirse a cosas
distintas dentro de un mismo programa y es una faceta importante con respecto a las
declaraciones y a las operaciones con tabla de símbolos. Como ejemplo el símbolo + se refiere
como mínimo a dos operaciones completamente distintas: La adicción de enteros y la adicción en
coma flotante.
Algunos lenguajes como C++ y Ada permiten una amplia sobrecarga tanto de operadores como
de nombres de funciones, Java sólo permite la sobrecarga en los nombres de funciones.

Un traductor consigue determinar entre los distintos usos de un símbolo buscando en los tipos de
datos de los operandos. Para que esto sea posible hay que ampliar el mecanismo de búsqueda
para que no sólo se limite a una búsqueda por nombre sino que también distinga entre nº de
parámetro y el tipo de estos. A este proceso se le conoce como resolución de sobrecarga.

La tabla de símbolos puede determinar la función más apropiada para cada una de las llamadas
(ver ejem. Página 140 5.18) de la información contenida en cada llamada (contexto de
llamado).

Se presentan problemas de ambigüedad cuando los lenguajes permiten la conversión automática


de un tipo a otro, generando llamadas ambiguas. Por lo que las conversiones automáticas como
existe en C++ y en Java complican de manera significativa el proceso de la resolución de
sobrecarga (menos en Java al tener un conversión más restrictiva, sólo permite conversión
cuando no hay perdida de información).

Un problema adicional es si se permite información adicional en un contexto de llamada a parte


del nº de parámetros y sus tipos, como pudiera ser el tipo del valor de retorno en una función. En
Ada por ejemplo se permiten el tipo de retorno e incluso los nombres de los parámetros en un
definición.

Tanto Ada como C++ (pero no Java) permiten la sobrecarga en los operadores incorporados. En
estos casos hay que respetar las propiedades sintácticas del operador; no podemos cambiar su
asociatividad o su precedencia.

20
LENGUAJES DE PROGRAMACIÓN
Página 21 de 78
Autor: informatica_uned@[Link]

Se podría utilizar la sobrecarga para referirnos a cosas de tipos completamente diferentes con el
mismo nombre, si embargo esto resultaría extremadamente confuso, por lo que no está permitido
en la mayoría de los lenguajes.

5.5 Asignación, tiempo de vida y entorno.

Necesitamos estudiar el entorno, que mantiene las ligaduras de los nombres con las
localizaciones.
El entorno se puede construir estáticamente, dinámicamente o como una mezcla de ambos.
Entorno estático FORTRAN, dinámico LISP y mezcla C, C++, Ada, Java…

No todos los nombres en un programa están vinculados con localizaciones. En un lenguaje


compilado, los nombres de las constantes y de los tipos de datos pueden representar puramente
cantidades en tiempo de compilación: const int MAX = 10;

Las declaraciones se utilizan para construir la tabla de símbolos.

En un lenguaje con estructura de bloques las variables globales se asignan estáticamente. Las
variables locales, se asignan dinámicamente cuando la ejecución llega al bloque en cuestión.
Durante la ejecución, cuando se entra a cada uno de los bloques, las variables declaradas al
principio de cada bloque se asignan, y cuando se sale de cada bloque, estas mismas variables se
desasignan.
En un lenguaje con estructura de bloques y alcance léxico, se puede asociar el mismo nombre con
varias localizaciones diferentes. Debemos por tanto distinguir entre un nombre, una localización
asignada y la declaración que hace que queden vinculadas (Ejemplo Página 146-148).

El tiempo de vida o extensión de un objeto es la duración de su asignación en el entorno. Las


vidas de los objetos se pueden extender más allá de la región de un programa donde pueden ser
objeto de acceso.

Un apuntador es un objeto cuyo valor almacenado es una referencia a otro objeto: En C: int *x;
Para permitir la inicialización de los apuntadores que no apuntan a un objeto asignado, C permite
el uso del nombre null: En C: int *x=NULL;

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

Se dice que la variable x se puede desreferenciar utilizando el operador “*”, entonces podemos
asignar valores enteros a *x y referirnos a esos valores como lo haríamos en una variable
ordinaria:
*x=2;
*x también se puede desasignar haciendo una llamada al procedimiento free: free(x);
C++ incorpora new y delete como nombres reservados.

int *x=new int;


*x=2;
delete x;

21
LENGUAJES DE PROGRAMACIÓN
Página 22 de 78
Autor: informatica_uned@[Link]

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

En una implementación típica del entorno, la pila (para la asignación automática) y el montículo o
montón (para la asignación dinámica) se mantienen en secciones diferentes de la memoria, y las
variables globales también se asignan en un área por separado, estática.
Java permite la asignación, pero no la desasignación bajo el control del programador.
Resumiendo, en un lenguaje con estructura de bloques con asignación de montones, existen tres
tipos de asignación en el entorno: estático (para variables globales), automático (para variables
locales) y dinámico (para asignación de montones). Estas categorías también se conocen como
clases de almacenamiento de la variable.

5.6 Variables y constantes.

5.6.1 Variables.

Una variable es un objeto cuyo valor almacenado se puede cambiar durante la ejecución.
También se puede pensar en una variable como completamente especificada por sus atributos,
que incluyen su nombre, su localización, su valor y otros atributos como tipos de datos y
tamaños.
Una representación esquemática se puede dibujar como sigue. A la forma reducida se la conoce
como diagrama cuadro y círculo. La línea que une el nombre con el cuadro de localización se
puede pensar que une el nombre con la localización a través del entorno.

Nombre Valor

Localización

La forma principal en la que una variable cambia su valor es a través del enunciado de
asignación. Hay que distinguir entre la localización y el valor almacenado en dicha localización.
Al valor almacenado en un localización se conoce como valor r, mientras que al la localización se
conoce como valor l.

En C, además de la desreferenciación automática estándar de los valores l a valores r, existe un


operador explícito “dirección de” operador & que coloca una referencia y la convierte en un
apuntador.

La mezcla de apuntadores, valores r y valores l en C puede llevar a situaciones confusas y poco


seguras. Son sin embargo una consecuencia de la meta de diseño de C como lenguaje rápido y
poco seguro para programación de sistemas.

22
LENGUAJES DE PROGRAMACIÓN
Página 23 de 78
Autor: informatica_uned@[Link]

Asignación por compartición: En algunos lenguajes se da un significado diferente a la


asignación, se copian localizaciones en vez de valores. (Ejemplo Página 155)
La clonación es otra alternativa para x = y, es asignar una nueva localización, copiar el valor de y
a la nueva localización y cambiar la localización de x por la nueva localización. (Ejemplo Página
155)

En los dos casos anteriores se dice que este tipo de asignación como una semántica de
apuntador, y la más usual se conoce como semántica de almacenamiento. Java utiliza la
asignación por compartición para todas las variables de objetos, pero no para los datos simples.

5.6.2 Constantes.

Una constante es una identidad del lenguaje que tiene un valor fijo durante la duración de su
existencia dentro de un programa. Una constante es como una variable, excepto que no tiene
atributo de localización.

Decimos que tiene una semántica de valor en vez de una de almacenamiento como las
variables. Su valor no puede modificarse y su localización no puede ser referenciada de manera
explícita a través de un programa. Una constante es esencialmente el nombre de un valor. Lo
literales son representación de valores (como las secuencia de caracteres “pepe” o el número
42) que hay que diferencia de las constantes.

Muchos lenguajes, en especial los funcionales, hacen énfasis en la utilización de constantes en


detrimento de las variables debido a su semántica más simple. En el caso de Haskell, las variables
queda completamente prohibidas.

Las constantes pueden ser estáticas: Aquella cuyo valor se puede computar antes de su la
ejecución, o dinámicas: aquellas cuyo valor sólo puede se computado únicamente durante la
ejecución.

Las constantes estáticas a su vez pueden ser de dos tipos:

 Aquellas que sólo pueden ser calculadas en tiempo de traducción (tiempo de


compilación).
 Aquellas que son computables en tiempo de carga (como la localización estática de una
variable global).

Esta distinción es importante ya que las primeras (tiempo de compilación) pude ser utilizada por
el compilador para mejorar la eficiencia de un programa y no necesita ocupar memoria. Sin
embargo las que se computan en tiempo de carga o dinámica tienen que ser computada ya sea
en el arranque o conforme avanza la ejecución y debe ser almacenada en memoria.
A las primeras nos referiremos como constantes en tiempo de compilación y a las segundas como
constantes estáticas.
También se puede hacer una distinción entre las constantes en general (todas las vistas
anteriormente) y las constantes de manifiesto, que son el nombre de un literal.

23
LENGUAJES DE PROGRAMACIÓN
Página 24 de 78
Autor: informatica_uned@[Link]

Considere el ejemplo en C:

#include <time.h>

const int a=2;


const int b=27+2*2;

const int c=(int)time(0);

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

En este código a y b son constantes en tiempo de compilación (a es una constante de manifiesto).


En tanto que c es una constante estática (en tiempo de carga) y d es una constante dinámica.

En C, el atributo const se puede aplicar a cualquier variable en un programa que indique


simplemente que el valor de una variable no puede ser cambiado una vez establecido; otros
criterios determinan si una variable es o no estática (por ejemplo el alcance global en el cual se
definen).
Java es similar a C++ y a Ada, pero se indica una constante con la palabra clave final, y si es
constante estática, debe utilizar la palabra clave static.

5.7 Alias, referencias pendientes y basura.

5.7.1 Alias

Un alias ocurre cuando el mismo objeto está vinculado a dos nombres diferentes al mismo
tiempo, puede ocurrir de varias maneras:

• La llamada de procedimiento.
• Uso de variables de apuntador.

Ejemplo en C:

int *x,*y;
x= (int*)malloc(sizeof(int));
*x=1;
y=x; /* Despues de esta linea *y e *x se refieren a la misma variable */
*y=2;
printf(“%d\n”,*x); /* Se imprime 2 */

Los alias presentan un problema en el hecho de que causan efectos colaterales potencialmente
dañinos.
Efecto colateral: cualquier cambio en el valor de una variable que persiste más allá de la ejecución
de un enunciado. Los efectos colaterales no son todos dañinos. Los efectos colaterales que son
cambios a variables cuyos nombres no aparecen directamente en el enunciado son
potencialmente dañinos puesto que el efecto colateral no se puede determinar a partir del código
escrito.
El aliado debido a la asignación de apuntadores es difícil de controlar.

24
LENGUAJES DE PROGRAMACIÓN
Página 25 de 78
Autor: informatica_uned@[Link]

Una tercera manera como se pueden crear alias es a través de la asignación por compartición, ya
que la asignación por compartición utiliza apuntadores implícitamente, un ejemplo de ello es Java.

Ejemplo en Java:

class ArrTest
{ public static void main(String[] args)
{ int[] x={1,2,3}; //Se crea array tamaño 3, y con valores x[0]=1..x[2]=3
int[] y=x; //Se define otro arreglo y se inicializa con x
x[0]=42;
[Link](y[0]); //Imprime 42 debido a la asignación por compartición
}
}

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

int[] y=(int[]) [Link]();

5.7.2 Referencias pendientes.

Las referencias pendientes son un segundo problema que se puede presentar con el uso de
apuntadores. Un referencia pendiente es un localización que ha sido desasignada del entorno,
pero a la cual todavía tiene acceso el programa. Otra manera de definirlas es que son objetos que
pueden ser accedidos más allá de su tiempo de vida en el entorno.
Un ejemplo simple en C es el de un apuntador que apunta a un apuntador desasignado:

int *x, *y;


...
x=(int*)malloc(sizeof(int));
*x=2;
y=x; /*y se inicializa con x, ahora esta como alias */
free(x); /*Se libera x, entonces *y queda con una referencia pendiente */
...
printf(“%d\n”,*y); /* referencia ilegal */

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

{ int *x;
{ int y;
y=2;
x=&y; /* La variable x contiene la localización de y. *x es un alias de y */
}
/* *x es ahora una referencia pendiente */
}

Java no permite referencias pendientes pues no existen apuntadores explícitos, ni operador de


“dirección de”, ni operadores de desasignación de memoria como free o delete.

25
LENGUAJES DE PROGRAMACIÓN
Página 26 de 78
Autor: informatica_uned@[Link]

5.7.3 Basura.

La basura es memoria que ha sido asignada en el entorno pero que se ha convertido en


inaccesible para el programa.
En C ocurre basura si omites el llamar a free antes de reasignar una variable de apuntador:

int *x;
...
x=(int*)malloc(sizeof(int));
x=0;
/* la localización asignada *x por la llamada a malloc es ahora basura, ya que ahora x
contiene el apuntador nulo y no existe ninguna manera de tener acceso al objeto
anteriormente asignado */

Otro ejemplo similar ocurre cuando la ejecución sale de la región del programa en el cual x misma
está asignada:

void p(void)
{ int *x;
x=(int*)malloc(sizeof(int));
*x=2;
}
/* Cuando se sale del procedimiento p, la variable x se deasigna y *x ya no es más
accesible para el programa. Una situación similar ocurre con bloques anidados. */

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


desperdiciada. Los programas que producen basura pueden tener menos errores serios, que los
programas que contienen referencias pendientes, porque estos programas aunque dejen de
ejecutarse al quedarse sin memoria producen resultados correctos, pero un programa que tiene
acceso a referencias pendientes, aunque se ejecute, produce resultados incorrectos.

Los sistemas de lenguajes que recuperan automáticamente la basura se dice que llevan a cabo la
recolección de basura.
Los sistemas funcionales fueron pioneros en recolección de basura como LISP
También los sistemas orientados a objetos Smalltalk y Java lo usan automáticamente.

26
LENGUAJES DE PROGRAMACIÓN
Página 27 de 78
Autor: informatica_uned@[Link]

6 Tipos de datos.
La mayoría de los lenguajes incluyen un conjunto de entidades simples de datos, como enteros,
reales y booleanos, así como mecanismos para construir nuevos tipos a partir de los mismos.
Estas abstracciones contribuyen prácticamente a todas las metas del diseño de lenguajes como:
legibilidad, capacidad de escritura, confiabilidad e independencia de la máquina. Sin embargo
estas abstracciones pueden conllevar una serie de problemas como son:

- Dependencia de la máquina. Un ejemplo es que todos los datos en una computadora


son finitos, siempre va a haber un entero máximo o un entero mínimo, pero el
hadware de cada computadora puede aumentar o disminuir esos valores máximos.
- La falta de consenso entre los diseñadores de lenguajes en relación al grado de
información de tipos que debe de hacerse explícita para verificar la corrección del
programa antes de la ejecución.

Existe muchas razones para tener alguna forma de verificación de tipos estática (es decir en
tiempo de traducción):

1. Eficiencia de ejecución. La información de tipos estáticos permite a los compiladores


asignar memoria con eficiencia.
2. Eficiencia de traducción. Un compilador puede utilizar los tipos estáticos con el fin de
reducir el código necesario a compilar.
3. Capacidad de escritura. La verificación de tipos estáticos permite que muchos errores
estándar de programación sean detectados rápidamente.
4. Seguridad y confiabilidad. La verificación de tipos estáticos mejora la seguridad y la
confiabilidad de los programas al reducir la cantidad de errores de ejecución.
5. Legibilidad. La utilización de tipos explícitos mejora la legibilidad al documentar el diseño
de los datos.
6. Eliminar ambigüedades en los programas.
7. Herramienta de diseño, para corregir errores de diseño en tiempo de traducción.
8. Consistencia y corrección de la interfaz.

Se ha avanzado mucho en comprender la manera de hacer los tipos estáticos más flexibles y que
al mismo tiempo conserven las propiedades antes descritas, y la mayoría de los lenguajes
modernos utilizan tipos estáticos y a la vez tienen una gran flexibilidad.

6.1 Tipos de datos e información de tipos.

Los datos en los programas pueden clasificarse de acuerdo con sus tipos. Todo valor de datos
expresable en un lenguaje de programación tiene implícito un tipo. Por ejemplo, en C el valor -1
es de tipo entero, el 3,2334 es de tipo double...

Un tipo de datos es un conjunto de valores, junto con un conjunto de operaciones sobre dichos
valores y con ciertas propiedades.

Un intérprete de lenguaje puede utilizar el conjunto de propiedades algebraicas de una variedad


de maneras, para asegurarse de que los datos y las operaciones se utilizan de manera correcta en
un programa.
z = x / y puede determinar si x e y tienen los mismos tipos y si dicho tipo tiene un
operador de división definido para sus valores.

27
LENGUAJES DE PROGRAMACIÓN
Página 28 de 78
Autor: informatica_uned@[Link]

El proceso por el cual pasa un intérprete para determinar si la información de tipos en un


programa es consistente se conoce como verificación de tipos.

Dado un grupo de tipos básicos como int, double y char, todo lenguaje ofrece una diversidad de
maneras para construir tipos más complejos: constructores de tipo, y los tipos creados se
conocen como definidos por el usuario. Ejemplo el arreglo int a[10]

Los nombres para los tipos nuevos se crean utilizando una declaración de tipo.
Los tipos que no poseen nombre se definen como un tipo anónimo. Para darle un nombre a ese
tipo utilizamos en C un typedef:

typedef int Array_of_ten_integers[10];


Array_of_ten_integers a;

Durante la verificación de tipos un intérprete debe comparar dos tipos para determinar si se trata
del mismo, para lo que utiliza una serie de reglas: algoritmos de equivalencia de tipo.

Los métodos utilizados para la construcción de tipos, los algoritmos de equivalencia, las reglas de
inferencia y las de corrección de tipos, se conocen de manera colectiva como un sistema de
tipos.
Cuando en un lenguaje todos los errores de tipo se detectan en tiempo de traducción se dice que
es un lenguaje fuertemente tipificado.

Ada (imperativo) es un lenguaje fuertemente tipificado. ML y Haskell (funcionales) también son


fuertemente tipificados pero con menos carga para el programador. C se le conoce como un
lenguaje débilmente tipificado.
Los lenguajes sin sistemas de tipificación estática se conocen como lenguajes sin tipificación
(o lenguajes con tipificación dinámica). Estos lenguajes incluyen: Scheme y otros dialectos de
Lisp, Smaltalk y la mayoría de los lenguajes de scripts, como Perl.

6.2 Tipos simples.

Los lenguajes parecidos a Algol (C, Ada, Pascal), incluso los orientados a objetos (C++, Java),
clasifican los tipos de datos de acuerdo con un esquema estándar relativamente básico, con
desviaciones de poca importancia.

Todo lenguaje contiene un conjunto de tipos predefinidos, a partir de los cuales se construyen
todos los demás tipos. Sin embargo, existen tipos simples que no están predefinidos: los tipos
enumerados y tipos de subrango que también son simples.
Los tipos enumerados son conjuntos cuyos elementos se denominan y se listan de manera
explícita.
Ejemplo en C:

enum Color {red, blue, green}

Los tipos enumerados en la mayoría de los lenguajes presentan un orden atendiendo al orden en
el cual se listan, y normalmente existe una función predecesora o sucesora. En C si se declara
un tipo enum, los valores son tomados como nombres de enteros y automáticamente se les
asignan los valores 0,1,..., a menos que el programador inicialize los valores enum a otros
enteros.

28
LENGUAJES DE PROGRAMACIÓN
Página 29 de 78
Autor: informatica_uned@[Link]

Ejemplo en C:

#include <stdio.h>

enum Color {red, green, blue} /* red es 0, blue es 1, green es 2 */


enum NewColor {NewRed=3, NewGreen=1, NewBlue =2};

main()
{ enum Color x=Green; /* Ahora x vale 1 */
enum NewColor y=NewBlue; /*Ahora y es en realidad 2*/
....
}

Java omite completamente la construcción enum de C.

Los tipos subrangos son subconjuntos contiguos de tipos simples especificando por lo memos el
menor y el mayor elemento.
Los lenguajes de la familia C (C, C++ y Java) no tienen tipos subrango, dado que se puede lograr
el mismo efecto manualmente escribiendo los tipos de enteros de tamaño apropiado para ahorrar
almacenamiento y escribiendo verificaciones explícitas para los valores, como en el siguiente
ejemplo de Java:

byte digit; // digit puede contener -128...127


............
If (digit > 9 || digit < 0) … // digit se acota aún mas

Sin embargo, los tipos subrango siguen siendo útiles, ya que hacen que este tipo de código se
genere de manera automática.
Típicamente los subrangos se definen dando el primer y el último elemento de otro tipo y se
conocen como tipos ordinales pues tienen un orden discreto en el conjunto. Como tipos
ordinales en cualquier lenguaje tenemos: enteros numéricos, enumeraciones y los subrangos.
Estos siempre tienen operadores de comparación (<=, <>…) y a menudo también tienen
operaciones de sucesor y predecesor. No todos los tipos con operadores de comparación son
ordinales, como ejemplo tenemos los reales que tienen operaciones de comparación pero no
sucesor ni predecesor.

6.3 Constructores de tipos.

Ya que los tipos de datos son conjuntos, se pueden utilizar las operaciones de conjuntos para
crear nuevos tipos de datos a partir de los existentes. Estas operaciones incluyen: el producto
cartesiano, la unión, el conjunto potencia, el conjunto de función y el subconjunto.
Cuando se aplican estas operaciones de tipo a los tipos se les denomina constructores de
tipos. También existen algunas operaciones de conjuntos que no corresponden con ningún
constructor como por ejemplo la intersección.

6.3.1 Producto cartesiano.

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

29
LENGUAJES DE PROGRAMACIÓN
Página 30 de 78
Autor: informatica_uned@[Link]

struct IntCharReal
{ int i;
char c;
double r;
}; /* Construye el tipo de producto cartesiano int X char X double */

Existe una diferencia entre un producto cartesiano y una estructura de registro: en un estructura
los componentes tienen nombre, en tanto que en un producto cartesiano se hace referencia a
ellos por su posición.

Algunos lenguajes tienen una forma más pura del tipo estructura de registro, que es en esencia
idéntica al producto cartesiano, donde a menudo se les denomina tuplas (por ejemplo ML).

Type IntCharReal = int * char * real

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

6.3.2 Unión.

Se forma tomando el conjunto de unión teórica de sus conjuntos de valores.


Existen 2 variedades de unión:

• Discriminada: si se le agrega una etiqueta o discriminador para distinguir el tipo de


elemento.
• Indiscriminadas: no tienen etiqueta y debe suponerse el tipo de cualquier valor en
particular.
En C y C++ el constructor de tipo union proporciona uniones indiscriminadas:

union intOrReal /* No discriminador */


{ int i;
double r;
};

Al igual que con struct existen nombre para diferenciar los distintos componentes (i y r). Los
nombre son necesarios por que comunican al interprete el tipo con el que deben interpretarse los
bits dentro de la unión.
Estos nombre no deben de confundirse con los discriminante, que es un componente separado,
que indica el tipo de datos que es realmente el valor, a diferencia del tipo que puede pensar que
es. En C, puede imitarse un discriminante de la siguiente forma:

enum Disc {IsInt, IsReal};


Struct IntOrReal
{ enum Disc which;
union intOrReal
{ int i;
double r;
} val; /* si no nombramos a esta unión sería anónima y abajo valdría x.r=2,3*/
};

30
LENGUAJES DE PROGRAMACIÓN
Página 31 de 78
Autor: informatica_uned@[Link]

y podría utilizarse como sigue:

IntOrReal x;
[Link]=IsReal;
[Link].r=2.3;
....

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

6.3.3 Subconjuntos

En matemáticas se pueden definir subconjuntos al dar una regla para distinguir sus elementos,
como pos_int = {x|x es un entero y x > 0}. En los lenguajes de programación se puede hacer
algo parecido para definir nuevos tipos que serán subconjuntos de tipos conocidos.
En ocasiones los subconjuntos heredan operaciones de sus tipos padres.
La herencia en los lenguajes orientados a objetos se puede considerar como un mecanismo de
subtipo, en el mismo sentido de compartir operaciones.

6.3.4 Arreglos y funciones.

El conjunto de todas las funciones f:U->V puede dar lugar a un nuevo tipo de dos formas: como
un tipo arreglo o como un tipo de función. Cuando U es un ordinal, la función puede
considerarse como un arreglo con un tipo de índice U y tipo de componente V. En C, C++ y Java
el conjunto de índices siempre es un rango de enteros positivos que comienzan por 0.
Los arreglos pueden definirse con o sin tamaño, pero para definir una variable de tipo arreglo hay
que asignarle un tamaño ya que los arreglos son asignados estáticamente o en la pila.

En C podemos definir arreglos de la siguiente manera:

typedef int TenIntArray [10]; /*Aqui ya se determina el tamaño de este tipo estaticamente
*/
typedef int IntArray [];

y las variables las podemos definir:

TenIntArray x;
IntArray w={1,2}; /*Aqui se determina el tamaño por la lista de valores iniciales, en
este caso si no se da una lista de inicio de valores, no se sabría su
tamaño y sería incorrecto declararlo como símplemente IntArray w;
*/
int y[5];
int z[]={1,2,3,4};

31
LENGUAJES DE PROGRAMACIÓN
Página 32 de 78
Autor: informatica_uned@[Link]

En C el tamaño de un arreglo no puede ser una constante calculada:

const int Size=5;


int x[Size]; /* Seria incorrecto en C, aunque sería correcto en C++ */

Sin embargo, C sí permite que arreglos sin tamaño especificado sean parámetros de funciones:

int array_max (int a[], int size)


{ int tem, i;
assert(size>0); /* Avisa o crea una excepcion si size es menor que cero */
temp=a[0];
for(i=1; i<size; i++)
{ if (a[i] > temp) temp =a[i]; }
return temp;
};

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

A diferencia de C, Java si puede asignar arreglos de forma dinámica, en el montón, y su tamaño


puede especificarse en forma totalmente dinámica; y en Java el tamaño del arreglo constituye
una parte de la información almacenada cuando se asigna el arreglo (y se le llama length).
Ejemplos de utilización de arreglos en Java:

class ArrayTest
{
static int array_max (int [] a) //Aqui los [] pueden estar despues del tipo
{
int temp;
temp=a[0];
// El tamaña es parte del arreglo a
for (int i=1; i<[Link]; i++)
...
}
}

public static void main (String args[]) //Tambien se permite que los [] esten asi
{
...
/** Se utiliza el teclado para meter datos y.... **/
int u =[Link]([Link]());
int[] x= new int[u]; // Ubicación de un arreglo dinámico
...
}

Los arreglos multidimensionales también son posibles en C, C++ y Java, ya que se permiten
arreglos de arreglos:

int x[10][20]; /* código en C */


int [][] x= new int[10][20]; //código en Java

32
LENGUAJES DE PROGRAMACIÓN
Página 33 de 78
Autor: informatica_uned@[Link]

Los arreglos probablemente son los constructores más utilizados ya que su implementación puede
hacerse en forma muy eficiente.
Los lenguajes funcionales por lo general no contienen un tipo arreglo ya que estos están
pensados para la programación imperativa. Usualmente los lenguajes funcionales utilizan listas en
vez de arreglos.
En algunos lenguajes pueden crearse tipos generales de función y procedimiento. Un ejemplo en
C en el que se define un tipo de función de enteros a enteros:

typedef int (*IntFunction)(int);

Y podemos utilizar este tipo para definir ya sea variables o parámetros:

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


IntFunction f=square;
int evaluate (IntFunction g, int value)
{ return g(value);}

Obsérvese que C exige que definamos las variables, tipos y parámetros de una función utilizando
notación de apuntadores, pero entonces ya no requiere que llevemos a cabo ningún
desreferenciamiento.

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

6.3.5 Tipos apuntadores y recursivos.

Un constructor de tipos que no corresponde a una operación de conjuntos es constructor de


referencias o apuntadores, que construye el conjunto de todas las direcciones que se refieren a
un tipo especificado.

Ejemplo en C de una declaración que construye el tipo de todas las direcciones en que haya
enteros almacenados.

typedef int *IntPtr;

Si x es una variable de tipo IntPtr puede entonces desreferenciarse para asignar por
ejemplo el valor 10 a la localización dada en x: *x=10;
Por supuesto, a x debió asignársele previamente una dirección válida, lo que se suele
llevar a cabo de forma dinámica mediante el uso de la función malloc:
x=(int*)malloc(sizeof(int));

Los apuntadores están implícitos en lenguajes que tienen un gestión automática de memoria.
Esto es el caso de java para el cual todos los objetos son apuntadores implícitos que se asignan
de forma explícita (new) pero son desasignados automáticamente por un recolector de basura.

En ocasiones los lenguajes hacen distinción entre referencias y apuntadores, definiendo como
referencia la dirección de un objeto bajo el control del sistema, que no se puede utilizar como
valor ni operar de forma alguna. Tal vez C++ sea el único lenguaje donde coexisten apuntadores
y referencias. Los apuntadores en Java en realidad son referencias. En C++ los tipos de
referencia se crean con un operador postfijo & (lo cual no debe confundirse con el operador
prefijo de dirección &, que devuelve un apuntador). Ejemplo de C++:

33
LENGUAJES DE PROGRAMACIÓN
Página 34 de 78
Autor: informatica_uned@[Link]

double r = 2.3;
double& s=r; // s es solo una referencia a r , de esta forma comparten memoria
s += 1; // ahora tanto r como s valen 3.3

Este mismo ejemplo puede implementarse por medio de apuntadores (asi tambien se
podria hacer en C):

double r =2.3;
double *p = &r; // p tiene como valor la dirección de r
*p +=1; // Ahora r sería 3.3

Las referencias en C++ son en esencia apuntadores constantes que se desreferencian cada vez
que se usan.
En C, los arreglos apuntan implícitamente a su primer componente. a[2] que sería el tercer
elemento del array a, es tan sólo una abreviatura de a+2, asi que, tambien podemos escribirlo
como 2[a].

Los apuntadores son de gran utilidad en la creación de tipos recursivos: un tipo que se utiliza
así mismo en su declaración. Estos tienen una gran importancia en las estructuras de datos y
algoritmos, ya que corresponden naturalmente a los algoritmos recursivos y representan datos
cuya estructura y tamaño no se conocen de antemano. Dos ejemplos típicos son las listas y los
árboles.

En C, está prohibido el uso directo de la recursión en declaraciones de tipo:

struct CharList
{ char data;
struct CharList next; /* Sería incorrecto, ya que datos de este tipo deberían
contener una cantidad infinita de caracteres */
};

pero se permiten declaraciones recursivas indirectas por medio de apuntadores:

struct CharListNode
{ char data;
struct CharListNode* next;
};
typedef struct CharListNode* CharList; /* Ahora cada elemento individual en
CharListNode tiene un tamaño fijo y pueden
encadenarse para formar una lista de tamaño
arbitrario */

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


(*cl).data=’a’; /* En esta posición se tiene este caracter */
(*cl).next=0; /* No hay un siguiente elemento */

6.3.5 Tipos de datos y el entorno.

Entorno es cuando la estructura de un tipo de datos requiere que el espacio se asigne en forma
dinámica, éste es el caso de los tipos apuntador, tipos recursivos y los tipos de función general.
En sus formas más generales, estos tipos requieren de entornos completamente dinámicos con
asignación y deasignación automáticas.

34
LENGUAJES DE PROGRAMACIÓN
Página 35 de 78
Autor: informatica_uned@[Link]

6.4 Nomenclaturas de tipos en lenguajes de ejemplo.

6.4.1 C

En C los tipos simples se conocen como tipos básicos, y los tipos que se construyen mediante
constructores de tipos se conocen como tipos derivados.

Tipo C

Básicos Derivados

void Numérico Apuntador Arreglo Función struct union

Integral Flotante

6.4.2 Java

Los tipos simples se llaman primitivos, y los tipos se construyen utilizando constructores de tipos
se llaman tipos de referencia.
Sólo existen tres constructores de tipos en Java: el arreglo, la clase y la interfaz.

Tipo Java

Primitivo Referencia

Boolean Numérico Arreglo class interface

Entero Punto Flotante

6.5 Equivalencia de tipos.

¿En que casos dos tipos son iguales? Una manera de responder a esta pregunta es comparar los
conjuntos de valores simplemente como conjuntos. Dos conjuntos son iguales si contienen los
mismos valores.
Dos de las formas más habituales de equivalencia de tipos en los lenguajes de programación
actuales son: la equivalencia estructural y la equivalencia de nombre:

35
LENGUAJES DE PROGRAMACIÓN
Página 36 de 78
Autor: informatica_uned@[Link]

La equivalencia estructural viene a decir que dos tipos son iguales si tienen la misma
estructura: están construidos de la misma forma a partir de los mismos tipos simples y en el
mismo orden y con los mismo nombres de variables internas. Es fácil de implementar y aporta
toda la información necesaria para llevar a cabo la verificación de errores y asignación de
almacenamiento. Para verificar la equivalencia estructural, un intérprete puede representar los
tipos como árboles y verificar la equivalencia recursivamente en subárboles.

La equivalencia de nombres se refiere a que dos tipos son iguales sólo si tienen el mismo
nombre. La equivalencia de nombres en su estado más puro es incluso más fácil de implementar
que la estructural, siempre y cuando estemos obligados a dar nombre a todos los tipos. Ada es un
lenguaje que ha implementado un equivalencia de nombres muy pura. C tiene una equivalencia
que está entre la estructural y la de nombres y se puede decir que tiene una equivalencia de
nombre para structs y unions, y estructural para todo lo demás.

En las siguientes declaraciones en C:

struct RecA
{ char x;
int y;
};
typedef struct RecA RecA; /* Define un tipo nuevo llamado RecA, que es un struct RecA*/

struct RecA a; /* Declara a “a” como la estructura de arriba de todo */


RecA b; /* Declara a “b” como el tipo nuevo creado */
struct RecA c;

struct
{ char x;
int y;
}d;

/* Aquí a, b, c y d son equivalentes estructuralmente. Pero las unicas equivalentes en


nombres son a y c, que lo son entre ellas */

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

6.6 Verificación de tipos.

La verificación de tipos es el proceso que sigue el interprete para verificar que todas la
construcciones en un programa tengan sentido en términos de los tipos de constantes, variables,
procedimientos y otras entidades. Involucra al algoritmo de verificación de tipos para expresiones
y asignaciones, y este pude modificar el algoritmo de equivalencias al contexto.

La verificación de datos puede dividirse en dinámica o estática: La verificación es dinámica si


la información de tipos se conserva y se verifica en tiempos de ejecución. Por definición lo
interpretes realizan verificación dinámica de los tipos. La verificación dinámica de los tipos es
necesaria cuando los tipos sólo pueden determinarse en tiempo de ejecución.

36
LENGUAJES DE PROGRAMACIÓN
Página 37 de 78
Autor: informatica_uned@[Link]

En la verificación estática, los tipos de expresiones y de datos se extraen del texto del programa y
el intérprete lleva a cabo la comprobación antes de la ejecución, por lo que estos lenguajes deben
de tener tipificado estático.

Ejemplo1: Los compiladores de C efectúan una verificación estática durante la traducción,


pero realmente C no es un lenguaje con tipificado fuerte y que muchas inconsistencias en
los tipos no causan errores de compilación.

Ejemplo 2: El dialecto Scheme de Lisp es un lenguaje con tipificado dinámico, pero los
tipos se verifican en forma rigurosa: Todos los errores de tipo provocan la terminación del
programa.

Ejemplo 3: Ada es un lenguaje con tipificado fuerte y todos los errores de tipo generan
mensaje de error en la compilación, pero sin embargo, incluso en Ada, ciertos errores,
como los de rango en subíndice de arreglos, no puede detectarse antes de la ejecución.

Una parte esencial en la verificación de tipos es la inferencia de tipos, en la que los tipos de las
expresiones se determinan a través de las subexpresiones que la componen.

La inferencia de tipos y las reglas de corrección a menudo son las partes más complejas en la
semántica de un lenguaje.

6.6.1 Compatibilidad de tipos.

A menudo es necesario relajar las reglas de corrección de tipos de manera que los tipos de dos
componentes no sean precisamente iguales, según el algoritmo de equivalencia de tipos. Dos
tipos diferentes que incluso pueden ser correctos cuando se combinan en cierta forma se conocen
como tipos compatibles.

Un término relacionado es la compatibilidad de asignación, a menudo se utiliza para la


corrección de tipos de la asignación e1 = e2. El lado izquierdo debe ser un valor o referencia l, y
el lado derecho debe ser un valor r.

Igual que para la compatibilidad ordinaria, la compatibilidad de asignación pude ampliarse para
casos en los que ambos lados son de diferente tipo.

En C y en Java, todos los tipos numéricos son compatibles y las conversiones se llevan a cabo de
forma que se conserva tanta información como sea posible.

6.6.2 Tipos implícitos.

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

En C, las variables son implícitamente enteros, si no se les declara ningún tipo.

x; /* Esta variable aunque no tenga tipo, es implícitamente entera en C */

En todos los lenguajes los literales son el ejemplo más claro de entidades tipificadas
implícitamente.

37
LENGUAJES DE PROGRAMACIÓN
Página 38 de 78
Autor: informatica_uned@[Link]

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

Los tipos pueden superponerse cuando dos tipos contienen valores en común. Normalmente es
preferible que los tipos sean conjuntos no conexos.
Sin embargo, imponer esta restricción de forma arbitraria sería demasiado limitante, y eliminaría
una de las características principales de la programación orientada a objetos: la capacidad de
crear subtipos mediante la herencia que refina los tipos existentes, al tiempo que conservan su
pertenencia en el tipo más general.
Por ejemplo, en Java un entero pequeño pudiera ser un “short”, un “int” o un “long”. En los los
tipos “unsigned int” e “int” contienen una superposición sustancial.

6.6.4 Operaciones compartidas.

Los tipos tienen operaciones que usualmente están definidas de forma implícita. A menudo estas
operaciones son compartidas entre varios tipos, o tienen el mismo nombre que otras operaciones
que pueden ser diferentes (por ejemplo, Operador + puede ser la suma de reales o enteros, o
bien la unión de conjuntos). Se considera que estos operadores están sobrecargados. En este
caso el intérprete debe decidir a partir de los tipos de los operandos, a que operación se refiere.
En Java no existe ninguna clase de operación aritmética en tipos enteros que no sean int o long.

6.7 Conversión de los tipos.

En todos los lenguajes de programación actuales, existe la necesidad bajo ciertas circunstancias
de convertir un tipo a otro. Esta conversión de tipos puede incluirse en el sistema, de forma que
las conversiones se lleven a acabo de forma automática. Ejemplo en C:

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

Al final de este código x sigue valiendo 3, que 3.3 es truncado por el proceso de conversión. En
este ejemplo el intérprete ejecutó dos conversiones implícitas a veces conocidas como
coacciones. La conversión de int a doble es de extensión, mientras que al revés sería de
restricción.
La conversión implícita puede debilitar la verificación de tipos de forma que no se detecten
errores lo que pone en peligro el tipificado fuerte y la confiabilidad del lenguaje de programación.

La conversión explícita se presenta cuando las directrices de la conversión se escriben


directamente en el código también llamada como conversión forzada. Se utiliza en C y Java y
consiste en escribir el tipo resultante deseado entre paréntesis antes de la expresión que se desea
convertir:

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

En C++ se utiliza llamando a funciones, con el valor que desea convertirse como argumento del
parámetro de la función que convierte al tipo deseado:

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

La ventaja de utilizar conversiones forzadas es que las conversiones que se llevan a cabo se
documentan en forma precisa dentro del código, y existe menor probabilidad de comportamientos
inesperados.

38
LENGUAJES DE PROGRAMACIÓN
Página 39 de 78
Autor: informatica_uned@[Link]

En algunos lenguajes está prohibida la conversión implícita a favor de la explicita, la cual favorece
la documentación de la conversión minimizando los riesgos de comportamientos extraños y facilita
la sobrecarga. Como ejemplo de estos lenguajes tenemos a Ada. Un paso intermedio es permitir
la conversión implícita siempre que no involucre corrupción de los datos, en este sentido Java sólo
permite conversión por extensión.

Los lenguajes orientados a objetos tienen requerimientos especiales para la conversión, ya que la
herencia puede interpretarse como un mecanismo de subtipificación y en algunos casos es
necesario hacer conversiones de subtipos a supertipos y viceversa.

Una alternativa a las conversiones forzadas es tener funciones predefinidas o de biblioteca, que
lleven a cabo las conversiones. Por ejemplo, en Java la clase Integer que está en la biblioteca
[Link] contiene funciones de conversión toString.

6.8 Verificación de tipos polimórficos.

La mayoría de los lenguajes de tipificado estático exigen que en todos los nombres de cada
declaración se dé información explícita sobre los tipos.

Es posible aplicar una forma de inferencia de tipos para determinar los tipos de los nombres en
una declaración sin dar explícitamente estos tipos. Un intérprete puede obtener información sobre
los usos de un nombre, e inferir del conjunto de todos los usos el tipo probable, que es el tipo
más general en el cual todos los usos son correctos. (Ejemplo Página 214-216)

Polimórfico en un lenguaje de programación se aplica a nombres que pueden tener más de un


tipo al mismo tiempo (Ej. funciones sobrecargadas)

Polimorfismo paramétrico, es del tipo “arreglo de α” es en realidad un conjunto de tipos


múltiples e infinitos, dependiendo de las posibles inicializaciones por instancias de la variables de
tipo α.

Polimorfismo paramétrico implícito, ya que los parámetros de tipo son introducidos


implícitamente por el verificador de tipos, en vez de escritos por el programador explícitamente
(polimorfismo paramétrico explícito).

6.9 Polimorfismo explícito.

El polimorfismo paramétrico implícito está bien para definir funciones polimórficas, pero no nos
ayuda si deseamos definir estructuras de datos polimórficos, para ello debemos escribir en forma
explícita mediante la sintaxis apropiada.

En C++ y Java los constructores siempre tienen el mismo nombre que su clase y tipo asociados.
C++ es un ejemplo de lenguaje polifórmico paramétrico explícito y usa para ello las plantillas que
pueden usarse ya sea con funciones o con constructores de tipos class o struct.

39
LENGUAJES DE PROGRAMACIÓN
Página 40 de 78
Autor: informatica_uned@[Link]

7 Control I. Expresiones y enunciados.


Las expresiones representan el mecanismo fundamental de cómputo en los programas. Una
expresión en su forma más pura (matemática) devuelve un valor y no produce efectos colaterales
(es decir, no hay cambios en el programa), en tanto que un enunciado se ejecuta por sus efectos
colaterales y no devuelve ningún valor.
En los lenguajes funcionales o de expresión, la mayoría de las construcciones de lenguaje son
expresiones. Las estructuras de control explícitas comenzaron en los lenguajes de programación
como GOTO.
La programación estructurada condujo a una enorme mejoría en la legibilidad y confiabilidad de
los programas. Algunos lenguajes han suprimido por completo el GOTO.

7.1 Expresiones.

Las expresiones básicas son los literales (constantes manifiestas) y los identificadores. Las
expresiones más complejas se elaboran en forma recursiva a partir de las básicas mediante la
aplicación de operadores y funciones, lo que a veces involucra símbolos de agrupamiento como
los paréntesis.

Los operadores pueden tomar uno o más operandos (unarios, binarios, etc.…). Se pueden
escribir con notación infija, postfija y prefija que corresponde con un recorrido en orden,
postorden y preorden del árbol sintáctico de la expresión respectivamente. Las formas prefijas y
postfijas tienen la ventaja de no necesitar paréntesis para expresar el orden en que se aplican los
operadores. La asociatividad de operadores también queda implícita con las notaciones prefija y
postfija sin la necesidad de reglas.

Muchos lenguajes hacen distinción entre operadores y funciones. Los operadores si son
binarios se escriben en forma infija con reglas de asociatividad y precedencia específicas. Las
funciones que pueden ser predefinidas o definidas por el usuario se escriben en forma prefija y
los argumentos se consideran como parámetros o argumentos reales.

Ej. en C, si escribiéramos 3 + 4 * 5 con operaciones de adición y multiplicación definidas por el


usuario, aparecería como add(3, mul(4,5)).

Todos los lenguajes tienen reglas para evaluar las expresiones. Una de las más utilizadas es la
evaluación de orden aplicativo o evaluación estricta la cual consiste en evaluar primero los
operandos y luego aplicarle los operadores. Corresponde con una evaluación de abajo arriba de
los nodos del árbol sintáctico que representa a la expresión.

El orden natural en el que se evalúan las subexpresiones de izquierda a derecha (corresponde a


un recorrido de izquierda a derecha en el árbol). Sin embargo muchos lenguajes establecen en
forma explícita que no existe orden específico. Una de las razones para ello es que el intérprete
puede reorganizar el orden del cálculo para que este sea más eficiente.
En presencia de efectos colaterales el orden en que se realiza la evaluación pude presentar
diferencias.

En C una asignación (la quintaesencia del efecto colateral) es una expresión no un enunciado, no
sólo asigna un valor sino que devuelve este copiado como un resultado.
x = (y = z).

40
LENGUAJES DE PROGRAMACIÓN
Página 41 de 78
Autor: informatica_uned@[Link]

Un operador de secuencia es aquel que permite que se combinen varias expresiones en una
sola y que se evalúen secuencialmente.
La evaluación de una expresión puede llevarse a cabo incluso sin evaluar todas las
subexpresiones. Un ejemplo interesante son las expresiones booleanas o lógicas.

La evaluación en corto circuito consiste en evaluar las expresiones booleanas de izquierda a


derecha hasta el punto que se conoce el valor verdadero de toda la expresión y en ese momento
la evaluación se detiene. La evaluación en corto circuito conlleva una serie de beneficios como
pude ser la evaluación en primer lugar del índice de un arreglo en un expresión: if (i <= lastindex
and a[i] >= x)….

Muchos lenguajes disponen de expresiones que imitan enunciados de control pero devuelven
valores (sentencias if then else y las expresiones Case). En estos casos al igual que con las
expresiones booleanas, ciertas subexpresiones pueden no ser evaluadas. Las expresiones
booleanas de cortocircuito pueden expresar en forma de expresiones if. Los operadores booleanos
de corto circuito y de if son un caso especial de operadores que difieren la operación de sus
operandos, evaluación diferida o no estricta.

En aquellos lenguajes donde no existen efectos colaterales o están fuertemente controlados


(como pueden ser los lenguajes funcionales puros), el orden de evaluación de las subexpresiones
no altera el resultado final de la expresión. Esto hace que se comparta una propiedad importante
con las expresiones matemáticas, la regla de sustitución o transparencia referencial.

La transparencia referencial permite que se utilice un forma muy sólida de evaluación diferida,
evaluación de orden normal, lo que significa que en una expresión los operadores (o
funciones) comienzan a evaluarse antes de que sus operandos sean evaluados, y estos son
evaluados sólo si son necesarios para el cálculo del valor de la operación. La evaluación de orden
normal se conoce como evaluación perezosa en el lenguaje funcional Haskell.

7.2 Enunciados y guardias condicionales.

La forma más típica del control estructurado es la ejecución de un grupo de enunciados sólo bajo
ciertas condiciones. Esto incluye llevar a cabo una prueba booleana o lógica antes de entrar a una
secuencia de enunciados.
Enunciado if es la forma más común.
El enunciado if cauteloso, es un tipo de case o de switch

If B1 -> S1
| B2 -> S2
| B3 -> S3
.......
| Bn -> Sn
fi >

Las Bi son expresiones booleanas conocidas como guardas y las Si son secuencias de enunciados.
Si uno de los Bi se evalúa como cierto, la secuencia correspondiente es ejecutada, pero si más de
un Bi es verdadero, se selecciona solo una de las Si. Si ningún Bi es verdadero ocurre un error.

El if cauteloso introduce el no determinismo, una característica resulta muy útil en programación


concurrente.
Las dos formas principales en las cuales los lenguajes de programación implementan los
enunciados condicionales, como los if cautelosos, son los enunciados if y los enunciados case.

41
LENGUAJES DE PROGRAMACIÓN
Página 42 de 78
Autor: informatica_uned@[Link]

7.2.1 Enunciados if.

Enunciado if -> if (expresión) enunciado [else enunciado]

Enunciado puede ser un solo enunciado o una secuencia de enunciados encerrados entre
corchetes.
Esta forma de if (también existe en Java y Pascal) presenta un problema: es ambiguo: el
enunciado

If (e1) if (e2) S1 else S2

presenta dos árboles de análisis gramatical diferentes.

Esta ambigüedad se conoce como el problema del else ambiguo. C resuelve el problema mediante
una regla para eliminar la ambigüedad: el else se asociará con el if anterior más cercano que no
tenga ya una parte else. A esta regla también se le conoce como la regla del anidamiento más
cercano.
El problema del else ambiguo es de diseño de lenguaje, nos obliga a establecer una nueva regla
para describir algo que es esencialmente una característica sintáctica y dificulta al lector
interpretar el enunciado if, viola el criterio de legibilidad del diseño.

Es posible escribir reglas BNF que especifiquen con precisión la asociación (Java).
Además de esta regla, se puede solucionar el problema del else ambiguo empleando una palabra
clave enmarcadora (End If,…) con lo que también se elimina la necesidad de utilizar corchetes.

También se pude utilizar el “Else If” compactado “elseif” que proporciona una nueva secuencia de
enunciados en el mismo nivel.
En Java la condición siempre debe tener tipo booleano. C no tiene tipo booleano por lo que en C y
C++ la expresión de control puede ser de tipo entero o de tipo puntero. El valor resultante se
compara entonces con 0 (el apuntador nulo, en el caso del tipo apuntador); una comparación
desigual es equivalente a verdadero, la comparación igual es equivalente a falso.

7.2.2 Enunciados case y switch.

El enunciado case (switch en Java, C y C++) fue creado como una variante del If Cauteloso,
donde cada guarda en vez de ser una expresión booleana, representa un valor ordinal
seleccionado por una expresión ordinal.
Los valores de los casos pueden ser literales o expresiones como en C. Si el valor de la expresión
de control no estuviera en algún caso, el control se transfiere a la opción “default” si existe, en
caso contrario la ejecución seguiría con la primera sentencia que sigue al bloque switch.
Ejemplo en C:

switch (x-1)
{ case 0:
y = 0;
z = 2;
break;
case 1:
case 2: z = 10;
break;
default:
break;
}

42
LENGUAJES DE PROGRAMACIÓN
Página 43 de 78
Autor: informatica_uned@[Link]

7.3 Ciclos y variables sobre While.

Una forma general de la construcción ciclo aparece en la estructura correspondiente al do


cauteloso:

do B1 -> S1
| B2 -> S2
| B3 -> S3
| B4 -> S4

| Bn -> Sn
od

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

Las formas básica de la construcción de ciclo, que en esencia es un do cauteloso con solo una
guardia (eliminando así el no determinismo), es el ciclo while de C, C++ y Java: while (e) S

La mayoría de los lenguajes cuentan con un enunciado alterno, que asegura que el código del
ciclo se ejecute por lo menos una vez. En C y en Java este enunciado es el do-while: do S while
(e)

Las construcciones while y do tienen la propiedad de que la terminación del ciclo se especifica
explícitamente solo al principio (while) o al final (do).

A menudo también es conveniente salir del ciclo en uno o más puntos intermedios. Por esta razón
C al igual que Java, incluye dos opciones: break para salir por completo de un bucle y continue
que se salta el resto del cuerpo del bucle.
Un caso especial, muy común de la construcción de ciclos es la construcción en C, C++ y Java:

for ( e1 ; e2 ; e3) S;
e1 es el inicializador, e2 es la condición y e3 es la actualización.

Tanto en C++, Java o C el inicializador puede contener declaraciones de forma que el índice de
un ciclo puede definirse dentro del ciclo.

A menudo los lenguajes incluyen esta forma de ciclo porque puede optimizarse más
efectivamente que otras construcciones de ciclo. Restricciones típicas de este tipo de bucle:

• El valor de i no puede cambiarse dentro del cuerpo del ciclo.


• El valor de i es indefinido después de terminar el ciclo.
• i debe ser de tipo restringido.

7.4 La controversia GOTO.

A pesar de que el GOTO sigue siendo el elemento principal en algunos lenguajes como Fortran y
Basic, a medida que se fue introduciendo la programación estructurada en el diseño de la mayoría
de los lenguajes, el uso de esta etiqueta se ha sido cada vez más cuestionado (acusado de
generar código espagueti ilegible). Si bien ciertos programadores defienden su uso restringido a
ciertos casos, lo cierto es que lenguajes como Java han prescindido por completo de la etiqueta,
pero sin embargo incluye alternativas importantes: La devolución del control a un nivel de
anidamiento externo, los breaks etiquetados (severamente restringidos) y el enunciado continue.

43
LENGUAJES DE PROGRAMACIÓN
Página 44 de 78
Autor: informatica_uned@[Link]

7.5 Manejo de excepciones.

Hasta ahora todos los mecanismos de control han sido explícitos, pero existen situaciones donde
la transferencia de control es implícita, como puede ser el control de condiciones de error y otros
eventos no usuales durante la ejecución de un programa, esta situación es denominada manejo
de excepciones.

Una excepción es cualquier evento inesperado o poco frecuente. En los lenguajes interpretados
también incluyen errores estáticos, como los de sintaxis y tipo, esto no ocurre así en los lenguajes
compilados ya que el programa que los contiene no pude ser ejecutado.

Pero las excepciones no solo se limitan a los errores: una excepción puede ser cualquier evento
no usual, como el fallo en la entrada de datos o una pausa. Un manejador de excepciones es un
procedimiento o secuencia de código diseñado para ejecutarse cuando se pone de manifiesto una
excepción en particular. Se dice que un manejador de excepciones maneja o atrapa excepciones.

El manejo de excepciones intenta imitar en el lenguaje de programación las características de una


interrupción de hardware. A menudo resulta inaceptable que el programa le permita a la máquina
o al sistema operativo subyacente que tome el control.
Los programas que exhiben este comportamiento no pasan las pruebas de robustez, la cual es
parte de los criterios de diseño de seguridad y confiabilidad.

Los errores que el programa no puede atrapar se les denominan excepciones asíncronas y
suelen estar producidas a un nivel demasiado bajo, por lo que es el sistema operativo el que tiene
que entrar en acción para evitar algún fallo catastrófico. Las excepciones síncronas, son
aquellas que el programa puede atrapar y suelen ocurrir en respuesta a errores del programa. Las
excepciones definidas por el usuario sólo pueden ser síncronas.

En aquellos lenguajes que no cuentan con un mecanismo de excepciones implícito, se hace


necesario el escribir en los programas pruebas explícitas de excepciones lo que hace que un
programa sea menos legible ya que el programador tiene que someter a prueba por adelantado
todas la condiciones de excepción. Ejemplo en C:

If (y==0)
handleError(“Denominador del radio es cero”);
else
radio=x/y;

7.5.1 Excepciones.

Una excepción está definida por lo general como un objeto de datos el cual puede estar
predefinido o definido por el usuario.
En C++ no existe un tipo especial para las excepciones por lo que no hay una palabra reservada
para tal fin. En vez de ello cualquier tipo estructurado puede servir (struc o class). Normalmente
se querrá agregar cierta información a estas excepciones, lo que se puede lograr añadiendo
nuevos elementos a estas estructuras.
Las excepciones por lo general obedecen a las mismas reglas de alcance que el resto de
declaraciones. Ya que las excepciones ocurren en tiempo de ejecución puede ocurrir que un
excepción se salga del alcance de una declaración en particular, para que esto no ocurra sería
conveniente declarar la excepciones en forma global, de esta manera no se presentarán
problemas de alcance.
La mayoría de los lenguajes proveen de un conjunto de tipos de excepciones predefinidas.

44
LENGUAJES DE PROGRAMACIÓN
Página 45 de 78
Autor: informatica_uned@[Link]

7.5.2 Manejadores de excepciones.

En C++ los manejadores de excepciones están asociados al try-catch, que pueden aparecer en
cualquier parte donde pueda haber un enunciado. Normalmente el alcance de los manejadores se
limita al enunciado/expresión a los que está adjunto. Si una excepción llega a un manejador de
esta forma definido, este reemplaza a todos los que haya en otro lugar, incluyendo los
predefinidos.
Los manejadores predefinidos usualmente se limitan a imprimir un mensaje de error mínimo, que
indica el tipo de excepción y quizás cierta información sobre el lugar del programa donde ocurrío,
y después terminará el programa.

Un bloque try-catch de C++:

try
{ // para realizar algún proceso
}
catch (Trouble t)
{ // manejar el problema 1
}
catch (Big_Trouble b)
{ // manejar el problema 2
}
catch (…)
{ // manejar todas las excepciones restantes
}

7.5.3 Control.

Normalmente una excepción predefinida puede ser puesta de manifiesto automáticamente por el
sistema o bien de manera manual ambas en tiempo de ejecución, sin embargo las excepciones
definidas por el programador sólo pueden ser puestas de manifiesto manualmente por el
programa.
Las excepciones definidas por el usuario, sólo pueden ser puestas de manifiesto manualmente por
el programa.
C++ utiliza la palabra reservada throw y un objeto de excepción para poner de manifiesto una
excepción.

Cuando se pone de manifiesto una excepción, por lo general se abandona el cálculo que se esté
haciendo y el sistema en tiempo de ejecución comienza a buscar un manejador. Si no se
encuentra un manejador, se consulta la sección de manejador del bloque siguiente, y así
sucesivamente (a este proceso se le conoce como propagación de excepción). Este proceso
continúa hasta que se encuentre un manejador o bien se salga del programa principal.

Una vez encontrado el manejador se tienen varias opciones respecto en donde se debe continuar
la ejecución:

- Modelo de reanudación: Se sigue la ejecución a partir del enunciado que provocó la


excepción.
- Modelo de terminación: Se continúa la ejecución en el bloque siguiente al bloque o
expresión dónde se produjo la excepción.

Casi todos los lenguajes modernos utilizan el modelo terminación.

45
LENGUAJES DE PROGRAMACIÓN
Página 46 de 78
Autor: informatica_uned@[Link]

Hay que tener el cuenta que el manejo de excepciones supone una carga sustancial en el tiempo
de ejecución de un programa. Por esta razón es recomendable no abusar del uso de las
excepciones para implementar situaciones de control ordinarias, que podrían ser reemplazadas
por pruebas simples.

7.5.4 Especificaciones de excepciones

Una especificación de excepción es una lista de excepciones que se añade a la declaración de una
función para garantizar que ésta sólo devuelva las excepciones que están en la lista y ninguna
otra más. La sintaxis en C++ es:

int f (int x) throw (Trouble, BigTrouble)...

/* La lista de tipos de excepción en paréntesis después de throw indica que la función f


sólo arrojará las dos excepciones Trouble y BigTrouble, y ninguna otra más */

46
LENGUAJES DE PROGRAMACIÓN
Página 47 de 78
Autor: informatica_uned@[Link]

8 Control II Procedimientos y ambientes.


8.1 Definición y activación de los procedimientos.

Un procedimiento es un mecanismo en un lenguaje de programación para abstraer un conjunto


de acciones o de computaciones. Este grupo de acciones se conoce como cuerpo. Un
procedimiento se define al proveer una especificación o interfaz y un cuerpo. La especificación
le da nombre al procedimiento así como una lista de tipos y nombres de sus parámetros, así
como el tipo de su valor devuelto si existiese alguno.

Se llama o activa un procedimiento al enunciar su nombre junto con el conjunto de argumentos


de la llamada que corresponde con sus parámetros. Una llamada a un procedimiento transfiere el
control al principio del procedimiento llamado (el llamado). Cuando la ejecución llega al final del
cuerpo, el control es devuelto al llamador. En algunos lenguajes la devolución del control se
puede forzar a través del enunciado return.

En C y C++, todos los procedimientos implícitamente son funciones; aquellas que no devuelven
valores se declaran como void, las que devuelven algún valor se declaran del tipo del valor
devuelto.
En algunos lenguajes sólo existen funciones, los lenguajes funcionales son un ejemplo.

Se podría decir que una declaración de procedimiento crea un valor de procedimiento


constante y asocia un valor simbólico (el nombre de dicho procedimiento) a dicho valor.
Un procedimiento se comunica con el resto del programa a través de sus parámetros y también a
través de sus referencias no locales (referencias a variables declaradas fuera de su propio
cuerpo.

8.2 Semántica de los procedimientos.

Un procedimiento es un bloque cuya declaración está separada de su ejecución.

El ambiente determina la asignación de la memoria y mantiene el significado de los nombres


durante la ejecución. En la programación estructurada en bloques vimos que cuando se encuentra
un bloque durante la ejecución, hace que ocurra las asignación de variables locales y otros
objetos correspondientes a las declaraciones del bloque. Esta memoria asignada para los objetos
locales del bloque se conoce como registro de activación (o marco de pila) del bloque, y se
dice que el bloque está activado. Conforme se introducen bloques durante la ejecución, el control
pasa de la activación del bloque que rodea a la activación del bloque interno. Cuando sale el
registro de activación del bloque interno, se libera, volviendo al ambiente del registro de
activación del bloque que lo rodea. Las referencias no locales son referencias a variables
declaradas fuera del bloque. Cada bloque necesita tener información de la activación que lo
rodea.

En el caso de los procedimientos cuando un procedimiento (A) llama a otro (B), este último no
tiene acceso a las variables locales de A, si no a las variables definidas en el ambiente local. Esto
es debido a que el ambiente local es el ambiente definidor de B (o ambiente estático), en
tanto que el ambiente definidor de A se conoce como ambiente llamador de B (o ambiente
dinámico). Para bloques que no son procedimientos el ambiente definidor y el ambiente
llamador son el mismo. Por el contrario un procedimiento tiene ambiente invocador y definición
distintos. Por lo que un procedimiento puede tener cualquier número de ambientes invocadores y
un solo ambiente definidor.

47
LENGUAJES DE PROGRAMACIÓN
Página 48 de 78
Autor: informatica_uned@[Link]

El alcance léxico permite a un bloque tener acceso a las variables del bloque que le rodea que no
están declaradas en sus propias declaraciones. Por el contrario un procedimiento sólo puede
comunicarse con su bloque de definición a través de las variables no locales. No tiene manera de
acceder a las variables en su ambiente invocador. El método de comunicación de un
procedimiento con su ambiente invocador es a través de parámetros. Los parámetros son
conocidos como parámetros formales, en tanto que los argumentos son llamados parámetros
actuales.

Cuando un procedimiento sólo depende de sus parámetros y de características fijas del lenguaje
se dice que está en una forma cerrada ya que no contiene dependencias no locales. En caso de
no hacer esto necesitaríamos lo que se denomina cerradura, la cual consiste en el código del
procedimiento junto con una representación del ambiente que la rodea, y todo esto para poder
resolver referencias no locales.

Un problema adicional que afecta a la semántica de un procedimiento es la correspondencia que


existe entre sus parámetros y los argumentos durante una llamada. Se puede decir que esta
asociación es en sí una ligadura que puede interpretarse de muchas maneras diferentes, y esta
interpretación se conoce como mecanismo de paso de parámetros.

8.3 Mecanismo de paso de parámetros.

La naturaleza de las ligaduras de los parámetros tienen un efecto significativo en la semántica de


las llamadas a los procedimientos y los lenguajes difieren de manera sustancial en los
mecanismos de paso de parámetros disponibles. Algunos lenguajes como Java, C y los lenguajes
funcionales tienen un solo mecanismo de paso de mensajes, en tanto que otros pueden tener dos
(C++) o más.

8.3.1 Paso por valor.

Es el mecanismo más común para paso de parámetros. En este mecanismo los argumentos son
expresiones que se evalúan en el momento de la llamada y sus valores se convierten en los
valores de los parámetros (como si fueran constantes) durante la ejecución del procedimiento. Se
puede interpretar como el reemplazo de todos lo parámetros en el cuerpo del procedimiento por
los valores de los argumentos.

Este mecanismo es el único disponible en los lenguajes funcionales, C y Java, y por omisión el de
C++. Los parámetros se consideran como variables locales del procedimiento, con valores
iniciales dados por los valores de los argumentos en la llamada, por lo que en C y en Java a los
parámetros de valor se les pueden asignar valores, igual que en el caso de las variables locales.

Paso por valor no implica que no puedan ocurrir cambios fuera del procedimiento mediante el uso
de parámetros. En el caso de que el parámetro fuese un tipo apuntador o de referencia, el valor
es una dirección y puede utilizarse para cambiar la memoria fuera del procedimiento.
Por ejemplo, esta función en C cambia definitivamente el valor del entero al cual el parámetro p
apunta:

void init_p (int *p)


{ *p=0; }

Otro ejemplo en C seria cuando un parámetro es un valor de arreglo (que son implícitamente
apuntadores que apuntan a su primer elemento) y puede siempre cambiarse los valores
almacenados en ese arreglo:

48
LENGUAJES DE PROGRAMACIÓN
Página 49 de 78
Autor: informatica_uned@[Link]

void init_p (int p[])


{ p[0]=0; }

En Java por ejemplo todos los objetos son apuntadores, por lo que cualquier parámetro de objeto
pude ser utilizado para cambiar sus datos:

void append_1 (Vector v)


{ [Link] (new Integer(1)); }

Pero al igual que en C, las asignaciones directas a los parámetros no funcionan:

void make_new (Vector v)


{ v= new Vector (); } //Error - no tiene efecto

8.3.2 Paso por referencia.

Con este mecanismo el argumento debe de ser en principio un variable con una memoria
asignada, por lo que en vez de pasar el valor de la variable se le pasaría la ubicación de la
variable. El parámetro sería en realidad un alias del argumento, cualquier cambio que se haga
sobre el parámetro quedará reflejado en el argumento. En C++ se hace poniendo el símbolo & a
continuación del tipo de datos:

void inc (int& x)


{ x++ ; }

C puede lograr el mismo efecto de paso por referencia, pasando una ubicación o refeferencia de
manera explícita como apuntador (C utiliza & antes del nombre de una variable para indicar la
ubicación de esa variable y el * para desreferenciar a esa variable).

void inc (int *x) /* Imitación en C del paso por referencia */


{ (*x)++; }
...
int a;
...
inc(&a); /* pasa la dirección de “a” hacia la función inc */

Un problema adicional con este mecanismo es la respuesta del lenguaje a los argumentos de
referencia que no son variables (no está declarados previamente).

8.3.3 Paso por valor-resultado.

Este mecanismo es similar al paso por referencia, con la diferencia que en este caso el
parámetro no es un alias del argumento: el valor del argumento es copiado en la llamada al
procedimiento y utilizado por este, y cuando el procedimiento termina el valor final del parámetro
se copia de regreso a la ubicación del argumento (por lo que el valor del argumento no cambia
durante la ejecución del procedimiento). Este mecanismo es conocido a veces como copia al
entrar – copia al salir, o como copia-restauración.

49
LENGUAJES DE PROGRAMACIÓN
Página 50 de 78
Autor: informatica_uned@[Link]

En siguiente código C, “a”tiene el valor 2 al utilizar el paso por valor-resultado.

void p (int x, int y)


{ x++;
y++;
}

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

8.3.4 Paso por nombre y evaluación retardada.

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

Los adelantos en la evaluación retardada (diferida) en los lenguajes puramente funcionales como
Haskell han aumentado el interés en este mecanismo, y vale la pena comprenderlo como base
para otros mecanismos de evaluación retardada (diferida) como la evaluación perezosa más
eficiente.

8.3.5 Mecanismos de pasos de parámetros vs. La especificación de los parámetros.

Ada tiene dos conceptos de comunicación de parámetros: parámetros in y parámetros out.


Cualquier parámetro puede ser declarado como in, out o in out. El parámetro por omisión es in.

8.3.6 Verificación de tipos de los parámetros.

En los lenguajes con tipificación fuerte, la verificación de tipos en las llamadas a los
procedimientos debe de comprobar que los tipos y número de argumentos con cuerda con los
parámetros.
En caso del paso por referencia, por lo general, los argumentos deben de tener los mismos tipos
que los parámetros, sin embargo en el paso por valor la verificación de tipos puede relajarse y
contempla la compatibilidad de asignación, permitiéndose conversiones como las que hace C,
C++ y Java.

8.4 Ambientes, activación y asignación de los procedimientos.

El ambiente para un lenguaje estructurado en bloquear con un alcance léxico puede mantenerse
de manera parecida a una pila, creando un registro de activación en la pila de ambiente al entrar
el bloque y liberándolo cuando este sale

8.4.1 Ambientes totalmente estáticos (los más sencillos)

En un lenguaje como Fortran donde toda la asignación de memoria puede llevarse a cabo en
tiempo de carga, y las localizaciones de todas las variables quedan fija durante toda la ejecución
del programa. Las definiciones de funciones y procedimientos no pueden ser anidados y además
no se permite la recursión, por lo que toda la información de una función un rutina pude
asignarse estáticamente. Cada procedimiento tiene un registro de activación fijo.

50
LENGUAJES DE PROGRAMACIÓN
Página 51 de 78
Autor: informatica_uned@[Link]

La forma de un registro de activación para un programa de este tipo quedaría como sigue:

Área COMMON
Registro de activación de programa principal

Registro de la activación de la subrutina S1


Registro de la activación de la subrutina S2

Etcétera

Y cada registro de activación se subdivide en varias áreas:

Espacio para las variables locales

Espacio para los parámetros pasados

Dirección de retorno
Espacio temporal para evaluación de la expresión

Hay que hacer notar que todas las referencias a variables no locales deben ser relativas al área
COMMON global, y no es necesaria una cerradura para la resolución de esas referencias.

8.4.2 Ambientes de tiempos de ejecución basados en pilas.

En lenguajes estructurados en bloques y con recursión (C y otros parecidos a Algo), las


activaciones de los bloques de procedimientos no puede asignarse estáticamente, ya que el
procedimiento pudiera volver a ser llamado antes de haber salido de una asignación anterior, por
lo que debe de crearse una nueva activación. Esto se puede gestionar a través de pilas, como ya
se ha echo notar.
Para una nueva activación a parte del espacio reservado para las variables locales, espacio
temporal y un apuntador de retorno, también habrá que reservar espacio para la siguiente
información adicional:
 Debe de tener un apuntador a la activación presente, ya que cada procedimiento no
tiene una ubicación fija para su registro de activación. Este apuntador a la activación
presente debe de conservarse en una ubicación fija, conocido dicho registro como
apuntador de ambiente o ep.
 Un apuntador al registro de activación del bloque desde el que entró dicha activación.
Este estará almacenado en el registro denominado enlace de control (enlace
dinámico).

Los campos en cada uno de los registros de activación deben contener la información:

Enlace de control
Dirección de retorno
Parámetros pasados
Variables locales
Temporales

51
LENGUAJES DE PROGRAMACIÓN
Página 52 de 78
Autor: informatica_uned@[Link]

Ejemplo en C:

void p(void)
{...}
void q(void)
{p();}

main()
{q();}

Al principio de la ejecución del programa existe sólo un registro de activación para main y
el ep apunta ahí.
Después de la llamada a q, se ha agregado a la pila un registro de activación para q, y el
ep ahora apunta al registro de activación de q; pero q ha almacenado el ep anterior (el
que apuntaba al registro de main) en forma de su enlace de control.
Cuando se llama a p desde el interior de q, se agrega un nuevo registro de activación para
p. Con lo que el ep apunta al registro de activación de p, pero p ha almacenado el ep
anterior (el que apuntaba al registro de q) en forma de su enlace de control.
Así, cuando sale p, su registro de activación es devuelto a la memoria libre, y ep pasa a
apuntar al registro de activación de q (tal y como estaba guardado en el enlace de control
de p). De manera similar, cuando sale q, se restablece ep para que apunte al ambiente
original del programa principal.

Cada variable local puede ser ubicada en la misma posición en el registro de activación con
relación al principio del mismo. Esta posición se conoce como desplazamiento de la variable
local. Puede localizarse cada variable local a partir de la ubicación marcada por ep y su
desplazamiento.

8.4.3 Procedimientos calculados dinámicamente y ambientes totalmente dinámicos.

El ambiente de tiempo de ejecución basado en pilas es totalmente adecuado para prácticamente


todos los lenguajes estructurados en bloques con alcance léxico.
Pero tiene sus limitaciones, por ejemplo, cualquier procedimiento que pueda devolver un
apuntador a un objeto local, ya sea mediante un valor devuelto o a través de un parámetro de
paso por referencia, dará como resultado una referencia pendiente al salir del procedimiento.
Esta situación no puede suceder en Java, dado que no está disponible la dirección de una variable
local.

Existen situaciones en las que un compilador no puede detectar este error de manera estática.
Ocurre una situación severa, si el diseñador del lenguaje desea extender la expresividad y la
flexibilidad del lenguaje al permitir que los procedimientos puedan ser creados dinámicamente, es
decir, permitiendo la devolución de procedimientos a partir de otros procedimientos. Esto ocurre
en los lenguajes funcionales e incluso en lenguajes orientados a objetos. En un lenguaje de este
tipo no puede utilizarse un ambiente basado en pilas.

52
LENGUAJES DE PROGRAMACIÓN
Página 53 de 78
Autor: informatica_uned@[Link]

8.5 Administración de la memoria dinámica.

En un lenguaje típicamente imperativo como C la asignación y desasignación de memoria


automática, ocurre únicamente para los registros de activación de una pila. Este es un proceso
relativamente fácil: se asigna memoria para un registro de activación cuando se llama a un
procedimiento y se desasigna cuando se sale. También está disponible bajo control manual la
asignación dinámica explícita, así como el uso de apuntadores. Sin embargo, la administración
manual de la memoria sobre el montón de memoria sufre varios posibles problemas, creación de
basura y de referencias pendientes.

Los lenguajes con necesidades significativas de almacenamiento en el montón, como Java, están
mejor al dejar el almacenamiento dinámico fuera de la pila a un administrador de la memoria que
incluya recolección automática de basura.
Se podría intentar resolver este problema, no desasignando ninguna memoria una vez que ésta
ha sido asignada. Este método tiene dos ventajas: es correcto y fácil de implementar y puede
funcionar para programas pequeños, pero los lenguajes funcionales y los orientados a objetos
tienen la propiedad de que se asignan dinámicamente grandes cantidades de memoria.
Este método ha sido utilizado en conjunto con sistemas de memoria virtual.

La administración automática de memoria se ubica en dos categorías: la recuperación de


almacenamiento previamente asignado pero ya no utilizado, (recolección de basura) y el
mantenimiento del espacio libre disponible para la asignación.

8.5.1 Mantenimiento de espacio libre.

Por lo general, el sistema operativo pone un bloque de memoria contiguo a disposición de un


programa en ejecución. Este bloque de memoria está conservado por una lista de bloques libres
(en principio sólo habrá uno) por medio de una lista vinculada.

Cuando es necesario asignar un bloque de un determinado tamaño, el administrador de la


memoria busca en la lista un bloque libre que tenga suficiente espacio. Cuando se devuelven
bloques de memoria a la lista de espacio libre, se deberán unir aquellos bloques que estén
contiguos, a este proceso se le llama fusión o unión.
Para evitar que la memoria quede excesivamente fragmentada, con bloques lo suficientemente
pequeños para que no sea posible la asignación de un bloque grande, se tiene que de vez en
cuando compactar la memoria libre, moviendo todos los bloques libres para unirlo y crear uno
sólo; aunque la compactación involucra gran cantidad de carga general.

8.5.2 Recuperación de almacenamiento

Conocer cuando un bloque de memoria ya no está referenciado, ya sea directa o indirectamente


mediante apuntadores, es una tarea mucho más difícil. Históricamente se han utilizado dos
métodos principalmente: conteo de referencias, y marcar y barrer.

El conteo de referencias es un método “entusiasta” ya que pretende recoger la memoria tan


pronto queda esta deje de estar referenciada (libre). Cada bloque de memoria tiene un registro
adicional que lleva el conteo de las referencias que tiene el bloque en cada momento. Cuado el
valor de este registro llega a 0, el bloque queda en disposición de ser recuperado. Aunque
parezca un buena solución tiene serios inconvenientes: La memoria extra que debe usarse para
los registro del conteo y otra más importante es el esfuerzo para llevar la cuenta de las
referencias.

53
LENGUAJES DE PROGRAMACIÓN
Página 54 de 78
Autor: informatica_uned@[Link]

El método alternativo es el de marcar y barrer, se le conoce como “perezoso” ya que pospone la


recuperación de memoria hasta que el asignador se queda sin ella. Para recuperar los bloques
libre utiliza dos pasadas: En la primera pasada recorre todos los punteros de manera recurrente,
iniciándose en el ambiente o la tabla de símbolos actual, y marca cada bloque de almacenamiento
localizado (en un bit adicional). En una segunda pasada se barre de manera lineal a través de la
memoria, devolviendo los bloques no marcados a la lista libre.
Este método a diferencia del conteo de referencias, no tiene dificultad al liberar bloques con las
referencias circulares. Sin embargo también necesita almacenamiento adicional y tiene otro
problema serio con la doble pasada que representa un retraso significativo. Esto no es aceptable
para aplicaciones que necesitan respuestas interactivas inmediatas.

El método conocido como recolección generacional de basura añade un área de


almacenamiento permanente al método anterior. Los objetos que viven lo suficiente, simplemente
son copiados al espacio permanente y nunca son desasignados. Se ha demostrado que este
proceso funciona muy bien en sistemas con memoria virtual.

8.6 Manejo de excepciones y ambientes.

Las operaciones de poner de manifiesto y de manejar las excepciones son similares a las llamadas
de procedimientos, y pueden implementarse de forma similar.
También tienen importantes diferencias:

1. No puede crearse una activación en la pila en tiempo de ejecución para representar que se
pone de manifiesto una excepción.
2. Debe localizarse un manejador y “llamar” dinámicamente, en vez de estáticamente.
3. Las acciones de un manejador se basan en el tipo de la excepción, más que en el valor de
la excepción.

Una vez resueltas esas diferencias, la idea básica es recolectar todo el código de manejador
agregado a un bloque en particular, formando un solo manejador implementado como un solo
enunciado switch que esté basado en el tipo del parámetro de excepción recibido, con una caso
por omisión que saca la pila del manejador, y si es necesario, sacando la pila en tiempo de
ejecución antes de volver a poner de manifiesto la misma excepción.

El principal problema es que el mantenimiento de la pila del manejador genera una penalización
potencial significativa en tiempo de ejecución; por lo tanto, tiene mucho más sentido evitar el uso
de excepciones para el flujo de control rutinario, guardándolos para eventos verdaderamente
necesarios.

54
LENGUAJES DE PROGRAMACIÓN
Página 55 de 78
Autor: informatica_uned@[Link]

9 Tipos de datos abstractos y módulos.


Sería deseable en un lenguaje tener un mecanismo para poder construir tipos de datos con tantas
características de un tipo incorporado como se posible (con las operaciones sobre estos).
Este mecanismo debería cumplir con los siguientes requisitos:

1- Un método para definir un tipo de datos y a la vez las operaciones sobre este.
2- Un procedimiento para reunir en un solo sitio los detalles de implementación y las
operaciones, así como permitir su restricción.

Un tipo que satisfaga parte o la totalidad de estos criterios se le denomina un tipo de datos
abstracto (o TDA).
Los dos criterios antes mencionados promueven tres metas del diseño que originalmente habían
justificado la utilización de los tipos de datos como medio de ayuda: Capacidad de modificación,
capacidad de reutilización y seguridad.

Otra alternativa para los criterios 1 y 2 para algunos autores es la ocultación y el encapsulado.
El encapsulado significa reunir en una sola localización todas las definiciones relacionadas con
un tipo de datos, y restringiendo el uso del tipo a las operaciones definidas en dicha localización.
La ocultación se refiere a la separación de las definiciones de los detalles de implementación, y
la supresión de dichos detalles en el uso del tipo.
En este tema utilizaremos los criterios 1 y 2 para definir los TDA.

Los mecanismos de tipos de datos abstractos no proporcionan el nivel de control activo que se
espera en la verdadera programación orientada a objetos. La idea de los TDA es de hecho
independiente del paradigma del lenguaje (función, imperativo, orientado a objetos) utilizado para
su implementación.
Un problema adicional es que un mecanismo de TDA a menudo se conoce como un concepto algo
más general, denominado módulo, y que es una colección de servicios que pueden o no incluir
un tipo o tipos de datos.

9.1 Especificaciones de los tipos de datos abstractos.

Una especificación general de un tipo de datos necesita incluir el nombre del tipo y los nombres
de las operaciones, incluyendo una definición de sus parámetros y valores devueltos. Esta es la
especificación sintáctica de un tipo de datos abstracto (conocido también como signatura del
tipo).
Para una especificación independiente del lenguaje sería apropiado utilizar la notación de
funciones para las operaciones del tipo de datos: f: X -> Y (X es el dominio e Y es el rango).
Como ejemplo, los números complejos podrían tener la siguiente signatura:

Tipo complex imports real


Operaciones:
+: complex x complex -> complex
-: complex x complex -> complex
*: complex x complex -> complex
/: complex x complex -> complex
-: complex -> complex
Makecomplex: rea x real –> complex
Realpart: complex -> real
Imaginarypart: complex -> real

55
LENGUAJES DE PROGRAMACIÓN
Página 56 de 78
Autor: informatica_uned@[Link]

En matemáticas es frecuente que las propiedades semánticas de las funciones se describan


mediante ecuaciones o axiomas. Si se trata de operaciones aritméticas, las leyes asociativa,
conmutativa y distributiva son ejemplos de axiomas.

La especificación algebraica de un tipo abstracto combina signatura, variables y axiomas


ecuacionales la cual proporciona una especificación concisa de un tipo de datos y de sus
operaciones asociadas, y la semántica ecuacional indica claramente cual va a ser su
comportamiento en la implementación. Los axiomas de error aportan las limitaciones en la
aplicación de las operaciones. Una operación que crea un nuevo objeto del tipo de datos que se
está definiendo se conoce como constructor, en tanto que una operación que recupera valores
anteriormente construidos se conoce como inspector. Las operaciones de los inspectores
también pueden ser subdivididas en: predicados, que devuelven valores booleanos; y selectores,
que devuelven valores no booleanos.
Ejemplo del tipo de datos abstracto pila:

tipo stack(element) imports boolean

operaciones:
createstk: stack //constructor
push: stack X element --> stack //constructor
pop: stack --> stack //selector
top: stack --> element //selector
emptystk: stack --> boolean //predicado
variables:
s: stack; x: element
axiomas:
emptystk(createstk)=true
emptystk(push(s,x))=false
top(createstk)=error
top(push(s,x))=x
pop(createstk)=error
pop(push(s,x))=s

9.2 Mecanismos de tipos de datos abstractos y módulos.

9.2.1 Mecanismos de tipos de datos abstractos.

Algunos lenguajes tienen un mecanismo específico para expresar los TDA. Dicho mecanismo
deberá tener alguna forma de separar la especificación o signatura del TDA de su
implementación. Dicho mecanismo también debe garantizar que cualquier código fuera de la
definición del TDA no puede utilizar detalles de la implementación, pero puede operar sobre un
valor del tipo definido sólo a través de las operaciones proveídas.

Ej. ML tiene un mecanismo especial para TDA.

56
LENGUAJES DE PROGRAMACIÓN
Página 57 de 78
Autor: informatica_uned@[Link]

9.2.2 Módulos.

Un módulo un una unidad de programación con una interface pública y un implementación


privada; todos los servicios disponibles de un módulo quedan descritos en su interface pública y
se exportan a otros módulos, así como todos los servicios requeridos por un módulo deben
importarse de otros módulos.

Como proveedores de servicios, los módulos pueden exportar cualquier combinación de tipos de
datos, procedimientos, variables y constantes. Debido a que los módulos tienen interfaces
explícitas (públicas) e implementaciones separadas (privadas), son los mecanismos ideales para
proveer servicios de compilación y de biblioteca independientes dentro de un ambiente de
desarrollo del software. En Java se liga la estructura de los módulos a problemas independientes
de compilación.

Los módulos son una herramienta esencial en la descomposición, control de complejidad y la


creación de bibliotecas de un programa, para compartir y reutilizar el código. Adicionalmente, los
módulos auxilian en el control de proliferación de nombres.

9.3 Compilación individual en C, espacios de nombres de C++ y paquetes de Java.

9.3.1 Compilación por separado en C y C++

C no tiene mecanismos modulares como tales, pero si tiene características de compilación por
separado y de control de nombres, que pueden utilizarse para imitar módulos de una forma
razonablemente efectiva. Pero la efectividad de este mecanismo depende totalmente de reglas
convencionales, ya que ni los compiladores de C ni los enlazadores estándar hacen obligatoria
ninguna de las reglas de protección normalmente asociadas con el módulo o con mecanismos
TDA.

C++ permite un mejor control sobre el acceso al tipo de datos. También ofrece un mejor control
sobre los nombres y el acceso a los mismos a través del mecanismo espacio de nombre. Pero
C++ no ofrece características adicionales que pudieran mejorar el uso de la compilación por
separado para simular módulos.

9.3.2 Espacios de nombres de C++ y paquetes Java

Una característica de C++ que sí ofrece un complemento efectivo a la simulación de módulos en


C es el mecanismo namespace. Permite la introducción explícita de un alcance nombrado a fin
de evitar choques de nombres entre bibliotecas compiladas por separado. En C++ puede
utilizarse un espacio de nombres para eliminar la ambigüedad.

También Java tiene un mecanismo parecido al espacio de nombres, llamado paquete, los
paquetes se construyen como grupos de clases [Link] archivo compilado por
separado en un programa Java solamente puede tener una clase pública, y esta clase puede
colocarse en una declaración de paquete como la primera declaración en el archivo del código
fuente:

package paqueteEjemplo;

Todos los nombres en un paquete Java pueden ser importados utilizando un asterisco después del
nombre del paquete:

import paqueteEjemplo.*;

57
LENGUAJES DE PROGRAMACIÓN
Página 58 de 78
Autor: informatica_uned@[Link]

9.4 Paquetes Ada

El mecanismo de módulos de Ada es el paquete. Un paquete de Ada está dividido en una


especificación del paquete y un cuerpo del paquete.
Una especificación de paquete es la interfaz pública al paquete y corresponde a la sintaxis o
signatura de un TDA.
Las especificaciones y los cuerpos de paquetes también representan unidades de compilación en
Ada y pueden ser compilados por separado.

9.5 Módulos en ML

ML también tiene un servicio de utilería de módulo más general, que consiste en tres
mecanismos: signaturas, estructuras y functores.

Signatura es esencialmente una definición de interfaz.

Una estructura es una implementación de una signatura, el tipo de la estructura.

Los functores son funciones de estructuras a estructuras, permiten la parametrización de las


estructuras por otras estructuras.

9.7 Problemas que se presentan con los mecanismos de tipos de datos abstractos

En esta sección se catalogan algunos de los inconvenientes principales de los mecanismos TDA.
Muchos aunque no todos de estos inconvenientes han sido corregidos en los lenguajes orientados
a objetos:

1. Los módulos no son tipos. En C pueden presentarse dificultades porque un módulo


debe exportar un tipo así como operaciones. Sería más práctico definir un módulo para
que sea un tipo.

2. Los módulos son entidades estáticas.

3. Los módulos que exportan tipos no controlan adecuadamente las operaciones


sobre variables de dichos tipos. El módulo exportador no puede garantizar que un
procedimiento sea llamado antes de que se utilicen las variables, por lo que no puede
asegurarse la correcta asignación e inicialización. Aún peor, pueden hacerse copias y
llevar a cabo desasignaciones fuera del control del módulo, sin que el usuario sea
consciente de las consecuencias y sin la capacidad de devolver la memoria desasignada a
almacenamiento disponible.
Los lenguajes orientados a objetos resuelven el problema de inicialización utilizando
constructores.

4. Los módulos no siempre representan adecuadamente la forma en la que


dependen de los tipos importados.

5. Las definiciones de los módulos no incluyen especificación de la semántica de


las operaciones incluidas.

58
LENGUAJES DE PROGRAMACIÓN
Página 59 de 78
Autor: informatica_uned@[Link]

10 Programación orientada a objetos


10.1 Reutilización e independencia del software.

Los lenguajes de programación orientada a objetos encaran tres problemas de diseño del
software: la necesidad de volver a utilizar componentes de software tanto como sea posible, la
necesidad de modificar el comportamiento de los programas mediante cambios mínimos al código
existente, y la necesidad de conservar la independencia de los diferentes componentes.

Existen 5 maneras básicas para modificar un componente de software de modo que pueda
reutilizarse: extensión, restricción, redefinición, abstracción y “polimorfización”.

1. Extensión de los datos y/o las operaciones. Ejemplo: se define una ventana en la
pantalla como un rectángulo especificado por sus cuatro esquinas, con operaciones:
trasladar, redimensionar…, una ventana de texto puede definirse como una ventana con
algún texto agregado para su despliegue.
2. Restricciones de los datos y/o de las operaciones. Esta operación es lo contrario que la
extensión, si por ejemplo está disponible una cola de dos puntas, puede obtenerse una
normal restringiendo las operaciones.
3. Redefinición de una o más operaciones. Incluso si las operaciones de un nuevo elemento
se conservan esencialmente igual, pude ser necesario redefinir alguna de las mismas para
que acepte un nuevo comportamiento. En algunos casos, la estructura básica de cada
aplicación es tan similar a otras que los desarrolladores han empezado a utilizar
estructuras de aplicación que ofrecen los servicios básicos en forma orientada a objetos y
son utilizados por estos en forma de redefinición y reutilización. Como ejemplo tenemos el
juego de herramientas para ventanas en Java (swing) y las Microsoft fundations classes en
C++.
4. Abstracción, o la reunión de operaciones similares de dos componentes diferentes en
uno nuevo. Por ejemplo, un circulo y un rectángulo son objetos que tienen una posición y
que pueden trasladarse y desplegarse.
5. Polimorfización o la extensión del tipo de datos a los cuales pueden aplicarse las
operaciones.

El diseño para la reutilización no es el único objetivo de los lenguajes orientados a objetos otro es
la restricción del acceso a los detalles internos de los componentes del software.

Los dos objetivos de restricción de acceso y de capacidad de modificación para la reutilización a


veces pueden resultar mutuamente incompatibles.
El control sobre el acceso puede tomar varias formas:

• Listar de manera explícita todas las operaciones y los datos accesibles para los clientes en
una lista de exportación.
• Listar aquellas propiedades de un componente que son inaccesibles en una parte privada.
• Exportación implícita, como en el caso de nombres de C++.

Puede ser necesario que los clientes declaren explícitamente el uso que hagan de un componente
mediante un enunciado import (Java) o using (C++).
Aquí denominaremos al mecanismo de protección de los detalles como mecanismo de
protección en vez de encapsulado o mecanismos de ocultación que pueden parecer poco claros.

59
LENGUAJES DE PROGRAMACIÓN
Página 60 de 78
Autor: informatica_uned@[Link]

10.2 Java: objetos, clases y métodos.

Los componentes básicos de un lenguaje de programación orientado a objetos son: Clase,


Objetos y métodos.
En primer término, el estado de un objeto está representado por variables locales declaradas
como parte del objeto e inaccesible a componentes externos del objeto. En segundo término,
cada objeto tiene un conjunto de funciones y procedimientos a través de los cuales pude
accederse y modificarse el estado local (estos son conocidos como métodos). Llamar a un
método de una clase lo llamaremos como “llamar o invocar a un método” (envía mensajes al
objeto).

Creando un patrón para los métodos y el estado se pueden declarar los objetos. A este patrón se
le llama Clase. Esta es esencialmente un tipo de datos. Se dice que un objeto es una instancia
de una clase.
Las variables de locales que representan el estado de un objeto se conoce como variables de
instancia.

Se denominan constructores a aquellos métodos que tienen el mismo nombre de la clase y no


devuelven ningún valor. Estos especifican valores iniciales para las variables de instancia y pueden
ejecutar otras operaciones que se requieran cuando se construye el objeto por primera vez.
Las variables de instancia son declaradas Private en tanto que los constructores y lo métodos son
Public. El que las variables de instancia queden inaccesibles a objetos externos asegura que el uso
de una clase sea independiente de la representación interna de los datos.

Ejemplo en Java del tipo o clase Complejo (utiliza coordenadas polares):

public class Complex


{
public Complex() //Constructor
{ radius=0; angle=0; }

public Complex (double realpart, double imagpart) //Otro constructor


{ .... }

public double realpart() //Método público


{ return radius * [Link](angle); }

....

private double radius, angle; //Variables de instancia privadas


}

En Java se crearían las instancias de la clase Complex de la siguiente forma:

Complex z,w;
...
z= new Complex();
w=new Complex(-1,1);

Utilizando el punto pueden invocarse métodos de la clase (después de haber sido asignada).

z= [Link](w);

60
LENGUAJES DE PROGRAMACIÓN
Página 61 de 78
Autor: informatica_uned@[Link]

Sin recolección de basura automática, resulta difícil evitar serias fugas de espacio. Por esta razón
la mayoría de los lenguajes orientados a objetos requieren de recolección automática de la
basura.
Naturalmente una clase pude referirse a sí misma en su definición y un objeto puede contener
otro objeto de su propia clase como una variable de instancia. Una característica especial que
facilita lo anterior en Java es la palabra clave This para hacer referencia a la instancia presente.

Los conceptos de clase y objeto pueden visualizarse como la generalización de la idea de tipo de
registro o estructura y de variable, respectivamente.

10.3 Herencia.

La herencia es el principal mecanismo de los lenguajes OO que permite compartir datos y


métodos entre clase, así como la capacidad de redefinir estas operaciones sin necesidad de
modificar el código existente. En java esto se hace utilizando la palabra clave “extends”:

public class B extends A


{
….
}

En java, B hereda todas las variables de instancia y los métodos de A. A la clase B se la denomina
subclase de A y a A superclase de B.
Un B es una extensión de la clase A, y todas las operaciones aplicables a A también son aplicables
a B.
Las definiciones de clase en Java (y en la mayoría de los lenguajes OO) también son definiciones
de tipo, por lo que B se puede definir como un subtipo de A, y a A como supertipo de B
(obedecen al principio de subtipo: Un objeto de un subtipo puede ser utilizado en cualquier
sitio donde sea correcto un objeto de supertipo.).
La herencia que responde al principio de subtipo expresa la relación es-un: si A es una subclase
de B, entonces todos los objetos pertenecientes a A también pertenecen a B, es decir, cada A “es-
un” B.

En Java todos los objetos obedecen al principio de subtipo, pero en otros lenguajes (sobre todo
en C++) la herencia se define de forma más general, ya que es posible restringir el acceso a
datos y métodos de una superclase, por lo que el principio de subtipo desaparece.

La asignación a un objeto de una subclase se ha de hacer en Java mediante una conversión


forzada de tipo, en contraposición a la asignación a un objeto de un superclase (que se hace de
forma natural). A este proceso se conoce como downcasting.

En Java por definición todas las clases son de manera implícita una extensión de la clase Object.
Los métodos Equals y ToString son ejemplos de métodos comunes a todos los objetos en Java. El
comportamiento por omisión para estos métodos no son generalmente lo que deseamos, pero
afortunadamente la herencia no sólo permite la extensión de las operaciones si no que a demás
permite modificar o hacer caso omiso del comportamiento de los métodos en las subclases.

Un método abstracto, es un método que está siempre disponible para la superclase, pero que se
la dan diferentes implementaciones para las diferentes subclases. A estos métodos abstractos se
les conoce como diferidos, y una clase que tiene métodos diferidos se conoce como clase diferida.
La ventaja de definir métodos abstractos o diferidos no es sólo para consolidad el código, sino
también para asegurarnos que cualquier subclase tenga un método determinado.

61
LENGUAJES DE PROGRAMACIÓN
Página 62 de 78
Autor: informatica_uned@[Link]

public abstract class ClosedFigure


{ public ClosedFigure (Point c)
{ center= c; }
...
public abstract double area();
private Point center;
}

Ahora tenemos que definir area() en las subclases.

public class Circle extends ClosedFigure


{ public Circle (Point c, double r)
{ super(c);
radius=r;
}
...
public double area()
{ return [Link] * radius * radius; }
private double radius;
}

** La palabra clave super se utiliza para indicar operaciones en el objeto presente


interpretado como un miembro de la clase padre. En tanto, que this se refiere al objeto
presente como miembro de la clase que se está definiendo.

Se puede relajar la protección de “center” de forma que quede accesible dentro de las
subclases, pero no por lo clientes; y esto se puede lograr declarando a esa variable como
protected.

La herencia se puede representar gráficamente como un árbol (gráfica de herencia).


La herencia proporciona muchos tipos de polimorfismo, ya que cualquier método que se aplicable
a una clase base puede ser aplicable también a una subclase, donde además podría ser redefinido
(polimorfismo de subtipo). Aquí tiene un efecto similar a la sobrecarga.

La herencia sencilla se puede representar como un árbol. También es posible la herencia


múltiple, en la que una clase pude heredar de una o más clase (C++ tiene herencia múltiple,
Java no pero tiene una solución alternativa llamada herencia de interfaz múltiple).
La herencia múltiple agrega una complejidad sustancial a un lenguaje, en un lenguaje con
herencia múltiple, las gráficas de herencia pueden ser gráficas acíclicas dirigida. Esto significa que
los métodos pueden heredar de más de una manera.

El mecanismo para el almacenamiento de objetos de diferentes clases utilizando la clase universal


object se conoce el enfoque basado en objetos y es típico de Java. La desventaja es que la
clase del objeto recuperado es posible que no se conozca. Java provee de una palabra clave
especial para esta situación, instanceof. Por su parte, C++ utiliza su mecanismo de plantillas
para establecer estructuras de datos uniformes que son verificadas en su tipo en tiempo de
compilación.
Java proporciona un mecanismo denominado interface, que es en apariencia una clase, pero
que sólo especifica los nombres y los tipos (signaturas) de los métodos, sin proporcionar
implementación, variables de instancia o constructores. Una interface es similar a una clase
abstracta, pero a diferencia de ésta puede no dar detalles con respecto a una implementación.
Todos los métodos en una interface son implícitamente públicos, ya que no tiene sentido que
sean privados.

62
LENGUAJES DE PROGRAMACIÓN
Página 63 de 78
Autor: informatica_uned@[Link]

10.4 Ligadura dinámica.

En un lenguaje orientado a objetos, una de las características principales que distinguen a las
clases de los módulos o paquetes en lenguajes como Ada o ML es la naturaleza dinámica de las
clases en contraste con la naturaleza estática de los módulos.

Dependiendo del lenguaje del que se trate esta asignación dinámica puede quedar en manos del
programador o bien estar totalmente automatizada. Java es un lenguaje híbrido en este sentido
ya que la asignación (y la inicialización) de los objetos queda bajo el control del programador a
través de la expresión new, sin que exista un delete correspondiente. En su lugar, las
asignaciones de los objetos son recuperados bien cuando salen de su alcance (al estilo basado en
pilas), o mediante alguna forma de recolección de basura.

Los métodos también pueden variar dinámicamente. También puede presentarse una ligadura
dinámica cuando se redefine un método en una clase derivada.
Es posible ofrecer a la vez, en un lenguaje orientado a objetos, la ligadura estática y dinámica de
los métodos (C++).
En la mayoría de los lenguajes orientados a objetos “puros”, como Java y SmallTalk, todos los
métodos son “Virtuales”, es decir, tienen ligadura dinámica. De ser necesario es posible obtener
una ligadura estática aplicando la palabra clave “super”. En Java también es posible obtener la
ligadura estática al llamar a los métodos static, final o private.

Un problema adicional al uso de la ligadura dinámica es la forma de implementar la ligadura


dinámica como parte del entorno de ejecución.

10.5 C++

C++ también fue diseñado para ser un lenguaje eficiente y práctico, se convirtió en el principal
lenguaje orientado a objetos de principios de la década de 1990.
C++ contiene declaraciones de clase y de objetos similares a los de Java.
Las variables de instancia se conocen como miembros de datos y los métodos como funciones
miembro.
A las subclases se les llama clases derivadas y a las superclases clases base.

Una diferencia fundamental entre C++ y la mayoría de los demás lenguajes orientados a objetos,
incluyendo Java, es que los objetos no son automáticamente apuntadores o referencias.
A excepción de las ligaduras dinámicas y de la herencia de las funciones miembro, el tipo de datos
class en C++ es en esencia idéntico al tipo de datos struct. La herencia en C++ se indica
mediante “:” después del nombre de la subclase, y antes del nombre de la clase padre (con public
generalmente delante del nombre de la clase padre).

Al igual que en Java, en C++ se incluyen tres niveles de protección para los miembros de clase:
público, privado y protegido.

• Públicos: son accesibles para el código del cliente, y clases derivadas.


• Privados: no son accesibles ni para los clientes ni para las clases derivadas.
• Protegidos: no son accesibles para el código cliente, si son accesibles para clases
derivadas.

class A
{ // Una clase de C++
public:
// Aqui todos los miembros son públicos

63
LENGUAJES DE PROGRAMACIÓN
Página 64 de 78
Autor: informatica_uned@[Link]

protected:
//Aquí todos los miembros están protegidos
private:
//Aqui todos los miembros son privados
};

En C++ la inicialización de los objetos se efectúa como en Java mediante constructores que
tienen el mismo nombre que el de la clase.

C++ no tiene un recolector de basura incorporado, por lo que C++ también tiene destructores
que al igual que los constructores utilizan el mismo nombre de la clase, pero están antecedidos
por el símbolo ~. Los destructores son llamados automáticamente al desasignar un objeto (ya sea
por haberse salido del alcance o por el uso del operador delete).

Una declaración de clase en C++ no siempre contiene el código de implementación para todas las
funciones miembro. Estas últimas pueden ser implemetadas con un procedimiento fuera de la
declaración utilizando el operador de resolución de alcance indicado por “::” después del
nombre de clase.

class Queue
{
public:
Queue() {...}; //Constructor
void dequeue(); //Función sin su implementación
~Queue() //Destructor
{ while (¡empty())
{ delete... ;}
};
protected:
...
};

void Queue::dequeue() //Implementación de una función de la clase Queue


{ .... }

En C++ se incluye la ligadura dinámica de las funciones miembro como una opción, aunque no es
lo preestablecido. Solamente los métodos definidos utilizando la palabra clave virtual son
candidatos para ligadura dinámica. Si deseamos redefinir una función area para un cuadrado
como una derivación de un rectángulo, lo haríamos en C++:

class Rectangle
{
public:
virtual double area()
{return length * width;};
...
private:
double length, width;
};

class Square: public Rectangle


{

64
LENGUAJES DE PROGRAMACIÓN
Página 65 de 78
Autor: informatica_uned@[Link]

public:
double area()
//Redefine dinámicamente el área del rectángulo
{ return width * width; };
...
};

Si lo que queremos es crear una clase abstracta con un método abstracto para asegurarnos que
se redefine en todas las subclases que heredan esa clase abstracta, en C++ se consigue mediante
el uso de una declaración virtual pura:

class ClosedFigure //clase abstracta


{
public:
virtual double area()=0; /* Virtual pura. El cero en una declaración virtual
indica que en ese punto la función es nula, no tiene
cuerp y por lo tanto no puede ser llamada, y tiene que
ponérsele el cuerpo en las subclases */
};
class Rectangle: public ClosedFigure
{
public:
double area()
{ return length * width;};
private:
double length, width;
};

La herencia múltiple en C++ generalmente crea copias por separado de cada clase en una
trayectoria de herencia.
Por ejemplo las declaraciones:

class A {...};
class B: public A {...};
class C: public A {...};
class D: public B, public C {...};

proporcionan cualquier objeto de la clase D con dos copias por separado de objetos de la
clase A y crea la siguiente gráfica de herencia, lo que se llama herencia repetida o
múltiple:

A A
^ ^
| |
| |
B C
^ ^
\ /
\ /
D

Para obtener sólo una copia de una A en la clase D, debe declararse la herencia usando la palabra

65
LENGUAJES DE PROGRAMACIÓN
Página 66 de 78
Autor: informatica_uned@[Link]

clave virtual:

class A {...};
class B: virtual public A {...};
class C: virtual public A {...};
class D: public B, public C {...};

Con lo que se tendría la siguiente gráfica conocida como herencia compartida:

A
^
/ \
/ \
B C
^ ^
\ /
\ /
D

10.6 Smalltalk

De todos los lenguajes orientados a objetos Smalltalk ofrece el procedimiento más completo y
consistente con relación al paradigma orientado a objetos.

En Smalltalk, prácticamente toda entidad del lenguaje es un objeto, incluyendo las constantes, los
números y los caracteres. Smalltalk está orientado a objetos de manera pura.

Fue creado como un sistema completo para una estación de trabajo personal. La interfaz de
usuario también era una novedad en cuanto a que incluía sistemas de ventanas con menús y un
ratón.

Como lenguaje es interactivo y está orientado dinámicamente. Todas las clases, objetos y
métodos se crean por interacción con el sistema, utilizando ventanas y plantillas de pantalla
provistas por el sistema.

La jerarquía de clases es un árbol, ya que Smalltalk-80 sólo tiene herencia simple. La clase Object
es la raíz de la jerarquía de clases, igual que ocurre en Java.
Los nombres de las clases deben comenzar por mayúscula, y los nombres de los objetos y
métodos con minúsculas.

En Smalltalk se especifican las clases dando su superclase correspondiente, sus variables de


instancia, así como sus métodos en los sitios apropiados dentro del sistema Smalltalk.

Los nombres de los métodos se conocen como selectores:

- Unarios: operaciones sin parámetros.


- De palabras clave: operaciones con parámetros.

66
LENGUAJES DE PROGRAMACIÓN
Página 67 de 78
Autor: informatica_uned@[Link]

10.7 Cuestiones de diseño en lenguajes orientados a objetos.

Las características orientas a objetos representan capacidades dinámicas más que estáticas (como
la ligadura dinámica de los métodos con los objetos), por lo que un aspecto del diseño de los
lenguajes orientados a objetos es introducir características de modo que se reduzca la
penalización en tiempo de ejecución debido a la flexibilidad adicional.
En la sección de C++ ya se vio una característica que promueve la eficiencia, las funciones en
línea: aquellas funciones miembro cuya implementación estén incluidas en la clase, son
automáticamente funciones en línea. Con esto se evita la penalización del la llamada a la función
en tiempo de ejecución.
Esta sección analiza los problemas de diseño del lenguaje y no al diseño de programas.

10.7. 1 Clases en contraposición con tipos.

Hay varias formas en las que las clases se introducen en un lenguaje con tipos:

1. Las clases se excluyen de la verificación de tipos. Los objetos se convierten en


entidades sin tipo. Este método es el más sencillo.
2. Las clases son constructores de tipos, haciendo las declaraciones de tipos parte del
sistema de tipos. En C++ una clase es simplemente una forma diferente de tipo
registro o estructura. En este caso, la asignación de los objetos de clases descendentes
a los objetos de cases ascendentes debe incluirse en los algoritmos de verificación de
tipos. Si x es un objeto de una clase descendente de la clase del objeto y, entonces la
verificación de tipos debe de permitir y = x, pero no x = y.
3. Las clases pueden convertirse simplemente el sistema de tipos. Esto es, todos los
demás tipos estructurados quedan excluidos del lenguaje. Este método es seguido por
muchos lenguajes oo, tal como SmallTalk o Java. En este último las clase y arreglos
son los únicos tipos estructurados, sin embargo existen los tipos básicos con bolean,
int, etc…
Utilizar este tipo significa que bajo la tipificación estática, puede verificarse la validez de
todas las asignaciones de objetos antes de la ejecución a pesar de no conocer la
membresía exacta de clase hasta el tiempo de ejecución.

10.7. 2 Clases en contraposición con módulos.

Las clases proporcionan un mecanismo versátil para organizar código que también abarca algunas
características propias de los tipos de datos abstractos: proporcionan un mecanismo para la
especificación de interfaces, para la localización de código que se aplica a colecciones específicas
de datos (los métodos) y para la protección de la implementación a través del uso de datos
privados. Pero desafortunadamente las clases no permiten una diferenciación clara entre la
interface y la implementación.

Otro problema es con el espacio de nombre: ocasionalmente se puede usar una clase como un
espacio de nombres o un almacén para un almacén de servicios de utilería. Las clases no función
bien como espacios de nombres o módulos estáticos. Por esta razón algunos lenguajes oo
incluyen mecanismos de módulos independientes al sistema de objetos (C++ y Java, con los
paquetes).
En ocasiones los mecanismos orientados a objetos están relacionados más de cerca con una
estructura de módulo (Ada95).

El problema de cual es el mecanismo de módulo que se ajusta mejor a un escenario orientado a


objetos, así como la mejor manera de integrar los dos conceptos clase y módulo, queda hasta
cierto punto todavía abierto.

67
LENGUAJES DE PROGRAMACIÓN
Página 68 de 78
Autor: informatica_uned@[Link]

10.7. 3 Herencia en contraposición con polimorfismo.

Existen tres tipos de polimorfismos:

1. Polimorfismo paramétrico: Donde los parámetros de tipo pueden quedarse sin


especificar en las declaraciones (Paquetes genéricos de Ada y las plantillas de C++ son
algunos ejemplos).
2. La sobrecarga o el polimorfismo ad-hoc: diferentes funciones o declaraciones de
métodos comparten el mismo nombre y se elimina la ambigüedad mediante los tipos de
parámetros en cada declaración.
3. Polimorfismo de subtipo: Las operaciones de un tipo pueden aplicarse a otro tipo.

La herencia se puede catalogar como un polimorfismo de subtipo (siempre y cuando pueda


garantizarse la relación de subtipo para las subclases.). También pude verse la herencia como un
tipo de sobrecarga sobre el tipo de parámetro del objeto implícito (al apuntador this).

Con la mezcla de herencia y sobrecarga se presenta el problema conocido como doble despacho
(double-dispatch) o multidespacho (multi-dispatch): Consiste básicamente en que no se toma
en consideración métodos binarios (o de orden n) que pudieran necesitar sobrecarga en la
membresía de clases basados en uno o dos parámetros. Como ejemplo tenemos la clase Complex
que define el método “Complex add(Complex Y)” que lo que hace es sumar dos números
complejos: entonces [Link] y sumaría dos números complejos. Si y fuera un double, esto se podría
solucionar sobrecargando add, pero si x fuera un double esto no sería posible. Un solución sería el
sobrecargar la operación + (esto se puede hacer en C++), pero daría una solución poco clara, ya
que dividiría las operaciones con números complejos en una definición de clase y un número
indefinido de definiciones de funciones independientes. Se han hecho intentos para solucionar
este problema vía los llamados multimétodos: métodos que pueden pertenecer a más de una
clase y cuyo despacho de sobrecarga pude basarse en la membresía de clase de varios
parámetros. Uno de los lenguajes que con más éxito ha llevado el multimétodo a sido Dylan (de
la mano de Appel), pero por desgracia, con el auge de java a quedado algo relegado.

Aunque la sobrecarga y la herencia son conceptos a priori, y salvando el problema del despacho
múltiple, relacionados y aparentemente bien acoplados, se da el problema de polimorfismo
paramétrico, que es más difícil de integrar.

10.8 Cuestiones de implementación en lenguajes orientados a objetos.

Se trata principalmente de problemas de generación eficiente de código por parte de un


compilador, por lo que son menos aplicables a un lenguaje dinámico principalmente interpretado
como Smalltalk.

10.8.1 Implementación de objetos y métodos.

Los objetos se implementan por lo general de la misma manera que las estructuras de registro en
C o en Ada., con variables de instancia que representan los campos de datos en la estructura.
Un objeto de una subclase puede asignarse como una extensión del objeto de datos precedente,
con las nuevas variables de instancias con espacio asignado al final del registro
Es posible implementar los métodos como funciones normales, sin embargo, estos tienen dos
diferencias fundamentales: La primera es que un objeto puede tener acceso directo a los datos
del objeto presentes en la clase. El segundo problema es más significativo y se presenta con
ligadura dinámica de los métodos durante la ejecución, ya que estos dependen de la membresía
de clase del objeto actual cuando se efectúa la llamada.

68
LENGUAJES DE PROGRAMACIÓN
Página 69 de 78
Autor: informatica_uned@[Link]

10.8.2 Herencia y ligaduras dinámicas.

En una implementación de objetos clásica como la vista anteriormente sólo se le asigna espacio a
cada objeto para las variables de instancias, no para los métodos. Pero cuando se usan ligaduras
dinámicas se presentan problemas con dicha implementación. Una solución sería conservar todos
los métodos ligados dinámicamente como campos adicionales en las estructuras asignadas a cada
objeto. El problema con lo anterior es que todos los objetos tienen que mantener una lista con
todos los métodos virtuales disponibles en ese momento, y dicha lista podría resultar demasiado
grande.
Una solución alternativa sería mantener una tabla de métodos “virtuales” para cada clase en
alguna localización centralizada, como por ejemplo el área de almacenamiento global. A esta tabla
se le conoce como tabla de método virtual (VMT). Hay que hacer notar que ninguna de estas
soluciones de implementación requiere que durante la ejecución se mantenga una gráfica de
herencia.

10.8.3 Asignación e inicialización.

Los lenguajes orientados a objetos pueden mantener un entorno de ejecución de la manera


tradicional de pilas y montículos, pero en vista de que los objetos generalmente son de naturaleza
dinámica, es más comuna que sean asignados al montículo, y este es el procedimiento adoptado
en Java y Smalltalk. C++ permite que objeto sea asignado a la pila o como un apuntado
(montículo).

Java por su parte no tiene rutinas específicas para la desasignación explícita, por lo que requiere
el uso de un recolector de basura para devolver el almacenamiento ya no accesible al montículo.
Esta característica genera una carga de ejecución y una complejidad adicional en tiempo de
ejecución significativas.

Durante la ejecución de un código de inicialización es necesario ejecutar el código de inicialización


de clase anterior antes de llevar a cabo las inicializaciones actuales. En C++ y Java la
inicialización de ancestros están programadas automáticamente por el compilador. En C++ donde
existe la herencia múltiple y se pueden crear trayectorias diferentes de una clase descendiente a
una clase anterior, el código de inicialización se ejecuta de acuerdo al orden topológico de la
gráfica de dependencia determinado por en que las clase padres aparecen listadas en cada
declaración de clase derivada.

Además del orden de inicialización pueden aparecer problemas relacionados con la existencia de
diferentes constructores para una misma clase. Por ejemplo, en Java cuando no existe un
constructor predeterminado para una superclase, debe de proporcionarse una llamada explícita
del constructor en todos los constructores de las subclases.

69
LENGUAJES DE PROGRAMACIÓN
Página 70 de 78
Autor: informatica_uned@[Link]

11. Programación funcional


La programación funcional tiene varias ventajas claras sobre la programación imperativa que la
hacen atractiva para el prototipazo, la inteligencia artificial, los sistemas de comprobación
matemática y las aplicaciones lógicas.

El principal inconveniente ha sido la ineficiencia en la ejecución. Debido a su naturaleza dinámica


estos han sido tradicionalmente interpretados, no compilados, resultando una pérdida sustancial
en la ejecución. Estos últimos años han evolucionado en este sentido, lo que sumado a su
simplicidad y ortogonalidad de diseño lo han convertido en una alternativa seria a los lenguajes
imperativos, sobre todo en aquellos ambiente dónde el velocidad de ejecución no es primordial.

Existen varias razones por las cuales nunca han llegado a tener la popularidad de otros lenguajes
(imperativos u orientados a objetos):

1. Los programadores aprenden a programar con lenguajes imperativos o bien orientados a


objetos.
2. Los lenguajes orientados a objetos se construyen sobre conceptos imperativos tradicionales.

Si bien los lenguajes funcionales tienen también poderosos mecanismos para controlar la
complejidad y la estructuración de código, pero son más abstractos y matemáticos.

11.1 Programas como funciones.

Desde este punto de vista un programa es una función matemática:

Y = f(x)

x es el dominio de f, Y se conoce como el rango de f. la x se conoce como variable


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

El punto de vista funcional de la programación elimina las variables (como apuntadores de


memoria) y por lo tanto la asignación, a esto se le define como programación funcional pura.
Y esta es completa en Turing pues cualquier cálculo pude ser descrito utilizando sólo funciones.
Un consecuencia de la programación funcional pura es que no pueden existir ciclos (bucles), esto
es solventado gracias a la recursividad.

Otra consecuencia es que no existe el concepto de estado interno de la función. La propiedad de


una función de que su valor sólo dependa de los valores de los argumentos (y de sus constantes
no locales) se conoce como transparencia referencial. Por ejemplo, una función random no
depende sólo de los argumento, si no del estado de la máquina y de las llamadas anteriores a sí
misma. Como consecuencia un función transparente referencialmente que no tenga argumentos
siempre devuelve el mismo valor, por lo que no se diferenciaría de una constante.

70
LENGUAJES DE PROGRAMACIÓN
Página 71 de 78
Autor: informatica_uned@[Link]

La carencia de asignación y la transparencia referencial de la programación funcional hacen que


los programas funcionales sean particularmente simples. El ambiente de ejecución asocia nombre
sólo a valores no a localizaciones de memoria, una vez introducido en nombre en el ambiente su
valor nunca cambia. A esto se le denomina semántica de valor (para distinguirla de la
semántica de almacenamiento, más conocida).

Se puede expresar la generalidad de las funciones definiéndolas como valores de primera


clase. En este sentido la composición de funciones es posible pasando funciones como parámetro
a otra función (funciones de orden superior).

Resumiendo las cualidades de los lenguajes de programación funcional y de la programación


funcional como sigue:

1. Todos los procedimientos son funciones y distinguen claramente los valores de entrada
(parámetros) de los de salida (resultados).
2. No existen variables ni asignaciones. Las variables han sido reemplazadas por los
parámetros.
3. El valor de la función sólo depende de los valores de los parámetros.
4. Las funciones son valores de primera clase.

11.2 Programación funcional en un lenguaje imperativo.

Las técnicas de programación funcional pueden ser ampliamente utilizadas con un lenguaje
imperativo como el C o Pascal. El motivo por el cual estas técnicas están siendo cada vez más
utilizadas son las mismas por las cuales estos lenguajes están siendo cada vez más usados: La
claridad de su semántica y la claridad resultante de sus programas.

Unos de los requisitos básicos para poder aplicar estas técnicas en cualquier lenguaje son la
posibilidad de la recursión y un mecanismo adecuado de funciones.

Una de las desventajas de la programación de estilo funcional es sobrecosto de la recursión sobre


los ciclos estándar. Una forma de recursión que mejora lo anterior es la llamada recursión de
cola, dónde la última operación en un procedimiento es llamarse a sí mismo con diferentes
argumentos. Pero desafortunadamente muchos de los usos simples de la recursión no cumplen
con la recursividad de cola. Para solventar estos casos se ha inventado unas técnicas que
convierten funciones a funciones recursivas de cola mediante parámetros de acumulación,
que se utilizarán para precalcular las operaciones que se efectuaran después de la llamada
recursiva y pasar los resultados a la llamada recursiva.

Los lenguajes imperativos tienen una serie de restricciones que impiden que estos interpreten
todos los programas en este estilo (funcional):

1. Los valores estructurados como los arreglos o los registros no pueden ser valores
devueltos de las funciones.
2. No existe forma de construir un valor de un tipo estructurado de forma directa.
3. Las funciones no son valores de primera clase, por lo que no es posible escribir
funciones de orden superior.

71
LENGUAJES DE PROGRAMACIÓN
Página 72 de 78
Autor: informatica_uned@[Link]

11.3 Scheme: Un dialecto de LISP.

En Scheme todos los programas y los datos son expresiones, y éstas son de dos variedades:
átomos y listas.

Los átomos son como las constantes y los identificadores de un lenguaje imperativo: incluyen
números, cadenas, nombres, funciones y algunos constructores.

Una lista es simplemente una secuencia de expresiones separadas por espacios y rodeadas por
paréntesis.

Como los programas en Scheme son expresiones y los programas necesitan ser ejecutados o
evaluados, la semántica de Scheme está dada por una regla de evaluación para las expresiones:

1. Átomos constantes, como números y cadenas que se evalúan por sí mismos.


2. Los identificadores se buscan en el ambiente actual y se reemplazan por el valor allí
encontrado (tabla de símbolos)
3. Cada elemento de la lista se evalúa recursivamente como una expresión.

Tanto en Scheme como en otros LISP, la estructura básica de datos es la lista.

11.4 ML: Programación funcional con tipificado estático (fuerte).

ML es un lenguaje de programación funcional muy distinto de las versiones de LISP, tiene una
sintaxis más parecida a Algol, que trata de evitar e uso de demasiados paréntesis.

Es de tipificado fuerte, por lo que el tipo de cada operación se determina antes de la ejecución,
verificándose los tipos para ser consistentes.

El lenguaje es más seguro y pueden detectarse más errores antes de la ejecución, y también
permite el polimorfismo paramétrico.

En ML el programa básico, es como Scheme, una declaración de una función.

La palabra reservada fun en ML, introduce una declaración de función. El identificador que sigue
inmediatamente después de fun es el nombre de la función, y después siguen los nombres de los
parámetros, hasta llegar al signo igual. Después del signo igual aparece el cuerpo de la función.

11.5 Evaluación retrasada.

Un problema importante que se presenta en el diseño y en el uso de los lenguajes funcionales es


la distinción entre las funciones “normales” y las formas especiales.

En un lenguaje con una regla de evaluación de orden aplicativo, como son Scheme y ML todos
los parámetros a las funciones definidas por el usuario se evalúan en el momento de la llamada,
aunque no sea necesario hacerlo.

En el caso de la función and, la expresión Scheme (and a b) o la expresión infija a && b en un


lenguaje como C o Java no necesitan evaluar el parámetro b, si a se evalúa como falso
(evaluación en cortocircuito de las expresiones booleanas y es un ejemplo de la utilidad de
evaluación retrasada).

72
LENGUAJES DE PROGRAMACIÓN
Página 73 de 78
Autor: informatica_uned@[Link]

En el caso de la función if, no es solo un caso de simple utilidad, sino de necesidad: para que la
expresión (if a b c) en Scheme tenga la semántica apropiada, la evaluación de b y de c debe
retrasarse hasta que se conozca el resultado de a, y con base en lo anterior se evalúa b o c, pero
nunca ambas.

Scheme y ML deben distinguir entre dos clases de funciones predefinidas aquellas que utilizan la
evaluación estándar y aquellas que no lo hacen. Esto compromete la uniformidad y la extensión
de estos lenguajes.

Un posible argumento para restringir la evaluación de la función a la evaluación de orden


aplicativo es que la semántica (y la implementación) son más simples.

11.6 Haskell: Un lenguaje perezoso, completamente Curry.

Haskell es un lenguaje funcional puro.


Haskell incluye varias características novedosas, incluyendo la sobrecarga de funciones y un
mecanismo de nombre mónadas, para ocuparse de efectos colaterales, por ejemplo E/S.

La sintaxis de Haskell es muy similar a ML, aunque con algunas diferencias notables. Haskell
reduce la sintaxis a un mínimo absoluto utilizando indicios internos del programa, incluyendo la
regla de disposición que utiliza la sangría y el formato de los renglones para resolver
ambigüedades.

11.8 Las matemáticas en la programación funcional I: Cálculo lambda.

Es un formalismo matemático para expresar el cómputo por medio de funciones, similar a la


forma en la que una maquina de Turing es un formalismo para expresar el cómputo mediante una
computadora.
El calcula lambda puede ser usado como modelo para lenguajes de programación puramente
funcionales.

73
LENGUAJES DE PROGRAMACIÓN
Página 74 de 78
Autor: informatica_uned@[Link]

12. Programación lógica


La lógica está relacionada con las computadoras y con los lenguajes de programación de varias
maneras: En primer lugar los circuitos de las computadoras está diseñados con ayuda del algebra
booleana, y los datos y las ecuaciones boolenas están siendo utilizados prácticamente de forma
universal en los lenguajes de programación para el control de sus acciones.

Los enunciados lógicos, o por lo menos un forma restringida de ellos, pueden considerarse como
un lenguaje de programación y ejecutarse en una computadora, si existe un sistema de
interpretación lo suficientemente sofisticado para ello. Trabajando sobre este enunciado se llegó
al lenguaje Prolog, el cual logró una fama instantánea cuando el gobierno japonés lo utilizó para
el proyecto de quinta generación, para el desarrollo de sistemas de cómputo basados en técnicas
de razonamiento y comprensión de lenguaje humano. Pero tras el abandono del proyecto Prolog
ha ido perdiendo difusión a excepción del área de la comprensión del lenguaje natural y sistemas
expertos.

Actualmente Prolog sigue siendo el ejemplo más significativo de un lenguaje de programación


lógico, aunque ha surgido varias extensiones que se ha hecho populares: Programación lógica con
restricciones y sistemas basados en ecuaciones que utilizan ecuaciones en vez de la lógica para
describir la computación.

12.1 Lógica y programas lógicos.

El tipo de lógica que se utiliza en la programación lógica es el cálculo de predicados de primer


orden.

El cálculo de predicados de primer orden clasifica las distintas pares de estos enunciados de la
siguiente forma:

1. Constantes: números o nombres (átomos)


2. Predicados: nombres de funciones que son verdaderas o falsas.
3. Funciones: el cálculo de predicados distingue entre funciones que son verdaderas o falsas
(predicados) y todas las demás funciones (representan valores no booleanos)
4. Variables: representan cantidades todavía no especificadas.
5. Conectores: incluyen operaciones AND, OR, implicación ->, y equivalencia <->.
6. Cuantificadores: son operaciones que introducen variables (cuantificador universal y
existencial)
7. Símbolos de puntuación: incluyen paréntesis izquierdo y derecho, la coma y el punto.

Un lenguaje de programación lógico es un sistema de notación para escribir enunciados lógicos


junto con algoritmos especificados para implementar las reglas de inferencia.
El conjunto de enunciados lógicos que se toman como axiomas pueden considerarse como el
programa lógico.
En ocasiones los sistemas de programación lógica se conocen como bases de datos deductivas.

12.2 Cláusulas Horn.

Es un enunciado de la forma: a1 y a2 y a3 … y an -> b, donde los ai solo se les permite ser


enunciados simples sin involucrar conectores.
En las cláusulas Horn no existen conectores “o” ni cuantificadores.
La cláusula Horn dice que b es verdadero si todos los ai son verdaderos.

74
LENGUAJES DE PROGRAMACIÓN
Página 75 de 78
Autor: informatica_uned@[Link]

A b se le llama cabeza de la cláusula y a a1 y a2 y a3 … y an cuerpo de la cláusula, la cantidad de


a puede ser 0.
-> b , una cláusula como ésta significa que b siempre es verdadero, a estas cláusulas se les llama
hechos.
Las cláusulas Horn pueden utilizarse para expresar la mayoría, pero no la totalidad de los
enunciados lógicos.

12.3 Resolución y unificación.

La resolución es una regla de inferencia para cláusulas Horn que tiene una eficiencia especial. Si
tenemos dos cláusulas Horn, y podemos parear la cabeza de la primera cláusula Horn con uno de
los enunciados en el cuerpo de la segunda cláusula, entonces pueden utilizarse la primer cláusula
para reemplazar su cabeza en la segunda utilizando su cuerpo.

b <- a y c <- b, lo que nos da b,c <- a,b y cancelando b c <- a

La unificación consiste en hacer coincidir enunciados con variables a través de establecer las
variables iguales a los términos. Las variables que establecen iguales se dice que están
instanciadas. Para que esto se implante con efectividad hace falta un algoritmo para la
unificación.

Para lograr una ejecución eficiente, un sistema de programación lógica debe aplicar un algoritmo
fijo que especifique:

1- El orden de resolución de un conjunto de metas.


2- El orden en el cual se usarán las cláusulas para resolver metas.

Los sistemas de programación lógica que usan cláusulas Horn y resolución con ordenes
preespecificados para (1) y para (2) violan el principio básico que dichos sistemas pretenden
lograr: que un programador se preocupe de la lógica misma, pudiendo ignorar el control.

12.4 El lenguaje Prolog

Prolog es el lenguaje de programación lógica más utilizado. Prolog utiliza cláusulas Horn e
implementa la resolución utilizando una estrategia “primero en profundidad” estrictamente lineal y
un algoritmo de unificación.

Notación y estructuras de datos.

Prolog utiliza casi una notación idéntica a la desarrollada anteriormente para las cláusulas Horn,
excepto que la flecha de implicación “←” se reemplaza con dos puntos seguidos de un guión :-

Las variables se escriben con mayúsculas y las constantes y los nombres con minúsculas.
Las estructuras básicas de datos son términos como padre(X,Z), o bien sucesor(sucesor(0)).
Prolog también incluye listas como una estructura básica de datos.

Ejecución en Prolog.

Existen compiladores para Prolog, pero la mayoría de los sistemas se ejecutan como intérpretes.
Un programa Prolog incluye un juego de cláusulas Horn en la sintaxis de Prolog que generalmente
se toma de un archivo y se almacena en una base de datos de cláusulas mantenida
dinámicamente.

75
LENGUAJES DE PROGRAMACIÓN
Página 76 de 78
Autor: informatica_uned@[Link]

Aritmética.

Prolog tiene operaciones aritméticas incorporadas así como un evaluador aritmético.


Para obligar a la evaluación de un término aritmético se requiere de una nueva operación : el
predicado incorporado is: X is 3+5, write (X).

Unificación.

La unificación es el proceso mediante el cual se hace la instanciación de las variables o se les


asigna memoria y valores asignados, de manera que coincidan los patrones durante la resolución.
La unificación es el proceso para hacer en cierto sentido que dos términos se conviertan en “el
mismo”.

La expresión básica es la igualdad: s = t

Estrategia de búsqueda.

Prolog aplica la resolución de una forma estrictamente lineal, reemplazando metas de izquierda a
derecha y considerando las cláusulas en la base de datos en orden descendente, de arriba abajo.

Ciclos y estructuras de control.

Para que Prolog ejecute ciclos y búsquedas repetitivas podemos utilizar búsqueda primero en
profundidad con retroceso. Lo que debemos lograr es forzar el retroceso incluso cuando se haya
encontrado una solución.

12.5 Problemas que se presentan con la programación lógica.

La naturaleza de los algoritmos utilizaos en los lenguajes de programación lógica como la


resolución, la unificación o la búsqueda en profundidad primero, ha introducido algunos escollos
en lo específico a escribir programas.

Los problemas que analizaremos son:

1- El problema de “verificación de ocurrencias” en la unificación.


2- Problemas con la negación (el operador Not).
3- Las implicaciones de las cláusulas Horn.
4- La necesidad de control en un programa lógico.

12.5.1 El problema de la verificación de ocurrencias en la unificación.

El algoritmo de unificación de Prolog es de hecho erróneo: al unificar una variable con un término,
Prolog no verifica que esta variable ya esté presente en el término en el cual está siendo
instanciada.

12.5.2 Negación como fracaso.

Todos los sistemas de programación lógica tienen la propiedad básica de que algo que no pueda
ser probado como verdadero tiene que ser falso, a esto se le conoce como “suposición del
mundo cerrado”.

76
LENGUAJES DE PROGRAMACIÓN
Página 77 de 78
Autor: informatica_uned@[Link]

¿De que manera se puede implementar el operador not en la programación lógica?, la respuesta
sencilla es que la meta not(x) tiene éxito siempre que la meta x fracase. Esto es lo que quiere
decir “negación como fracaso”.
El hecho de que añadir información a un sistema puede reducir el conjunto de cosas que pueden
ser probadas se conoce como razonamiento no monótono, y es una consecuencia de la
suposición del mundo cerrado.

Un problema asociado es que el fracaso hace que se liberen la instanciaciones de la variables


mediante el regreso, por lo que después del fracaso es posible que la variable no tenga un valor
apropiado.

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

No todas las expresiones lógicas pueden representarse con cláusulas Horn, en concreto aquellas
que involucran cuantificadores posiblemente no podrán expresarse en formato de cláusulas Horn.

12.5.4 Información de control en la programación lógica.

Los programas Prolog también tienen información de control implícita que hacen que pueden
hacer que fracasen los programas.
Lo ideal sería que los sistemas de programación lógicos aceptaran la definición matemática de
una propiedad y este encuentre un algoritmo para calcularlo. Pero tanto en Prolog como en
cualquier otro sistema de programación lógica no sólo se le suministra las especificaciones de
nuestros programas si no también, hay que facilitar los mecanismos de control algorítmico.
Al especificar algoritmos secuenciales, por ejemplo ordenar, Prolog no se diferencia tanto como
podría pensarse de la programación imperativa.
Existe una ventaja de poder especificar un propiedad para su ejecución, incluso si no es aceptable
como algoritmo para el cálculo de dicha propiedad: nos permite probar si la especificación es
correcta.

12.6 Extensión de la programación lógica: programación lógica con restricciones y


sistemas basados en ecuaciones.

12.6.1 Programación lógica con restricciones.

Uno de los problemas más importantes en Prolog es el tratamiento especial de la aritmética: no se


aplica la aritmética si no hasta que se obliga mediante el operador “is”, y el uso de esta sentencia
hace que el programa se comporte más como un programa imperativo que como uno lógico, por
ejemplo ya no puede ejecutarse hacia a tras. Por otra parte las pruebas de desigualdad como X
<= 0, requieren que todas las variable estén instanciadas cuando se encuentre la prueba, o de lo
contrario fracasará.

Podrían mejorarse los programas que utilizan este tipo de cálculo si se eliminaran los requisitos de
instanciación y el operador “is”. Existe un buen número de lenguajes que hacen exactamente eso,
a estos lenguajes se les conoce como lenguajes de programación lógica con restricciones.

La razón de denominarlos “con restricciones” es que convierten la aritmética y las desigualdades


no instanciadas en restricciones que se van acumulando conforme avanza la ejecución del
programa, y entonces se resuelven mediante un revolvedor de restricciones para producir una
solución.

77
LENGUAJES DE PROGRAMACIÓN
Página 78 de 78
Autor: informatica_uned@[Link]

12.6.2 Sistemas basados en ecuaciones.

Una de las características principales de un lenguaje de programación lógica es que puede


utilizarse para escribir especificaciones ejecutables para un sistema de programación. Estas
especificaciones no sólo pueden ser probadas para verificar que son o no correctas si no que
también sirven para determinar los requerimientos del usuario.
Por lo que los lenguajes de programación lógica pueden ser utilizados como sistemas de
prototipado rápido para el desarrollo de software.

Prolog y otros sistemas de cláusulas Horn no aceptan las ecuaciones como reglas, por lo que una
especificación algebraica no puede ser interpretada directamente en Prolog. Sería recomendable
si las especificaciones basadas en ecuaciones pedieran escribirse directamente en un lenguaje de
programación lógica. Esto es lo que ha propiciado el desarrollo y estudio de los lenguajes de
programación de lógica basada en ecuaciones.

Los sistemas de programación lógica basado en ecuaciones permiten la interpretación de los


axiomas que definen un tipo de datos abstracto, si ninguna suposición subyacente con respecto a
la representación de dichos tipos de datos.
Estos sistemas permiten probar directamente las especificaciones de un tipo de datos en lo que se
refiere a la consistencia y su capacidad de aplicación para un propósito en particular.

78

También podría gustarte