Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Ej.: int x;
Estructuradas:
o La estructura de los datos es el método principal para la abstracción de colecciones de valores de datos
relacionados entre sí.
Ej.: registros, arreglos, tipos estructurados.
Unitarias:
o Incluyen al encapsulado de datos y la ocultación de la información, a menudo relacionado con los tipos de datos
abstractos.
Ej: paquete, clase.
Estructuradas:
o Dividen un programa en grupos de instrucciones que están anidadas.
o Una ventaja es que se pueden anidar estas estructuras dentro de otras estructuras de control.
o Los mecanismos de bucle o ciclos estructurados se presentan de muchas formas, incluyendo los ciclos while, for
y do de C y C++, los ciclos repeat de Pascal y el enunciado loop de Ada.
o Otro mecanismo útil es el procedimiento (subrutina o subprograma).
o Se definirse un procedimiento dándole un nombre y asociándolo con las acciones que se van a llevar a cabo
(declaración de procedimiento).
o El procedimiento debe ser llamado (invocación o activación) en el punto en que las acciones deben ejecutarse.
o Una función puede considerarse un procedimiento pero que devuelve un valor o resultado a su invocador.
o En C y C++ a los procedimientos se les llama funciones nulas.
Unitarias:
o Consiste en efectuar abstracciones de control con la finalidad de incluir una colección de procedimientos que
proporcionan servicios relacionados lógicamente con otras partes del programa y que forman una parte
unitaria, o independiente, del programa.
o Lo descrito anteriormente es en esencia lo mismo que una abstracción de datos de nivel unitario. La única
diferencia consiste en que aquí el enfoque está en las operaciones más que en los datos.
o Un tipo de abstracción de control que resulta difícil de clasificar son los mecanismos de programación en
paralelo, por ejemplo, las tareas (task) de ADA son una abstracción unitaria, pero los hilos o hebras de Java se
consideran estructuradas.
Programación lógica:
o Basado en la lógica simbólica.
o El programa está formado por un conjunto de enunciados que describen lo que es verdad con respecto a un
resultado deseado, en oposición a dar una secuencia particular de enunciados que deben ser ejecutados en un
orden fijo para producir el resultado.
o No tienen necesidad de abstracciones de control como ciclos o selección.
o También se le conoce como programación declarativa o lenguajes de muy alto nivel. Las variables no
representan localizaciones de memoria sino que se comportan más como nombres para los resultados de las
computaciones parciales.
o Un lenguaje importante es Prolog.
Es necesario resaltar, que aunque un LP pudiera tener la mayor parte de las propiedades de uno de los anteriores
paradigmas, contienen generalmente características de varios.
La sintaxis de casi todos los lenguajes está dada ahora utilizando gramáticas libres de contexto:
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.
2.3. Regularidad
Es una cualidad algo mal definida de un lenguaje que expresa lo bien que están integradas las características del mismo.
Una mayor regularidad implica:
o Pocas restricciones en el uso de constructores.
o Menos interacciones raras entre dichos constructores.
o En general, menos sorpresas en la forma en que se comportan las características del lenguaje.
o Uniformidad:
Las cosas similares deben verse de forma similar y tener significados similares, y a la inversa, las
cosas diferentes se tienen que ver de forma diferente.
Las no uniformidades son de dos tipos:
Cosas similares no parecen ser o se no comportan de manera similar.
Cosas no similares, parecen ser o se comportan de manera similar.
Ejemplo de violación de este concepto puede ser:
En C++ es necesario un punto y coma después de la definición de clase, pero está prohibido
después de la definición de una función.
class A { ... };
int f() { ... }
El afán por hacer imponer una meta, como puedan ser la generalidad o la ortogonalidad en el diseño de un lenguaje,
puede resultar peligroso.
Si una no regularidad no puede justificarse de forma razonable entonces se puede decir que es un error de diseño.
while(*s++ == *t++);
Extensibilidad:
o Principio que indica que debería de haber algún mecanismo general que permita al usuario añadir nuevas
características al lenguaje.
o Podría significar simplemente el añadir:
Nuevos tipos al lenguaje.
Nuevas funciones a una biblioteca.
Nuevas palabras claves.
o A lo largo de los últimos 10 años la extensibilidad ha pasado a ser de importancia primordial como una
propiedad de los lenguajes. En concreto la simplicidad sin extensibilidad, prácticamente tiene garantizado el
fracaso del lenguaje.
Capacidad de restricción:
o Un diseño de lenguaje debería dar la posibilidad de que un programador pudiera programar de una forma útil
empleando un conocimiento mínimo del lenguaje, por lo que el diseño de un lenguaje debería promover la
capacidad de definir subconjuntos del lenguaje.
o Esto resulta útil de dos maneras:
No es necesario que el programador aprenda todo el lenguaje para utilizarlo con efectividad.
Un escritor de compilador podría elegir implementar únicamente un subconjunto en el caso de que la
implementación de todo lenguaje resultara demasiado costosa e innecesaria.
DO 99 I = 1.10
Pudiera parecer que se quiere ejecutar 99 veces la asignación del valor 1.10 a la variable I, cuando
en realidad lo que se hace es asignar el valor 1.10 a la variable DO99I.
Precisión:
o A veces conocida como claridad.
o Es la existencia de una definición precisa para un lenguaje, de tal manera que el comportamiento de los
programas pueda ser predecible.
o Pasos para lograrlo:
Publicación de un manual o informe del lenguaje por parte del diseñador.
Adopción de un estándar emitido por una organización nacional o internacional de estándares:
ANSI.
ISO.
…
Seguridad:
o Este principio se basa en minimizar los errores de programación y permitir que dichos errores sean detectados e
informados.
o La seguridad está íntimamente relacionada con la confiabilidad y con la precisión.
o Este es el principio que condujo a los diseñadores del lenguaje a introducir los tipos, la verificación de tipos y las
declaraciones de variables en los lenguajes de programación.
o Con esto se puede comprometer tanto la expresividad como lo conciso del lenguaje.
Existe suficiente razón para estudiar la programación funcional incluso si uno no espera jamás escribir aplicaciones reales en
un lenguaje funcional, y que los métodos funcionales como la recursión, la abstracción funcional y las funciones de orden
superior han influido y/o se han convertido en parte de la mayoría de los lenguajes de programación y deberían formar parte
del conjunto de técnicas de todo programador profesional.
El punto de vista funcional de la programación elimina las variables (como apuntadores de memoria) y por lo tanto la
asignación, a esto se le define como programación funcional pura.
Una consecuencia de la programación funcional pura, es que no pueden existir ciclos (bucles), esto es solventado gracias
a la recursividad.
Otra consecuencia de la falta de variables es que no existe el concepto de estado interno de la función, por lo tanto, el
valor de cualquier función depende solo de los valores de sus argumentos.
La propiedad de una función de que su valor sólo dependa de los valores de los argumentos (y de sus constantes no
locales) se conoce como transparencia referencial.
Tomemos como ejemplo la función rand, que no depende sólo de los argumento, sino del estado de la máquina y de
las llamadas anteriores a sí misma, por lo tanto nunca podrá ser transparente referencialmente.
Como consecuencia una función transparente referencialmente que no tenga argumentos siempre devuelve el mismo
valor, por lo que no se diferenciaría de una constante.
La carencia de asignación y la transparencia referencial de la programación funcional hacen que los programas
Estas reglas permiten que las operaciones sobre listas largas calculen únicamente la porción de la lista que resulte
necesaria.
También es posible incluir listas potencialmente infinitas en leguajes con evaluación perezosa ya que la parte “infinita”
jamás será calculada, sino solo lo necesario de la lista como se requiera para un cálculo en particular.
Para expresar la naturaleza potencialmente infinita de este tipo de listas, aquella que obedecen a las reglas de evaluación
perezosa a menudo se llaman flujos.
Un flujo puede considerarse como una lista parcialmente calculada donde los elementos restantes pueden seguir siendo
calculados hasta cualquier número deseado.
Los flujos son un tema importante en la programación funcional.
El ejemplo principal de un lenguaje funcional con evaluación perezosa es Haskell.
La evaluación perezosa permite un estilo de programación funcional que nos da la libertad de separar un cómputo en
partes, formadas por procedimientos que generan flujos (generadores) y otros procedimientos que aceptan flujos (filtros), y
a este tipo de programación lo llamaremos programación de generadores y filtros.
fact 0 = 1
fact n = n * fact ( n - 1 )
suma: Z → Z → Z
Esto se interpreta como, suma es una función que recibe un entero, seguido de otro entero y devuelve la
suma de ambos.
Esto se deduce de la notación utilizada para dar el tipo de la función Z → Z → Z.
Esta forma de denotar los tipos de las funciones se denomina currificada.
La notación currificada no es simplemente una notación, agrega poder de expresividad a los tipos de las
funciones.
El símbolo → (que se lee “implica”), asocia hacia la derecha, esto significa que las dos expresiones
siguientes son equivalentes:
suma: Z → Z → Z
suma: Z → (Z → Z)
Por lo tanto se puede decir también que suma es una función que recibe un entero y devuelve una función
que a su vez, recibe un entero y devuelve un entero.
Todos los operadores binarios predefinidos están en forma Curry y pueden ser aplicados parcialmente a
cualquier argumento utilizando paréntesis (esto se conoce en Haskell como una sección):
plus2 = (2 + )
> plus2 3
5
Haskell tiene funciones anónimas (explesiones lambda), las cuales representan a “lambda” con una diagonal invertida:
> (\ x -> x * x ) 3
9
> ( ( * 3 ) . ( 2 + ) ) 5
21
Haskell tiene listas y tuplas incorporadas, así como sinónimos de tipo y tipos polimórficos definidos por el usuario:
Los nombres de tipos y de constructores deben escribirse en mayúsculas, en tanto que los nombres de funciones y de
valores deben aparecer en minúsculas.
Haskell tiene un sistema de notación especial para operaciones aplicadas sobre listas, conocido como comprensión de
lista, mismo que está diseñado para tener una apariencia de definiciones de conjuntos matemáticos:
Para una lista potencialmente infinita, resultan útiles las funciones que calculan la lista en forma parcial:
o take: extrae los primeros n elementos.
o drop: descarta los primeros n elementos.
Cuando una función puede utilizarse con diferentes tipos de argumentos se dice que está sobrecargada. La función +, por
ejemplo, puede utilizarse para sumar enteros o para sumar reales. La resolución de la sobrecarga por parte del sistema
Haskell se basa en organizar los diferentes tipos en lo que se denominan clases de tipos (un conjunto de tipos que definen
ciertas funciones).
Consideremos el operador de comparación ==. Existen muchos tipos cuyos elementos pueden ser comparables, sin
embargo, los elementos de otros tipos podrían no ser comparables. Por ejemplo, comparar la igualdad de dos funciones es
una tarea computacionalmente intratable, mientras que a menudo se desea comparar si dos listas son iguales.
Las clases de tipos solucionan ese problema permitiendo declarar qué tipos son instancias de unas clases determinadas y
proporcionando definiciones de ciertas operaciones asociadas con cada clase de tipos:
class Eq a where
(==), (/=), :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)
Naturalmente, existen clases de tipos adicionales que requieren o dependen de otras funciones. Esta dependencia de las
clases de tipo sobre otras se llama herencia de clase de tipo, y establece una jerarquía de éstas.
Para establecer los requerimientos de herencia para cada clase de tipo, Haskell utiliza una notación similar a la calificación
de tipo.
class (Ea a, Show a) => Num a where ... class (Eq a) => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a
Como ejemplo de lo anterior, tomemos una instantánea de la jerarquía de clases de tipos numéricos en Haskell:
Considerar una función como un conjunto tiene ciertas ventajas para el estudio de la definición de función en los
lenguajes de programación:
Este método de definición de conjunto a veces se conoce como definición por extensión.
Sin embargo, la definición más común de una función se hace mediante una fórmula o propiedad, conocida como
definición por comprensión:
Importante, de aquí en adelante las definiciones dadas no son del texto base, principalmente debido a que en mi
opinión, este apartado del texto base confunde más que aporta.
En la definición de una función se pueden usar las funciones estándar y las funciones definidas por el usuario. Pero
también se puede usar la propia función que se define en su definición. A tal definición se la llama definición recursiva.
En Haskell no tenemos posibilidad de definir bucles. La forma de iterar es utilizando la recursividad.
La recursividad se apoya en el principio de inducción. Este principio es ampliamente utilizado en matemáticas para
demostrar que se cumple una propiedad para cualquier valor del ámbito que se esté tratando.
Principio de inducción:
o La afirmación es cierta para un valor inicial (caso base).
o será cierta para un valor , si es cierta para el valor anterior a , es decir, si es cierta para
entonces lo será para .
Como ejemplo, podemos utilizar el principio de inducción para definir los números naturales:
o El número es natural.
o es natural si es natural.
En Haskel:
natural 1 = True
natural n = natural (n-1)
> natural 5
True
Aunque aparentemente el principio de inducción funciona, no se ha tenido en cuenta que, el valor tiene que ser mayor
que el caso base. Por lo tanto si lo intentásemos con el valor de -1, la definición anterior nunca terminaría.
natural 1 = True
natural n
| n > 1 = natural (n-1)
| otherwise = False
Recomendación importante: No seguiremos mentalmente la secuencia de llamadas recursivas para resolver los
problemas. Nos centraremos en definir bien el caso base (el primer elemento que cumple la propiedad) y la relación de un
valor con el anterior.
Enunciado Traducción
0 es un nº natural natural(0)
2 es un nº natural natural(2)
Para toda , si es nº natural, entonces también lo es el sucesor de Para todas , natural( ) natural(sucesor( ))
-1 es un nº natural natural(-1)
Entre estos enunciados lógicos, el primer y tercer enunciado pueden considerarse como axiomas para los números
naturales: enunciados que se suponen verdaderos y a partir de los cuales todos los enunciados verdaderos con respecto
a los nos naturales pueden probarse.
El cálculo de predicados de primer orden clasifica las distintas partes de estos enunciados de la siguiente forma:
1. Constantes (o átomos):
Éstos son por lo general número o nombres.
Ej.: 0.
2. Predicados:
Éstos son nombres de funciones que son verdaderas o falsas. Pueden tomar varios argumentos.
Ej.: natural(0).
3. Funciones:
El cálculo de predicados distingue entre funciones que son verdaderas o falsa (predicados), y todas las
demás funciones.
Ej.: sucesor( ).
4. Variables que representan cantidades todavía no especificadas:
Ej.: .
5. Conectores:
Éstos incluyen las operaciones , , , , .
6. Cuantificadores:
Éstas son operaciones que introducen variables, cuantificadores universal y existencial.
Ej.: Para todas …, Existe un …
7. Símbolos de puntuación:
Éstos incluyen paréntesis izquierdo, derecho, la coma y el punto.
Aunando a las siete clases de símbolos que se han descrito, el cálculo de predicados de primer orden tiene reglas de
inferencia: formas de derivar o de probar nuevos enunciados a partir de un conjunto dado de enunciados.
Ej.: Dado los enunciados podemos derivar .
Esta es la esencia de la programación lógica: una colección de enunciados que se supone son axiomas, y a partir de
ellos se deriva un hecho deseado mediante la aplicación de reglas de inferencia de alguna manera automatizada.
Por lo tanto, un lenguaje de programación lógico es un sistema de notación para escribir enunciados lógicos junto con
algoritmos especificados para implementar las reglas de inferencia.
El conjunto de enunciados lógicos que se toman como axiomas pueden construirse como el programa lógico, y el
enunciado o enunciados que vayan a derivarse pueden considerarse como la entrada que inicia el cómputo. Estas
entradas son proporcionadas también por el programador y se llaman consultas o metas.
Por esta razón, en ocasiones los sistemas de programación lógica se conocen como bases de datos deductivas, bases
de datos formadas por un conjunto de enunciados y por un sistema de deducción que puede responder a las consultas.
La secuencia de pasos que elige un sistema automático de deducción para derivar un enunciado es el problema de
control para un sistema de programación lógica.
Los enunciados orinales representan la lógica del cómputo, en tanto que el sistema deductivo proporciona el control con
el cual se deriva un enunciado nuevo. Esta propiedad de los sistemas de programación lógica llevo a Kowalski a plantear
el paradigma de acuerdo con la pseudoecuación:
Lo que contrasta con la expresión dada por Niklaus Wirth de la programación imperativa:
Donde a los sólo se les permite se enunciados simples sin involucrar conectores.
A se le llama cabeza de la cláusula, y a el cuerpo de la cláusula.
En la cláusula Horn, la cantidad de puede ser 0, en cuyo caso la cláusula Horn tiene esta forma:
Una cláusula como esta significa que es siempre verdadero, esto es, que es un axioma y por lo general se escribe
sin el conector . En ocasiones a estas cláusulas se les llama hechos.
Las cláusulas Horn pueden utilizarse para expresar la mayoría, pero no la totalidad, de los enunciados lógicos.
La idea básica es eliminar conectores al describir cláusulas por separado, y tratar la carencia de cuantificadores
suponiendo que las variables que aparecen en la cabeza de una cláusula están universalmente cuantificadas, en tanto
que las variables que aparecen en el cuerpo de una cláusula están existencialmente cuantificadas.
Ej.: Enunciado lógico traducción al cálculo de predicados cláusulas Horn
En general, mientras más conectores aparezcan a la derecha de un conector en un enunciado, más difícil será
traducirlo a un conjunto de cláusulas Horn.
Las cláusulas Horn son de interés particular para los sistemas automáticos de deducción, por ejemplo, los sistemas de
programación lógica, ya que puede dárseles una interpretación procedimental.
Si escribimos una cláusula Horn en orden inverso:
podemos considerar lo anterior como una definición de procedimiento b. Esto es muy similar a la forma en la que se
interpretan reglas gramaticales libre de contexto como definiciones de procedimientos en el análisis sintáctico recursivo
descendente.
La mayoría de los sistemas de programación lógica no solamente escriben cláusulas Horn hacia atrás, sino también
cambia los conectores existentes en el cuerpo por comas.
Ahora se puede ver la forma en la que un sistema de programación lógica puede tratar una meta o una lista de metas
como si fueran una cláusula Horn sin cabeza.
El sistema intentará aplicar la resolución al parear una de las metas en el cuerpo de la cláusula sin cabeza con la cabeza
de una cláusula conocida. Acto seguido, reemplaza la meta pareada con el cuerpo de dicha cláusula, creando así una
nueva lista de metas, que sigue modificándose al utilizar el mismo método.
Las nuevas metas se conocen como submetas.
Si el sistema tiene éxito finalmente en eliminar todas las metas, derivando por lo tanto la cláusula Horn vacía, entonces
se ha comprobado el enunciado.
Como se ha podido ver, para hacer coincidir enunciados que contienen variables, debemos establecer las variables
iguales a los términos de modo que los enunciados se hagan idénticos y puedan ser cancelados de ambos lados.
Este proceso de pareamiento de patrones para hacer que los enunciados sean idénticos se llama unificación, y las
variables que se establecen iguales a patrones se dice que están instanciadas.
Para implementar la resolución con efectividad se debe también proporcionar una algoritmo para la unificación ( ver
inferencia de tipos Hindley-Milner, tema 6).
Para lograr una ejecución eficiente, un sistema de programación lógica debe aplicar un algoritmo fijo que especifique:
1. El orden en el cual el sistema intenta resolver una lista de metas.
2. El orden en el cual se usarán las cláusulas para resolver metas.
Por tanto, el orden en el cual se utilizan las cláusulas también puede tener un efecto importante en el resultado de aplicar
la resolución (repasar problema del ancestro pág. 504).
Los sistemas de programación lógica que usan cláusulas Horn y resolución con órdenes preespecificados para 1 y 2,
violan el principio básico que dichos sistemas se proponen lograr (un programador sólo debe preocuparse de la lógica
misma, pudiendo ignorar el control).
Prolog tiene varios predicados estándar que siempre están incorporados como not, =, call, append, …
Como se puede observar, en la última consulta existen varias respuestas. La mayoría de los sistemas Prolog encontrarán
una respuesta y esperarán la indicación del usuario antes de imprimir más respuestas. Si el usuario suministra un punto y
coma ( lógico), entonces Prolog continuará encontrando más respuesta.
4.4.3 Aritmética
Prolog tiene operaciones aritméticas incorporadas así como un evaluador aritmético.
Los términos aritméticos pueden escribirse en notación infija usual o prefija +(3, 4).
Para forzar la evaluación de un término aritmético se requiere de una nueva operación, con el predicado incorporado is:
Como se observa en la consulta 3, dos términos aritméticos pueden no ser iguales como términos aunque tengan el
mismo valor, por esa razón el operador anterior fuerza la evaluación aritmética, obteniendo el resultado deseado, como
en la consulta 2.
2. Una variable que no está instanciada se unifica con cualquier cosa y se hace instanciada a dicha cosa:
Como se puede observar, en la consulta 3, la unificación hace que las variables no instanciadas
compartan memoria, es decir, que se conviertan en alias una de la otra.
3. Un término estructurado, es decir, una función aplicada a argumentos, se unifica con otro término sólo
si se tiene el mismo número de argumentos y los argumentos pueden unificarse recursivamente:
Se puede utilizar la unificación en Prolog para obtener expresiones muy cortas para muchas operaciones. Se tomará
como ejemplo la operación cons de LISP, escribiendo una clausula como sigue:
A tener en cuenta:
1. Se pueden utilizar variables en un término como parámetro de entrada o de salida y las clausulas
Prolog pueden ejecutarse hacia atrás igual que hacia delante ¡¡¡algo que la interpretación
procedimental de las cláusulas Horn nunca informó!!!
2. La presencia del patrón [X|Y] como tercera variable, automáticamente se unifica con una variable
utilizada en dicho lugar en una meta. Este proceso podría identificarse como invocación dirigida por
patrones.
?- menu(paella, X, naranja)
1 5
6
3
5 8
4 7
Sin embargo, es posible que el uso de fail en determinadas circunstancias nos introduzca en ciclos infinitos.
Por tanto, lo que se requiere es alguna forma de detener la búsqueda para que no continúe a través de todo el árbol.
Prolog tiene para ello el operador cut, que generalmente se escribe con el signo de exclamación.
cut congela la elección que se haga cuando ésta se encuentre. Si se llega a un cut en retroceso, cut poda el árbol de
búsqueda de todos los parientes hacia la derecha del nodo que contiene el cut:
cut, también puede ser utilizado para imitar construcciones if then else en lenguajes imperativos y funcionales.
Para escribir un cláusula como:
Esto último es correcto, incluso en ausencia de cualquier otra clausula para f(X).
Prolog no tiene una verificación para evitar estas ocurrencias por cuestiones de eficiencia.
Esto se conoce como la suposición del mundo cerrado (CWA: Closed World Assumption).
La negación lógica () no está implementada en Prolog, en su lugar, define un operador not, el cual se asocia con la
falta de una afirmación, es decir, la meta not(X) tiene éxito siempre que la meta X fracase (negociación como fracaso):
El segundo par de metas fracasa porque X está instanciada a 1 para hacer que X = 1 tenga éxito y entonces,
not(X=1) fracasa. Jamás se alcanza la meta X = 0.
Podemos observar, como Prolog intenta instanciar la variable Z para que ancestor sea verdadero, creando un
descenso infinito en el árbol de búsqueda que usa la primera clausula.
En el mejor de todos los mundos posibles, se desearía que un sistema de programación lógica acepte la definición
matemática de una propiedad y encuentre un algoritmo eficiente para calcularlo.
Naturalmente en Prolog o en cualquier otro sistema de programación lógica no sólo suministramos especificaciones en
nuestro programas, sino que debemos también proveer la información de control algorítmica.
if, while
o Literales o constantes:
o Símbolos especiales:
o Identificadores:
x24 o monthly_balance
Se llaman palabras reservadas porque un identificador no puede tener la misma cadena de caracteres que una palabra
reservada.
A veces existe confusión en un lenguaje entre las palabras reservadas y los identificadores predefinidos (aquellos a los
cuales se les ha dado una interpretación inicial para todos los programas en el lenguaje, pero pueden ser capaces de redefinición,
como pueden ser integer o boolean).
En algunos lenguajes los identificadores tienen un tamaño máximo fijo, en tanto que en la mayoría de los lenguajes los
identificadores pueden tener una longitud arbitraria.
Los identificadores de longitud variable pueden presentar problemas con las palabras reservadas, esto es, el identificador
doif puede verse como un simple identificador o como dos palabras reservadas do e if. Por ello, se utiliza el principio de la
subcadena de mayor longitud o principio de trozo máximo.
Este principio es una regla estándar en la determinación de tokens, en cada punto se reúne en un solo token la cadena
más larga posible de caracteres, requiriendo que ciertos tokens vengan separados por delimitadores de tokens o
espacios en blanco.
Esto significa que doif sería tratado como un único token y por tanto un identificador.
El formato de un programa puede afectar la forma en la que se recogen los tokens:
o Formato fijo:
Los tokens deben aparecer en localizaciones preespecificadas sobre la página (FORTRAN).
(a|b)*c
A menudo, la notación de expresiones regulares se amplía mediante operaciones adicionales y caracteres especiales
como:
o [ - ] Indican rango de caracteres.
o + Indica una o más repeticiones.
o ? Indica un elemento opcional, es decir, que la expresión a la que sigue, aparece como mucho una vez:
ob?curo obscuro
oscuro
[0-9]+(\.[0-9]+)?
Una gramática libre de contexto consiste en un conjunto de reglas gramaticales, cada una de las cuales están formadas
de:
o Un lado izquierdo que es un solo nombre de estructura y a continuación el meta símbolo (a veces se
reemplaza por “:=”).
o Un lado derecho formado por una secuencia de elementos que pueden ser símbolos u otros nombres de
estructuras.
Las cursivas sirven para distinguir los nombres de las estructuras de las palabras reales, es decir, de los tokens que
pudieran aparecer en el lenguaje (sees).
La barra vertical | es también un metasímbolo y significa “o”.
Algunas veces un metasímbolo es un símbolo real en un lenguaje, en cuyo caso, es recomendable entrecomillar el
símbolo para distinguirlo del metasímbolo, o de lo contrario, el metasímbolo puede escribirse en un tipo de letra diferente.
Algunas notaciones también se apoyan en metasímbolos (<, >), en cuyo caso también se reemplaza la flecha por el
metasímbolo (::=):
Para indicar que una oración debe estar seguida por algún tipo de marcador final, se hace mediante el signo $:
entrada oración $
E=E+E N
Las soluciones a ecuaciones recursivas aparecen frecuentemente en las descripciones formales en los lenguajes de
programación donde se conocen como puntos fijos mínimos.
Expresión +
Expresión + Expresión 3 *
Dígito Dígito
3
4 5
Para el programador los árboles de sintaxis abstractas no son importantes (algunas veces la sintaxis ordinaria se distingue de
la sintaxis abstracta por el nombre de sintaxis concreta).
No ocurre así con los diseñadores del lenguaje y para los autores de traductores, ya que es la sintaxis abstracta y no la
concreta la que expresa la estructura esencial del lenguaje.
Sin embargo, diferentes derivaciones también pueden conducir a diferentes árboles de análisis gramaticales:
4 5 3 4
3+4*5 3+4*5
Una gramática para la cual sean posibles dos árboles diferentes para un mismo análisis sintáctico se dice que es
ambigua.
Ciertas derivaciones construidas en un orden especial corresponden al mismo árbol de análisis sintáctico.
Una derivación que tienes esta propiedad es la derivación por la izquierda, que consiste en tomar como reemplazo el no
terminal restante más a la izquierda.
Una forma de determinar si una gramática es ambigua es buscar dos derivaciones por la izquierda distintas de una
misma cadena.
Una gramática para que sea útil no puede ser ambigua, por lo que si lo fuera habría que aplicarle alguna regla para
eliminar dicha ambigüedad.
Para eliminar dicha ambigüedad se debe revisar la gramática o incluir nuevas reglas que eliminen la ambigüedad.
La forma más habitual de revisar este tipo de gramáticas es escribiendo una nueva regla gramatical (llamada un
“término”) que establece una “cascada de precedencia”.
Por ejemplo, la siguiente gramática es ambigua:
Algunas veces el proceso de volver a escribir una gramática para eliminar ambigüedad hace que sea extremadamente
compleja, y en estos casos preferimos enunciar una regla de no ambigüedad.
6 Longinos Recuero Bustos (http://longinox.blogspot.com)
5.5 EBNF y diagramas sintácticos
EBNF (Backus-Naur extendida) surge para dotar al BNF de una notación especial para el tipo de reglas gramaticales que
expresan con mayor claridad la naturaleza repetitiva de su estructura:
Una representación gráfica a veces es útil para una regla gramatical es el diagrama sintáctico, el cual refleja la
secuencia de terminales y no terminales que se encuentran en el lado derecho de la regla.
Estos diagramas:
o Utilizan círculos u óvalos para representar los terminales y rectángulos para representar los no terminales,
conectándolos entre sí mediante líneas y flechas con el fin de indicar la secuencia apropiada.
o Pueden condesar varias reglas gramaticales en un solo diagrama.
o Se escriben siempre partiendo de una notación EBNF, y nunca de una BNF.
o Son atractivos visualmente pero ocupan mucho espacio por lo que actualmente son poco utilizados a favor de
las notaciones EBNF y BNF.
El analizador de abajo arriba es más poderoso que el otro, por lo que es más utilizado generalmente por los
generadores de analizadores sintácticos (o compiladores de compiladores).
Un generador ampliamente utilizado es el YACC o su versión libre Bison.
Otro método más antiguo de general analizadores a partir de su gramática que resulta muy efectivo, es el análisis
sintáctico por descenso recursivo:
o El segundo error es que no hay forma de determinar que opción tomar, si expresión + término o término, por lo
tato para seguir manteniendo la asociatividad por la izquierda y eliminar la recursividad, utilizamos la notación
EBNF en la que las llaves representan la eliminación de la recursión por la izquierda:
En el caso de la recursividad por la derecha no se presenta el problema señalado anteriormente para el análisis recursivo
descendente, sin embargo se da la situación de que el código para una regla de recursión por la derecha:
Por lo tanto en situaciones de recursión por la izquierda y en la factorización por la izquierda, las reglas EBNF o los
diagramas sintácticos corresponden con el código de un analizador sintáctico por descenso recursivo, siendo esta una de
las razones de amplia utilización.
Un analizador sintáctico que basa su acción únicamente en el token disponible en el flujo de entrada, se conoce como
analizador sintáctico predictivo.
Este uso de un solo token para dirigir la acción del analizador sintáctico se conoce con el nombre preanálisis de un
solo símbolo.
Estos analizadores requieren que las gramáticas a analizar cumplan ciertas condiciones:
o La primera condición que requiere es la capacidad de escoger entre varias alternativas en una regla gramatical:
A | | |…|
Para decidir cual elegir, los tokens que inician cada tienen que ser distintos:
donde Primero es la función que devuelve el conjunto de tokens que pueden presentarse al principio de cada .
o La segunda condición se presenta con las reglas gramaticales que contienen estructuras opcionales:
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( ) =
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.
de programación.
o Léxico:
3. m. Vocabulario, conjunto de las palabras de un idioma, o de las que pertenecen al uso de una región,
Una gramática libre de contexto típicamente incluye una descripción de los tokens de un lenguaje al incluir en las reglas
gramaticales las cadenas de caracteres que forman los tokens.
Algunas clases típicas de tokens, como las literales o constantes y los identificadores no son por sí mismos secuencias
fijas de caracteres, sino que se elaboran a partir de un conjunto fijo de caracteres, como los dígitos del 0 al 9.
Estas clases de tokens pueden tener su estructura definida por la gramática, sin embargo, es posible e incluso deseable
utilizar un analizador léxico para reconocer estas estructuras, pues puede hacerlo mediante una operación repetitiva
simple.
Los límites entre la sintaxis y el léxico no están claramente definidos, ya que si utilizamos la notación BNF, EBNF o
diagramas sintácticos, se pueden incluir las estructuras léxicas como parte de la descripción del lenguaje.
Se ha definido la sintaxis como todo lo que se puede definir mediante una gramática libre de contexto, y semántica como
todo lo demás que no se puede definir así.
Algunos autores incluyen como sintaxis, la declaración de variables antes de que sean utilizadas y la regla de que no se
pueden declarar identificadores dentro de un procedimiento. Estas son reglas sensibles al contexto y por lo tanto un
analizador sintáctico debería tener información sobre el contexto.
Otro conflicto es cuando un leguaje requiere que ciertas cadenas sean identificadores predefinidos y no palabras
reservadas.
La diferencia de ambos es que las palabras reservadas son cadenas fijas de caracteres, que son tokens ellas mismas y
que no pueden utilizarse como identificadores, mientras que identificadores predefinidos son cadenas fijas de caracteres
a las que se les ha dado un significado predefinido en el lenguaje, pero este significado puede ser redefinido.
Por estas razones, el analizador sintáctico debería tener información del contexto sobre los identificadores disponibles a
fin de eliminar ambigüedades.
La especificación de la semántica de un LP es una tarea más difícil que la especificación de su sintaxis, como podríamos
esperar cuando hablamos de significado en contraposición a forma o estructura.
El significado de un nombre queda determinado por las propiedades o atributos asociados con el mismo:
const int n = 5; Asocia al nombre n el atributo de tipo de datos "constante entera" y el atributo de valor 5.
double f( int n ) Asocia el atributo “función” al nombre f y los siguientes atributos:
{
...
1. Cantidad de parámetros, nombres y tipos.
} 2. Tipo de datos del valor devuelto.
3. Cuerpo del código cuando se llama a la función.
Los nombres pueden vincularse con atributos aun antes del tiempo de compilación, como por ejemplo los
identificadores predefinidos como los tipos de datos boolean y char.
Por lo anterior tenemos los siguientes tiempos de ligadura para los atributos de nombres:
Todos los tiempos anteriores representan ligaduras estáticas excepto el último que representa ligaduras dinámicas.
El compilador debe conservar las ligaduras de tal manera que se den significados apropiados durante la compilación y
la ejecución.
Esto se lleva a cabo mediante una estructura de datos que de una manera abstracta puede verse como una función que
expresa la ligadura de los atributos a los nombres.
Esta función es parte fundamental de la semántica y se conoce como tabla de símbolos.
Esta función cambiará durante el proceso de compilación y/o ejecución para reflejar adiciones o eliminaciones de
ligaduras.
Existe una diferencia fundamental entre la forma en la que se mantiene una tabla de símbolos a través de un:
o Compilador:
Puede, por definición, procesar únicamente atributos estáticos.
La tabla de símbolos se ilustra como:
Tabla de símbolos
Nombres Atributos estáticos
Durante la ejecución de un programa compilado, deben mantenerse ciertos atributos como por ejemplo
las localizaciones en memoria y los valores.
Un compilador genera código que conserva estos atributos durante la ejecución.
La parte de asignación de memoria en este proceso, es decir, la ligadura de nombres a localizaciones,
se denomina entorno:
Entorno
Nombres Localizaciones
Las ligaduras de las localizaciones de almacenamiento junto con los valores se conocen como la
memoria, almacén o estado:
Memoria
Localizaciones Valores
o Intérprete:
En un intérprete, se combinan la tabla de símbolos y el entorno, ya que durante la ejecución se
procesan atributos estáticos y dinámicos. Por lo general, en esta función también se incluye la
memoria:
Entorno Atributos (incluyendo
Nombres localizaciones y valores)
Aquellos lenguajes que tienen declaración implícita, generalmente tienen reglas convencionales de nombre para
establecer otros atributos:
o FORTRAN:
Todas las variables no explícitamente declaradas se supone que son enteros, si sus nombres
empiezan con I, J, K, L, M, N.
Las declaraciones que vinculan ciertos atributos se conocen como definiciones, mientras que aquellas que sólo
especifican parcialmente los atributos se conocen simplemente como declaraciones:
Las declaraciones están asociadas tanto sintácticamente como semánticamente con ciertos constructores del lenguaje
como:
o Bloque:
Consiste en una secuencia de declaraciones seguidas por una secuencia de enunciados, rodeado por
marcadores sintácticos como son las llaves o los pares begin-end.
Las declaraciones que se encuentran dentro del bloque se conocen como locales y las que se
encuentran fuera de cualquier enunciado compuesto se llaman no locales (globales o externas).
/* Declaraciones no locales */
int x;
double y;
main()
{
/* Declaraciones locales */
int i, j;
…
}
struct
{
int* x; /* Declaraciones de miembros
char y; anidadas */
} z;
};
o Agrupaciones grandes:
Las declaraciones pueden reunirse en grupos más grandes como una manera de organizar los
programas y de obtener un comportamiento especial:
Paquetes y tareas en Ada.
Paquetes en Java.
Los módulos en Haskell y ML.
Los espacios de nombres en C++.
…
Las declaraciones vinculan varios atributos a los nombres, dependiendo del tipo de declaración.
Cada una de estas ligaduras tiene por sí misma un atributo que queda determinado por la posición dentro de la
declaración en el programa.
El alcance de un vínculo es la región del programa sobre la cual se conserva el vínculo.
A veces nos referimos erróneamente al alcance de un nombre, pero esto no es cierto pues puede haber varias
declaraciones diferentes sobre el mismo nombre y cada una de ellas con un alcance distinto.
En los lenguajes estructurados por bloques, se conoce como alcance léxico a aquel (alcance de vínculo) que queda
limitado al bloque dónde aparece su declaración asociada.
C tiene la regla adicional conocida como declaración antes de uso, y define que:
o El alcance de una declaración se extiende desde el punto justo después de la misma hasta el final del bloque en
el que está localizado.
Una característica de la estructura de bloques es que las declaraciones en los bloques anidados toman preferencia sobre
declaraciones anteriores:
int x;
void p( void )
{
char x;
x = 'a'; // x global no puede ser accedido desde p
}
main()
{
x = 2;
}
En Ada, el comportamiento de C++, citado anteriormente se conoce como visibilidad por selección, utilizando una
notación de puntos similar al acceso de la escritura de registros:
B1: declare
a: integer;
begin
a := 2;
B2: declare
a: integer; -- a local oculta a 'a' en B1
begin
...
if y then a := B1.a; -- selecciona la a en B1
...
end B2;
end B1;
En C, las declaraciones variables globales (no cualificadas con la palabra clave static) pueden ser accedidas a través de
archivos, utilizando la palabra clave extern.
Las reglas de alcance necesitan también construirse de manera tal que las declaraciones recursivas o de referencia a sí
mismas sean posibles cuando tengan sentido.
Las vinculaciones establecidas por las declaraciones se conservan mediante tablas de símbolos.
La forma en la que la tabla de símbolos procesa las declaraciones debe corresponder con el alcance de cada
declaración.
Reglas de alcance diferentes requieren de un comportamiento diferente de la tabla de símbolos.
Por este motivo la ligadura estática de los tipos de datos (tipificado estático) y el alcance dinámico son
inherentemente incompatibles.
A pesar de todos esto, el alcance dinámico sigue siendo una opción posible para aquellos lenguajes muy dinámicos,
interpretados cuando no esperamos que los programas sean extremadamente grandes (APL, Snobol, Perl, Lisp).
Independientemente de la cuestión del alcance léxico en contraste con el dinámico, existe una dificultad añadida con el
tratamiento de las estructuras.
Cualquier estructura de alcance que pueda ser referenciada directamente en un lenguaje también debe tener su propia
tabla de símbolos.
Los ejemplos incluyen todos los alcances nombrados en Ada; las clases, las estructuras y los espacios de nombres en
C++; y las clases y los paquetes en Java. Por lo que una estructura más típica para la tabla de símbolos de un programa
en cualquiera de estos lenguajes es tener una tabla para cada uno de los alcances, que a su vez tienen que estar
anidados con sus propias tablas dentro de las tablas que las encierran.
Nuevamente éstos pueden mantenerse en un estilo basado en pila.
Es muy aconsejable repasar todos los ejemplos de este apartado.
/*
* Solo hay que recordar la estructura llamada A cuyos campos son:
* A.datos
* A.siguiente
*/
typedef struct A A;
struct A
{
int datos;
A* siguiente;
};
...
}
int* x;
Para permitir la inicialización de los apuntadores que no apuntan a un objeto asignado, C permite el uso del nombre
NULL a 0.
int* x = NULL;
Para que x apunte a un objeto asignado, debemos asignarlo manualmente mediante el uso de una rutina de asignación.
C usa malloc, por lo que para asignar una nueva variable entera y al mismo tiempo asignar su localización en el valor
de x, se haría:
x = ( int* )malloc( sizeof( int ) );
Se dice que la variable x se puede desreferenciar utilizando el operador *, entonces podemos asignar valores enteros a
*x y referirnos a esos valores como lo haríamos en una variable ordinaria:
*x = 2;
free( x );
C++ simplifica la asignación dinámica de memoria incorporando los operadores new y delete como nombres
reservados:
Para permitir la asignación arbitraria y la desasignación utilizando new y delete (o bien malloc y free), el entorno
debe tener un área en la memoria a partir de la cual se pueden asignar las localizaciones en respuesta a las llamadas de
new, y a la cual se pueden devolver las localizaciones en respuesta a las llamadas de delete.
Tradicionalmente esta área se conoce como un montículo o montón (aunque no tiene nada que ver con la estructura de datos
montículo).
La asignación en el montón se conoce como asignación dinámica.
Muchos lenguajes requieren que la desasignación del montón sea administrada de manera automática al igual que la
pila, excepto que la asignación debe conservarse bajo control del usuario.
Todos los lenguajes funcionales requieren que el montón sea completamente automático, sin ninguna asignación o
desasignación bajo control del programador. Java, por otra parte, permite la asignación, pero no la desasignación.
El motivo por el que los LP no permiten el control sobre la asignación y desasignación del montón, es que éstas son
operaciones poco seguras y pueden introducir un comportamiento erróneo en tiempo de ejecución.
En un lenguaje con estructura de bloques con asignación de montones, existen tres tipos de asignación en el entorno:
o Estático, para variables globales.
o Automático, para variables locales
o Dinámicos, para asignación de montones.
Esas categorías también se conocen como clases de almacenamiento de la variable.
o La asignación por clonación es otra alternativa para x = y, que consiste en asignar una nueva localización,
copiar el valor de y a la nueva localización y cambiar la localización de x por la nueva localización.
En ambos casos esta interpretación de la asignación a veces se conoce como una semántica de apuntador a fin de
distinguirla de la semántica más usual, lo cual se conoce como semánticas de almacenamiento.
6.6.2 Constantes
Es una identidad del lenguaje que tiene un valor fijo durante la duración de su existencia dentro de un programa, siendo
como una variable, excepto que no tiene atributo de localización.
Una constante por lo tanto tiene una semántica de valor en vez de una semántica de almacenamiento como las
variables.
Su valor no puede modificarse y su localización no puede ser referenciada de manera explícita a través de un programa.
Esta idea de constante es simbólica, esto es, una constante esencialmente es el nombre de un valor.
Hay que distinguir a los literales (representación de valores como 42 o "esto es un string") de las constantes.
Las constantes pueden ser:
o Estáticas:
Aquellas cuyo valor se pude calcular antes de la ejecución.
A su vez pueden ser de dos tipos:
Solo pueden ser calculadas en tiempo de traducción (tiempo de compilación).
Solo son computables en tiempo de carga (como la localización estática de una variable
global) o al principio de la ejecución del programa.
También se puede hacer una distinción entre constantes de tipo general (todas las vistas anteriormente) y las constantes de
manifiesto (nombre de un literal).
Considerando el siguiere ejemplo en C:
#include <stdio.h>
#include <time.h>
const int a = 2;
const int b = 27 + 2 * 2;
const int c = ( int )time( 0 );
int f( int x )
{
const int d = x + 1;
return b + c;
}
...
En este código a y b son constantes en tiempo de compilación (a es una constante de manifiesto), en tanto que c es una
constante estática (en tiempo de carga) y d es una constante dinámica.
En C, el atributo const se puede aplicar a cualquier variable en un programa que indique simplemente que el valor de
una variable no puede ser cambiado una vez establecido. Otros criterios determinan si una variable es o no estática (por
ejemplo el alcance global en el cual a, b , c se han definido más arriba).
6.7.1 Alias
o Un alias ocurre cuando el mismo objeto está vinculado a dos nombres diferentes al mismo tiempo.
o Esto puede ocurrir cuando:
Durante la llamada de procedimiento (capítulo 8).
A través del uso de variables de apuntador:
El aliado debido a la asignación de apuntadores es difícil de controlar.
Ejemplo:
1) int *X, *Y;
2) X = ( int* ) malloc( sizeof( int ) );
3) *X = 1
4) Y = X;
5) *Y = 2;
6) printf( "%d\n", *X );
x[0] = 42;
System.out.println( y[0] );
}
}
o Los alias presentan un problema en el hecho de que causan efectos colaterales potencialmente dañinos.
o Un efecto colateral de un enunciado es cualquier cambio en el valor de una variable que persiste más allá de la
ejecución de un enunciado.
o No todos los efectos colaterales son dañinos, puesto que en una asignación se pretende explícitamente que
cause uno.
o Sin embargo los efectos colaterales que son cambios a variables cuyos nombres no aparecen directamente en
el enunciado son potencialmente dañinos puesto que el efecto colateral no se puede determinar a partir del
código escrito.
o Otro ejemplo en C serian las referencias pendientes que resultan de la desasignación automática de las
variables locales cuando se sale del bloque de la declaración local (esto es debido, a que C tiene & “direccion de”
que permite asignar la localización de cualquier variable a una variable apuntador ):
{
int *x;
{
int y;
y = 2;
x = &y; /* x contiene la localización de y. *x es un alias de y */
}
o Java no permite referencias pendientes pues no existen apuntadores explícitos, ni operador &, ni operadores de
desasignación de memoria como free o delete.
6.7.3 Basura
o La basura es memoria que ha sido asignada en el entorno pero que se ha convertido en inaccesible para el
programa.
o En C, se genera basura si se omite la llamada a free antes de reasignar una variable de apuntador:
int *x;
...
x = ( int* )malloc( sizeof( int ) );
x = 0;
La localización asignada *x por la llamada a malloc es ahora basura, ya que ahora x contiene el apuntador
nulo y no existe ninguna manera de tener acceso al objeto anteriormente asignado.
o Otro ejemplo similar ocurre cuando la ejecución sale de la región del programa en el cual x misma está
asignada:
void p( void )
{
int *x;
x = ( int* )malloc( sizeof( int ) );
*x = 2;
}
Cuando se sale del procedimiento p, la variable x se desasigna y *x ya no es más accesible para el programa.
De manera similar ocurre con el anidamiento de bloques.
Los datos en su forma más primitiva en el interior de la computadora son simplemente una colección de bits.
La mayoría de los lenguajes incluyen un conjunto de entidades simples de datos, como enteros, reales y booleanos, así
como mecanismos para construir nuevos tipos a partir de los mismos.
Estas abstracciones contribuyen prácticamente a todas las metas del diseño de lenguajes como: legibilidad, capacidad de
escritura, confiabilidad e independencia de la máquina.
Sin embargo estas abstracciones pueden conllevar una serie de problemas como son:
o Dependencia de la máquina:
Un ejemplo de lo anterior es lo finito de todos los datos en una computadora, lo cual queda enmascarado
por las abstracciones.
o Precisión de los números y operaciones aritméticas con reales.
o La falta de consenso entre los diseñadores de lenguajes en relación al grado de información de tipos que
debe de hacerse explícita para verificar la corrección del programa antes de la ejecución.
o Existen muchas razones para tener alguna forma de verificación de tipos estática (en tiempo de traducción):
La información de tipos estáticos permite a los compiladores asignar memoria con eficiencia y generar
código máquina que manipula los datos eficientemente, mejorando la eficiencia de la ejecución.
Un compilador puede utilizar los tipos estáticos a fin de reducir la cantidad de código que necesita compilar,
mejorando la eficiencia de traducción.
La verificación de tipos estáticos permite que muchos errores estándar de programación sean detectados
rápidamente, lo que mejora la capacidad de escritura.
La verificación de tipos estáticos mejora la seguridad y la confiabilidad de un programa al reducir la cantidad
de errores de ejecución que pueden ocurrir.
Los tipos explícitos mejoran la legibilidad al documentar el diseño de los datos.
Pueden utilizarse los tipos explícitos para eliminar ambigüedades en los programas.
Los programadores pueden utilizar los tipos explícitos combinados con la verificación de tipos estáticos
como una herramienta de diseño, de manera que las decisiones erróneas de diseño se pongan de
manifiesto en forma de errores en tiempo de traducción.
La tipificación estática de interfaces mejora el desarrollo de los programas grandes al verificar la
consistencia y corrección de la interfaz.
z = x / y;
El interprete puede determinar si x e y tienen los mismos tipos y si dicho tipo tiene un operador de división
definido para sus valores.
El proceso por el cual pasa un intérprete para determinar si la información de tipos en un programa es consistente se
conoce como verificación de tipos.
El proceso de asignar tipos a expresiones (x / y) se conoce como inferencia de tipos.
Este proceso puede considerarse como una operación por separado y llevarse a cabo durante la verificación de tipos o
considerarse como parte de la verificación de tipos misma.
Los nombres para los nuevos tipos se crean utilizandos una declaración de tipos (a veces definición de tipos).
Cada lenguaje con declaraciones de tipo tienen reglas para ello, y estas se conocen como algoritmos de equivalencia de
tipo.
Los métodos utilizados para la construcción de tipos, el algoritmo de equivalencia de tipos y las reglas de inferencia y de
corrección de tipos, se conocen de manera colectiva como un sistema de tipos.
Si en un lenguaje, todos los errores de tipo se detectan en tiempo de traducción, se dice que es un lenguaje fuertemente
tipificado.
Un tipificado fuerte asegura que la mayoría de los programas peligrosos (programas con errores que corrompen datos) se
rechazarán en tiempo de traducción, y aquellos que no se rechacen, causarán un error antes de cualquier corrupción de
datos.
Ada, ML, Haskell, Modula son lenguajes fuertemente tipificado. C se le conoce como un lenguaje débilmente tipificado.
Los lenguajes sin sistemas de tipificación estática se conocen como lenguajes sin tipificación (o lenguajes con tipificación
dinámica). Estos lenguajes incluyen, Scheme y otros dialectos de Lisp, Smaltalk y la mayoría de los lenguajes de scripts,
como Perl.
En un lenguaje sin tipos, toda la verificación de seguridad se lleva a cabo en tiempo de ejecución.
Las enumeraciones se definen en una declaración de tipo y son verdaderos nuevos tipos.
En la mayoría de lenguajes, las enumeraciones están ordenadas, considerando que el orden en el cual
se listan los valores es importante, existiendo a menudo, una operación sucesora o predecesora para
todo tipo enumerado.
struct IntCharReal
{
int i;
char c;
double r;
}
Las proyecciones en la estructura de registro están dadas por la operación de selector de componentes (o miembro
de la estructura):
o Si x es del tipo IntCharReal, entonces x.i es la proyección de x hacia enteros.
La mayoría de los lenguajes de programación consideran los nombres componentes como parte del tipo definido por
un registro, por lo que la siguiente estructura puede considerarse diferente de la anterior aunque representen el
mismo producto cartesiano:
struct IntCharReal
{
int j;
char ch;
double d;
}
Algunos lenguajes tienen una forma más pura del tipo estructura de registro, que es en esencia idéntica al producto
cartesiano, donde a menudo se les denomina tuplas. Por ejemplo en ML podemos definir IntCharReal como:
4 Longinos Recuero Bustos (http://longinox.blogspot.com)
Type IntCharReal = int * char * real;
Un tipo de datos que se encuentra en los lenguajes orientados a objetos, que está relacionado con las estructuras,
es la clase.
Un esquema típico de asignación para los tipos de producto cartesiano es la asignación secuencial, según el espacio
que requiere cada componente. Por ejemplo:
7.3.2. Unión
Se forma tomando el conjunto de unión teórica de sus conjuntos de valores.
Existen dos variedades de unión:
o Discriminada:
Si se le agrega una etiqueta o discriminador para distinguir el tipo de elemento.
o Indiscriminadas:
No tienen etiqueta y debe suponerse el tipo de cualquier valor en particular.
union IntOrReal
{
int i;
double r;
};
Al igual que con struct, existen nombres para diferenciar los distintos componentes (i y r).
Los nombres son necesarios porque comunican al intérprete el tipo con el que deben interpretarse los bits dentro de
la unión.
Estos nombres no deben de confundirse con los discriminantes, que es un componente separado que indica el tipo
de datos que es realmente el valor, a diferencia del tipo que puede pensar que es:
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.
En los lenguajes de programación se puede hacer algo parecido para definir nuevos tipos que serán subconjuntos de
tipos conocidos.
En ocasiones los subconjuntos heredan operaciones de sus tipos padres.
Una perspectiva alternativa a la relación de subtipos es definirla en términos de operaciones compartidas. Esto es,
un tipo es subtipo de un tipo si y sólo si todas las operaciones de los valores de también pueden aplicarse a
valores del tipo .
La herencia en los lenguajes orientados a objetos se puede considerar como un mecanismo de subtipo, en el mismo
sentido de compartir operaciones.
Cuando es un ordinal, la función puede considerarse como un arreglo con un tipo de índice y tipo de
componente .
En C, C++ y Java el conjunto de índices siempre es un rango de enteros positivos que comienzan por 0.
Los arreglos pueden definirse con o sin tamaño, pero para definir una variable de tipo arreglo hay que asignarle un
tamaño ya que los arreglos son asignados estáticamente o en la pila.
C puede definir arreglos y variables de arreglo de la siguiente manera:
const int Size = 5;
// Defienición de tipos
typedef int TenIntArray[ 10 ];
typedef int IntArray[];
// Definición de variables
TenIntArray x;
int y[ 5 ];
int z[] = { 1, 2, 3, 4, 5 };
IntArray w = { 1, 2 };
// IntArray w; /* Incorrecto!! */
// int x[ Size ]; /* Incorrecto en C!!, correctoe n C++ */
Java si puede asignar arreglos de forma dinámica (en el montón) y su tamaño puede especificarse en forma
totalmente dinámica.
Dicho tamaño constituye una parte de la información almacenada cuando se asigna el arreglo (length).
Los arreglos multidimensionales también son posibles, declarándolos como arreglos de arreglos.
Los arreglos probablemente son los constructores más utilizados ya que su implementación puede hacerse en forma
muy eficiente.
Los lenguajes funcionales por lo general no contienen un tipo arreglo ya que estos están pensados para la
programación imperativa.
Usualmente los lenguajes funcionales utilizan listas en vez de arreglos.
Algunos lenguajes pueden crear tipos generales de función y procedimiento.
// Def. varaiable
IntFunction f = square;
// Def. parámetro
int evaluate( IntFunction g, int value )
{
return g( value );
}
La mayoría de los lenguajes orientados a objetos, como Java y Smalltalk, no tienen variables o parámetros de
función, debido a que están enfocados a los objetos en vez de a funciones.
Los apuntadores están implícitos en lenguajes que tienen administración automática de memoria.
Este es el caso de Java, para el cual todos los objetos son apuntadores implícitos que se asignan de forma explícita
(new) pero son desasignados automáticamente por un recolector de basura.
A veces los lenguajes hacen distinción entre referencias y apuntadores, definiendo como referencia la dirección de
un objeto bajo el control del sistema, que no se puede utilizar como valor ni operar de forma alguna, no así con los
apuntadores.
En este sentido los apuntadores en Java en realidad son referencias.
Tal vez C++ es el único lenguaje donde coexisten apuntadores y referencias.
En C++ los tipos de referencia se crean con un operador postfijo & (lo cual no debe confundirse con el operador
prefijo de dirección &, que devuelve un apuntador):
Las referencias en C++ son en esencia apuntadores constantes que se desreferencian cada vez que se usan.
En C y C++, los arreglos son implícitamente apuntadores constantes hacia su primer componente.
int a[] = { 1, 2, 3, 4, 5 };
int* p = a;
Los apuntadores son de gran utilidad en la creación de tipos recursivos (un tipo que se utiliza así mismo en su
declaración).
Estos tipos tienen una gran importancia en las estructuras de datos y algoritmos, ya que corresponden naturalmente
a los algoritmos recursivos y representan datos cuya estructura y tamaño no se conocen de antemano. Dos ejemplos
típicos son las listas y los árboles.
En C se permiten declaraciones recursivas indirectas por medio de apuntadores:
struct CharListNode
{
char data;
struct CharListNode* next;
};
Java:
o Los tipos simples se llaman tipos primitivos, y los que se construyen utilizando constructores de tipos se llaman
tipos de referencia.
o Los tipos primitivos se dividen en el tipo boolean y tipos de punto flotante (cinco enteros y dos en coma flotante).
Ada:
o Los tipos simples se llaman tipos
escalares.
o Los tipos ordinales se llaman discretos,
los tipos numéricos comprenden los
tipos reales y enteros.
o Los tipos apuntador se llaman tipos
access.
o Los tipos de arreglo y de registro se
llaman tipos compuestos.
Un factor que complica las cosas es el uso de nombres de tipo en las declaraciones:
struct RecA struct RecB
{ {
char x; char x;
int y; int y;
}; };
o Equivalencia de nombres:
Dos tipos son iguales sólo si tienen el mismo nombre y dos variables son equivalentes en tipo solo si sus
declaraciones usan exactamente el mismo nombre de tipo..
La equivalencia de nombres en su estado más puro es incluso más fácil de implementar que la estructural,
siempre y cuando estemos obligados a dar nombre a todos los tipos.
Ada es un lenguaje que ha implementado una equivalencia de nombres muy pura.
C tiene una equivalencia que está entre la estructural y la de nombres y se puede decir que tiene una
equivalencia de nombre para struct, union y estructural para todo lo demás.
En el siguiente fragmento de código en C:
struct RecA
{
char x;
int y;
};
struct RecA a;
RecA b;
struct RecA c;
struct
{
char x;
int y;
}d;
o Estática:
Los tipos de expresiones y de objetos se extraen del texto del programa y el intérprete lleva a cabo la
verificación de tipos antes de la ejecución.
Ejemplo 1: Los compiladores de C efectúan una verificación estática durante la traducción, pero realmente C no es un
lenguaje con tipificado fuerte y que muchas inconsistencias en los tipos no causan errores de compilación.
Ejemplo 2: El dialecto Scheme de Lisp es un lenguaje con tipificado dinámico, pero los tipos se verifican en forma
rigurosa. Todos los errores de tipo provocan la terminación del programa.
Ejemplo 3: Ada es un lenguaje con tipificado fuerte y todos los errores de tipo generan mensaje de error en la
compilación, pero sin embargo, incluso en Ada, ciertos errores, como los de rango en subíndice de arreglos, no pueden
detectarse antes de la ejecución.
Una parte esencial en la verificación de tipos es la inferencia de tipos, en la cual los tipos de expresiones se infieren a
partir de los tipos de las subexpresiones que la componen.
Las reglas de verificación de tipos y las reglas de inferencia a menudo están entremezcladas:
Por ejemplo, una expresión pueden determinarse como de tipo correcto, si y son del mismo tipo y este
tipo contiene una operación „ ‟ (verificación de tipos) y el tipo de la expresión resultante es del tipos de y (inferencia
de tipos).
Las reglas de verificación y de inferencia de tipos tienen una estrecha relación con el algoritmo de equivalencia de tipos:
La declaración de anterior es un error, según el algoritmo de equivalencia de tipos de C, ya que ningún parámetro real
puede tener el tipo del parámetro formal x.
La inferencia de tipos y las reglas de corrección a menudo son las partes más complejas en la semántica de un lenguaje.
int x = 3;
...
x = 2.3 + x / 2;
o La segunda es utilizad por C++ y Ada y se le conoce como sintaxis de llamado de funciones:
o La ventaja de utilizar conversiones forzadas es la de documentan en forma precisa dentro del código, existiendo
menor probabilidad de comportamientos inesperados.
o Además, la eliminación de las conversiones implícitas facilita al intérprete resolver la sobrecarga:
Una alternativa a las conversiones forzadas es tener funciones predefinidas o de biblioteca, que lleven a cabo dichas
conversiones.
Como ejemplo, en Java la clase Integer en la biblioteca java.lang contiene las funciones de conversión
toString, que convierte un int en un String, y parseInt que convierte un String en un int.
En algunos lenguajes está prohibida la conversión implícita a favor de la explícita, la cual favorece la documentación de
la conversión minimizando los riesgos de comportamientos extraños, facilitando la sobrecarga.
Como ejemplo de estos lenguajes tenemos a Ada.
Un paso intermedio es permitir la conversión implícita siempre que no involucre corrupción de los datos, en este sentido
Java sólo permite conversión por extensión.
Los lenguajes orientados a objetos tienen requerimientos especiales para la conversión, ya que la herencia puede
interpretarse como un mecanismo de subtipificación y en algunos casos es necesario hacer conversiones de subtipos a
supertipos y viceversa.
int i;
int a[] = { ... };
...
a[ i ] + i;
struct Stack
{
StackNode< T >* theStack;
};
8.1 Expresiones
Las expresiones básicas son los literales (constantes manifiestas) y los identificadores.
Las expresiones más complejas se elaboran en forma recursiva a partir de las expresiones básicas mediante la
aplicación de operadores y funciones, lo que a veces involucra símbolos de agrupamiento como los paréntesis.
Los operadores pueden tomar uno o más operandos (unarios, binarios, …).
Los operadores pueden escribirse con notación infija, postfija y prefija.
Esta notación corresponde con un recorrido en orden, postorden y preorden del árbol sintáctico de la expresión.
Las formas prefijas y postfijas tienen la ventaja de no necesitar paréntesis para expresar el orden en que se aplican los
operadores.
La asociatividad de operadores también queda implícita con las notaciones prefija y postfija sin la necesidad de reglas.
Muchos lenguajes hacen distinción entre operadores y funciones.
Los operadores si son binarios se escriben en forma infija con reglas de asociatividad y precedencia específicas.
Las funciones que pueden ser predefinidas o definidas por el usuario, se escriben en forma prefija y los argumentos se
consideran como argumentos reales o parámetros para llamadas de las funciones:
3 + 4 * 5 add( 3, mul( 4, 5 ) )
El orden natural en el que se evalúan las subexpresiones sería de izquierda a derecha, lo cual corresponde a un
recorrido de izquierda a derecha en el árbol sintáctico.
Sin embargo muchos lenguajes establecen en forma explícita que no existe orden específico para la evaluación de
argumentos a funciones definidas por el usuario.
Una de las razones para ello es que el intérprete puede reorganizar el orden del cálculo para que este sea más eficiente.
Si la evaluación de una expresión no provoca efectos colaterales, éste dará el mismo resultado, independientemente del
orden en que se evalúan las subexpresiones.
En presencia de efectos colaterales, sin embargo, el orden en el que se realice la evaluación puede provocar diferencias:
int f( void )
{
x += 1;
return x;
}
main()
{
printf( "%d\n", p( x, f() ) );
return 0;
}
Si los argumentos de la llamada p( x, f() ) son evaluados de izquierda a derecha, el resultado será 3, por el
contrario, si son evaluados de derecha a izquierda el resultado será 4.
Esto se debe a que una llamada a la función f tiene un efecto colateral (modifica el valor de la variable global x).
Los programas que dependen del orden de evaluación para sus resultados, son incorrectos.
C, así como otros lenguajes de expresiones, también tienen un operador de secuencia, que permite que se combinen
varias expresiones en una sola y que se evalúen secuencialmente. C especifica que el orden de evaluación es de
izquierda a derecha y que el valor de la expresión más a la derecha es el valor devuelto de toda la expresión:
int x = 1, y = 2;
x = ( y++, x += y, x + 1 );
// la expresión devuelve 5, x = 5, y = 3
En un lenguaje de programación se puede especificar que las expresiones booleanas deben evaluarse en orden de
izquierda a derecha, hasta el punto en que se conoce el valor verdadero de toda la expresión, y en ese momento la
evaluación se detiene. Esta regla se conoce como evaluación en cortocircuito de las expresiones booleanas:
true || x; // true
e1 ? e2 : e3; // A diferencia del enunciado if, esta expresión debe tener parte else opcional
Los operadores booleanos de corto circuito y de if son un caso especial de operadores que difieren la evaluación de sus
operandos. Esta situación de conoce como evaluación diferida, o como evaluación no estricta.
En ausencia de efectos colaterales (cambios a las variables en memoria, en la entrada, en la salida), el orden de la evaluación de las
expresiones no tiene importancia en relación con el valor final de la expresión.
En un lenguaje en el cual los efectos colaterales no existen, como los lenguajes funcionales, las expresiones en los
programas comparten una propiedad importante con las expresiones matemáticas, lo que se puede definir como la regla
de sustitución o transparencia referencial, es decir, dos expresiones cualesquiera en un programa, que tengan el mismo
valor, pueden ser sustituidas la una por la otra en cualquier parte del programa.
La transparencia referencial permite que se utilice una forma muy sólida de la evaluación diferida, la cual tiene
importantes consecuencias teóricas y prácticas, conocida como evaluación de orden normal.
Esta evaluación de una expresión significa que todas la operaciones o funciones comienzan a evaluarse antes de que
sus operandos sean evaluados y los operandos son evaluados sólo si son necesarios para el cálculo del valor de la
operación:
if B1 -> S1
| B2 -> S2
. . .
| Bn -> Sn
fi>
8.2.1 Enunciados if
La forma básica del enunciado if es como aparece en la regla EBNF para el enunciado if en C, con una parte
else opcional:
en el cual puede ser ya sea un enunciado o una secuencia de enunciados encerrados entre corchetes.
Esta forma del if presenta, sin embargo, un problema de ambigüedad sintáctica. El siguiente enunciado:
A esta regla también se le conoce como la regla del anidamiento más cercano para los enunciados if.
El problema del else ambiguo es de diseño del lenguaje y es discutible desde dos puntos de vista:
o Obliga a establecer una nueva regla.
o Dificulta al lector la interpretación de enunciado if.
Esto viola el criterio de legibilidad del diseño.
Una mejor forma para eliminar esta ambigüedad es empleando una palabra clave enmarcadora para el enunciado
if, como en la siguiente regla en ADA:
if e1 then S1 if e1 the S1
else if e2 then S2 elsif e2 then S2
else if e3 then S3 elsif e3 then S3
end if ; end if ; end if ; end if ;
Una cuestión adicional con respecto al enunciado if es el tipo de expresión de control que debe ser:
o En Java, Ada y Pascal, la prueba siempre debe tener tipo booleano.
o C, no tienen tipo booleano y la expresión de control puede ser de tipo entero o de tipo puntero. El valor
resultante se compara con 0, siendo verdadero si la comparación es desigual y falso en caso contrario:
switch( x % 11 )
{
case 0:
y = 0;
break;
case 2:
case 3:
case 4:
z = 2;
break;
case 7:
case 9:
z = y = 1;
break;
default:
// NOP
break;
}
La semántica de este enunciado es evaluar la expresión de control x % 5 y transferir el control al punto del
enunciado en donde está listado el valor.
Los casos listados han de ser mutuamente excluyentes.
Los valores de los casos pueden ser literales o expresiones constantes.
Si el valor de la expresión de control no está listado en la etiqueta de un caso, entonces el control es transferido al
caso default, si existe. Si no, el control es transferido al enunciado que sigue al switch (el enunciado switch
fracasa).
La estructura de este enunciado en C cuenta con una cantidad de características relativamente nuevas:
o Las etiquetas son manejadas sintácticamente como etiquetas ordinarias, lo que permite que ocurran
ubicaciones potencialmente extrañas:
o Al no contar con un enunciado break, la ejecución fracasa y pasa al siguiente caso. Esto permite que los
casos sean listados juntos sin necesidad de repetir el código del siguiente caso; también tiene como
resultado una ejecución incorrecta, en caso de que no se incluya un enunciado break:
switch(x)
{
case 1: if( x > 2 )
case 2: x++;
default: break;
}
ADA, tiene una versión algo más estándar, desde el punto de vista histórico:
case x % 11
when 0 when
y = 0;
when 2 .. 5 =>
z = 2;
when 7 | 9 C
z = y = 1;
when others =>
null;
end case;
Los valores case deben ser distintos y además deben ser completos, ya que si no existe un valor correcto listado y
no existe el caso por omisión (when others), ocurriría un error de compilación.
Esto implica que se debe conocer todo el conjunto de valores posibles de la expresión de control en tiempo de
compilación.
El lenguaje de programación funcional ML, también cuenta con una construcción case, pero es una expresión que
devuelve un valor y no es un enunciado:
fun caseDemo x =
case x - 1 of
0 => 2 |
2 => 1 |
_ => 10
;
Los casos en una expresión case ML son sólo patrones a comparar, en vez de rangos o listas de valores.
El guión bajo es el patrón comodín, el cual funciona como el caso por omisión.
do B1 -> S1
| B2 -> S2
. . .
| Bn -> Sn
od
while (e) S
o el ciclo while de Ada:
En los enunciados anteriores, la expresión de prueba e se evalúa primero. Si resulta verdadera, entonces se ejecuta el
enunciado[s] S[i]. Después, vuelve a evaluarse e y así sucesivamente.
Si para empezar, e resultara falsa, entonces el código interior nunca es ejecutado.
Por ello la mayoría de los lenguajes cuentan con un enunciado alternativo, que asegura que el código del ciclo se ejecute
al menos una vez:
do S while (e);
S;
while (e) S
así que el enunciado do es azúcar sintáctica (una construcción de leguaje que es completamente expresable en términos
de otras construcciones).
Las construcciones while y do tienen la propiedad de que la terminación del ciclo se especifica al principio ( while) o al
final (do) del ciclo. Por esta razón, C, C++ y Java, incluye dos opciones:
o Puede utilizarse un enunciado break (exit en ADA) dentro de un ciclo para salir por completo del ciclo.
o Puede utilizarse un enunciado continue que salta el resto del cuerpo del ciclo y continúa la ejecución en la
siguiente evaluación de la expresión de control.
Los puntos de terminación complican la semántica de los ciclos, así que varios lenguajes (Pascal entre ellos) los
prohíben.
Un caso muy común de la construcción de ciclos es la construcción for-loop en C, C++ y Java:
Muchos lenguajes restringen el formato para for-loop de forma que sólo pueda utilizarse en situaciones de indización.
A menudo los lenguajes incluyen esta forma de ciclo porque puede optimizarse más eficientemente:
o La variable de control puede incluirse en los registros del microprocesador, lo cual permite operaciones
extrarápidas.
o Muchos procesadores tienen una sola instrucción según la cual pueden incrementar un registro, probarlo y
ramificarlo, de forma que el control y el incremento del ciclo pueden ocurrir con una sola instrucción
máquina.
Para obtener esta eficiencia, deben imponerse muchas restricciones como:
o El valor de i no puede cambiarse dentro del cuerpo del ciclo.
o El valor de i es indefinido después de finalizar el ciclo.
o i no puede declararse en ciertas formas (parámetro de un procedimiento, campo de registro, etc).
Algunas cuestiones adicionales sobre el comportamiento de los ciclos son las siguientes:
o ¿Se evalúan los límites solo una vez?
o Si el límite inferior es mayor al límite superior, ¿es ejecutado el ciclo?
o ¿Es indefinido el valor de la variable de control, aunque se utilice un enunciado exit o break para salir
del ciclo antes de terminarlo?
o ¿Qué verificaciones de intérpretes se llevan a cabo en las estructuras de ciclo?
Algunos lenguajes, contienen una forma general de la construcción for-loop que incluye un nuevo objeto del
lenguaje, un iterador.
En una definición abstracta, un iterador debe ocuparse de la definición de variables de control, aportar un esquema de
iteración para estas y un servicio utilitario para llevar a cabo pruebas de terminación.
Por tanto, un iterador se convierte en algo semejante a una nueva declaración de tipo.
Tomemos un ejemplo de esto en C++:
return EXIT_SUCCESS;
...
}
...
8.4.1 Excepciones
Típicamente el suceso de una excepción en un programa se representa con un objeto de datos, y ese objeto de datos
puede ser predefinido o definido por el usuario.
Resulta extraño que en C++ no existe un tipo especial para las excepciones y por tanto, no hay palabra reservada para
declararlas. En vez de ello, cualquier tipo estructurado (class o struct) puede servir para representar una excepción:
struct Trouble
{
string error_message;
int wrong_value;
} trouble;
La declaración de excepciones anterior por lo general obedece las mismas reglas de alcance que las demás
declaraciones del lenguaje.
Ya que las excepciones ocurren en tiempo de ejecución, éstas, pueden hacer que la ejecución se salga del alcance de
una declaración de excepción en particular, sucediendo que en su manejo no pueda ser referenciada por nombre.
Es deseable minimizar el problema que esto provoca declarando excepciones definidas por el usuario en forma global en
el programa, para evitar problemas de alcance. Sin embargo, bajo ciertas circunstancias, las excepciones locales pueden
tener sentido, con el fin de evitar la creación de gran cantidad de excepciones globales superfluas.
En C++, según la práctica usual, no existen en el lenguaje excepciones predeterminadas. Sin embargo, mucho módulos
de biblioteca estándar aportan excepciones y mecanismos de excepción. Algunas de estas excepciones estándar son:
o std::bad_exception
o std::bad_alloc
o std::bad_array_new_length
try
{
division = divendo / divisor;
}
catch( Trouble t )
{
// manejar el problema si es posible
printf( "\nt.error_message" );
}
// set_terminate example
#include <iostream>
#include <exception>
#include <cstdlib>
using namespace std;
void myTerminate()
{
cerr << "terminate handler called\n";
abort(); // forces abnormal termination
}
8.4.3 Control
Una excepción predefinida o incorporada puede ser puesta de manifiesto, ya sea automáticamente por el sistema en
tiempo e ejecución o manualmente por el programa.
C++ utiliza la palabra reservada throw y un objeto de excepción para poner de manifiesto una excepción:
while( 1 )
{
try
{
int* myarray = new int[10000];
break; // Éxito!!!
}
catch( bad_alloc& ba )
{
cerr << "bad_alloc caught: " << ba.what() << endl;
Casi todos los lenguajes modernos utilizan el modelo de terminación para el manejo de excepciones.
El manejo de excepciones a menudo implica una carga general sustancial en tiempo de ejecución. Por esta razón y
porque las excepciones no representan una buena alternativa de control estructurado, es mejor evitar el uso excesivo de
excepciones para implementar situaciones de control ordinarias, que podrían ser reemplazadas por pruebas simples.
// C++
void c( int& x, int& y ) // Especificación
{
int t = x; // Cuerpo
x = y; // Cuerpo
y = t; // Cuerpo
}
En algunos lenguajes y en algunas situaciones, puede separarse una especificación de procedimiento de su cuerpo, en
el caso de que la especificación deba estar disponible por adelantado:
Nótese como esta especificación no requiere que estén especificados los nombres de los parámetros.
En C++ esta clase de especificación se conoce como una declaración, mientras que la definición completa se llama
definición.
Se llama o activa un procedimiento al enunciar su nombre, junto con los argumentos de la llamada, que corresponden a
sus parámetros:
intSwap( a, b );
Una llamada a un procedimiento transfiere el control al principio del procedimiento llamado (el llamado). Cuando la
ejecución llega al final del cuerpo (o algún enunciado de tipo return), el control es devuelto al llamador.
Algunos lenguajes de programación pueden hacer la distinción entre procedimiento y funciones.
Un procedimiento se comunica con el resto del programa a través de sus parámetros y también a través de sus
referencias no locales, esto es, referencias a variables declaradas fuera de su propio cuerpo.
A:
{
int x, y;
... Registro de la activación de A
x = y * 10; x
y
B:
{
Registro de la activación de B
int i = x / 2;
i
}
}
int x;
void B( void )
{
int i = x / 2;
}
Entorno global
void A( void ) x
{
int x, y; Registro de la activación de A
... x
x = y * 10; y
B();
Registro de la activación de B
}
i
main()
{
A();
return 0;
}
Bajo la regla de alcance léxica, la x en B es la x global del programa. Por lo tanto, la activación de B debe retener
información con respecto al ambiente global.
Esto es debido a que el ambiente global es el ambiente definidor de B, en tanto que al registro de activación de A se le
conoce como ambiente invocador de B.
Para bloques que no sean procedimientos, al ambiente definidor es igual al ambiente invocador.
El método de comunicación de un procedimiento con su ambiente invocador es a través de sus parámetros.
Para escribir esta función en forma cerrada, tendríamos que incluir la operación en la lista de parámetros:
Si optamos por no hacer esto, entonces la semántica de esta función solamente puede determinarse con relación al
ambiente que la rodea y el código de esta función junto con una representación de su ambiente de definición se llama
cerradura (también llamada clausura léxica o closures), porque puede ser utilizado para resolver todas las referencias no
locales excepcionales con relación al cuerpo de la función.
Sin embargo, las asignaciones directas a los parámetros no cambia el argumento fuera del parámetro.
C C++ Pascal
void inc( int* x ) void inc( int& x ) procedure inc( var x: integer );
{ { begin
( *x )++; x++; x:= x + 1;
} } end
Después de la llamada a inc el valor del argumento aumenta en 1, de manera que ha ocurrido un efecto colateral.
Es posible el alias múltiple.
Un problema adicional que debe ser resuelto en un lenguaje con paso por referencia es la respuesta del lenguaje a los
argumentos de referencia que no son variables.
a tiene el valor 3 después de que p es llamado en el caso de que se utilice el paso por referencia, en tanto que a tiene el
valor 2 si se utiliza el paso por valor-resultado.
Los problemas en este mecanismo, son el orden en el que los resultados se copian de regreso a los argumentos y si las
ubicaciones de los argumentos se calculan solamente a la entrada y se almacenan o son recalculados a la salida.
int i;
int a[ 10 ];
main()
{
i = 1;
a[ 1 ] = 1;
a[ 2 ] = 2;
p( a[ i ] );
return 0;
}
Cuando se llame al procedimiento con la sentencia Una_Prueba(5 + P, 48, Q), se evalúan las expresiones 5 + P y 48
(sólo se permiten expresiones en el modo in), después se asignan a los parámetros formales A y B, que se comportan
como constantes. A continuación, se asigna el valor A + B a la variable formal C. Obsérvese que especificando el modo
out no se puede conocer el valor del parámetro real Q. En este caso, el parámetro formal C es una nueva variable cuyo
valor se asignará al parámetro real Q cuando finalice el procedimiento. Si se hubiera querido obtener el valor de Q,
además de poder modificarlo, se debería haber empleado C: in out Integer.
Área COMMON
Registro de activación
de programa principal
Registro de la
activación de S1
Registro de la
activación de S2
…
enlace de
control
dirección
de retorno
parámetros
pasados
variables
locales
temporales
main()
{
q(); Dirección de crecimiento de la pila
...
}
void p( int y )
{
int i = x;
char c;
...
}
void q( int a )
{
int x;
...
p( 1 );
}
main()
{
q( 2 );
return 0;
}
En el caso de las referencias no locales, por ejemplo la referencia x a en p. Si el lenguaje no permite anidar
procedimientos (C, Fortran), todas las referencias no locales por fuera de un procedimiento son realmente globales y se
asignan estáticamente.
Sin embargo, si el lenguaje permite anidar procedimientos (Pascal, Ada, Modula 2), las referencias no locales ahora
pueden ser variables locales en el alcance de procedimiento que lo rodea:
procedure q is
x: integer;
procedure p( y: integer ) is
i: integer := x;
begin
...
end p;
procedure r is
x: float;
begin
p( 1 );
...
end r;
begin
r;
end q;
En el ejemplo anterior, para encontrar la referencia no local a la x de q desde el interior de p, se podría seguir el enlace
de control hasta el registro de activación de r, pero con ello encontraríamos la x local de r, logrando un alcance dinámico
en vez de un alcance léxico.
Para lograr el alcance léxico, un procedimiento es que p mantenga un enlace a su ambiente léxico o de definición.
A este enlace se le conoce como enlace de acceso o enlace estático. Ahora cada registro de activación necesita de un
nuevo campo, e campo del enlace de acceso.