Documente Academic
Documente Profesional
Documente Cultură
Procesadores
Prefacio 1
2 Procesadores de lenguajes 53
2.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
2.2 Tipos de procesadores de lenguajes . . . . . . . . . . . . . . . . . . . . 54
2.2.1 Traductores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
ix
x Í NDICE
2.2.2 Ensambladores . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.2.3 Compiladores . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
2.2.4 Intérpretes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
2.2.5 Máquinas virtuales . . . . . . . . . . . . . . . . . . . . . . . . 63
2.2.6 Otros tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
2.3 Estructura de un compilador . . . . . . . . . . . . . . . . . . . . . . . 69
2.3.1 Análisis léxico . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.3.2 Análisis sintáctico . . . . . . . . . . . . . . . . . . . . . . . . 78
2.3.3 Análisis semántico . . . . . . . . . . . . . . . . . . . . . . . . 81
2.3.4 Generación de código intermedio . . . . . . . . . . . . . . . . 86
2.3.5 Optimización de código intermedio . . . . . . . . . . . . . . . 86
2.3.6 Generación y optimización de código objeto . . . . . . . . . . . 87
2.4 Traducción dirigida por la sintaxis . . . . . . . . . . . . . . . . . . . . 87
2.4.1 Definiciones dirigidas por la sintaxis . . . . . . . . . . . . . . . 89
2.4.2 Esquemas de traducción . . . . . . . . . . . . . . . . . . . . . 92
2.4.3 Métodos de análisis . . . . . . . . . . . . . . . . . . . . . . . . 93
2.4.4 Herramientas para la construcción de compiladores . . . . . . . 109
2.5 Ejercicios resueltos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
2.6 Ejercicios propuestos . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
2.7 Notas bibliográficas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Bibliografía 334
Índice de figuras
xv
xvi Í NDICE DE FIGURAS
xvii
Prefacio
1
2 P REFACIO
• Presentación del tema al comienzo de cada capítulo para que el lector tenga una
visión panorámica de los contenidos que va a encontrar y pueda conocer las difi-
cultades que implica su estudio.
Prerrequisitos
En cuanto a programación de ordenadores se refiere, se le suponen al lector conocimien-
tos de programación estructurada imperativa y nociones de orientación a objetos. Tam-
bién se le presuponen conocimientos de las estructuras de datos básicas como arrays, lis-
tas, colas, árboles y sus operaciones básicas. Por otro lado, conocer más de un lenguaje
de programación puede facilitar la comprensión de algunos conceptos relacionados con
la teoría de los lenguajes de programación.
También se le presuponen al lector conocimientos de los conceptos relacionados con
la teoría de lenguajes formales: autómatas finitos, expresiones regulares y gramáticas.
Aunque en el desarrollo de los contenidos de este libro se repasan los conceptos de
autómatas finitos, lenguajes regulares, expresiones regulares, gramáticas regulares, autó-
matas a pila, y lenguajes y gramáticas independientes del contexto, si el lector ya está
familiarizado con ellos, la lectura y comprensión de los capítulos dedicados a los lengua-
jes de programación y los procesadores de lenguajes se verá facilitada.
P REFACIO 3
La primera parte consta de dos capítulos que introducen respectivamente los aspectos
fundamentales de la teoría de los lenguajes de programación y los procesadores de
lenguajes:
El contenido más detallado de cada capítulo junto con las recomendaciones para su mejor
aprovechamiento es el siguiente:
• El capítulo 3 depende del capítulo 1, dado que en éste se introducen conceptos que
son utilizados a la hora de presentar los diferentes paradigmas, como el ámbito de
variables, el concepto de subprograma, tipos de datos, etc.
• El capítulo 2 depende también del capítulo 1, puesto que se centra en los proce-
sadores de lenguajes y se utiliza la notación BNF para explicar los conceptos re-
lativos al análisis sintáctico.
Ejercicios
En cada capítulo hay un apartado de ejercicios resueltos y otro de ejercicios propues-
tos. El primero proporciona al lector ejemplos de cómo resolver cuestiones teóricas,
problemas, utilizar ciertos lenguajes o tecnologías que cubren los aspectos fundamen-
tales desarrollados en el capítulo.
6 P REFACIO
Además, los ejercicios resueltos constituyen una herramienta fundamental para la auto-
evaluación en el caso de que el lector quiera comprobar su grado de comprensión de los
contenidos expuestos en cada capítulo.
Los ejercicios propuestos también cubren los aspectos fundamentales del capítulo. Estos
ejercicios se dejan abiertos como un reto para el lector para que los intente solucionar.
Los autores animamos al lector a la lectura y resolución de ambos tipos de ejercicios.
Capítulo 1
En este capítulo se exponen las nociones básicas necesarias para entender los lenguajes
de programación y se introducen conceptos que serán utilizados durante el resto del
libro. Comienza explicando las notaciones sintácticas y semánticas para la descripción
de lenguajes de programación y después profundiza en el diseño de lenguajes, realizando
un recorrido por diferentes conceptos como tipos de datos, procedimientos y ambientes,
expresiones, tipos abstractos de datos o modularización. Aunque estos conceptos se
presentan de forma resumida, el texto contiene referencias a fuentes externas que el
lector puede consultar para ampliar el contenido sobre algún aspecto concreto.
1.1 Introducción
Sir Charles Antony Richard Hoare, inventor del algoritmo Quicksort (quizá el algoritmo
de computación más utilizado del mundo) dijo en una ocasión [14]:
7
8 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
Quinta generación. Los lenguajes de quinta generación son los utilizados principal-
mente en el área de la inteligencia artificial. Se trata de lenguajes que permiten
especificar restricciones que se le indican al sistema, que resuelve un determinado
problema sujeto a estas restricciones. Algunos ejemplos de lenguajes de quinta
generación son Prolog o Mercury.
1.2 Sintaxis
Cuando aparecieron los primeros lenguajes de tercera generación, los compiladores se
construían para un lenguaje concreto basándose en la descripción del lenguaje de progra-
mación correspondiente. Esta descripción se proporcionaba en lenguaje natural (inglés)
en forma de manuales de referencia del lenguaje y constaba de tres partes: la descripción
léxica del lenguaje, que especifica cómo se combinan símbolos para formar palabras; la
descripción sintáctica, que establece las reglas de combinación de dichas palabras para
formar frases o sentencias; y la descripción semántica, que trata del significado de las
frases o sentencias en sí. El problema de realizar la descripción en lenguaje natural es la
S INTAXIS 11
ambigüedad: el lenguaje natural está lleno de ambigüedades y es difícil ser muy preciso
en la descripción de estos elementos del lenguaje.
Por ello, a finales de los años 50, mientras diseñaba el lenguaje Algol 58 para IBM, John
Backus ideó un lenguaje de especificación para describir la sintaxis de este lenguaje.
Posteriormente, Peter Naur retomó su lenguaje de especificación y lo aplicó para definir
la sintaxis de Algol 60 y lo llamó Backus Normal Form. Finalmente, este lenguaje
de especificación acabó llamándose BNF (de Backus-Naur Form) y ha sido el estándar
de facto para la descripción de la sintaxis de muchos lenguajes de computadora, como
lenguajes de programación o protocolos de comunicación.
En lingüística, la sintaxis es la ciencia que estudia los elementos de una lengua y sus
combinaciones. Es la parte que se encarga de establecer la estructura que deben tener
las sentencias del lenguaje. En los lenguajes de computadora no es diferente: la sintaxis
de un lenguaje establece las normas que han de cumplir los diferentes elementos del
lenguaje para formar sentencias válidas de ese lenguaje. Para describir la sintaxis de
los lenguajes de programación se pueden utilizar diferentes notaciones. El lenguaje
BNF, o algún derivado como EBNF (Extended BNF), es la notación más habitualmente
utilizada. Sin embargo, en ocasiones también se utiliza una notación gráfica denominada
diagramas sintácticos.
Desde un punto de vista formal la sintaxis de un lenguaje de programación es un lenguaje
independiente del contexto. Un lenguaje independiente del contexto es un lenguaje que
se puede describir con una gramática independiente del contexto. Este tipo de gramáti-
cas se estudian como parte de la teoría de lenguajes formales, que define las propiedades
de los diferentes tipos de lenguajes formales. Estas propiedades son las que permiten
realizar razonamientos matemáticos sobre los lenguajes y, en última instancia, construir
herramientas que permitan reconocer una determinada cadena de un programa como
perteneciente al lenguaje de programación cuya sintaxis viene definida por una deter-
minada gramática independiente del contexto. La construcción de compiladores, y en
concreto de las diferentes fases de análisis de un compilador, se asienta en la teoría de
lenguajes formales.
Las notaciones BNF y EBNF, así como los diagramas sintácticos, permiten representar
textualmente (las primeras) o gráficamente (la segunda) una gramática independiente del
contexto.
N → w, (1.1)
S
donde N ∈ N y w ∈ {N T }∗. Al no terminal N se le denomina antecedente, y a la
cadena de símbolos w consecuente.
Una producción puede interpretarse como una regla de reescritura que permite sustituir
un elemento no terminal de una cadena por el consecuente de alguna de las producciones
en las que ese no terminal actúa como antecedente. Esta sustitución se conoce como
paso de reescritura o paso de derivación. El lenguaje definido por una gramática
independiente del contexto son todas aquellas cadenas que se pueden derivar en uno o
más pasos de derivación desde el símbolo inicial S.
Supóngase que se desea describir formalmente el lenguaje, que denominaremos L1, de
las cadenas sobre el alfabeto {a, b, +} en el que el símbolo terminal + aparece siempre
entre alguno de los símbolos a o b. La siguiente gramática describe dicho lenguaje:
S → A + A|A
(1.2)
A → a|b|S
Las gramáticas pueden utilizarse para decidir si una cadena pertenece al lenguaje: basta
comprobar si es posible generar la cadena partiendo del símbolo inicial realizando susti-
tuciones utilizando las producciones de la gramática.
No existe un consenso claro sobre la forma de representar los símbolos terminales, pero
en ocasiones se expresan entre comillas simples o dobles, como en ′ i f ′ , ′ f or′ o ′ int ′ ,
aunque esto no es obligatorio. Las gramáticas BNF se utilizan para definir la sintaxis
de los lenguajes de programación. Por ejemplo, la siguiente gramática en notación BNF
define la parte de declaraciones de variables de un lenguaje:
S INTAXIS 13
Para determinar si una cadena representa una expresión o enunciado válido en un lenguaje
cuya sintaxis viene definida por una gramática BNF se utilizan las producciones de la
gramática. Así, se trata de obtener la cadena buscada a partir del símbolo inicial en
sucesivos pasos de derivación. Este proceso da lugar a lo que se conoce como árbol
de análisis sintáctico. Por ejemplo, supóngase que se tiene el lenguaje definido por la
gramática 1.5, la cadena a, b : bool tiene el siguiente árbol de análisis sintáctico:
<declaracion>
’bool’
<variable> ’,’ <lista-variables>
’a’ <variable>
’b’
Este árbol de análisis sintáctico se corresponde con la siguiente derivación utilizando las
producciones de la gramática:
a) S b) S
S b S
S b S
S b S a
a S b S
a a
a a
(sustituyen en cada paso el no terminal más a la izquierda), mientras que otros proceden
sustituyendo el no terminal más a la derecha.
Considérese a continuación la gramática BNF para el lenguaje de las cadenas de aes y
bes donde toda b aparece siempre entre dos aes:
< S >⇒< S > b < S >⇒< S > b < S > b < S >⇒ ab < S > b < S >⇒
(1.7)
abab < S >⇒ ababa
Esta derivación se puede representar mediante dos árboles sintácticos diferentes, como
se muestra en la Figura 1.1.
Cuando una misma cadena se puede representar mediante dos o más árboles sintácticos
se dice que la gramática es ambigua. La ambigüedad es un problema en el contexto
de los lenguajes de programación porque un analizador sintáctico no puede decidir cuál
de los dos árboles sintácticos construir. Análogamente, cuando para toda cadena del
lenguaje existe únicamente un árbol sintáctico se dice que la gramática es no ambigua.
El problema de la ambigüedad radica en que cambia el significado dado a la cadena. Si
se considera el lenguaje de las expresiones aritméticas de sustracción, podría pensarse
en construir una gramática como la siguiente:
< expresion >::=< expresion >′ −′ < expresion > | < constante > (1.8)
Según la gramática 1.8, la expresión 2 − 1 − 1 se puede representar como los dos ár-
boles sintácticos de la Figura 1.2. Estos dos árboles representan diferente asociatividad
para la resta. El árbol a) representa la expresión 2 − (1 − 1) = 2 (asociatividad por la
derecha), mientras que el árbol b) representa la expresión 2 − 1 − 1 = 0 (asociatividad
por la izquierda). Cuando una gramática para un lenguaje de programación es ambigua,
S INTAXIS 15
a) <expresion>
<expresion> - <expresion>
<constante>
<expresion> - <expresion>
2
<constante> <constante>
1 1
b) <expresion>
<expresion> - <expresion>
<constante>
<expresion> - <expresion>
1
<constante> <constante>
2 1
< expresion >::=< expresion >′ −′ < constante > | < constante > (1.9)
<expresion>
<expresion> - <constante>
1
<expresion> - <constante>
<constante> 1
Notación EBNF
La notación EBNF (Extended BNF) es una forma derivada de BNF que fue introducida
para describir la sintaxis del lenguaje Pascal, y que actualmente es un estándar ISO
(ISO/IEC 14977). EBNF simplifica la notación BNF mediante el uso de paréntesis para
agrupar símbolos (( )), llaves para representar repeticiones ({ }), corchetes para repre-
sentar una parte optativa ([ ]) –correspondiente a una cardinalidad de 0 ó 1–, o repre-
sentando los símbolos terminales entre comillas sencillas. Además, se permite el uso
del cuantificadores: {} ∗ para representar 0 ó más veces, y {} + para representar una
cardinalidad de 1 ó más veces.
La gramática EBNF 1.10 representa una posible gramática para expresiones aritméticas.
Como puede verse, no se explicita la recursividad en la definición de < expresion > y
de < termino >.
<expresion>
- 1
2 1
Figura 1.4: Árbol de sintaxis abstracta para la expresión 2 − 1 − 1 según la gramática 1.9
✲
✲ hterminoi ✲✛
❄ ’+’ hterminoi
’-’
la Figura 1.4. Este árbol es mucho más conciso que el árbol sintáctico de la Figura 1.3,
pero representa exactamente la misma expresión aritmética.
Diagramas sintácticos
Los diagramas sintácticos representan en forma gráfica una gramática independiente del
contexto. En un diagrama sintáctico, los símbolos no terminales se representan habitual-
mente mediante rectángulos y los símbolos terminales se representan por círculos. Una
palabra reconocida se representa como un camino entre la entrada (izquierda) y la salida
(derecha). Sin embargo, en ocasiones los símbolos de la gramática se representan con la
notación BNF. Esta última será la convención que se utiliza en este texto.
La Figura 1.5 muestra la siguiente producción mediante un diagrama sintáctico:
< expresion >::=< termino > {(′ +′ |′ −′ ) < termino >} (1.11)
La gramática 1.12, correspondiente a la sintaxis de for de Pascal, se representaría en
forma de diagrama sintáctico como se muestra en la Figura 1.6.
< sent − f or >::=′ f or′ < var >′ :=′ < exp > (′ to′ |′ downto′ ) < exp >′ do′ < sent >
(1.12)
Esta sentencia es válida desde el punto de vista sintáctico: está bien formada, tiene un
sujeto y un predicado, un verbo, etc; también lo es desde el punto de vista semántico,
la frase tiene un significado concreto. La sentencia que se muestra a continuación, pese
a ser correcta sintácticamente (sólo cambia el verbo respecto al ejemplo anterior), no es
correcta semánticamente:
Tipos de datos simples. Son aquellos que no pueden descomponerse en otros tipos de
datos más elementales. Se incluyen dentro de esta clase los tipos de datos entero,
carácter, lógico (booleano), real, enumerado o subrango.
Tipos de datos estructurados. Son aquellos que se componen de tipos de datos más
básicos. Ejemplos de tipos de datos estructurados son los registros, arrays, listas,
cadenas de caracteres o punteros.
T IPOS DE DATOS 19
Entero
El tipo entero en los lenguajes de programación es normalmente un subconjunto orde-
nado del conjunto infinito matemático de los enteros. El conjunto de valores que el
tipo entero puede representar en un lenguaje viene determinado por el diseñador del
lenguaje en ciertos casos, o por el diseñador del compilador en otros. Por ejemplo, en
Java el tamaño del conjunto de los enteros viene determinado por la especificación del
lenguaje, mientras que en C la precisión de la mayoría de los tipos viene determinada
por el diseñador del compilador y suele depender de la arquitectura específica para la
que se construyó ese compilador.
El tipo de datos de los enteros suele incluir las operaciones típicas entre valores de este
tipo: aritméticas (tanto unarias como binarias), relacionales y de asignación.
La implementación del tipo de datos suele consistir típicamente en una palabra de memo-
ria (32 bits en arquitecturas x86) donde se almacena el valor entero en complemento a 2,
aunque no cualquier combinación de bits es válida para representar valores. Es posible
que ciertos bits estén reservados para propósitos específicos de la implementación. Por
ejemplo, algunos lenguajes reservan dentro de estos 32 bits un bit para indicar desbor-
damiento (una operación cuyo resultado no cabe dentro del rango de enteros soportado),
otros pueden utilizar algún bit como descriptor del tipo de datos, etc.
Algunos lenguajes soportan operaciones a nivel de bit sobre operandos de tipo entero,
como desplazamiento, desplazamiento circular, etc. El siguiente código, escrito en C,
equivale a multiplicar el primer operando por 2 elevado a la potencia indicada por el
segundo operando:
1 y = x << 4;
Real
El tipo de datos real representa números decimales en notación de punto flotante o punto
fijo. Se trata de un subconjunto del conjunto infinito matemático de los números reales.
20 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
Este tipo de datos incluye las operaciones típicas: aritméticas (unarias y binarias), rela-
cionales y de asignación. Generalmente los lenguajes proporcionan al menos dos tipos
de datos reales con distinta precisión, típicamente uno de 32 y otro de 64 bits.
La implementación de números reales suele estar basada en el estándar IEEE 754, donde
el número real se divide en una mantisa y un exponente. Diferentes implementaciones
de este tipo de datos pueden establecer diferente tamaño de mantisa y exponente. El
estándar IEEE 754 define valores especiales para representar los conceptos de infinito,
-infinito y Not a Number (NaN). Por ejemplo, en Java la siguiente expresión da como
resultado NaN:
Booleano
El tipo de datos booleano consiste en dos únicos valores posibles: verdadero y falso.
Incluye las operaciones lógicas típicas: and, or, not, etc. Aunque teóricamente estos
valores podrían almacenarse en un único bit, dado que no es posible direccionar un único
bit, la implementación suele consistir al menos en un byte.
Algunos lenguajes no incluyen este tipo de dato y en su lugar lo representan mediante
otros tipos. Por ejemplo, en C los valores booleanos se representan con enteros, donde
todo valor distinto de cero es equivalente al valor verdadero, y el valor cero representa
el valor falso.
Carácter
Enumerados
1 type
2 DiaSemana = ( Lunes , Martes , Miercoles , Jueves , Viernes , Sabado
, Domingo );
Algunos lenguajes implementan internamente los valores del tipo enumerado como en-
teros, como es el caso de C.
Subrango
Los valores de tipo subrango se crean especificando un intervalo de otro tipo de datos
simple. Soportan las mismas operaciones que el tipo de datos básico del que derivan.
Pascal es uno de los lenguajes que permiten la definición de subrangos de tipos de datos
básicos. Las siguientes definiciones proporcionan un nuevo tipo basado en algunos de
los tipos básicos de Pascal:
1 type
2 DiaSemana = ( Lunes , Martes , Miercoles , Jueves , Viernes , Sabado
, Domingo );
3 Laborable = Lunes .. Viernes ;
4 Mes = 1 .. 12
Número de componentes. Puede ser fijo, como suele suceder en el caso de arrays o
registros, o variable, como en el caso de listas, pilas, colas, etc.
Tipo de cada componente. El tipo de los componentes puede ser homogéneo, como en
el caso de los arrays, o heterogéneo, como en el caso de los registros.
Acceso a los componentes. La forma de acceder a los componentes puede ser mediante
un índice (arrays, listas) o mediante el nombre de un campo (registros).
Arrays
Los arrays son estructuras de datos de tamaño fijo donde todos los elementos son del
mismo tipo. Esto hace muy apropiada la representación secuencial para representarlos.
Conociendo la dirección de memoria del primer elemento del array y el tamaño de cada
uno de sus elementos, la posición del i-ésimo elemento es posicion[i] = posicion[0] +
i × T , donde T es el tamaño del tipo de datos que contiene el array.
En determinados lenguajes es posible definir exactamente el rango de índices que se
desean utilizar para el array, como en el caso de Pascal, donde es posible una declaración
como la siguiente:
Los arrays bidimensionales pueden verse como arrays de arrays. Cada posición del ar-
ray original es a su vez un array del tamaño que indica la segunda dimensión. Igual que
sucede con los arrays unidimensionales, el tipo de datos que contiene un array bidimen-
sional es siempre el mismo.
Los arrays bidimensionales también se representan en memoria de forma secuencial.
Podemos ver el ejemplo anterior como una matriz de 10 filas y 100 columnas. La re-
presentación secuencial de esta matriz consistiría en disponer en primer lugar las 100
columnas de la fila 0 (dado que en Java los arrays comienzan siempre en 0), a continua-
ción las 100 columnas de la fila 1, y así sucesivamente hasta la fila 9.
Es importante notar que en algunos lenguajes de programación es necesario reservar
memoria para cada una de las columnas de cada fila. Este enfoque también es útil si no
se conoce de antemano el tamaño del array, dado que generalmente los índices sólo se
pueden especificar mediante una constante. Así, el fragmento anterior podría haber sido
escrito en Java de la siguiente forma (donde numColumnas es una variable definida en
alguna parte del método o pasada como parámetro):
Registros
Un registro es una estructura de datos compuesta de un número fijo de elementos que
pueden tener distinto tipo. Cada uno de los elementos de un registro se denomina campo
y tiene asociado un nombre que permite identificarlo, como en el siguiente ejemplo:
1 struct Fraccion {
2 int numerador ;
3 int denominador ;
4 };
1 Fraccion f1 ;
2 f1 . numerador = 1;
3 f1 . denominador = 2;
miento desde la dirección de memoria del comienzo del registro, dado que éstos tienen
tamaño fijo y se conoce el tamaño de cada uno de los campos del mismo.
Listas
Una lista es una colección ordenada de datos. En este sentido se parecen a los arrays.
Sin embargo las listas son dinámicas: pueden crecer y disminuir durante la ejecución
del programa conforme se añaden y eliminan elementos de la misma. Estos elementos
pueden ser del mismo tipo o de tipos distintos, generalmente dependiendo del lenguaje.
La representación más común para las listas es la representación vinculada. La lista se
representa como un puntero al primer elemento. Este elemento típicamente contiene
el dato propiamente dicho y un puntero al siguiente elemento. A este tipo de imple-
mentación se la denomina lista enlazada. También es posible implementar una lista con
dos punteros, uno apuntando al elemento siguiente y otro apuntando al elemento ante-
rior, por motivos de eficiencia. Este tipo de listas se denominan doblemente enlazadas.
En determinados lenguajes las listas representan un tipo de datos más del propio lenguaje
y por tanto se pueden declarar variables de dicho tipo. Esto es especialmente habitual en
los lenguajes funcionales (como LISP o Haskell) y en los lenguajes con tipado dinámico
(como PHP o Ruby). En otros lenguajes las listas son un tipo de dato definido por el
programador que generalmente se proporcionan como librerías. Es el caso de lenguajes
como Java o C, en los que es necesario incluir dichas librerías para poder utilizar las
definiciones incluidas en las mismas.
1.5.1 Expresiones
Además de tipos de datos y variables de esos tipos, en un lenguaje hay también constan-
tes (o literales). Una constante es un valor, normalmente de un tipo primitivo, expresada
literalmente. Por ejemplo, 5 y ’Hola Mundo’ son constantes. Algunas constantes están
26 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
predefinidas en el lenguaje, como nil en Pascal, null en Java o NULL en C. También las
constantes true y false, en aquellos lenguajes que soportan los tipos booleanos, suelen
estar predefinidas.
Una expresión puede ser una constante, una variable, una invocación de subprograma
que devuelva un valor, una expresión condicional (como ?: en C o Java), o un ope-
rador cuyos operandos son a su vez expresiones. Las expresiones representan valores,
es decir, su evaluación da como resultado un valor. Este valor puede ser almacenado
en una variable, pasado como argumento a un subprograma o utilizado a su vez en una
expresión.
Los operadores utilizados en las expresiones tienen una aridad que se refiere al número
de operandos que esperan. Un operador puede verse como un tipo especial de función,
donde los argumentos son los operandos. Normalmente los operadores son unarios (un
operando), binarios (dos operandos) o ternarios (tres operandos). Un ejemplo de opera-
dor unario es el operador de cambio de signo (-): -4. Los operadores unarios pueden ir
antes o después del operando. Por ejemplo, el operador ++ que incrementa en una unidad
el operando sobre el que se aplica, puede ir antes o después, con comportamientos ligera-
mente diferentes en cada caso. No hay unanimidad sobre cómo se utilizan los operandos
unarios. Por ejemplo, desreferenciar un puntero en Pascal se hace utilizando el operador
↑ en notación postfija (a continuación del operando): miPuntero↑ := miValor. En C
sin embargo el operador de desreferenciación, *, es prefijo: *miPuntero := miValor.
En los lenguajes imperativos, es habitual que los operadores binarios sean infijos (como
los operadores matemáticos usuales): +, -, *, %, div, mod, etc. Un caso especial es la
asignación. En algunos lenguajes la asignación es también una expresión, que devuelve
el valor que es asignado. En estos lenguajes es posible encadenar asignaciones: a = b
= c. También es posible utilizar asignaciones allí donde se espere una expresión: if(a
= getChar()). El problema es que la legibilidad disminuye y es fácil cometer erro-
res como: while(a = NULL), en lugar de while(a == NULL) en un lenguaje donde el
operador de comparación por igualdad es ==.
Por otro lado, los operadores suelen estar sobrecargados: + puede aplicarse a enteros
y reales. Incluso puede aplicarse en algunos lenguajes a valores de tipos diferentes:
en C puede sumarse un entero y un real. Algunos lenguajes permiten sobrecargar los
operadores, es decir, redefinirlos para un nuevo tipo de datos. C++ por ejemplo permite
redefinir el operador + para actuar sobre un tipo de datos matriz.
Evaluación en cortocircuito
La siguiente invocación del método fuerza la completa evaluación de los argumentos del
mismo (dado que C implementa evaluación estricta):
1 (Integer,Integer,Integer,Integer) ->Integer
2 maxMin (a ,b , exprA , exprB ) = if(a > b) then exprA else exprB
Sin embargo, la misma invocación del método maxMin difiere la evaluación de los argu-
mentos hasta saber cuál de las dos expresiones será devuelta (evaluación diferida):
1.5.4 Enunciados
En los lenguajes imperativos los enunciados se disponen en secuencias formando sub-
programas e incluso programas enteros. Algunos enunciados permiten controlar el orden
de ejecución de otros enunciados, posibilitando así repetir conjuntos de enunciados un
número determinado de veces, o escoger entre una de varias secuencias de enunciados
en base a los datos manejados por el programa.
E XPRESIONES Y ENUNCIADOS 29
El enunciado más básico es el enunciado de asignación que cambia el estado del pro-
grama asignando el valor obtenido por alguna expresión a alguna variable del programa.
Como se ha destacado anteriormente este enunciado a veces es considerado también una
expresión.
Enunciado compuesto
Un enunciado compuesto es una secuencia de enunciados que se ejecuta secuencial-
mente comenzando por el primer enunciado y acabando en el último. Los enunciados
compuestos pueden incluirse allí donde se espere un enunciado para construir enuncia-
dos más grandes.
En Pascal, un enunciado compuesto se forma agrupando una serie de enunciados entre
las palabras reservadas begin y end:
1 begin
2 enunciado_1 ;
3 enunciado_2 ;
4 ...
5 end
Enunciados condicionales
Los enunciados condicionales permiten controlar cuál, de un grupo de enunciados (ya
sean simples o compuestos) se ejecuta en base a una determinada condición que es nece-
sario evaluar.
El enunciado condicional más habitual es el if. Este enunciado tiene dos formas. En
la forma más básica, if evalúa una condición y de cumplirse ejecuta la secuencia de
enunciados que contiene. El siguiente es un ejemplo de este tipo de construcción en
Java:
1 if ( denominador == 0) {
2 System . out . println (" Division por cero ");
3 }
1 if ( denominador == 0) {
30 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
1 case (a / 2) of
2 0: begin
3 enunciado_1 ;
4 enunciado_2 ;
5 ...
6 end;
7 1: begin
8 enunciado_3 ;
9 enunciado_4 ;
10 ...
11 end;
12 else begin
13 enunciado_5 ;
14 enunciado_6 ;
15 ...
16 end;
17 end;
Enunciados de iteración
Los enunciados de iteración permiten repetir un enunciado un determinado número de
veces o hasta que se cumpla una condición. Los enunciados de repetición con contador
pertenecen al primer caso. Estos enunciados utilizan una variable como un contador.
El programador establece el valor inicial de dicha variable, cuál es el valor máximo
que puede tomar y el enunciado que se debe ejecutar. Se comprueba si la variable ha
superado el valor máximo y en caso contrario se ejecuta el enunciado especificado y se
incremente el contador. Cuando éste llega al valor máximo se abandona el enunciado de
iteración y se sigue ejecutando por el siguiente enunciado. A continuación se muestra
un ejemplo de enunciado de repetición con contador en Pascal:
1 for i := 1 to 10 do
2 writeln(i);
E XPRESIONES Y ENUNCIADOS 31
Los enunciados iterativos con condición ejecutan el enunciado especificado por el pro-
gramador mientras se cumpla la condición:
1 int i = 0;
2 do {
3 System . out . println (i);
4 } while (i < 10) ;
Nótese que en el ejemplo anterior los enunciados que se encuentran dentro del enunciado
iterativo se ejecutan al menos una vez, dado que la condición en este caso se evalúa al
final de cada iteración. Normalmente los lenguajes proporcionan otra versión de este
enunciado iterativo donde la condición se evalúa al principio. En este caso, si la condi-
ción no se cumple no se ejecutan las sentencias del enunciado iterativo. El siguiente
ejemplo en Java es equivalente al anterior, pero la condición se comprueba antes de
comenzar cada iteración:
1 int i = 0;
2 while (i < 10) {
3 System . out . println (i);
4 }
Manejo de excepciones
Cualquier programa debe ser capaz de enfrentarse en un momento u otro de su ejecución
a determinadas condiciones de error. En algunos casos los errores son irrecuperables
(como falta de espacio en disco), en otros es posible realizar alguna acción que permita
al programa recuperarse. Pero en cualquier caso no es deseable que el programa finalice
sin más, al menos debería informar al usuario de lo que ha sucedido.
Algunos lenguajes de programación incorporan manejo de excepciones que posibilitan
al programador capturar determinadas situaciones de error con el objeto de darles el
tratamiento apropiado. En Java, por ejemplo, cuando se produce una situación de error
es habitual que el código donde dicho error es detectado eleve una excepción. Una
excepción es un tipo de datos que contiene información sobre el error y que puede ser
capturado estableciendo enunciados específicos para ello en determinadas partes clave
32 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
del programa, como por ejemplo cuando se trata de abrir un fichero. Dado que es posible
que el fichero no exista, pero no se quiere que el programa termine sin más, se puede
utilizar un enunciado especial para capturar el error:
1 try {
2 FileReader fr = new FileReader ( file );
3 } catch( FileNotFoundException e) {
4 // Aquí típicamente utilizaríamos un fichero de log
5 // Por motivos de legibilidad simplemente escribimos el
mensaje por pantalla
6 System . out . println (" Fichero no encontrado : " + e.
getMessage () );
7 }
Hay dos aspectos fundamentales relacionados con los subprogramas: el paso de pará-
metros y el ámbito de las variables.
C++ permite tanto paso por valor como paso por referencia. Por defecto, se realiza
paso por valor. Si queremos que un determinado parámetro sea pasado por referencia es
necesario anteponer el símbolo & al nombre del parámetro formal.
En el paso por copia y restauración (o valor y resultado) los parámetros reales son
evaluados y pasados al subprograma llamado en los parámetros formales. Cuando la
ejecución del subprograma termina, los valores de los parámetros formales son copia-
dos de vuelta en las direcciones de memoria de los parámetros reales. Evidentemente,
esto sólo se puede hacer con aquellos parámetros reales que representen posiciones de
memoria (como variables o indexación de arrays).
Aunque Pascal no tiene paso por copia y restauración, en el siguiente ejemplo se ha
intentado ejemplificar lo que pasaría con este tipo de paso de parámetros:
P ROCEDIMIENTOS Y AMBIENTES 35
1 program copiarest ;
2
3 var
4 a : integer;
5
6 procedure inc ( num : integer);
7 begin
8 { En este punto se ha copiado a en el parámetro num }
9 num := num +1;
10 { En este punto sólo se ha modificado num , la variable a
11 permanece inalterada }
12 end;
13
14 begin
15 a := 10;
16 inc (a);
17 { En este punto el valor de num ha sido copiado de vuelta en a}
18 end.
1 int a = 10;
2 a = a + 1; // Se ha sustituido la llamada a inc (a) por la
implementación de inc
variables globales que son accesibles desde cualquier parte del programa. Sin embargo,
típicamente los subprogramas pueden declarar sus propias variables locales. Cuando un
subprograma que declara variables locales es llamado, se enlazan los nombres de sus
variables locales a sus posiciones de memoria correspondientes, de forma que puedan
utilizarse en el cuerpo del subprograma. Cuando éste termina, estos enlaces se destruyen
y no están accesibles desde el subprograma que llamó.
La asociación de nombres a datos o subprogramas tiene lugar en lo que se denomina
ámbito o alcance. Un ámbito puede tener sus propias declaraciones locales y un con-
junto de enunciados. El cuerpo de un subprograma es un ejemplo de ámbito. El cuerpo
de un enunciado iterativo como el for también define un ámbito: el del cuerpo del for,
que en determinados lenguajes puede contener también su propias declaraciones locales,
como en el ejemplo siguiente:
En este ejemplo, la función swap define un ámbito en el cual están definidos los nombres
values e i. El enunciado for define su propio ámbito donde además de los anteriores
está definido el nombre temp.
Como se puede observar los ámbitos se pueden anidar. Un ámbito más interno tiene
acceso a los nombres definidos en un ámbito más externo, a menos que el ámbito más
interno contenga una definición con el mismo nombre que la del ámbito externo. En
el siguiente ejemplo, el parámetro formal i queda oculto por la declaración de i en el
enunciado del for:
Al salir del enunciado del for, vuelve a ser accesible el parámetro formal i, dado que se
sale del ámbito del for y se destruyen los enlaces creados en dicho ámbito, en concreto
el enlace local al for para i.
Atendiendo a cómo se realice el enlazado de nombres a datos en memoria, subprogra-
mas, etc., podemos distinguir entre ámbito estático y ámbito dinámico. En el ámbito
estático (también denominado ámbito léxico), el enlace se puede determinar simple-
P ROCEDIMIENTOS Y AMBIENTES 37
mente en base al código fuente. Basta echar un vistazo a los ámbitos para saber qué
nombres son accesibles desde qué ámbitos. Considérese el siguiente ejemplo en Pascal:
1 program ambitos ;
2 type
3 TArray : array [1..3] of integer;
4 var
5 a : TArray ;
6 procedure uno (i : integer);
7
8 procedure dos ;
9 var j : integer;
10 a : TArray ;
11 begin
12 a [1] := 0;
13 a [2] := 0;
14 a [3] := 0;
15 intercambia (1 , 2) ;
16 end;
17
27 begin
28 a [1] := 1;
29 a [2] := 2;
30 a [3] := 3;
31 dos ;
32 end; { uno }
33
34 begin { ambitos }
35 ...
36 uno (1) ;
37 end. { ambitos }
El programa ambitos define un ámbito en el cual están definidos los nombres TArray
y a. En el procedimiento uno, se tiene acceso a estos dos nombres, y además define
un ámbito donde están definidos los nombres i, dos e intercambia. El procedimiento
dos define un ámbito en el cual el nombre de la variable a del programa principal queda
oculto por la variable local a de dos (línea 10). Dentro del ámbito de dos también está
38 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
1 2 1 3
1 0 0 0
La diferencia con el ámbito estático es por tanto que pese a que intercambia no está
dentro del ámbito de dos, al ser llamado por dos, hereda sus enlaces, y por tanto la
variable a se enlaza con la declaración de a en dos y no a la declaración de a en el
programa principal.
La mayoría de lenguajes de programación utilizan ámbito estático, que fue el utilizado en
el lenguaje ALGOL. Algunas excepciones notables son LISP (aunque las versiones más
recientes han pasado a utilizar ámbito estático) y las versiones iniciales de Perl. Algunos
lenguajes permiten utilizar ambos enfoques (ámbito estático y ámbito dinámico) como
en el caso de Common LISP.
comenzó a pensarse en los tipos de datos como los propios datos junto con las operacio-
nes que se pueden realizar sobre ellos. De esta forma surgieron los tipos abstractos de
datos. En un tipo abstracto de datos tenemos:
• Un conjunto de datos que se acojen a una definición de tipo (que puede ser más o
menos compleja).
En algunos lenguajes sólo es posible simular tipos abstractos de datos, dado que la en-
capsulación no forma parte del propio lenguaje, como en el caso de Pascal. Sin embargo,
hoy día la mayoría de los lenguajes de programación modernos proporcionan mecanis-
mos de encapsulación que permiten implementar tipos abstractos de datos. En el caso
del lenguaje Ada se introdujo el concepto de paquete que permitía encapsular defini-
ciones de tipos de datos y las operaciones permitidas sobre estos tipos. En el caso de
C++ o Java se proporciona el concepto de clase que encapsula datos y operaciones den-
tro de la misma unidad sintáctica. De la programación orientada a objetos se hablará en
el capítulo 3.
La encapsulación de la información es una parte muy importante del tipo abstracto de
datos. Mediante la encapsulación se evita que los usuarios del tipo abstracto de datos
tengan que conocer la implementación concreta del tipo. Esto ayuda a pensar en tér-
minos de abstracciones. Por ejemplo, considérese el siguiente programa en el cual la
función estadoCivil devuelve el estado civil de una persona dado su dni:
1 program estadoCivil ;
2
3 var
4 dni , estado : String ;
5
6 function estadoCivil ( dni : String ) : String ;
7 begin
8 ...
9 end;
10
11 begin
12 dni := readln( ’ DNI = ’);
13 estado := estadoCivil ( dni );
14 if estado = ’ SOLTERO ’ then
15 ...
16 else
17 ...
40 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
18 end.
1 program estadoCivil2 ;
2
3 type
4 TEstadoCivil = ( SOLTERO , CASADO );
5
6 var
7 estado : TEstadoCivil ;
8 dni : string ;
9
12 begin
13 dni := readln( ’ DNI = ’);
14 estado := estadoCivil ( dni );
15 if estado = S then
16 ...
17 else
18 ...
19 end.
En esta nueva versión del programa anterior el compilador puede ayudar al programador
a identificar errores. Dado que la comparación en la línea 9 es incorrecta (no existe un
T IPOS ABSTRACTOS DE DATOS Y MÓDULOS 41
Este procedimiento obliga al programador a conocer los detalles de todos los campos
de datos de un tipo Alumno. Es más, sin información del contexto se podría pensar que
este procedimiento se puede invocar, en el contexto de una aplicación para gestionar la
información de una universidad, tanto para alumnos como para profesores, porque no
hay ninguna información que limite la aplicabilidad del procedimiento. Si invocáramos
el procedimiento con los datos de un profesor, este profesor acabaría dado de alta en
la base de datos de alumnos. Considérese ahora el mismo ejemplo utilizando un tipo
abstracto de datos Alumno:
Ahora no hay ninguna duda de que el procedimiento insertar sólo se puede utilizar con
alumnos. Si se considera que existe el tipo abstracto de datos TProfesor (análogamente
al tipo TAlumno), entonces un intento de invocar el procedimiento insertar con una
variable de tipo TProfesor resultaría en un error de compilación. Pero además, desde
el punto de vista del programador resulta mucho más cómodo fijar su atención en la
abstracción alumno en lugar de tener que manejar directamente los datos de los que se
compone dicha abstracción (nombre, apellidos, dni).
Algunos lenguajes, como Java, mediante el uso de la herencia y el polimorfismo (que se
estudiarán en el capítulo 3) permiten llevar más allá aún este concepto de abstracción.
Java proporciona, como parte del conjunto de librerías incluido en la plataforma Java
Standard Edition, implementaciones de diferentes estructuras de datos como listas, pilas,
colas, tablas hash, etc. Por ejemplo, en el caso concreto del tipo de datos lista, este
lenguaje proporciona dos implementaciones distintas de listas, una basada en arrays y
otra basada en listas enlazadas:
42 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
Los detalles concretos de implementación de una lista en Java pueden quedar ocultos al
programador si éste utiliza la interfaz List común a ambas clases. Esta interfaz define
un conjunto de operaciones que se pueden realizar sobre cualquier lista sin especificar
cómo se realizan. La implementación de las operaciones queda delegada a las clases
ArrayList y LinkedList. Por tanto, si al programador se le proprociona una variable
de tipo List, no solamente se le está proporcionando una abstracción de una lista que
le permite pensar en términos de operaciones sobre listas, sino que se le oculta la imple-
mentación concreta de esa lista. En el siguiente ejemplo, no se puede saber a priori (ni
en la mayoría de los casos hace falta saberlo) qué tipo de lista se está utilizando:
7 }
con definiciones de clases o interfaces, y un paquete puede contener a su vez otros pa-
quetes formando una estructura jerárquica.
El hecho de utilizar paquetes posibilita además disponer de un espacio de nombres que
ayuda a evitar colisiones. En Java, por ejemplo, el paquete java.util contiene la defini-
ción de una clase List. Pero una clase con el mismo nombre existe también en el pa-
quete java.awt que define el comportamiento y la apariencia de una lista de elementos
seleccionables en una interfaz gráfica de usuario. Las dos clases se pueden distinguir
porque están definidas en diferentes paquetes. Cada paquete define lo que se denomina
un espacio de nombres. Para hacer referencia a la clase List del paquete java.util, el
nombre de la clase se cualifica con el nombre del paquete, es decir, el nombre completo
de la clase sería la concatenación del nombre del paquete y el nombre de la clase (ha-
bitualmente separados por puntos): java.util.List. Si se quisiera hacer referencia a
la clase List del paquete java.awt se utilizaría el nombre java.awt.List. La mayo-
ría de lenguajes de programación proporcionan alguna forma de importar el espacio de
nombres de forma que no haga falta cualificar el nombre de cada tipo de datos con el
nombre del paquete donde está definido. Continuando con el ejemplo anterior, en Java
es posible importar la clase List del paquete java.util mediante el enunciado:
A partir de ese momento todas las referencias a List se resuelven como referencias a
java.util.List.
1 TAlumno = record
2 nombre : string ;
3 apellidos : string ;
4 edad : integer;
5 asignaturas : TListaAsignaturas ;
6 end;
1 < registro > ::= <id > ’=’ ’record ’ <lista - campos > ’end ’ ’;’
2 <lista - campos > ::= <id > ’:’ <id > ’;’ < lista - campos >
3 <lista - campos > ::= <id > ’:’ <id > ’;’
44 I NTRODUCCIÓN A LOS LENGUAJES DE PROGRAMACIÓN
2. Escribe la gramática del ejericio anterior utilizando la notación EBNF. ¿Es más
compacta esta representación? ¿Cuál es la principal diferencia?
1 < registro > ::= <id > ’=’ ’record ’ <lista - campos > ’end ’ ’;’
2 <lista - campos > ::= { <id > ’:’ <id > ’;’ }+
La gramática es más compacta, dado que sólo necesita una producción para el no
terminal <lista-campos>.
a) I ::= aIb|ab
b) I ::= abI|ε
1 <E > ::= <E > ’or ’ <T > | <T >
2 <T > ::= <T > ’and ’ <F > | <F >
3 <F > ::= ’not ’ <E > | ’(’ <E > ’) ’ | <id >
4 <id > ::= ’a ’ | ’b ’ | ’c ’
(a) <E>
<T>
<T>
’(’ <E> ’)’ <F>
<F> <id>
<id> ’c’
’b’
(b) <E>
<T> <F>
<id>
<T> ’and’ <F>
’c’
<F> <id>
<id> ’b’
’a’
1 <S > => <B > => ’a ’ A => ’a ’ <S > ’a ’ => ’a ’ <B > ’a ’
2 => ’a ’ ’a ’ <A > ’a ’ => ’a ’ ’a ’ <S > ’a ’ ’a ’
3 => ’a ’’a ’’a ’’a ’
1 <S > => <C > <C > => ’a ’ <S > ’a ’ <C >
2 => ’a ’ <S > ’a ’ ’a ’ <S > ’a ’ => ’a ’ ’a ’ ’a ’ <S > ’a ’
3 => ’a ’ ’a ’ ’a ’ ’a ’
1 int errorCode ;
2 char * message ;
3
4 void log () {
5 printf (" Found error %d: %s\n" , errorCode , message );
6 }
7
12 int i;
13 for(i = 0; i < length ; i ++) {
14 if( array [i] < 0) {
15 errorCode = 20;
16 }
17 }
18 if( errorCode != 0) {
19 message = " Check positive failed ";
20 log () ;
21 }
22 }
23
26 errorCode = 10;
27 message = " File not found ";
28 log () ;
29
30 int test [5] = {1 ,2 ,3 ,4 , -1};
E JERCICIOS PROPUESTOS 47
31 checkPositive ( test , 5) ;
32
33 return EXIT_SUCCESS ;
34 }
7. ¿Cuál sería la salida por pantalla del código del ejercicio anterior si C tuviera
ámbito dinámico?
mientras que para las máquinas es simple interpretarlo y generarlo. Está basado
en un subconjunto del lenguaje de programación JavaScript. Se puede encontrar
información sobre este formato en la página web http://www.json.org. En este
ejercicio se considerará un subconjunto de JSON denominado SimpleJSON que
sólo permite letras, números y los símbolos { } : . , [ ].
El lenguaje SimpleJSON tiene las siguientes características:
9 {" id ": " originalview ", " label ": " original
view "} ,
10 null ,
11 {" id ": " quality "} ,
12 {" id ": " pause "} ,
13 {" id ": " mute "}
14 ]
15 }}
Se pide diseñar una gramática independiente del contexto en notación BNF que
defina el lenguaje SimpleJSON. Cuando sea necesario indicar en la gramática
que puede aparecer una cadena de caracteres se puede hacer uso del no terminal
< cadena >. No es necesario definir el no terminal cadena. En el caso de los
números se puede utilizar el no terminal < numero >.
En el caso de que haya que derivar el no terminal < cadena > o < numero >, se
pondrá una línea desde el no terminal hasta la palabra o el número que represente,
respectivamente. Por ejemplo:
<cadena>
’antonio’
1 <sent - try > ::= ’try ’ ’{’ <sent - list > ’}’ <catch - list >
2 <sent - list > ::= <sent > <sent - list > | <sent >
3 <sent > ::= <sent - try > | ...
4 <catch - list > ::= < catch > <catch - list > | <catch >
5 <catch > ::= ’ catch ’ ’(’< id > <id > ’) ’ ’{’< sent - list > ’} ’
1 try {
2 int v1 = matriz [i ][ j ];
3 int v2 = matriz [j ][ i ];
4 } catch ( ArrayIndexOutOfBoundsException e) {
5 System . out . println (" Índice fuera de límites : " + e.
getMessage () );
6 }
1 try {
2 FileReader fr = new FileReader (" message . eml ");
3 readMessage ( fr );
4 } catch ( FileNotFoundException e) {
5 // Un sistema de logging muy básico
6 System . out . println (" No se encuentra el fichero : " +
e. getMessage () );
7 } catch( IOException e) {
8 System . out . println (" Excepción no esperada : " + e.
getMessage () );
9 }
1 try {
2 fr = new FileReader (" config . ini ");
3 } catch ( FileNotFoundException e) {
4 try {
5 fr = new FileReader ("/ home / user / config . ini "
);
6 } catch( FileNotFoundException e) {
7 System . out . println (" No se encuentra el
fichero de configuración ");
8 }
9 }
Procesadores de lenguajes
En este capítulo se pretende ofrecer una visión general de las diferentes etapas de trans-
formación de un programa, desde un código fuente escrito por un programador, hasta un
fichero ejecutable que pueda “entender” directamente una máquina. El capítulo se inicia
con un breve repaso a los principales tipos de procesadores de lenguajes, ofreciendo una
definición sucinta de cada uno de ellos, así como una enumeración de sus objetivos y
características más importantes. De entre todos los tipos de procesadores de lenguajes
existentes se destacan los compiladores, de los que se ofrecerá una explicación más de-
tallada y serán usados como modelo para ejemplificar las diferentes fases que pueden
encontrarse en un traductor de lenguajes.
2.1 Introducción
Por procesadores de lenguajes se entiende el conjunto genérico de aplicaciones infor-
máticas en las cuales uno de los datos de entrada es un programa escrito en un lenguaje
de programación. Se trata, por tanto, de cualquier programa informático capaz de proce-
sar documentos escritos en algún lenguaje de programación.
La historia de los procesadores de lenguajes ha ido siempre de la mano del desarrollo
de los lenguajes de programación. Según iban apareciendo artefactos de programación
o propiedades nuevas en el diseño y desarrollo de los lenguajes, la tecnología asociada
a los procesadores ha tenido que adaptarse a ellos para dar respuesta a las nuevas rea-
lidades que se presentaban. Puede decirse, por tanto, que los avances alcanzados en el
desarrollo de los procesadores de lenguajes han sido consecuencia directa del desarrollo
de nuevos modelos y paradigmas de programación.
Como se indicaba en el capítulo anterior, las primeras computadoras ejecutaban instruc-
ciones consistentes únicamente en códigos binarios. Con dichos códigos los progra-
madores establecían los estados de los circuitos integrados correspondientes a cada una
de las operaciones que querían realizar. Esta expresión mediante ceros y unos se llamó
lenguaje máquina y constituye lo que se conoce como lenguajes de primera generación.
53
54 P ROCESADORES DE LENGUAJES
Pero estos lenguajes máquina no resultaban naturales para el ser humano y, por ello, los
programadores se inclinaron por tratar de escribir programas empleando abstracciones
que fueran más sencillas de recordar que esos códigos binarios que se empleaban hasta
ese momento. Después simplemente había que traducir esas abstracciones de forma
manual a lenguaje máquina. Estas abstracciones constituyeron los llamados lenguajes
ensamblador, y se generalizaron en el momento en que se pudo hacer un proceso auto-
mático de traducción a código máquina por medio de un ensamblador. Estos lenguajes
ensamblador constituyeron lo que se conoce como lenguajes de segunda generación.
Sin embargo, para un programador el lenguaje ensamblador continuaba siendo el lenguaje
de una máquina, aunque hubiera supuesto un avance respecto a los lenguajes máquina
propiamente dichos, expresados con códigos binarios. El desarrollo de los lenguajes se
orientó entonces hacia la creación de lenguajes de programación capaces de expresar las
distintas acciones que quería realizar el programador de la manera más sencilla posi-
ble. Se propuso entonces un lenguaje algebraico, el lenguaje FORTRAN (FORmulae
TRANslating system), que permitía escribir fórmulas matemáticas de manera traducible
por un ordenador. Representó el primer lenguaje de alto nivel y supuso el inicio de los
lenguajes de tercera generación.
Surgió entonces por primera vez el concepto de traductor como un programa que trans-
formaba un lenguaje en otro. Cuando el lenguaje a traducir es un lenguaje de alto nivel
y el lenguaje traducido es de bajo nivel, entonces se dice que ese traductor es un compi-
lador, y como alternativa al uso de compiladores se propusieron también los llamados
intérpretes. Mediante el uso de intérpretes no existe un proceso de traducción previo al
proceso de ejecución; al contrario que en el caso de los compiladores, las instrucciones
presentes en el código fuente se traducen mientras se van ejecutando. Compiladores e
intérpretes suponen ejemplos paradigmáticos de lo que es un procesador de lenguajes y
se verán en detalle a lo largo de este capítulo.
2.2.1 Traductores
Un traductor es un tipo de procesador en el que tanto la entrada como la salida son
programas escritos en lenguajes de programación. Su objetivo es procesar un código
fuente y generar a continuación un código objeto.
Se dice que el código fuente está escrito en un lenguaje fuente (LF), que en la mayoría
de los casos es un lenguaje de alto nivel, aunque podrían ser también de bajo nivel; por
tanto, el lenguaje fuente es el lenguaje origen que transforma el traductor.
T IPOS DE PROCESADORES DE LENGUAJES 55
El código objeto se dice que está escrito en un lenguaje objeto (LO) que podría ser
un lenguaje máquina de un microprocesador determinado, un lenguaje ensamblador o,
incluso, un lenguaje de alto nivel. Este lenguaje objeto es, por tanto, el lenguaje al que
se traduce el texto fuente.
Por último, se conoce como lenguaje de implementación (LI) al lenguaje en el que está
escrito el propio traductor, y puede ser cualquier tipo de lenguaje de programación, desde
un lenguaje de alto nivel a un lenguaje máquina.
En general para mostrar el esquema básico de un traductor se suele utilizar la notación
en T (tal como se muestra en la Figura 2.1), que puede representarse de forma abreviada
como: LFLI LO [7].
2.2.2 Ensambladores
Cuando se tiene un traductor donde el lenguaje fuente es lenguaje ensamblador y el
lenguaje objeto es lenguaje máquina, dicho programa se dice que es un ensamblador.
Los ensambladores son traductores en los que el lenguaje fuente tiene una estructura
sencilla que permite la traducción de una sentencia en el código fuente a una instrucción
en lenguaje máquina.
Hay ensambladores que tienen macroinstrucciones en su lenguaje que deben traducirse
a varias instrucciones máquina. A este tipo de ensambladores se les conoce como
macroensambladores y representan ensambladores avanzados con instrucciones com-
plejas que resultaron muy populares en los años 50 y 60, antes de la generalización de
los lenguajes de alto nivel.
Para que un programa fuente pueda ser ejecutado, el código máquina generado debe
ser ubicado en memoria a partir de una determinada dirección absoluta. En el caso de
programas formados por un único módulo fuente, y que no utilizan librerías, es posi-
ble generar código ejecutable a partir de código ensamblador de forma sencilla. Sin
embargo, con un ensamblador se crea por lo general código reubicable, es decir, con
direcciones de memoria relativas. Cuando se tienen subprogramas o subrutinas se gene-
ra un conjunto de códigos reubicables que después hay que enlazar en un único código
reubicable para su ejecución. Esto se hace en una fase posterior conocida como enlazado.
56 P ROCESADORES DE LENGUAJES
2.2.3 Compiladores
Los procesadores de lenguajes más habituales son los compiladores, aplicaciones ca-
paces de transformar un fichero de texto con código fuente en un fichero en código
máquina ejecutable. Un compilador es, por tanto, un programa traductor que transforma
un código fuente escrito en un lenguaje de alto nivel a un código en lenguaje de bajo
nivel.
El programa ejecutable generado por un compilador deberá estar preparado para su eje-
cución directa en una arquitectura determinada. Esta fase de traducción supone un pro-
ceso bastante complejo y el código máquina generado se espera que sea, en la medida
de lo posible, rápido y con un consumo de memoria lo más reducido posible. Además,
generalmente se dispone únicamente de un conjunto limitado de instrucciones de bajo
nivel de la máquina destino, mientras el código fuente puede estar escrito en un lenguaje
abstracto que requiera realizar transformaciones intermedias. Estos aspectos, entre otros,
revelan la dificultad que puede suponer el proceso de compilación de un código fuente
en una arquitectura determinada.
Por todo esto, el diseño de un compilador se suele dividir en etapas –o fases– que fa-
cilitan su construcción, y donde en cada etapa se reciben los datos de salida de la etapa
anterior, generando a su vez datos de salida para la etapa posterior. Las etapas en las que
se divide un compilador pueden agruparse en:
En el apartado 2.3 se explicarán en detalle cada una de estas etapas, así como sus fases.
Como se verá a lo largo del capítulo nos centramos más en las etapas de análisis que en
las de síntesis.
En el diseño de un compilador hay que tener en cuenta ciertos factores como pueden ser:
Respecto a la complejidad del lenguaje fuente, y como se verá más adelante en este
mismo apartado, esto puede implicar la necesidad de que el compilador necesite realizar
varias lecturas del código fuente para poder obtener toda la información necesaria para
la fase de análisis. Esto en su momento resultaba costoso y, por ello, remarcable. Sin
embargo, hoy en día y con la capacidad de cómputo de los ordenadores actuales, el hecho
de realizar varias pasada en el proceso de compilación no supone un gran problema en
términos de eficiencia del compilador.
A partir de la descripción que se ha dado de un compilador podría parecer que todos
los compiladores son prácticamente iguales, y que las únicas diferencias vendrán dadas
por las características de los lenguajes que se quieren compilar y de la arquitectura de
la máquina en la que se compila. Sin embargo, existen diferencias más allá de las que
implica considerar diferentes lenguajes fuente y objeto.
Los compiladores pueden clasificarse considerando diferentes criterios como, por ejem-
plo, el tipo de código máquina que generan o cómo se realiza en cada caso el proceso
de compilación. Así, dependiendo del tipo de código máquina que generan, pueden
clasificarse entre compiladores que generan:
• Código máquina puro; es decir, los compiladores que generan código para un
conjunto de instrucciones máquina particulares, sin asumir la existencia de ningún
sistema operativo o librería de rutinas. Este tipo de compiladores son de rara
aplicación, casi exclusiva en la implementación de sistemas operativos y otro tipo
de software de bajo nivel.
• Compilador cruzado. Éste es el caso en el que se genera código objeto para una
arquitectura diferente a la que se utiliza en el proceso de compilación. Cuando se
desea construir un compilador para un nuevo procesador, ésta es la única forma de
construirlo.
• Compilador incremental. Se conocen así a los compiladores que, tras una primera
fase de compilación en la que se detectan errores, vuelve a compilar el programa
corregido pero analizando únicamente las partes del código fuente que habían sido
modificadas.
Por último, cuando se usa un compilador, el programa fuente y los datos se procesan en
diferentes momentos, de modo que al tiempo que se requiere para traducir el lenguaje
de alto nivel a lenguaje objeto se le denomina tiempo de compilación, y al tiempo que
se emplea en ejecutar el programa objeto sobre los datos de entrada se le conoce como
tiempo de ejecución (véanse las Figuras 2.2 y 2.3).
T IPOS DE PROCESADORES DE LENGUAJES 59
Enlazadores
En ocasiones, como hemos visto, un código objeto en lenguaje máquina no puede ser
ejecutado directamente ya que necesita ser enlazado con librerías propias del sistema
operativo. Para ello, entre el proceso de compilación y ejecución existe un proceso de
montaje de enlaces, posible siempre y cuando en un lenguaje fuente se permita una frag-
mentación del código en partes, denominados de diferente modo según sea el lenguaje
de programación empleado: módulos, unidades, librerías, procedimientos, funciones,
subrutinas, . . .
De este modo, el enlazador (también conocido como montador de enlaces) genera un
archivo binario final que ahora sí podrá ejecutarse directamente. Las diferentes partes
en las que se divide el código fuente pueden compilarse por separado, produciéndose
códigos objetos para cada una de ellas. El montador de enlaces se encargará entonces
de realizar la unión de los distintos códigos objeto, produciendo un módulo de carga
que será el programa objeto completo, y siendo después el cargador quien lo coloque en
memoria para iniciar su ejecución.
Por tanto, un enlazador deberá tomar los ficheros de código objeto generados en los
primeros pasos del proceso de compilación, así como la información de todos los recur-
sos necesarios para la compilación completa, y deberá eliminar recursos que no necesite
60 P ROCESADORES DE LENGUAJES
y enlazar el código objeto, con lo que finalmente producirá un fichero ejecutable (véase
Figura 2.4). En el caso de programas enlazados dinámicamente, el enlace entre el eje-
cutable y las librerías se realiza en tiempo de carga o ejecución del programa.
Cargadores
Un cargador tiene como función principal asignar el espacio necesario en memoria a
un programa, pasando después el control a la primera de las instrucciones a ejecutar, y
comenzando a continuación el proceso de ejecución.
Como ya se ha visto, los procesos de ensamblado y carga están muy relacionados, y así
algunos tipos especiales de cargadores incluyen, además, los procesos de reubicación y
enlazado. En este sentido, las funciones generales de un cargador según [9] son:
3. Ajustar todas las direcciones del código de acuerdo al espacio disponible en memo-
ria (reubicación).
Otros sistemas tienen estas funciones separadas, con un enlazador para realizar las ope-
raciones de montaje y un cargador para manejar la reubicación y carga de los programas
en memoria.
2.2.4 Intérpretes
Los intérpretes son programas que analizan y ejecutan una a una las instrucciones que
encuentran en un código fuente. Por tanto coexisten en memoria con el programa fuente,
de modo que el proceso de traducción se realiza en tiempo de ejecución. En la Figura
2.5 se muestra el esquema funcional de este tipo de procesadores de lenguajes.
En general, este tipo de procesadores de lenguajes realiza dos tipos de operaciones.
En primer lugar, se realiza la traducción del código fuente a un formato intermedio
(aunque en realidad esta operación no sería obligatoria) y, a continuación, se interpreta el
código traducido. En el caso de realizarse esta transformación a un formato intermedio,
éste podría ser simplemente el resultado del análisis sintáctico-semántico como es la
traducción a notación posfija (véanse apartados 2.3.2 y 2.3.3). En este caso, la primera
fase de análisis se correspondería con la compilación a código intermedio.
En algunos lenguajes las dos operaciones descritas anteriormente se han separado por
completo, de modo que se tiene un compilador que traduce el código fuente a un código
intermedio, comúnmente denominado bytecode, y un intérprete que ejecuta dicho código
intermedio.
T IPOS
DE PROCESADORES DE LENGUAJES
Figura 2.4: Proceso de compilación, montaje y ejecución
61
62 P ROCESADORES DE LENGUAJES
Las máquinas virtuales de aplicación se han extendido enormemente gracias, entre otras
cosas, al desarrollo de Internet y de las aplicaciones web programadas en Java. En este
caso, la máquina virtual es la encargada de traducir el código intermedio Java, conocido
como bytecode, y generado por el compilador de Java a partir de un código fuente, a ins-
trucciones en código máquina para la arquitectura hardware sobre la que esté corriendo
la máquina virtual. En este caso, la máquina virtual se ejecuta como un proceso dentro de
un sistema operativo. Su objetivo es proporcionar un entorno de ejecución independiente
de la plataforma y del sistema operativo, que oculte los detalles de la arquitectura y per-
mita que un programa se ejecute siempre de la misma forma sobre cualquier arquitectura
y sistema operativo. En el ejemplo de la Figura 2.6 se muestra cómo una aplicación Java
como es el entorno de desarrollo Eclipse (después de haber sido compilado el fuente
.java y obtenido el bytecode .class) se ejecuta sobre una máquina virtual de Java
(Java Virtual Machine, JVM) en un sistema operativo Linux (el sistema operativo 1 del
ejemplo). Si el sistema operativo 1 hubiera sido otro, por ejemplo un MacOS, entonces
la JVM sería la encargada de transformar el mismo bytecode a instrucciones en dicho
sistema operativo.
Por otro lado, también se han popularizado mucho las máquinas virtuales de sistema
entre usuarios que quieren correr aplicaciones compiladas en un sistema operativo en
otro no compatible; por ejemplo, si se quiere ejecutar una aplicación de Windows en una
máquina con sistema operativo Linux o MacOS. De este modo, gracias a las máquinas
virtuales de sistema pueden coexistir en el mismo ordenador varios sistemas operativos
distintos. El uso de este tipo de software es muy común, por ejemplo, para probar un
sistema operativo nuevo sin necesidad de instalarlo directamente, o para tener en una
misma máquina instalados varios servidores, de modo que todos ellos accedan a recursos
comunes. Algunos ejemplos de este tipo de máquinas virtuales son aplicaciones como
VirtualBox, VMWare, etc.
En el ejemplo de la Figura 2.6 se muestra cómo es posible ejecutar una aplicación com-
pilada (en el ejemplo, la suite ofimática Office de Microsoft compilado en un sistema
operativo Windows) en una máquina con un sistema nativo diferente (en el ejemplo,
Ubuntu GNU/Linux). De este modo, cualquier aplicación Windows podrá ejecutarse
en el sistema del ejemplo, ya que la máquina virtual de sistema (en el ejemplo, la apli-
cación VirtualBox) se encargará de transformar los ejecutables de Windows a instruc-
ciones propias del sistema operativo Linux. Por último, también podemos observar en
el ejemplo que las aplicaciones compiladas para el sistema operativo nativo se ejecutan
directamente.
Las máquinas virtuales de sistema permiten disponer de dispositivos virtuales (discos,
dispositivos de red, etc.) diferentes a los de la plataforma real sobre la que se está
trabajando. Otra característica importante es que no es necesaria realizar ninguna con-
figuración de red entre las máquinas con sistema operativo anfitrión y invitado, ya que
la configuración que se tenga en el anfitrión es tomada directamente por el invitado.
Además, los sistemas de ficheros empleados por las máquinas virtuales permiten el ac-
ceso y modificación de archivos desde el sistema operativo anfitrión hasta el invitado.
T IPOS DE PROCESADORES DE LENGUAJES 65
Decompiladores
Desensambladores
Depuradores
nera que éste continúe en un punto diferente al punto en el que fue detenido. En algunos
casos, los depuradores permiten incluso modificar el código fuente introduciendo nuevas
instrucciones para continuar después con su ejecución. Además, permiten comprobar el
código objeto generado por cada instrucción del código fuente.
Por otro lado, es importante también destacar que un programa en depuración puede pre-
sentar un comportamiento diferente al que tendría si se ejecutara directamente. Esto es
debido a que el depurador puede cambiar ligeramente los tiempos internos del programa,
algo que puede afectar especialmente a sistemas complejos.
Analizadores de rendimiento
Los analizadores de rendimiento permiten examinar el comportamiento de los progra-
mas en tiempo de ejecución, de modo que podemos saber qué partes del código son
más eficientes y cuáles deberían mejorar su rendimiento y, por tanto, deberían ser re-
programadas o simplemente revisadas. La mayor parte de los compiladores incorporan
analizadores de rendimiento.
Optimizadores de código
Los optimizadores de código son programas que realizan modificaciones sobre el código
intermedio para mejorar la eficiencia de un programa. Se trata de programas que sue-
len estar incluidos en los compiladores, de modo que pueden ser llamados por medio
de opciones específicas de compilación. Entre estas opciones de optimización destacan
la velocidad de ejecución y el tamaño final del código ejecutable; otras opciones posi-
bles son: eliminar la comprobación de rangos o desbordamientos de pila, evaluación en
cortocircuito para expresiones booleanas, eliminación de código y rutinas no utilizadas,
etc.
En definitiva, el objetivo final de un programa optimizador es crear un nuevo código más
compacto y eficiente, eliminando instrucciones que no se ejecutan nunca, simplificando
expresiones aritméticas, etc. Existen teoremas que demuestran que la optimización per-
fecta es indecidible, es decir, que no hay manera de decidir cuál es la mejor forma de
optimizar un código [2]. Las optimizaciones que pueden realizarse en un código pueden
clasificarse entre las dependientes de la máquina, como son la asignación de registros
o la reordenación de código, y las independientes de la máquina, como la eliminación
de redundancias. Este tipo de procesos resultan especialmente complejos y consumen la
mayor parte del tiempo de ejecución del compilador.
A continuación se muestra un ejemplo de una de las técnicas más comunes de opti-
mización de código: la eliminación de operaciones redundantes en subexpresiones. Si,
por ejemplo, se tiene una expresion como x = A ∗ f (y) + B ∗ f (y)2 , realmente se puede
considerar como t = f (y) y x = A ∗ t + B ∗ t 2 . Existe una redundancia, de forma que
f (y) se puede evaluar una sola vez en lugar de dos. Esta eliminación de subexpresiones
comunes se realiza frecuentemente al evaluar expresiones.
68 P ROCESADORES DE LENGUAJES
Preprocesadores
Otro tipo muy común de procesadores de lenguajes son los llamados editores de lengua-
jes de programación. A diferencia de otros tipos de procesadores que forman parte del
proceso de compilación, los editores de lenguajes son programas independientes cuya
función es ayudar al programador en el proceso de creación de un código fuente.
Los editores de lenguajes de programación suelen ofrecer resaltado de sintaxis, es de-
cir, que permiten llamar la atención del programador mientras éste está escribiendo un
programa; y lo hacen por medio del uso de diferentes colores para diferenciar diferen-
tes tipos de tokens como palabras reservadas, declaración de variables, etc. Algunos
permiten incluso la función de auto-completar código. Resultan muy útiles para un pro-
gramador, mostrando sugerencias según el contexto.
Además del resaltado de sintaxis, los editores de lenguajes de programación suelen ofre-
cer otras funcionalidades, como la posibilidad de editar varios documentos a la vez u
ofrecer una multi-vista, lo que significa que se puede tener más de una vista de un mismo
código, de modo que el programador puede visualizar simultaneamente dos versiones o
dos partes de un mismo documento. Permiten realizar también acciones de búsqueda
y reemplazo, pudiéndose en muchos casos utilizar incluso expresiones regulares para
E STRUCTURA DE UN COMPILADOR 69
definir los patrones que se deseen reemplazar. En muchos casos ofrecen, ademnás,
menús contextuales y detección automática del estado del documento, algo muy útil
en caso que se desee guardar un archivo que había sido modificado por otro usuario o
programa. Otras características son los asistentes de código para diferentes lenguajes, lo
que suele realizarse por medio de menús contextuales, o la existencia de herramientas
integradas para el trabajo con base de datos.
Algunos editores permiten incluso realizar tareas de refactorización del código, es decir,
que permite cambiar la estructura interna del programa sin modificar su comportamiento
funcional, con el fin de hacerlo más fácil de entender y de modificar en un futuro. Esta
función de refactorización es especialmente útil si queremos hacer modificaciones o
actualizaciones en el código que afecten a diferentes partes del mismo. Un ejemplo
sería renombrar el nombre de un método, de forma que resulte más claro. Se podrían
modificar automáticamente todas las llamadas al método presentes en el código de un
programa. Otra refactorización muy común es convertir un fragmento de código en un
método. Para ello el programador debería crear un nuevo método, copiar el fragmento
de código en el cuerpo del método, buscar en el código extraído referencias a variables
locales en el código original y convertirlas en parámetros y variables locales del nuevo
método, reemplazar el código extraído por una llamada al nuevo método y, por último,
eliminar las declaraciones correspondientes a las variables que ahora son variables lo-
cales del nuevo método. Algunos editores como Eclipse, Komodo Edit o JEdit permiten
realizar estas operaciones de forma automática.
primeras fases se corresponden con el análisis de un programa fuente y las últimas con
la síntesis de un programa objeto. De las dos fases, la síntesis es la que requiere técnicas
más elaboradas.
Otra forma alternativa de estructurar las fases de las que se compone un compilador
es distinguiendo entre: front-end, parte en la que se analiza el código, se comprueba
su validez, se genera el árbol de derivación y se rellena la tabla de símbolos; y back-
end, donde se genera el código máquina. La idea de esta división es poder aprovechar
la parte del front-end si lo que se busca es implementar compiladores de un mismo
lenguaje para diferentes arquitecturas. De este modo, toda la fase relativa al análisis
del código y generación de código intermedio sería común para todos los compiladores
de este lenguaje, pudiendo ser reutilizada, al contrario que las fases relacionadas con la
generación del código objeto final, que dependerán de la arquitectura del ordenador y,
por tanto, deberán ser diferentes en cada caso.
De este modo, el front-end se compone de las fases de análisis léxico, sintáctico, semán-
tico y generación de código intermedio, mientras que el back-end comprende las fases
de generación y optimización de código. En la Figura 2.8 se muestra un esquema de
las etapas de análisis y síntesis de un compilador, así como de las fases de front-end y
back-end.
• Lexema, definido como la secuencia de caracteres del programa fuente que coin-
cide con el patrón que describe un token, es decir, cada una de las instancias de un
token que el analizador léxico identifica.
• Patrón, que define la forma que pueden tomar los diferentes lexemas.
• Token, definido como un par formado por un nombre de token y un valor de atri-
buto opcional. El nombre del token es un símbolo abstracto que representa un
tipo de unidad léxica, por ejemplo, una secuencia de caracteres que representa un
identificador [2].
Por tanto, un token se describe por medio de un patrón y un lexema representa un con-
junto de caracteres que concuerdan con dicho patrón. Los tokens pueden definirse en-
tonces como secuencias de símbolos que tienen un significado propio que representan
símbolos terminales (constantes) de la gramática del analizador sintáctico. Cada token
puede tener una información asociada a su lexema con un significado coherente en un
determinado lenguaje de programación. Entre esta posible información asociada pode-
mos encontrar un valor concreto, un literal, un tipo de datos, etc. Ejemplos de tokens,
podrían ser palabras clave (if, else, while, int, ...), identificadores, números, signos,
o un operador (:=, ++, -).
La cadena de símbolos que constituye el programa fuente se lee de izquierda a derecha,
y durante este proceso de análisis léxico se leen los caracteres de la entrada y se genera
la secuencia de tokens encontrados.
A continuación se muestra un ejemplo ilustrativo del proceso llevado a cabo en esta
primera fase de análisis léxico. Sea una sentencia de un lenguaje:
los componentes léxicos detectados en esta fase serían los mostrados a continuación. En
este ejemplo, un token sería el par < ID,′ velocidad ′ >, mientras que un lexema sería
la sucesión de caracteres velocidad.
Durante esta fase se ignoran también los comentarios que pudiera haber en el código
fuente y que no forman parte de la semántica funcional del programa, así como los deli-
mitadores (espacios, caracteres de fin de línea, . . . ). También se relacionan los mensajes
de error que se pudieran producir con las líneas del programa fuente y se introducen los
identificadores encontrados, así como sus valores, en la tabla de símbolos. Como mues-
tra la Figura 2.8, la tabla de símbolos se utiliza durante todo el proceso de compilación,
pero es en la fase ulterior de análisis semántico en la que quizá tiene más importancia.
Por ello se ha decidido describirla y tratarla con más en detalle en el apartado correspon-
diente al estudio del análisis semántico.
La detección de tokens llevada a cabo en esta fase de análisis léxico de un compilador
se realiza con gramáticas y lenguajes regulares. Es necesario, por tanto, introducir una
serie de conceptos teóricos relacionados con gramáticas y patrones que nos servirán
para comprender cómo se realiza esta detección de tokens. A continuación se definen
los conceptos de alfabeto, cadena, gramática regular y lenguaje regular.
Alfabetos y cadenas
El conjunto de todas las cadenas que se pueden formar con los símbolos de un alfabeto
se denomina universo del discurso, W , y se representa por W (V ), donde V es el alfabeto
del lenguaje.
Gramáticas regulares
Se conoce como gramática formal a un conjunto de reglas que permite formar cadenas de
caracteres a partir de un alfabeto dado. Su objetivo no es describir el significado de ca-
denas bien formadas, sino simplemente su forma. Más específicamente, una gramática
74 P ROCESADORES DE LENGUAJES
regular es una gramática formal que se define como una cuadrupla formada por un vo-
cabulario terminal T (constantes), un vocabulario no terminal N (variables), un símbolo
inicial S, y un conjunto de producciones o reglas de derivación P.
G = (T, N, S, P)
donde todas las cadenas del lenguaje definidas por dicha gramática estarán formadas
por símbolos del vocabulario terminal T . Este vocabulario terminal se define por la
enumeración de símbolos terminales.
El vocabulario no terminal N es el conjunto de símbolos introducidos como elementos
auxiliares para la definición de las producciones de la gramática, y que no figuran en
las sentencias del lenguaje. La intersección entre los vocabularios T y N es el conjunto
vacío.
El símbolo inicial S es un símbolo no terminal a partir del cual se pueden obtener todas
las sentencias del lenguaje definido por la gramática.
Las producciones o reglas de derivación P son transformaciones de cadenas de símbolos
que se expresan mediante unos antecedentes y consecuentes separados por una flecha. A
la izquierda –como antecedente– está el símbolo o conjunto de símbolos a transformar,
y a la derecha –como consecuente– los símbolos obtenidos a partir de la transformación.
El conjunto de producciones se define por medio de la enumeración de las distintas pro-
ducciones. Un ejemplo producción sería:
A→w
S
donde A ∈ N, es decir, A es un no terminal; w es una cadena sobre T N, con un único no
terminal como máximo, y debiendo estar éste siempre situado al final de la producción.
Por tanto, una producción puede interpretarse como una regla de reescritura que permite
sustituir un elemento no terminal por la cadena de símbolos expresada en el consecuente
de una de las producciones y con ese no terminal como antecedente. Esta sustitución se
conoce como paso de reescritura o paso de derivación.
Dado el símbolo inicial de una gramática pueden realizarse varios pasos de derivación
hasta llegar a una palabra. Como ejemplo de paso de derivación se tiene lo siguiente.
A partir de una gramática con un vocabulario terminal T = {a, b}, un vocabulario no
terminal N = {S, A}, y el siguiente conjunto de producciones P:
S → bA
S→b
A → aaA
A→b
Además, una gramática se puede escribir de una forma más compacta, de modo que las
producciones siguientes:
S → bA
S→b
A → aaA
A→b
S → bA | b
A → aaA | b
S → bA | b
A → aaA | b | ε
S ⇒ bA ⇒ b
S ⇒ bA ⇒ baaA ⇒ baa
S ⇒ bA ⇒ baaA ⇒ baaaaA ⇒ baaaa
S ⇒ bA ⇒ baaA ⇒ baaaaA ⇒ baaaab
76 P ROCESADORES DE LENGUAJES
A partir de las gramáticas regulares se definen lo que se conoce como lenguajes regula-
res, que se verán en detalle a continuación.
Lenguajes regulares
Desde un punto de vista lingüístico, y a partir de la definición que dió Noam Chomsky
en su libro “Estructuras sintácticas” de 1957, un lenguaje es un conjunto finito o infinito
de oraciones, donde cada una de las cuales posee una extensión finita y está construida
a partir de un conjunto finito de elementos [6].
Reduciendo el ámbito de la definición, un lenguaje formal se define como un conjunto de
cadenas de símbolos de un alfabeto determinado; por tanto, se define como el conjunto
de todas las sentencias formadas por símbolos terminales que se puedan generar a partir
de una gramática.
Un lenguaje L(G) generado por una gramática G se expresa como:
Entonces se dice que una sentencia pertenece a un lenguaje si está compuesta de sím-
bolos terminales, y si es posible derivarla a partir del símbolo inicial S por medio de la
aplicación de producciones de la gramática G.
Un lenguaje regular es un tipo de lenguaje formal definido a partir de una gramática re-
gular o una expresión regular. Como ejemplo de cadena válida dentro de una gramática
regular, un identificador de un lenguaje de programación se puede definir como una letra
seguida de cero o más letras o dígitos. Decimos entonces que el lenguaje regular está
formado por todos los nombres de identificadores posibles generados con las reglas de
la gramática regular. De este modo, un identificador sería cualquier palabra derivada de
las siguientes producciones:
S → aR | bR | . . . | zR | a | b | . . . | z
R → aR | bR | . . . | zR | a | b | . . . | z | 0R | 1R | . . . | 9R | 0| . . . | 9
Un lenguaje regular se puede definir a partir de expresiones regulares, que son equiva-
lentes a las gramáticas regulares pero con una notación mucho más compacta.
Las expresiones regulares se construyen utilizando los operadores unión ( | ), concate-
nación (.) y cierre de Kleene (∗) -el carácter que lo precede puede aparecer cero, una, o
más veces-. Además, se pueden emplear cuantificadores para especificar la frecuencia
con la que un carácter puede ocurrir. Los cuantificadores más comunes son los siguien-
tes: +, que indica que el carácter al que sigue debe aparecer al menos una vez; y ?,
que indica que el carácter al que sigue puede aparecer como mucho una vez. El uso
de paréntesis permite definir el ámbito y precedencia de estos operadores que pueden
combinarse libremente dentro de la misma expresión.
E STRUCTURA DE UN COMPILADOR 77
Además podrán usarse paréntesis si fuera necesario. Otros ejemplos de expresiones re-
gulares:
a | b → {a, b}
(a | b)(a | b) → {aa, ab, ba, bb}
aa | ab | ba | bb → {aa, ab, ba, bb}
(a | b)∗ → {ε, a, b, aa, bb, ab, ba, abab, . . .}
7. ∗ es idempotente: r ∗ ∗ = r∗
78 P ROCESADORES DE LENGUAJES
puede decir que las gramáticas independientes de contexto suponen una superclase de
las gramáticas regulares. El término libre de contexto se refiere al hecho de que un no
terminal puede siempre ser sustituido sin tener en cuenta el contexto en el que aparece.
El proceso de derivación en este caso es igual que en las gramáticas regulares. Durante
este proceso se sustituye el símbolo inicial por alguna cadena válida definida en alguna
producción. A continuación, los no terminales de esta cadena se sustituyen por otra
cadena, y así sucesivamente hasta que solo se tengan cadenas de símbolos terminales.
Como ya se ha visto, la notación más frecuentemente utilizada para expresar gramáti-
cas libres de contexto es la forma Backus-Naur (BNF) y la Extended BNF (EBNF). Si-
guiendo con el ejemplo utilizado en la etapa de análisis léxico, a continuación se muestra
un ejemplo del proceso llevado a cabo en esta fase de análisis sintáctico.
Sea una gramática independiente de contexto descrita en notación BNF:
< asignacion >::=< identi f icador >′ :=′ < expresion >
< expresion >::=< termino > | < expresion >′ +′ < termino >
< termino >::=< f actor > | < termino >′ ∗′ < f actor >
< f actor >::=< identi f icador > |′ (′ < expresion >′ )′
• Recuperarse del error para poder seguir examinando errores sin necesidad de cor-
tar el proceso de compilación.
En esta fase de análisis, y una vez detectados los errores sintácticos en el código fuente,
existen varias estrategias para corregirlos.
• Por último, se puede realizar una correccion global, es decir, dada una secuencia
completa de tokens a ser reconocida, si hay algún error por el que no se puede
reconocer la cadena a partir de una gramática, entonces se trata de encontrar la
construcción sintáctica más parecida a la dada que sí pueda ser reconocida.
Gramáticas atribuidas
Tabla de símbolos
Figura 2.11: Código a partir del cual se genera la tabla de símbolos de la Tabla 2.1
Tabla 2.1: Ejemplo de entradas en una tabla de símbolos empleada en el análisis del código
que se muestra en la Figura 2.11
Las funciones asociadas a las tablas de símbolos están relacionados con las tareas de
búsqueda, inserción, cambio de valor, y borrado de una entrada. La búsqueda consiste en
encontrar, a partir del nombre de un elemento, su valor dentro de la tabla de símbolos. En
el caso de la inserción, dado un par nombre-valor, el objetivo es introducir un elemento
nuevo a la tabla. El cambio de valor consiste en buscar un elemento y modificar su valor
dentro de la tabla de símbolos. Por último, con el borrado se busca la eliminación de un
elemento de la tabla.
Normalmente los lenguajes de programación tienen lo que se conoce como ámbitos. En
el caso de las tablas de símbolos de estructuras de bloques, empleadas en lenguajes de
estructura de bloques, toda línea dentro de un programa está contenida en uno o más
bloques que definen ámbitos de validez de nombres. El ámbito definido por el bloque
más profundo que contiene la instrucción que estamos analizando se llama ámbito actual.
E STRUCTURA DE UN COMPILADOR 85
Los ámbitos que incluyen a una línea de un programa son abiertos respecto a esa línea.
Los que no la incluyen se dice que son cerrados respecto a esa línea. Por ejemplo, si se
tiene el siguiente código por bloques:
1 { // Bloque 1
2 int a , b , c , d;
3 { // Bloque 2
4 int e , f;
5 L1 :
6 }
7 { // Bloque 3
8 int g , h;
9 L2 : { // Bloque 4
10 int a;
11 }
12 L3 :
13 }
14 }
Si tomamos como referencia la línea 10 ( int a;), los ámbitos abiertos serían los co-
rrespondientes a los bloques 1, 3 y 4, mientras que el bloque 2 es un ámbito cerrado.
Gracias a la tabla de símbolos y a la información contenida en ella, se puede recuperar
correctamente el valor que tiene, por ejemplo, una variable a, dependiendo del ámbito
en el que nos encontremos.
Hay dos modos de implementar las tablas de símbolos de bloques: una tabla por ámbito y
una tabla común para todos los ámbitos. La tabla única se suele utilizar en compiladores
de un solo paso en los que se descarta la información referente a un ámbito cuanto éste se
cierra. Un compilador de múltiples pasos suele requerir una tabla individual por ámbito.
Tratamiento de errores
De un modo similar al de la tabla de símbolos, el tratamiento de errores se realiza a lo
largo de todas las fases de traducción, pero es en la fase de análisis semántico en la que
tiene más importancia. Este es el motivo de que se explique en más detalle en este punto.
Los errores se pueden encontrar en cualquiera de las fase de la compilación. Aunque
las fases de análisis sintáctico y semántico, por lo general, manejan una gran porción de
los errores detectados por el compilador, ya se ha visto que al manejador de errores se
puede acceder desde cualquier fase de la compilación. Cuando se encuentra un error,
el proceso de compilación continúa realizándose, permitiendo así la detección de más
errores en el programa fuente.
Durante el análisis semántico el compilador intenta detectar construcciones que tengan
estructura sintáctica correcta pero no tengan significado para la operación implicada; por
ejemplo, sumar un identificador de matriz y un identificador de procedimiento. Ejem-
plos típicos de errores detectados durante el análisis semántico son la comprobación de
86 P ROCESADORES DE LENGUAJES
1). La idea que subyace en esta optimización es la misma que en las sentencias condi-
cionales que se han visto antes.
Para facilitar la generación de código optimizado es conveniente modificar la gramática
independiente de contexto para discriminar explícitamente los operadores lógicos del
resto. De esa forma se pueden discriminar las expresiones lógicas del resto de expre-
siones del lenguaje, permitiendo optimizar las primeras mediante la generación de un
código con evaluación en cortocircuito.
Otra optimización de código típica es la traducción con precálculo de expresiones cons-
tantes. Por ejemplo, hay reglas de optimización para realizar el reemplazo de una cons-
tante por su valor o el reemplazo de variables con valor true y false por expresiones
constantes de valor true y false. Una instrucción if-then-else, si tiene una expre-
sión constante como condición, es equivalente a su bloque then si la condición es true,
y a su bloque else si la condición es false.
atribuidas hay dos mecanismos para especificar la semántica y traducción de las cons-
trucciones del lenguaje, es decir, existen dos tipos de notaciones que permiten asociar
reglas semánticas con reglas de producción.
E → E1 + T E.code = E1 .code||T.code||′ +′
Entre otras características diferenciables entre ambos tipos de notaciones, se puede des-
tacar que con las definiciones dirigidas por la sintaxis se ocultan muchos detalles de la
implementación y no es necesario que el usuario especifique explícitamente el orden en
el que tiene lugar la ejecución de las acciones. Sin embargo, con esquemas de traducción
sí se indica el orden en el que tiene lugar la traducción, el orden en que se deben evaluar
las reglas semánticas y, por tanto, en este caso algunos detalles de la implementación sí
son visibles.
En ambos casos se sigue el siguiente esquema. Primero se analiza sintácticamente la
cadena de componentes léxicos de entrada y se construye el árbol de análisis sintáctico.
El árbol se recorre para evaluar las reglas semánticas en sus nodos. Luego, esta evalua-
ción de reglas semánticas puede dar lugar a generar código, guardar información en una
tabla de símbolos, emitir mensajes de error, etc. Por tanto, la traducción de la cadena
de componentes léxicos es el resultado obtenido tras evaluar las reglas semánticas. Es
decir, que a partir de la cadena de entrada se genera el árbol de análisis sintáctico, un
grafo de dependencias y se establece el orden de evaluación de las reglas semánticas. A
continuación se verá este proceso con un poco más de detalle.
T RADUCCIÓN DIRIGIDA POR LA SINTAXIS 89
Una definición dirigida por la sintaxis que use exclusivamente atributos sintetiza-
dos se denomina definición con atributos sintetizados, o gramática S-atribuida.
• Un atributo heredado, por el contrario, es aquél cuyo valor en un nodo de un
árbol de análisis sintáctico está definido a partir de los atributos de su padre y/o de
sus hermanos. Más formalmente, un atributo a es heredado si, dadas unas produc-
ciones:
90 P ROCESADORES DE LENGUAJES
B → AY
A → X1 X2 . . . Xn
A.a = f (Y, B)
Por tanto, el valor de un atributo sintetizado se calcula a partir de los valores de los
atributos de los hijos de ese nodo en el árbol de análisis sintáctico, mientras que el
valor de un atributo heredado se calculará a partir de los valores de los atributos de los
hermanos y padre del nodo.
En una definición dirigida por la sintaxis se asume que los terminales solo tienen atri-
butos sintetizados, ya que la definición no proporciona ninguna regla semántica para
los terminales. Estos valores para los atributos de los terminales son proporcionados
generalmente por el analizador léxico.
Por otro lado, las reglas semánticas establecen las dependencias entre los atributos que
se representan mediante un grafo. Si cierto atributo en un nodo depende de uno o varios
atributos, entonces se deben evaluar primero las reglas semánticas para los atributos de
los que depende, para después aplicar la regla semántica que define al atributo depen-
diente. Las interdependencias entre atributos heredados y sintetizados de un árbol de
análisis sintáctico se pueden representar mediante un grafo dirigido llamado grafo de
dependencias. A continuación se muestra un ejemplo.
Un grafo de dependencias tiene un nodo por cada atributo y una arista que va desde el
nodo a al nodo b si el atributo b depende del atributo a. Por ejemplo, si la regla A.a =
f (X .x,Y.y) es una regla semántica asociada a la producción A → XY , entonces existe un
subgrafo como el que se muestra en la Figura 2.12 (las líneas discontínuas representan
el árbol sintáctico, mientras que las contínuas representan relaciones de dependencia).
Los atributos se representan junto a los nodos del árbol sintáctico.
Si la producción A → XY tiene asociada una regla semántica X .x = g(A.a,Y.y), entonces
habrá una arista hacia X .x desde A.a y también desde Y.y, puesto que X .x depende tanto
de A.a como de Y.y (véase Figura 2.13).
El grafo de dependencias proporciona el orden de evaluación de las reglas semánticas,
y la evaluación de las reglas semánticas define los valores de los atributos de los nodos
del árbol. Una regla semántica puede tener también efectos colaterales, es decir, puede
tener asociada acciones como imprimir un valor, actualizar una variable global, etc.
Así, finalmente, se puede decir que una gramática con atributos es una definición di-
rigida por la sintaxis en la que las funciones de las reglas semánticas no pueden tener
efectos colaterales. En la Figura 2.14 se muestra un ejemplo típico de definición dirigida
T RADUCCIÓN DIRIGIDA POR LA SINTAXIS 91
Figura 2.12: Ejemplo de construcción de un subgrafo de dependencias para una regla del
atributo sintetizado A.a = f (X.x,Y.y) asociada a una producción A → XY
Figura 2.13: Ejemplo de construcción de un subgrafo de dependencias para una regla del
atributo heredado X.x = g(A.a,Y.y) asociada a una producción A → XY
92 P ROCESADORES DE LENGUAJES
por la sintaxis para el caso de una calculadora. Hay que hacer notar que en este ejemplo,
la primera producción sí tiene efectos colaterales.
Tabla 2.2: Ejemplo de esquema de traducción que transforma expresiones infijas con suma y
resta en las expresiones posfijas correspondientes
T RADUCCIÓN DIRIGIDA POR LA SINTAXIS 93
2. Cada hoja se etiqueta con un componente léxico y las hojas de izquierda a derecha
forman la sentencia encontrada.
Figura 2.16: Árbol de análisis sintáctico y árbol abstracto de análisis sintáctico de las expre-
siones id ∗ id e (id + id) ∗ id
S→aAb
A→ab|c
se quiere analizar la cadena acb. Para ello, primero se toma la primera producción
(S → a A b), con lo que se obtiene el árbol:
S
a A b
Se toma entonces la primera opción de la segunda producción (A → a b) y se aplica
sobre al hoja A del árbol, con lo que se obtiene el árbol:
96 P ROCESADORES DE LENGUAJES
a A b
a b
Ahora la cadena acb se compara con la cadena formada por las hojas del árbol de
izquierda a derecha. La primera hoja concuerda, por lo que se avanza a la siguiente
hoja del árbol, etiquetada como a. Como no concuerda con acb, se detecta el error y se
vuelve a A por si hubiera otra alternativa entre las producciones con A como antecedente.
Se aplica entonces la otra alternativa en la producción (A → c), obteniéndose un árbol:
S
a A b
c
Y ahora sí coincide la cadena acb con las etiquetas de las hojas del árbol sintáctico, por
lo que el análisis ha concluido de forma exitosa. En este caso hemos visto un ejemplo
sencillo de método de análisis descendente con retroceso.
Análisis LL(1)
A → aBc | xC | B
B → bA
C→c
T RADUCCIÓN DIRIGIDA POR LA SINTAXIS 97
A ⇒ B ⇒ bA ⇒ baBc ⇒ babAc
Ahora habría que seguir desarrollando A usando los conjuntos de predicción. Como la
siguiente letra en la cadena de entrada es una x se elige la segunda opción de la primera
producción, es decir, A ⇒ xC.
Si, por el contrario, tuviéramos una producción como:
A → aBc | ac | B
donde hay dos opciones que comienzan con la letra a, entonces no se cumplirían los
requisitos para LL(1), por lo que el análisis no podría ser predictivo y la gramática no
sería LL(1).
Por tanto, estos conjuntos de predicción se calculan en función de los primeros símbo-
los terminales que puede generar la parte derecha de la regla. En el caso de que la parte
derecha pueda generar la cadena vacía, entonces estos conjuntos se calculan a partir de
los siguientes símbolos que aparecen en la parte izquierda de la regla.
Para poder definir estos conjuntos de predicción hay que determinar dos conjuntos: el
conjunto de PRIMEROS y el de SIGU IENTES.
S
a ∈ SIGU IENT ES(A) si a ∈ (T { $ }) y ∃α, β tal que
S ⇒ ∗αAaβ para algún par de cadenas α y β.
Las reglas que cumple este conjunto son:
S
= (PRIMEROS(α) − {ε} SIGU IENTES(A); de lo contrario es PRED(A → α) =
PRIMEROS(α)
A → αA′ | δi
A ′ → β1 | β2 | . . . | βn
Una gramática es recursiva por la izquierda si tiene alguna producción recursiva por
la izquierda, o si a partir de una sentencia Aδ se obtiene una forma sentencial Aβδ en
la que el no terminal A vuelve a ser el primer símbolo por la izquierda. Más formalmente,
La regla general para modificar una gramática y que deje de ser recursiva por la izquierda
es la siguiente. Si se tiene una gramática:
Una vez vistos los conjuntos de predicción y la forma que tenemos de asegurar que
una gramática sea LL(1), ahora ya podemos entender el análisis sintáctico descendente
predictivo dirigido por tabla. Este tipo de análisis se toma como ejemplo de análisis
LL(1), sigue el modelo de análisis sintáctico predictivo y requiere de la construcción de
las llamadas tablas de análisis sintáctico.
La construcción de este tipo de analizadores se basa en el uso de una pila de símbolos
terminales y no terminales. A partir de un token que se toma como entrada, se buscará en
la tabla de análisis. Para ello, primero es necesario construir la tabla y después realizar
el proceso de análisis. En la Figura 2.17 se muestra un ejemplo, donde A representa el
símbolo de la cima de la pila y a el símbolo inicial de la entrada a analizar.
El programa de análisis inicialmente comprueba si A = a = $ (siendo $ el símbolo de fin
de cadena), en cuyo caso se detendría el análisis y se anunciaría un fin de análisis exitoso.
T RADUCCIÓN DIRIGIDA POR LA SINTAXIS 101
Si, por el contrario, A = a 6= $ (es decir, la cima de la pila coincide con el siguiente
símbolo de la entrada, pero no con el símbolo de fin de cadena), entonces el programa
analizador sacaría A de la cima de la pila y movería el puntero al siguiente símbolo de la
misma. Si A 6= a 6= $ (es decir, la cima de la pila no coincide con el siguiente símbolo de
la entrada ni con el símbolo de fin de cadena), entonces el programa consulta la entrada
de la tabla correspondiente a los índices A y a, de modo que si encuentra una producción
en la tabla, sustituye A por dicha producción, mientras que si encuentra una cadena Error
llama a la rutina de recuperación de errores.
Por tanto, es necesario crear la tabla de análisis sintáctico. Para ello, a partir de un
conjunto de producciones, lo primero que se hace es etiquetar las filas de la tabla de
análisis sintáctico con los no terminales, mientras que las columnas se etiquetan con los
terminales y con el símbolo fin de cadena. En cada celda se copia la regla a aplicar para
analizar la variable de esa fila cuando el terminal de esa columna aparezca en la entrada.
Entonces se calculan los conjuntos de predicción para cada regla. Por ejemplo, para cada
símbolo no terminal a ∈ PRED(A → α) se introduce la producción A → α en la posición
[A, a] de la tabla. Cada entrada sin valor representa un error sintáctico. A continuación se
muestra un ejemplo de cómo calcular la tabla de análisis de una gramática. Si tenemos
la siguiente gramática:
E → T E′
E ′ → +T E ′ | − T E ′ | ε
T → FT ′
T ′ → ∗FT ′ | /FT ′ | ε
F → num | (E)
Calculamos los siguientes conjuntos de predicción para poder obtener la tabla de análisis
sintáctico que se muestra en la Tabla 2.3:
103
104 P ROCESADORES DE LENGUAJES
A B
a b a b a
En este caso, durante el análisis se van leyendo los tokens de entrada de uno en uno, y se
utiliza una pila para almacenar los símbolos que se van reconociendo y los estados por
los que pasa el analizador. Los símbolos terminales se introducen en la pila mediante
desplazamientos de la cadena de entrada a la pila, mientras que los no terminales se
apilan como resultado del proceso de reducción. Las modificaciones en la pila durante
el proceso de análisis de la cadena del ejemplo anterior pueden verse en la Tabla 2.4.
Como se indica en la propia tabla, en caso de que una determinada reducción no lleve
a ningún símbolo no terminal de las producciones de la gramática, se debe realizar un
retroceso para volver a la situación anterior a la última reducción llevada a cabo, y para
ello se emplea la pila. En el ejemplo, una vez se introduce en la pila la cadena AAa, se
comprueba que no es posible alcanzar ningún no terminal de la gramática, por lo que
se desapila hasta Aab y se toma otra opción, en este caso, se realiza un nuevo desplaza-
miento.
T RADUCCIÓN DIRIGIDA POR LA SINTAXIS 105
decir, que en todo análisis ascendente se necesita un mecanismo que determine el tipo
de acción a realizar (desplazar o reducir) y, en el caso de que se deba reducir, nos debe
proporcionar la subcadena de símbolos a reducir (el asidero) y qué producción utilizar.
Este mecanismo lo proporcionan los autómatas a pila deterministas.
Fundamentalmente existen tres tipos muy comunes de analizadores LR por desplaza-
miento - reducción: SLR(k), LALR(k) y LR(k), donde k identifica el número de símbolos
de preanálisis utilizados. Y dentro de estos tipos de análisis ascendente, se distinguen
principalmente tres técnicas a la hora de construir una tabla de análisisis sintáctico LR
para una gramática:
• Método SLR(k) (Simple LR). En este caso se trata de un LR(k) más sencillo de
implementar, aunque también es un método con menor potencia de análisis. Este
método no es capaz de asimilar ciertas gramáticas que los otros métodos sí se
puenen tratan, gramáticas que aún sin ser ambiguas pueden producir resultados
ambiguos en la tabla de análisis sintáctico.
• Método LALR(k) (Look Ahead LR(k)). Este método supone una simplificación del
método LR(k) que combina la eficiencia de los métodos SLR, en cuanto a tamaño
del autómata a pila, con la potencia del método LR(k) canónico. En definitiva
supone una solución de compromiso entre los métodos LR(k) y SLR(k), obtenién-
dose tablas de análisis sintáctico más compactas que en el caso del método LR(k).
Análisis SLR(1)
Este tipo de análisis usa un autómata finito determinista construído a partir de elementos
LR(0) y usa el token de la cadena de entrada para determinar el tipo de acción a realizar.
Un elemento de análisis sintáctico LR(0) de una gramática es una producción con un
punto en alguna posición del lado derecho. Por ejemplo:
A → X •Y
siendo la producción:
A → XY
T RADUCCIÓN DIRIGIDA POR LA SINTAXIS 107
El punto indica la parte que se ha analizado hasta ese momento y que, por tanto, se
encuentra en la parte alta de la pila.
Más formalmente, siendo S el estado de la cima de la pila, el algoritmo de análisis
sintáctico SLR(1) realizaría los siguientes pasos:
Se dice que una gramática es SLR(1) si la aplicación de las reglas anteriores no es am-
bigua; es decir, si cumple las siguientes condiciones:
Al tratar con la tabla de análisis sintáctico hay que tener en cuenta que un estado puede
admitir desplazamientos o reducciones, por lo que cada entrada deberá tener una eti-
queta de desplazamiento o reducción. A continuación se muestra un ejemplo. Sea una
gramática:
r0 : S → A
r1 : A → A + n
r2 : A → n
108 P ROCESADORES DE LENGUAJES
Tabla 2.5: Ejemplo de tabla de análisis sintáctico correspondiente al autómata finito determi-
nista de la Figura 2.19
Pero entre todo este tipo de herramientas destacan, como las más populares, los genera-
dores de analizadores léxicos y sintácticos, especialmente las herramientas Lex y Yacc,
que se verán en más detalle a continuación. Llegados a este punto, hay que tener cuidado
con la nomenclatura, ya que cuando se emplea el término lex, realmente se están con-
siderando dos posibles significados:
1 Declaraciones
2 %%
3 Reglas de producción
4 %%
5 Código adicional
escribir cualquier código de C en esta sección, y luego será copiado en el archivo fuente
generado. Permite también importar archivos de cabecera escritos en C.
Pueden encontrase entonces declaraciones de token si se ha usado la palabra clave %token,
o puede declararse el tipo de terminal por medio de la palabra reservada %union. En esta
sección también se puede incluir información sobre las precedencias de los operadores
y su asociatividad.
Para especificar el símbolo inicial de la gramática se emplea la palabra clave %start, y en
caso de no especificarse ninguno, se tomará por defecto la primera regla de la siguiente
sección: las reglas de producción.
La sección de reglas de producción es la única parte obligatoria en este tipo de archivos
si sirve de entrada para Yacc, es decir, que siempre debe aparecer en la entrada a un
programa Yacc. Puede contener desde declaraciones y/o definiciones encerradas entre
los caracteres %{ y %}, hasta reglas de producción de la gramática.
Esta sección de reglas, en la que se asocian patrones a sentencias de C, es la sección más
importante. Los patrones son expresiones regulares y cuando se encuentra un texto en la
entrada que encaja con un patrón dado, entonces se ejecuta el código C asociado.
En la última sección se puede incluir código adicional. Contiene sentencias en C y
funciones que serán copiadas en el archivo fuente generado. Es bastante común que esta
parte del código contenga un método, por ejemplo, main() desde donde se pueda llamar
a otras funciones como, por ejemplo, funciones que manejen errores sintácticos, etc.
Un analizador gramatical construido en Yacc genera una función que realizará el análi-
sis gramatical con el nombre de yyparse(); esta función solicita un token al analizador
léxico por medio de la función yylex().
A continuación se muestra un ejemplo de un código yacc para la creación de un programa
calculadora con los operadores básicos:
1
2 %{
3 #include < math .h >
4 %}
5
6 %union{
7 double dval ;
8 }
9
10 % token <dval > NUMBER
11 % token PLUS MINUS TIMES DIVIDE POWER
12 % token LEFT_PARENTHESIS RIGHT_PARENTHESIS
13 % token END
14
15
19 % right POWER
20
26 Input : Line
27 | Input Line
28 ;
29
30 Line : END
31 | Expression END { printf (" Result : %f\n" ,$1 ); }
32 ;
33
34 Expression : NUMBER { $$ = $1 ; }
35
En el código anterior pueden verse las secciones anteriormente descritas. Así, se pueden
observar:
• Declaraciones:
– Se definen los tokens que deben ser usados por el analizador por medio de la
directiva %token (líneas 10-13).
– Se definen los operadores y su precedencia con %le f t y %right (líneas 16-
19).
– La directiva %type define el tipo de dato para símbolos no terminales de
nuestra gramática (línea 21).
– %start establece el símbolo inicial de la gramática; en este caso, Input (línea
22).
1 %{
2 #include "y. tab .h"
3 #include < stdlib .h >
4 #include < stdio .h >
5 %}
6
7 white [ \t ]+
8 digit [0 -9]
9 integer { digit }+
10
11 %%
12
13 { white } { /* Ignoramos espacios en blanco */ }
14 " exit "|" quit "|" bye " { printf (" Terminando programa \n"); exit (0)
;}
15 { integer } {
16 yylval . dval = atof ( yytext );
17 return( NUMBER );
18 }
19
20 "+" return( PLUS );
21 " -" return( MINUS );
22 "*" return( TIMES );
23 "/" return( DIVIDE );
24 "^" return( POWER );
25 "(" return( LEFT_PARENTHESIS );
114 P ROCESADORES DE LENGUAJES
Solución:
S → SbS | ScS | a
Construye dos derivaciones por la izquierda (donde solo el no terminal más a la
izquierda se sustituye en cada paso) para la cadena abaca.
Solución:
E JERCICIOS RESUELTOS 115
A - B
A - B
C / B
B C
n C / B
C n
4 n C
n 3
2 n
8
2
4. A partir del siguiente conjunto de producciones:
A → BA′
A′ → +BA′ | ε
B → CB′
B′ → ∗CB′ | ε
C → (A) | ident
Calcula el conjunto de PRIMEROS.
Solución:
116 P ROCESADORES DE LENGUAJES
• PRIMEROS(A) = PRIMEROS(BA′)
• PRIMEROS(A) = PRIMEROS(B)
• PRIMEROS(B) = PRIMEROS(C)
• PRIMEROS(C) = {(, ident}
Hasta aquí ya sabemos que:
PRIMEROS(A) = PRIMEROS(B) = PRIMEROS(C) = {(, ident}
• PRIMEROS(A′) = {+, ε}
• PRIMEROS(B′) = {∗, ε}
Resultado:
5. A partir de la gramática:
A → BA′
A′ → +BA′ | ε
B → CB′
B′ → ∗CB′ | ε
C → (A) | ident
Calcula el conjunto de SIGU IENTES.
• SIGU IENTES(A) = { $ }
E JERCICIOS RESUELTOS 117
S
• SIGU IENTES(A) = SIGU IENTES(A) {)} = { $ , )}
• SIGU IENTES(A′ ) = SIGU IENT ES(A) = { $ , )}
S
• SIGU IENTES(B) = PRIMEROS(A′) SIGU IENTES(A) = {+, $ , )}
• SIGU IENTES(B′ ) = SIGU IENT ES(B) = {+, $ , )}
S S
• SIGU IENTES(C) = PRIMEROS(B′) SIGU IENT ES(B) SIGU IENT ES(B′ ) =
{∗, +, $ , )}
Resultado:
SIGU IENTES(A) = { $ , )}
SIGU IENTES(A′ ) = { $ , )}
SIGU IENTES(B) = {+, $ , )}
SIGU IENTES(B′ ) = {+, $ , )}
SIGU IENTES(C) = {∗, +, $ , )}
Solución:
• σ = {0, 1}
• σ = {a, b, c}
• σ = {a, b, c, 0, 1}
• σ = {int, f loat, i f , else, while}
1 if (a > (( b *3) / 4) ) {
2 a := (b * 4) + 1000.0;
3 }
4 else if (a = (( b *3.4) /4) ) {
5 b := 100;
6 }
7 else {
8 a := 100.0;
9 }
Se pide:
(a) Escribir la estructura de los tokens que debe reconocer un analizador léxico.
(b) Determinar los patrones léxicos (expresiones regulares) de cada uno de los
tokens.
• if
• if-else
Paradigmas y modelos de
programación
En este tema se introducen los cinco paradigmas más importantes desde el punto de
vista de los lenguajes de programación. Concretamente, se explican las bases de los
paradigmas imperativo, funcional, lógico, orientado a objetos y concurrente. Además,
a estos se añaden los lenguajes dinámicos (o de script), que aun no conformando un
paradigma en sí mismos, por su popularidad se han incluido en este tema.
3.1 Introducción
121
122 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
3.2.1 Funciones
Desde el punto de vista matemático una función establece una correspondencia entre
valores de entrada y valores de salida. Así, por ejemplo, la función capital establece
una correspondencia entre un país y su capital, y la función doble establece una corres-
pondencia entre un número y ese mismo número multiplicado por dos.
Además, los valores de entrada son elementos de un conjunto origen denominado do-
minio, y los valores de salida son elementos de un conjunto destino denominado imagen.
En el ejemplo de la función capital el conjunto dominio serían los países del mundo y
el conjunto imagen podría ser las ciudades del mundo. En el caso de la función doble
tanto el dominio como la imagen podrían ser el conjunto de los números enteros. Es
importante notar que esta regla de correspondencia que representa una función asigna a
P ROGRAMACIÓN FUNCIONAL 123
cada valor del conjunto que representa el dominio un único valor del conjunto imagen.
La aplicación de una función es la particularización de la regla de correspondencia
a un valor concreto del dominio, lo que da como resultado un valor de la imagen.
Así, doble(5) es una particularización de la función doble, cuyo resultado es 10, y
capital(Francia) es una particularización de la función capital. La aplicación de
una función se escribe de la siguiente forma:
doble(5) = 10
Las funciones pueden componerse, de forma que el argumento de entrada de una función
es el resultado de la salida de otra función. Por ejemplo, doble(doble(5)) es 20, y
doble(mayor(5,6)) es 12.
La definición de una función, es decir, la especificación de la regla de correspondencia
que representa, puede darse de dos formas distintas: por extensión o por comprensión.
Una definición por extensión consiste en proporcionar todos los valores posibles para
todas las entradas. En el ejemplo de la función capital consistiría en enumerar todas
las aplicaciones posibles de la función:
capital(Alemania) = Berlin
capital(Francia) = Paris
capital(Italia) = Roma
...
Es evidente que sólo en algunos casos es posible dar una definición por extensión para
una función. La mayoría de las veces se proporciona una definición por compren-
sión. Una definición por comprensión es una ecuación algebraica en la que en la parte
izquierda aparecen el nombre y los argumentos de la función y en la parte derecha
aparece la expresión algebraica que permite calcular el valor de la función. En el caso
de la función doble, puede representarse por comprensión de la siguiente forma:
doble(x) = 2 ∗ x
En lo sucesivo, utilizaremos el lenguaje Haskell[24] como lenguaje de programación
funcional. Haskell es un lenguaje funcional que surge a partir de la conferencia FPCA’87
(Functional Programming Languages and Computer Architecture) y que fue desarro-
llado por las universidades de Yale y Glasgow. En la década de los 80 hubo una auténtica
eclosión de lenguajes funcionales (SASL, Miranda, KRC, CAML, Hope, . . . ) y el obje-
tivo era reunir en Haskell aquellas características que eran esenciales a la programación
funcional, dejando fuera el resto. Haskell toma el nombre de Haskell Brooks Curry
(1900-1982), cuyos trabajos en lógica matemática fueron la base para el paradigma fun-
cional.
Los programas en Haskell se escriben en módulos, y el fichero suele llevar extensión
.hs. La declaración de un módulo tiene el siguiente formato:
3 ...
124 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
Haskell tiene normas muy estrictas respecto a la forma de los nombres para módulos,
tipos, funciones y argumentos. Los tipos y los módulos deben comenzar siempre por
mayúscula. Eso los distingue de los parámetros y nombres de funciones que deben
comenzar siempre en minúscula.
Un módulo puede contener tantas funciones como sea necesario. La definición de una
función se proporciona separada en dos partes. En primer lugar la declaración de los
tipos de datos de la función: tipo de los parámetros de entrada y tipo de los parámetros
de salida. En segundo lugar la definición de la función. El siguiente ejemplo muestra la
definición de la función doble para números enteros:
1 suma 5 6 + 1
1 ( suma 5 6) + 1
Aquellos tipos de datos cuyos valores están ordenados pueden hacer uso de los com-
paradores para reducir la complejidad de las expresiones. Considérese el caso de definir
una función para determinar si un carácter es una letra. Los valores de tipo carácter es-
126 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
tán ordenados, generalmente siguiendo algún estándar como la tabla ASCII. Por tanto la
siguiente definición de la función esLetra es perfectamente válida:
Esta función devolverá True para todos aquellos caracteres entre la a (minúscula) y la z
(minúscula). Se deja como ejercicio al lector modificar esta función para que considere
también las letras mayúsculas.
Adicionalmente, Haskell también dispone del tipo tupla. Una n-tupla es una colección
heterogénea de n elementos. Las tuplas se representan entre paréntesis con los elementos
de la tupla separados por comas. Por ejemplo, la siguiente función devuelve el conciente
y el resto de la división entera de dos enteros:
Transparencia referencial
Una de las características diferenciadoras del paradigma funcional es el concepto de
transparencia referencial. La transparencia referencial establece que el valor devuelto
por una función depende exclusivamente de los parámetros de entrada, y de nada más.
Es decir, aplicar una función con los mismos parámetros siempre produce el mismo
resultado. Esto que parece obvio, en otros paradigmas no lo es tanto. Considérese por
ejemplo el siguiente fragmento de código Pascal:
Recursividad
1 factorial n =
2 if n > 0 then
3 n * factorial (n -1)
4 else
5 1
Parámetros de acumulación
La función factorial tal como se ha definido es una función con recursividad no final.
Las funciones recursivas no finales son aquellas que requieren hacer cálculos a la vuelta
128 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
Métodos de evaluación
En el capítulo 1 se presentaron los métodos de evaluación estricta y evaluación diferida.
Haskell utiliza evaluación diferida en la evaluación de las expresiones. Esto implica
que una subexpresión no se calcula mientras no sea estrictamente necesario. Además,
en Haskell, una vez que se evalúa una parte de una expresión, si ésta aparece varias veces
P ROGRAMACIÓN FUNCIONAL 129
factorial 3
↓
if (3 > 0) then 3 * factorial (3-1) else 1
↓
if True then 3 * factorial (3-1) else 1
↓
3 * factorial (3-1)
↓
3 * if (2 > 0) then 2 * factorial (2-1) else 1
↓
3 * if True then 2 * factorial (2-1) else 1
↓
3 * 2 * factorial (2-1)
↓
3 * 2 * if (1 > 0) then 1 * factorial (1-1) else 1
↓
3 * 2 * if True then 1 * factorial (1-1) else 1
↓
3 * 2 * 1 * if(0 > 0) then 0 * factorial (0-1) else 1
↓
3 * 2 * 1 * if False then 0 * factorial (0-1) else 1
↓
3*2*1*1
↓
6*1*1
↓
6*1
↓
6
[] []
[1] 1:[]
[1,2] 1:[2]
[1,2] 1:2:[]
[1,2,3] 1:2:3:[]
[[1,2], [3,4]] [1,2]:[3,4]:[]
en la expresión de la que forma parte, todas las apariciones son sustituidas por el valor
calculado.
El tipo [Integer] representa una lista de enteros. Los valores de tipo lista se representan
encerrando los elementos que contiene la lista entre corchetes y separándolos por comas.
La lista vacía se representa por []. Los siguientes son ejemplos de listas de enteros:
[1,2,3], [-10], [7,21,121], . . .
Para definir listas se puede hacer uso del constructor de listas (:) junto con el constructor
de la lista vacía ([]). Un constructor es una función especial utilizada para construir
valores de un determinado tipo. El constructor : es un operador binario infijo. El
operando de la derecha tiene que ser una lista y el operando de la izquierda tiene que
ser un elemento del mismo tipo que los elementos de la lista. La lista que se obtiene
es el resultado de insertar el primer operando al principio de la lista representada por
el segundo operando. Así, por ejemplo la Tabla 3.1 muestra listas equivalentes. En
la columna de la izquierda se muestra la lista como una secuencia de elementos entre
corchetes separados por comas. La columna de la derecha representa la misma lista de
su izquierda utilizando el constructor :.
El constructor : de listas es una pieza fundamental en la construcción de programas. La
razón es que una técnica de definición de funciones utilizada habitualmente junto con
el constructor de listas es la técnica de ajuste de patrones. Si se considera la función
longitud cuya declaración se presentaba previamente, una posible definición recursiva,
desde un punto de vista matemático sería la siguiente:
P ROGRAMACIÓN FUNCIONAL 131
si l = 0/
0
longitud(l) = (3.2)
1 + longitud(l − primerelemento) si l 6= 0/
Se observan en esta definición dos cosas. En primer lugar, la función está definida a
trozos. Existen dos expresiones algebraicas diferentes que se aplican en función de una
determinada condición: la primera expresión se aplica si la lista está vacía, en cuyo caso
la longitud es cero (este es el caso base de la recursividad); la segunda expresión se
aplica si la lista no está vacía, y entonces la longitud se define como 1 más la longitud
de la lista resultante de quitarle un elemento (por ejemplo el primero) a la lista original.
En segundo lugar, necesitamos poder extraer un elemento de la lista en el paso recur-
sivo, para poder llamar recursivamente a la función longitud cada vez con una lista más
pequeña hasta que eventualmente lleguemos a tener la lista vacía.
Esta definición se puede conseguir en Haskell haciendo uso del ajuste de patrones pro-
porcionando dos definiciones para la función longitud: una para la lista vacía y otra para
una lista genérica con al menos un elemento. La lista vacía se representa como ya se
ha visto como [], mientras que genéricamente podemos hacer referencia a una lista con
un elemento mediante el constructor : como x:resto, donde x y resto son parámetros
que pueden ser utilizados en la parte derecha de la definición:
longitud [1,2]
↓ [1,2] es ajustado a (x=1):(resto=[2])
1 + longitud [2]
↓ [2] es ajustado a (x=2):(resto=[])
1 + 1 + longitud []
↓ [] es ajustado a []
1+1+0
↓
2+0
↓
2
Tabla 3.2: Patrón ajustado en cada caso para la evaluación de longitud [1,2]
Esta función presenta recursividad no final. Se puede conseguir una función equivalente
con recursividad final introduciendo un parámetro de acumulación donde ir almacenando
los elementos seleccionados:
Obsérvese que la función con recursividad final mayores2 devuelve la misma lista que
la función mayores pero invertida.
Supóngase que se desea implementar una función para calcular el área de un rectángulo
dado por dos puntos que se encuentran en la diagonal del mismo. Cada punto se repre-
senta mediante sus coordenadas x e y dadas como números en coma flotante. En lugar
de definir una función que reciba cuatro parámetros representando estas coordenadas,
podría considerarse definir un tipo Punto que renombre una 2-tupla de dos Float, y
definir la función en base a este tipo:
Por otro lado, la definición de un nuevo tipo de datos se puede proporcionar de dos
maneras: especificando todos los posibles valores del tipo (de manera similar a la defini-
ción de enumerados en otros lenguajes) o especificando constructores para el tipo. Un
constructor permite construir valores del tipo y se puede utilizar en el ajuste de patrones.
La definición mediante la enumeración de valores consiste simplemente en enumerar
todos los posibles valores del tipo separándolos por una barra vertical (|). Estos valores
se consideran constantes y por tanto se escriben comenzando en mayúsculas:
• Eq: permite comparar valores por igualdad, donde un valor sólo es igual a sí
mismo (Lunes == Lunes, pero Lunes /= Martes).
anterior Lunes < Martes < ...< Domingo. De esta forma se pueden utilizar
los operadores de comparación habituales con el tipo de datos.
• Show: permite mostrar un valor por pantalla. A menos que se especifique este tipo,
Haskell no sabe cómo mostrar los valores del tipo definido por el usuario. Show
permite considerar el propio nombre del valor como su representación textual para
mostrarla por pantalla. De esta forma el valor Lunes se imprimirá por pantalla
como la cadena “Lunes”, etc.
1 data DiaSemana =
2 Lunes | Martes | Miercoles | Jueves | Viernes | Sabado |
Domingo
3 deriving (Eq, Ord, Show)
A partir de esta definición, es posible construir una función que determine si un deter-
minado día es laborable mediante la siguiente definición:
La declaración de ambas funciones coincide con la definición del tipo Comparador: am-
bas reciben dos valores de tipo entero y devuelven un entero. Ahora es posible comparar
valores atendiendo cualquiera de los dos criterios, de la siguiente forma:
Las funciones de orden superior son un mecanismo muy potente en el diseño de algorit-
mos. Algunos lenguajes (no pertenecientes al paradigma funcional) las han incluido en
su definición (por ejemplo, Ruby mediante sus bloques, o la versión 7 de Java mediante
el mecanismo de closures).
de Horn. Las cláusulas de Horn son un tipo restringido de cláusulas lógicas. El objetivo
de esta restricción es poder definir un modelo computacional que a partir de dichas
cláusulas y una consulta sea capaz de encontrar una respuesta a dicha consulta.
Los ejemplos de esta sección se presentan en el lenguaje Prolog. Prolog fue desarrollado
en el año 1972 por Alain Colmerauer, quien estaba trabajando en reconocimiento de
lenguaje natural. Es uno de los lenguajes lógicos más populares.
3.3.1 Hechos
Los hechos son enunciados ciertos por definición. Por ejemplo, cuando se expresa que
“Ford es un coche”, se está expresando un hecho. Los hechos se utilizan en progra-
mación lógica para establecer enunciados ciertos que ayudarán en la resolución de un
determinado problema. Un hecho consiste en un nombre y uno o varios argumentos,
seguido de un punto. Considérese por ejemplo la siguiente lista de hechos sobre paradig-
mas y lenguajes:
1 funcional ( haskell ).
2 funcional ( ml ).
3 funcional ( hope ).
4
5 logico ( prolog ).
6
7 concurrente ( haskell ).
8 concurrente ( java ).
3.3.2 Consultas
A partir de los hechos del ejemplo anterior se pueden realizar consultas como las si-
guientes:
Las consultas se pueden realizar utilizando variables, en cuyo caso el sistema trata de
determinar qué variables hacen cierto el enunciado:
4 X = hope .
Se pueden hacer consultas más complejas. Por ejemplo, se podría determinar qué lengua-
jes son funcionales y concurrentes a la vez. Para ello se utiliza la conjunción, que se
representa separando los diferentes enunciados con una coma:
3.3.3 Reglas
Una regla tiene una cabeza y un cuerpo, separadas por el símbolo :-, y establece que
la cabeza es verdad si el cuerpo es verdad. El cuerpo puede contener predicados (como
conjunciones o disyunciones). Las conjunciones se representan separando las cláusulas
con comas, las disyunciones se representan separándolas con puntos y comas.
La cabeza de la regla puede contener variables y éstas pueden utilizarse en el cuerpo.
Continuando con el ejemplo anterior, supóngase que se desea escribir una regla para
comprobar cuándo un nombre representa un lenguaje de programación. Esta regla po-
dría considerar que dicho nombre será un lenguaje de programación si es un lenguaje
funcional, un lenguaje lógico o un lenguaje concurrente:
1 lenguaje (X) :-
2 funcional (X);
3 logico (X);
4 concurrente (X).
5
6 funcional ( haskell ).
7 funcional ( ml ).
8 funcional ( hope ).
9
10 logico ( prolog ).
11
12 concurrente ( haskell ).
13 concurrente ( java ).
Cargando este programa en el intérprete de Prolog se pueden realizar consultas como las
siguientes:
Considérese ahora el siguiente ejemplo. Se tiene una serie de hechos que determinan
qué lenguajes conocen una serie de programadores y se desea saber cuáles de estos
programadores conocen el paradigma funcional. Podría entonces describirse una regla
sabeFuncional que determine, para un programador dado, si este programador conoce
algún lenguaje del paradigma funcional:
1 sabeFuncional (X) :-
2 programa (X ,Z) ,
3 funcional (Z).
4
5 funcional ( haskell ).
6 funcional ( ml ).
7 funcional ( hope ).
8
9 logico ( prolog ).
10
11 concurrente ( haskell ).
12 concurrente ( java ).
13
En la última consulta el sistema trata de encontrar todos los valores de X que hacen
verdaderos a la vez los enunciados funcional(Z) y programa(X,Z). El sistema va
mostrando los resultados uno a uno, y el usuario debe introducir un ; para que se le
muestre el siguiente. Cuando no es capaz de encontrar más valores de X que hagan
cierto sabeFuncional(X), devuelve false.
Operadores
Reglas recursivas
En Prolog es posible definir reglas recursivas. Un ejemplo característico son las rela-
ciones familiares. Supóngase que se desea escribir un programa en Prolog para deter-
minar si una persona es ancestro de otra. Ello es posible definiendo una serie de hechos
padre que determinan la relaciones padre-hijo entre varias personas, y posteriormente
definiendo una regla ancestro de la siguiente forma:
5 ancestro (A ,B) :-
6 padre (A ,B).
7 ancestro (A ,B) :-
8 padre (A ,C) ,
9 ancestro (C ,B).
• Haskell es un lenguaje sin estado, con soporte para hilos, con gestión de memoria
y con funciones de orden superior.
1 % Sistema experto :
2
3 funcional (X) :-
4 orden_superior (X) ,
5 not( estado (X)) ,
6 gestion_memoria (X).
7
8 imperativo (X) :-
9 ansiosa (X) ,
10 estado (X).
11
12 objetos (X) :-
13 estado (X) ,
14 encapsulacion (X).
15
16 concurrente (X) :-
17 hilos (X).
18
19 % Base de conocimiento :
20
21 % java
22 estado ( java ).
23 encapsulacion ( java ).
24 ansiosa ( java ).
25 hilos ( java ).
26 gestion_memoria ( java ).
27
28 % haskell
29 estado ( haskell ) :- false .
30 hilos ( haskell ).
31 gestion_memoria ( haskell ).
32 orden_superior ( haskell ).
33
34 % c
35 estado (c).
36 ansiosa (c).
37
38 % scala
39 encapsulacion ( scala ).
40 orden_superior ( scala ).
41 hilos ( scala ).
42 gestion_memoria ( scala ).
P ROGRAMACIÓN ORIENTADA A OBJETOS 143
A partir del programa anterior, es posible realizar consultas como: ¿es Java un lenguaje
orientado a objetos? ¿Y funcional? ¿Puede Haskell considerarse un lenguaje concur-
rente? ¿Qué lenguajes son concurrentes?
de definir conjuntamente tanto los tipos definidos por el usuario como las operaciones
permitidas sobre estos tipos.
Algunos lenguajes de programación, como Ada, hicieron hincapié en el concepto de tipo
abstracto de dato, proporcionando incluso encapsulación para estos tipos. Sin embargo,
la mayoría de los lenguajes imperativos no proporcionan ningún tipo de encapsulación
para los valores del tipo. La encapsulación evita que los programadores accedan direc-
tamente a las variables que contienen la información del tipo modificándolas, sino que
deben modificar estos valores exclusivamente a través de las operaciones proporcionadas
por el tipo de dato.
La programación orientada a objetos proporciona el concepto de clase, evolución de un
tipo abstracto de datos. Cada clase representa un tipo con un estado (definido por unos
atributos) y operaciones (métodos) que se pueden invocar sobre los objetos (ejemplares)
de la clase. Pero además, la programación orientada a objetos introduce el concepto de
herencia, que permite crear jerarquías de tipos, y polimorfismo, que permite manipular
un conjunto de objetos de distinto tipo como algo uniforme sujeto a ciertas restricciones.
En esta sección se utilizará a modo de ejemplo el lenguaje Java. Este lenguaje, creado
en 1995 por James Gosling cuando trabajaba para Sun Microsystems (actualmente sub-
sidiaria de Oracle Corporation) es uno de los más utilizados en la actualidad.
Clase Una clase define un nuevo tipo de datos junto con las operaciones permitidas sobre
ese tipo. Estas operaciones definen el comportamiento de un conjunto de elemen-
tos homogéneos. Por ejemplo, la clase Fracción viene definida por un numerador
y un denominador (datos), y puede ofrecer operaciones como simplificar, multi-
plicar, sumar, etc.
Método Definición de una operación de una clase. La definición es la misma para todos
los objetos de la clase.
Atributo Un atributo es cada uno de los datos de una clase. En el caso de la clase Fracción,
numerador y denominador son los atributos de la clase.
16 ...
17 }
18 }
Dos fracciones son equivalentes cuando esta multiplicación cruzada da el mismo resul-
tado: 1/2 es equivalente a 2/4, porque 1*4 == 2*2. En base a estos resultados, podría
pensarse en la siguiente implementación:
Es importante en este momento destacar una característica de Java que no está presente
en todos los lenguaje orientados a objetos. En Java, dos objetos que sean de la misma
clase pueden acceder uno a los atributos del otro. Este comportamiento se puede ob-
servar en la implementación de los tres métodos esMayor, esMenor y esEquivalente,
donde se accede a los atributos de f que es un objeto pasado como parámetro. Esta es una
decisión de diseño, y se le presupone al creador de la clase la responsabilidad de man-
tener el estado de los objetos coherente. Aunque esto es una violación del principio de
encapsulación, en realidad el acceso está muy restringido: sólo desde las implementa-
ciones de los métodos de la clase se puede acceder a los atributos de otro objeto de
la misma clase.
Siguiendo con el ejemplo anterior, es posible extraer en un método privado en el que se
apoyen esMenor, esMayor y esEquivalente, el cálculo del producto cruzado:
26 }
En principio, con las operaciones de la vista pública debería ser suficiente para interac-
tuar con los objetos de una clase concreta. No debería ser necesario acceder a los atribu-
tos de una clase. Sin embargo, en ocasiones puede ser necesario consultar los atributos.
Esto debe evitarse en la medida de lo posible, pero si fuera necesario, la forma de pro-
porcionar acceso a los atributos es mediante métodos de acceso. Existen dos métodos
de acceso:
Proporcionar métodos de acceso debe ser una decisión muy razonada. Considérese por
ejemplo el caso de un objeto Colegio que mantiene una lista de alumnos. La clase
tiene un método matricular que se debe utilizar para matricular alumnos en el mismo.
Supóngase que se quiere proporcionar la posibilidad de obtener un listado de los alumnos
del colegio. Podría pensarse en una implementación como la siguiente, que define un
método de acceso para la lista de alumnos de forma que se pueda acceder a la lista de
alumnos matriculados:
P ROGRAMACIÓN ORIENTADA A OBJETOS 149
5 Colegio colegio ;
6
La vista privada del objeto incluye la creación, destrucción y paso de mensajes al objeto.
La creación de un objeto incluye la reserva de memoria para el estado del objeto, la ini-
cialización de cada uno de sus atributos y la invocación del constructor correspondiente.
La destrucción del objeto, en el caso de Java, incluye liberar la memoria reservada para
el objeto y las referencias al mismo.
El paso de mensajes implica la resolución del método a invocar, la introducción de los
parámetros en la pila y la transferencia de control al método correspondiente. A la salida,
se restaurará el estado de la pila extrayendo los parámetros del método y devolviendo el
control al método que llamó.
3.4.4 Herencia
La herencia es un mecanismo de la programación orientada a objetos por el cual la vista
pública y privada de una clase se transmiten a otra. Cuando esto ocurre se establece una
relación de herencia, que es una relación binaria entre una clase padre y una clase hija.
Esta relación de herencia establece una jerarquía por grado de clasificación, donde la
clase padre es más general y la clase hija es más específica. En Java, una relación de
herencia entre una clase padre Poligono y una clase hija Cuadrado se establecería de la
siguiente forma:
En general, una clase hija especializa a su clase padre. En este sentido la clase hija
hereda la vista pública y privada de su clase padre, pero a su vez añade su propia vista
pública y privada, sumando a los atributos y métodos de su padre los suyos propios.
Es por esto que la relación de herencia responde a la regla “¿es un?”. Supóngase que se
establece una relación de herencia entre una clase Cuadrado y una clase Polígono como
en el ejemplo anterior. Entonces, se puede decir que un cuadrado “es un” polígono, sin
embargo lo contrario no es cierto. La razón es que un cuadrado es una especialización
P ROGRAMACIÓN ORIENTADA A OBJETOS 151
de un polígono, por tanto, presenta todas las características del polígono más las propias
de ser un cuadrado.
Cuando se establece una relación de herencia, hay que distinguir entre los atributos y
métodos transmitidos de la clase padre a la clase hija, y los atributos y métodos añadidos
por la clase hija. Así, la vista pública de la clase hija consiste en todos los métodos públi-
cos transmitidos desde el padre, más los métodos públicos añadidos. La vista privada
son los atributos y métodos privados transmitidos más los atributos y métodos privados
añadidos.
Aunque la vista privada de la clase padre se transmite a la clase hija, desde la clase
hija no se puede acceder a la vista privada de la clase padre. Lo contrario violaría el
principio de encapsulación. Por tanto, la clase hija sólo podrá utilizar los métodos de la
vista pública de la clase padre.
Una de las características que introduce la herencia es la posibilidad de redefinir méto-
dos de la vista pública de la clase padre. La redefinición de un método de la clase padre
consiste en proporcionar un método con el mismo nombre y argumentos en la clase hija.
El método de la clase padre queda oculto por el método de la clase hija. La redefinición
hace que la invocación de un mensaje a un objeto de la clase hija conteniendo el nom-
bre del método redefinido desemboque en la llamada al método redefinido, en lugar del
método original de la clase padre.
3.4.5 Polimorfismo
El polimorfismo, en programación orientación a objetos, es la capacidad de varios ob-
jetos pertenecientes a clases diferentes de comportarse de forma homogénea. Esto per-
mite por ejemplo proporcionar diferentes implementaciones para una misma operación.
Supóngase que se tiene una clase que necesita enviar ficheros a un servidor para su pub-
licación. El servidor, sobre el cual no se tiene control, puede aceptar envío de ficheros
mediante los protocolos: ftp, sftp y scp. Dado que no se tiene control sobre el servidor,
la clase que envía los ficheros debe ser capaz de soportar los tres protocolos. Se podría
entonces pensar en una implementación como la siguiente:
13
28 }
12
es pasado responde al mensaje declarado por dicho método abstracto. Así, la imple-
mentación del ejemplo anterior utilizando polimorfismo requiere de una clase padre de
FTPSender, SFTPSender y SCPSender, que denominaremos FileSender. Esta clase pro-
porcionará un método abstracto send que deberá ser implementado en cada una de las
clases hijas utilizando el protocolo correspondiente:
Esta implementación basada en el uso del polimorfismo permitiría añadir nuevas im-
plementaciones de envío de ficheros sin tener que tocar la implementación de la clase
FilePublisher. Basta con añadir una nueva clase hija de FileSender e implementar
el método send como corresponda.
3.5.1 Concurrencia
La concurrencia es el acaecimiento de varios sucesos al mismo tiempo. Desde el punto
de vista de la programación concurrente, se dice que dos sucesos son concurrentes si uno
sucede entre el comienzo y el fin de otro.
La forma de ejecutar un programa concurrente puede variar en función del hardware
disponible. En sistemas con un único procesador estos programas se ejecutan en el único
procesador disponible y sus procesos internos deben compartir el tiempo de cómputo de
este procesador. A este tipo de asignación mediante compartición de tiempo se le llama
multiprogramación.
Por contra, en sistemas donde hay varios procesadores es posible asignar diferentes
procesadores a diferentes procesos. Se habla entonces de multiproceso. Es importante
notar que aunque se disponga de varios procesadores, es muy posible que el número de
procesos de los programas que deben ejecutarse en ellos sobrepase el número de proce-
sadores disponibles. En tal caso, podría necesitarse multiprogramación incluso aunque
existiera multiproceso.
por referencia pueden ser compartidos por diferentes procesos (si la misma referencia es
pasada a todos ellos). Por ejemplo, el siguiente fragmento de código en PascalFC define
un proceso que escribe por pantalla el valor de una variable:
Una vez definido un proceso es posible declarar una o varias variables de tipo proceso en
la sección de declaración de variables del programa principal. En el ejemplo anterior, se
podría considerar declarar dos variables de tipo proceso print, p1 y p2 de la siguiente
manera:
1 var
2 p1 , p2 : print ;
1 program print ;
2
3 process type print (i : integer);
4 begin
5 write( ’Soy el proceso ’, i);
6 end;
7
8 var
9 p1 , p2 : print ;
10
11 begin
12 cobegin
13 p1 (1) ;
14 p2 (2) ;
15 coend ;
16 end.
P ROGRAMACIÓN CONCURRENTE 159
o bien:
Sin embargo, este programa puede producir en total cuatro salidas diferentes:
Como se puede observar, en las dos últimas ejecuciones se intercalan las salidas de los
procesos. Esto se debe a que la instrucción write, cuando recibe dos (o más) argumen-
tos, se ejecuta realmente como dos (o más) instrucciones de escritura:
En cambio, la instrucción write con un sólo argumento se ejecuta como una instrucción
atómica. Las instrucciones atómicas son aquellas que son ejecutadas completamente
antes de que se ejecute ninguna otra instrucción de cualquier otro proceso del programa.
Por tanto, podemos estar seguros de que la cadena Soy el proceso se escribirá com-
pletamente en pantalla antes de que cualquier otra instrucción puede ejecutarse.
Para comprender qué resultados puede producir un programa, es necesario estudiar las
diferentes intercalaciones de instrucciones que se pueden producir. Una intercalación
de instrucciones de un programa concurrente es una secuencia concreta de ejecución
de las instrucciones atómicas de los procesos del programa. Así, partiendo del ejemplo
anterior, las intercalaciones de instrucciones posibles son las que se muestran en la Tabla
3.3.
Normalmente no todos los resultados son correctos y es necesario limitar ciertos resul-
tados. En el ejemplo anterior, los resultados de las intercalaciones a), b), c) y d) no son
correctos. La tarea de un programador concurrente consiste en evitar los resultados no
deseados limitando la concurrencia lo mínimo posible.
160 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
(a)
P1 P2
1 write(’Soy el proceso ’)
2 write(’Soy el proceso ’)
3 write(1)
4 write(2)
(b)
P1 P2
1 write(’Soy el proceso ’)
2 write(’Soy el proceso ’)
3 write(1)
4 write(2)
(c)
P1 P2
1 write(’Soy el proceso ’)
2 write(’Soy el proceso ’)
3 write(2)
4 write(1)
(d)
P1 P2
1 write(’Soy el proceso ’)
2 write(’Soy el proceso ’)
3 write(2)
4 write(1)
(e)
P1 P2
1 write(’Soy el proceso ’)
2 write(1)
3 write(’Soy el proceso ’)
4 write(2)
(f)
P1 P2
1 write(’Soy el proceso ’)
2 write(2)
3 write(’Soy el proceso ’)
4 write(1)
1 load a, R1
2 add R1, 1
3 store R1, a
wait(s) Este procedimiento sólo puede ser invocado desde un proceso. El efecto del
mismo es el siguiente:
P ROGRAMACIÓN CONCURRENTE 163
signal(s) Este procedimiento sólo puede ser invocado desde un proceso. El efecto
del mismo es el siguiente:
Los semáforos de PascalFC son generales aleatorios. Generales porque pueden tomar
valores mayores o iguales que cero; aleatorios, porque de entre los procesos bloqueados,
se desbloquea uno aleatoriamente. Además, los semáforos deben pasarse a los procesos
por referencia, de forma que puedan ser compartidos entre ellos. En el caso de PascalFC
este tipo de paso de parámetro se especifica anteponiendo la palabra reservada var al
parámetro correspondiente.
Los semáforos se pueden utilizar para implementar sincronización condicional y ex-
clusión mutua. Por ejemplo, en el caso de la sincronización condicional, supóngase que
dos procesos deben ejecutar dos tareas, representadas respectivamente por los procedi-
mientos tarea1 y tarea2. La tarea 1 debe completarse antes de que se comience a
ejecutar la tarea 2. Por tanto, el proceso que lleva a cabo la tarea 2 debe esperarse a que
el proceso que realiza la tarea 1 termine. Este es un caso de sincronización condicional
entre procesos. Para conseguir este comportamiento se puede utilizar un semáforo ini-
cializado a cero. Este semáforo es pasado por referencia a los dos procesos. El proceso
que lleva a cabo la tarea 2 invoca el procedimiento wait sobre el semáforo. El proceso
que ejecuta la tarea 1 invoca el procedimiento tarea1 y posteriormente invoca signal
sobre el semáforo. El siguiente código muestra la implementación de este ejemplo:
1 program sinccond ;
2
3 procedure tarea1 ;
4 begin
5 writeln( ’ Tarea 1 ’);
6 end;
7
8 procedure tarea2 ;
9 begin
10 writeln( ’ Tarea 2 ’);
11 end;
164 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
12
30 begin
31 initial (s ,0) ;
32 cobegin
33 p1 (s);
34 p2 (s);
35 coend ;
36 end.
1 program print ;
2
3 process type print (i : integer);
4 begin
5 write( ’Soy el proceso ’, i);
6 end;
7
8 var
9 p1 , p2 : print ;
10
11 begin
12 cobegin
13 p1 (1) ;
14 p2 (2) ;
15 coend ;
16 end.
P ROGRAMACIÓN CONCURRENTE 165
Este programa tiene el problema de permitir resultados que no son correctos. Los proce-
sos deberían ejecutar completamente la instrucción write antes de que el otro proceso
comenzara a ejecutar la suya. En este contexto, el recurso compartido de acceso ex-
clusivo es la pantalla: mientras un proceso está escribiendo por pantalla el otro proceso
no debería interferir con él. Desde el punto de vista de la exclusión mutua, la sección
crítica es la instrucción write. La forma de poner esta instrucción bajo exclusión mutua
es utilizar un semáforo, compartido por los dos procesos, inicializado a uno. Antes de la
sección crítica, los procesos hacen un wait sobre dicho semáforo. Al salir de la sección
crítica, hacen un signal. El primer proceso que hace el wait decrementa el valor del
contador (poniéndolo a cero) y entra en la sección crítica. Si justo después llega el otro
proceso, al hacer wait sobre el semáforo se quedará bloqueado (dado que el valor del
contador es cero), hasta que el proceso que está ejecutando la sección crítica haga el
signal al salir de la misma. A continuación se muestra la modificación del programa
anterior para poner la escritura por pantalla bajo exclusión mutua:
1 program exmutua ;
2
3 process type print (var s : semaphore ; i : integer);
4 begin
5 wait (s);
6 write( ’Soy el proceso ’, i);
7 signal (s);
8 end;
9
10 var
11 s : semaphore ;
12 p1 , p2 : print ;
13
14 begin
15 initial (s ,1) ;
16 cobegin
17 p1 (s , 1) ;
18 p2 (s , 2) ;
19 coend ;
20 end.
Ahora no hay otras intercalaciones posibles que las dos que hacen que uno de los proce-
sos escriba completamente antes del otro. En este sentido se suele decir que hemos con-
vertido la instrucción write(’Soy el proceso ’, i) en una instrucción atómica de
grano grueso, es decir, aunque esta instrucción no es atómica, al ponerla bajo exclusión
mutua se comporta como si lo fuera.
1 program sincbarr ;
2
3 const
4 NPROCESOS = 4;
5
6 procedure accion1 ;
7 begin
8 writeln( ’ accion 1 ’);
9 end;
10
11 procedure accion2 ;
12 begin
13 writeln( ’ accion 2 ’);
14 end;
15
P ROGRAMACIÓN CONCURRENTE 167
22 wait ( em );
23 if contador < NPROCESOS -1 then
24 begin
25 contador := contador + 1;
26 signal ( em );
27 wait ( sb );
28 end
29 else
30 begin
31 signal ( em );
32 for i := 1 to NPROCESOS -1 do
33 signal ( sb );
34 end;
35
36 accion2 ;
37
38 end;
39
40 var
41 procesos : array[1.. NPROCESOS ] of proceso ;
42 em : semaphore ;
43 sb : semaphore ;
44 cont : integer;
45 i : integer;
46
47 begin
48 initial (em , 1) ;
49 initial (sb , 0) ;
50 cont := 0;
51 cobegin
52 for i := 1 to NPROCESOS do
53 procesos [i ]( em , sb , cont );
54 coend ;
55 end.
haga falsa (señal de que es el último proceso), hay que liberar la exclusión mutua que se
adquirió antes del if.
En este ejemplo se ha incluido también una forma alternativa de iniciar los procesos que
resulta más cómoda. Cuando el número de procesos es un número relativamente grande
y son todos del mismo tipo, puede ser muy engorroso tener que iniciarlos todos dentro
del bloque cobegin..coend listándolos uno por uno. En PascalFC es posible declarar
un array del tipo proceso correspondiente (en este caso el tipo se llama proceso) y
posteriormente iniciar los procesos utilizando un bucle for que itera por el array.
diferentes tipos. Por ejemplo, el siguiente es un fragmento válido de Ruby (donde puts
es una instrucción que imprime por pantalla el argumento pasado como parámetro):
1 a = 10
2 puts a * 2
3 a = " Cadena "
4 puts a
3.6.2 Portabilidad
El hecho de que estos lenguajes se ejecuten directamente en un intérprete o mediante
una máquina virtual hace que sean completamente portables. Si existe un intérprete o
una máquina virtual para una arquitectura y un sistema operativo específico, entonces el
programa puede correr en dicha combinación de arquitectura y sistema operativo. Así,
Ruby dispone de intérpretes para Windows, MacOS y Linux.
El alto grado de portabilidad de estos lenguajes los hace muy atractivos de cara a los
desarrolladores, que pueden programar aplicaciones sin preocuparse del sistema en el
que éstas se van a ejecutar.
Esto contrasta con la poca portabilidad de otros lenguajes como C, donde la compilación
del programa para una arquitectura diferente de aquella para la que se diseñó puede
obligar a cambiar librerías y parte del código fuente original.
1 frutas = [" manzana " , " platano " , " pera " , " melocotón "]
2 puts frutas
Se pueden añadir elementos a una lista con el operador «. Este operador añade elementos
al final de la lista. Alternativamente se puede utilizar también el método push. Para
acceder a un elemento de la lista se utiliza el operador corchetes, indicando el índice (los
índices comienzan en cero):
1 c = []
2 c << 2
3 c << 3
4 c. push (4)
5 puts c
6 puts c [1]
1 [2, 3, 4]
2 3
Los diccionarios, o arrays asociativos, son estructuras de datos que asocian un valor a
una clave. La clave sólo puede aparecer una vez en el diccionario (es única), pero un
mismo valor puede aparecer asociado a diferentes claves. Los diccionarios se represen-
tan de forma literal entre llaves, indicando los pares clave-valor separados por comas y
separando la clave del valor por el operador =>. Cualquier objeto puede usarse de clave.
El acceso a los elementos del diccionario se realiza utilizando la clave con la notación
corchetes (como en los arrays):
1 1
Las listas en Ruby son objetos de la clase Array. Cuando se crea una lista utilizando
la notación literal, se está creando un objeto Array incializado con los valores propor-
cionados. Los diccionarios son objetos de la clase Hash. No es posible crear objetos
diccionario vacíos especificando las llaves vacías como se hace con los corchetes en el
caso de las listas. En su lugar tenemos que crear explícitamente el objeto Hash. Para
crear un objeto en Ruby utilizamos el nombre de la clase junto con el operador punto
para invocar el método new, que crea un objeto de la clase especificada e invoca su cons-
tructor automáticamente. Si el constructor requiriera parámetros se podrían especificar
a continuación de new entre paréntesis:
P ROGRAMACIÓN CON LENGUAJES DINÁMICOS 171
Los dos fragmentos de código anteriores son equivalentes, pero en el segundo, en lugar
de proporcionar el contenido del diccionario de manera literal, se ha creado un objeto
Hash y después se han creado dos claves con sus correspondientes valores. Obsérvese
cómo se ha utilizado la notación corchetes para crear los dos pares clave-valor en el
diccionario.
Ruby es un lenguaje orientado a objetos, por tanto el usuario puede definir sus propios
tipos de datos mediante la definición de clases. La siguiente es una mínima definición
de la clase Intervalo en Ruby:
1 class Intervalo
2 end
1 class Intervalo
2 def initialize ( limiteInferior , limiteSuperior )
3 @limiteInf = limiteInferior
4 @limiteSup = limiteSuperior
5 end
6 end
Obsérvese que no se indica el tipo en los parámetros del método initialize. Aunque
no existe declaración alguna, @limiteInf y @limiteSup son atributos. Es importante
dar valor a los atributos en el constructor. De otra manera, podría darse la circunstancia
de intentar acceder a un atributo en un método de la clase que no ha sido previamente
inicializado.
172 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
1 class Intervalo
2
3 def initialize ( limiteInferior , limiteSuperior )
4 @limiteInf = limiteInferior
5 @limiteSup = limiteSuperior
6 end
7
8 def contiene ( valor )
9 return (( @limiteInf <= valor ) and ( valor <= @limiteSup ))
10 end
11
12 end
Por defecto en Ruby los métodos definidos en una clase tienen visibilidad pública. Para
definir métodos privados hay que abrir una sección privada en la clase mediante la pala-
bra reservada private. Por ejemplo, supongamos que quisiéramos poder comprobar la
validez del intervalo, es decir, que el límite inferior es estrictamente menor que el límite
superior. Podríamos utilizar un método privado valido invocado desde el constructor.
Si el método devuelve true, los argumentos son copiados en los atributos del objeto, en
caso contrario se eleva una excepción (una condición de error que puede ser capturada
desde el código que llamó al constructor):
1 class Intervalo
2
16 private
17
P ROGRAMACIÓN CON LENGUAJES DINÁMICOS 173
En ocasiones es necesario acceder a los atributos de otro objeto. Esta situación debe
evitarse en la medida de lo posible, pero considérese el siguiente escenario: se desea
implementar un método incluye que dado un intervalo determine si éste está incluido
completamente dentro del intervalo definido por el objeto que recibe el mensaje. Podría
pensarse en una implementación como la siguiente:
1 class Intervalo
2
3 def initialize ( limiteInferior , limiteSuperior )
4 if( valido ( limiteInferior , limiteSuperior )) then
5 @limiteInf = limiteInferior
6 @limiteSup = limiteSuperior
7 else
8 raise (" Intervalo no válido ")
9 end
10 end
11
20 def limiteInf
21 return @limiteInf
174 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
22 end
23
24 def limiteSup
25 return @limiteSup
26 end
27
28 private
29
30 def valido (inf , sup )
31 return ( inf < sup )
32 end
33 end
3.6.4 Clausuras
Las clausuras o bloques (closures en inglés) son métodos anónimos (sin nombre) que
pueden recibir parámetros y asociarse a otros métodos o pasarlos como argumentos. En
este sentido son similares a las funciones de primer orden en los lenguajes funcionales.
Por ejemplo, supóngase que se desea ordenar un array de intervalos en función de su
longitud. Ruby no puede saber cómo ordenar un array que contiene objetos de la clase
Intervalo. Lo habitual es que el algoritmo de ordenación utilice un comparador de ele-
mentos para determinar el orden relativo de cada par de elementos a ordenar. Este com-
parador es proporcionado por el programador de la clase Intervalo, que conoce la lógica
de ordenación de los intervalos (en este caso en base a su longitud).
En Ruby, el método sort de la clase Array nos permite asociar un bloque a dicho
método donde se especifique cómo se comparan dos elementos del array. Basándose en
el valor devuelto por este bloque para determinados pares de elementos sort es capaz
de ordenar el array completo:
palabra reservada return para devolver el valor. En Ruby, la última expresión evaluada
es devuelta siempre, tanto en bloques como en métodos. En el caso de los bloques no
puede utilizarse return, dado que el bloque no es un método en el sentido estricto, y la
invocación de esta sentencia provocaría la salida del método sort, que es el que invoca
realmente el bloque.
El método sort implementa el algoritmo de ordenación y cada vez que lo requiere llama
al bloque pasándole dos elementos del array que necesita comparar. El programador
proporciona en este bloque la política de comparación que desee, desacoplando así la
implementación del algoritmo de ordenación respecto al tipo de datos que se quiere
ordenar.
Para que esta ordenación funcione es necesario añadir el método longitud a la clase
Intervalo. Si este método sólo fuera a utilizarse como un método interno de la clase,
debería hacerse privado. En este caso, se supone que el método longitud es parte de la
vista pública de la clase, y puede ser llamado por los usuarios de la clase:
1 def longitud
2 return @limiteSup - @limiteInf
3 end
3.6.5 Iteradores
Ruby proporciona iteradores para manejar colecciones. Los iteradores son métodos que
permiten recorrer una colección de elementos. El uso de iteradores oculta al progra-
mador la implementación concreta de la colección. Por ejemplo, considérese el siguiente
fragmento de código Java:
1 lista = [1 ,2 ,3]
176 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
Nótese como en este caso el programador queda liberado de recorrer la lista, o de conocer
exactamente los índices o la forma de obtener elementos de la misma. El método each es
un iterador de la clase Array. Por cada elemento de la lista (en este caso tres), invocará
al bloque pasándole dicho elemento. El bloque se limita a imprimir por pantalla dicho
elemento.
Considérese un ejemplo un poco más elaborado: se desea sumar todos los elementos de
la lista. Esto se puede hacer también con el iterador each, apoyándose en una variable
local. Los bloques pueden utilizar variables locales definidas en el ambiente exterior
al bloque. Esta es una gran ventaja de los bloques, dado que no es necesario pasar
argumentos adicionales al iterador. En tiempo de ejecución el bloque creado incluye
toda la información de variables accesibles desde el bloque, de forma que cuando es
invocado desde el método each, tiene acceso a dicha información:
1 lista = [1 ,2 ,3]
2 suma = 0
3 lista . each () {| e| suma = suma + e}
4 puts suma
Los diccionarios también definen un iterador each. En este caso, el iterador invoca el
bloque asociado con dos argumentos: la clave y el valor. La siguiente podría ser una
forma de imprimir el contenido de un diccionario por pantalla:
Es posible que el programador defina sus propios iteradores. Supóngase que se desea
implementar un iterador que recorra los valores de un intervalo (asumiendo que el in-
tervalo se define sobre los números enteros). Para cada uno de esos valores se invocará
el bloque asociado al iterador. La invocación del bloque asociado, dado que no tiene
nombre, se realiza mediante la palabra reservada yield y puede llevar argumentos (se
han obviado la mayoría de métodos por claridad):
1 class Intervalo
2
11
12 def each
13 for i in @limiteInf .. @limiteSup do
14 yield(i)
15 end
16 end
17
18 private
19
1 5
2 6
3 7
4 8
5 9
6 10
14
15 a == b && coincide restoA restoB
3. Se pide escribir un programa funcional que, dada una cadena de caracteres que
contiene una frase, devuelva la lista de palabras de la frase considerando que
las palabras están siempre separadas por espacios. Puede utilizarse la función
reverse para invertir una lista si es necesario.
4. Implementa en Haskell el tipo Matriz y una función dimension que dada una ma-
triz devuelva la dimensión de la misma (número de filas y número de columnas).
Se permite utilizar la función predefinida length que dada una lista de cualquier
tipo devuelve su longitud.
1 % Sistema experto
2
3 programador_bueno (X) :-
4 experiencia (X) ,
5 ( documenta (X);
6 test (X)).
7
8 programador_regular (X) :-
9 experiencia (X) ,
10 not( test (X)) ,
11 not( documenta (X)).
12
13 % Base de conocimiento
14
15 experiencia ( programador2 ).
16 experiencia ( programador3 ).
17 experiencia ( programador4 ).
18
19 documenta ( programador2 ).
20 documenta ( programador4 ).
21
22 test ( programador4 ).
23 test ( programador1 ).
1 programador_bueno(programador1).
2 false.
3
4 programador_bueno(programador2).
5 true
E JERCICIOS RESUELTOS 181
7 programador_bueno(X).
8 X = programador2 ;
9 X = programador4 ;
10 X = programador4.
11
12 programador_regular(X).
13 X = programador3 ;
14 false.
6. Dos ciudades están conectadas por AVE si existe una línea de AVE directa entre
ellas, o se pueden coger varias líneas de AVE para llegar de una a otra. Se tiene la
siguiente información:
Se pide escribir en Prolog una regla que determine si dos ciudades están conec-
tadas, ya sea directamente o a través de otras ciudades. Se pide también contestar
las siguientes preguntas:
1 conectadas (X ,Y) :-
2 ave (X ,Y).
3 conectadas (X ,Y) :-
4 ave (X ,Z) ,
5 conectadas (Z ,Y).
6
1 conectadas(sevilla,barcelona).
2 true
182 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
4 conectadas(malaga,valladolid).
5 false.
6
7 conectadas(sevilla,X).
8 X = madrid ;
9 X = valladolid ;
10 X = barcelona ;
11 X = gijon ;
12 false.
7. Uno de los grandes problemas a la hora de licenciar software libre es decidir bajo
qué licencia liberar dicho software. Existen ciertas compatibilidades entre licen-
cias que pueden ayudar a tomar dicha decisión. Consultando a un experto se ha
obtenido la siguiente información:
• ¿Se puede combinar software con licencias Apache 2.0 y GPL v3.?
• ¿Con qué licencia/s se puede combinar software licenciado bajo Apache 2.0?
• ¿Hay alguna licencia poco restrictiva compatible con MIT/X11?
E JERCICIOS RESUELTOS 183
12 compatible (A ,B) :-
13 compatible_directa (A ,B).
14
15 compatible (A ,B) :-
16 compatible_directa (A ,C) ,
17 compatible (C ,B).
18
19 permisiva ( ’MIT / X11 ’).
20 permisiva ( ’BSD - new ’).
21 permisiva ( ’ Apache 2.0 ’).
22
8. Se desea implementar un carrito de la compra en Java para una tienda online que
vende dos tipos de productos: libros y cds. Tanto los libros como los cds tienen un
título y un precio sin IVA. En el precio final a los libros se les aplica el 8% de IVA,
mientras que a los cds se les aplica el 18%. Se pide implementar un carrito de la
compra de forma que se puedan añadir productos al mismo y permita calcular el
precio total de los productos en el carrito (IVA incluido).
14 }
1 program cliserv ;
2
3 process type pcliente (var peticion , respuesta : semaphore ;
var dato : integer);
4 begin
5 signal ( peticion );
6 wait ( respuesta );
7 writeln( ’ Datos recibidos : ’, dato );
8 end;
9
10 process type pservidor (var peticion , respuesta : semaphore ;
var dato : integer);
11 begin
12 wait ( peticion );
13 dato := random (100) ;
14 signal ( respuesta );
15 end;
16
17 var
18 peticion , respuesta : semaphore ;
19 cliente : pcliente ;
20 servidor : pservidor ;
21 dato : integer;
22
23 begin
24 initial ( peticion , 0) ;
25 initial ( respuesta , 0) ;
26 cobegin
27 cliente ( peticion , respuesta , dato );
28 servidor ( peticion , respuesta , dato );
29 coend ;
30 end.
10. Implementa la clase Intervalo en Ruby y una clase Gestor que gestione un con-
junto de invervalos con un método addIntervalo para añadir un nuevo intervalo
al gestor. Se pide también implementar un método eachMayores(n) que recorra
la lista de intervalos de mayor longitud que n.
1 class Intervalo
2
12 def longitud
13 return @limiteSup - @limiteInf
14 end
15
16 private
17
3 class Gestor
4
5 def initialize
6 @intervalos = []
7 end
8
2. Un camino se puede definir como una secuencia de puntos que deben visitarse
en orden para ir de un extremo al otro. Cada punto viene definido por sus co-
ordenadas x e y en un plano. La distancia entre dos puntos se puede calcular
188 PARADIGMAS Y MODELOS DE PROGRAMACIÓN
p
como (x2 − x1 )2 + (y2 − y1 )2 . Se pide definir los tipos de datos necesarios y una
función longitud, en Haskell, que dado un camino calcule la longitud total del
mismo.
3. Amplia el programa Haskell del ejercicio resuelto 4 incluyendo una función suma
que dada una matriz devuelva la suma de todos los elementos de la misma.
4. Auméntese el sistema de reglas del programa Prolog del ejercicio resuelto 5 con
la siguiente regla:
5. Un servidor contiene ficheros binarios y ficheros de texto que los clientes pueden
descargar del mismo. Los ficheros binarios tienen un tamaño en bytes y no se
pueden comprimir. Los ficheros de texto tienen también un determinado tamaño
en bytes y un ratio de compresión, que es lo máximo que se pueden comprimir.
Los ficheros de texto se envían siempre comprimidos. El administrador desea
disponer de estadísticas sobre los ficheros descargados por los clientes. Concre-
tamente, se desea saber la cantidad de bytes transferidos por la red. Escribir un
programa en Java para calcular, dada una lista de ficheros, la cantidad de bytes
que se transferirían si un cliente los descargara todos.
6. Modifica el programa PascalFC del ejercicio resuelto 9 para que el cliente realice
10 peticiones y el servidor las atienda.
7. Implementa en Ruby una clase Matriz que represente un array de filas que com-
ponen una matriz. La clase debe incluir los siguientes métodos:
lenguaje. [3] es otro libro sobre Prolog más reciente, pero que presenta los conceptos
también de forma ordenada y gradual.
El paradigma orientado a objetos está basado en [5]. Otro libro adecuado para aprender
programación orientada a objetos desde el diseño de programas es [29]. Para la parte de
programación concurrente se utilizó principalmente el libro [30]. Para una introducción
a la programación concurrente con Java, [13] es uno de los libros más recomendables. La
parte de lenguajes dinámicos fue preparada utilizando principalmente material de [27]
(disponible también online en http://www.ruby-doc.org/docs/ProgrammingRuby/). Otro
buen libro sobre Ruby es [12], co-escrito por el propio autor del lenguaje.
Capítulo 4
4.1 Introducción
Una anotación, que también se suele denominar metadato o metainformación, es una
información añadida a un documento que no forma parte del mensaje en sí mismo.
Las anotaciones permiten hacer explícita una información que puede estar implícita en
el propio texto, o que de alguna manera se quiere hacer patente para facilitar así el
aprovechamiento y procesamiento de los documentos.
Los lenguajes de marcado, o de anotaciones, son un conjunto de reglas que describen
cómo deben realizarse las anotaciones, bajo qué condiciones se permiten y, en ocasiones,
su significado. Las anotaciones de un lenguaje de marcado deben poder distinguirse
sintácticamente del texto al que se refieren. Por ejemplo:
1 <s >El día <date > 21/11/2000 </ date > tuvo lugar ... </s >
contiene un segmento de texto anotado con la etiqueta <s>, que se refiere a una sentencia,
dentro del que se encuentra una fecha anotada con la etiqueta <date>. Esas anotaciones
permitirán delimitar dichos elementos estructurales para, por ejemplo, seleccionar sen-
tencias completas o recuperar las fechas que aparecen en un documento. La notación
191
192 L ENGUAJES DE MARCADO . XML
utilizada para las anotaciones (<s>, <date>, </s>, </date>) permite distinguirlas sintác-
ticamente del texto.
Se pueden distinguir los siguientes tipos de lenguaje de marcado:
Dentro de los lenguajes de marcado en este capítulo nos centraremos en los de tipo
estructural, en particular en XML. SGML y XML se suelen denominar metalenguajes
porque permiten definir lenguajes de marcado. Es decir, el usuario puede definir las
anotaciones que considere convenientes y a priori dichas anotaciones no tienen ningún
significado concreto. Serán las aplicaciones que usen dichas anotaciones las que las
doten de la semántica oportuna.
Uno de los primeros metalenguajes fue SGML (Standard Generalized Markup Lan-
guage). Aunque su origen es anterior a 1986, procedía del lenguaje GML de IBM,
y se estandarizó en ese año en la International Organization for Standardization (ISO
8879:1986). SGML se diseñó con dos principales objetivos1 :
• Las anotaciones deben ser rigurosas de manera que las técnicas disponibles para
procesar objetos, como programas y bases de datos, se puedan aplicar también al
procesamiento de documentos.
1 http://en.wikipedia.org/wiki/Standard_Generalized_Markup_Language
I NTRODUCCIÓN 193
SGML define la estructura de un tipo o clase de documento con lo que se denomina DTD
(Document Type Definition). Una DTD es un conjunto de declaraciones que definen un
tipo de documento perteneciente a la familia de lenguajes de marcado SGML (SGML,
XML, HTML). En el apartado 4.4 veremos la sintaxis de las DTD.
Un lenguaje de marcado muy conocido por su uso en la Web es HTML. HTML (Hy-
perText Markup Language) es un lenguaje de marcado que permite formatear dinámica-
mente texto e imágenes de páginas web. Es un lenguaje de marcado definido en SGML
y no es un metalenguaje ya que tiene un número fijo de etiquetas o anotaciones con
un significado preestablecido. Entre las etiquetas estructurales están, por ejemplo, las
que describen un título, párrafo, enlace, etc. Empezó a desarrollarse en 1989 por Tim
Berners-Lee en el Laboratorio Europeo de Física de Partículas (CERN) con el objetivo
de presentar información estática. Actualmente es el lenguaje de marcado más utilizado
para páginas web. Un documento HTML puede embeber scripts para determinar el
comportamiento de las páginas web HTML, que pueden estar escritos por ejemplo en
JavaScript, PHP, etc. También puede tener asociada una hoja de estilo tipo Cascading
Style Sheets (CSS) para definir la apariencia de los contenidos.
XML (Extensible Markup Language), el lenguaje de marcado en el que nos centraremos
en este capítulo, es una forma restringida de SGML optimizada para su utilización en
Internet y al igual que SGML es un metalenguaje. Fue descrito en 1996 por el consor-
cio WWW (World Wide Web Consortium, W3C2 ) con los siguientes objetivos iniciales
principales:
XML describe una clase de objetos de datos (documentos XML) y parcialmente el com-
portamiento de los programas que los procesan. XML está definido en la denominada
Especificación 1.03 que proporciona su especificación y los estándares adoptados para
los caracteres (Unicode e ISO/IEC 10646)4 , identificación de lenguas5 , códigos de nom-
bres de lenguajes6 y códigos de nombres de países7 . La especificación contiene toda
la información necesaria para comprender XML y para poder construir programas que
procesen documentos XML.
Para finalizar esta introducción vamos a distinguir entre diferentes tipos de software
relacionado con XML.
2 http://www.w3.org/
3 http://www.w3.org/TR/xml/
4 http://www.iso.org/iso/home.htm
5 http://tools.ietf.org/html/rfc3066
6 http://www.loc.gov/standards/iso639-2/php/English_list.php
7 http://www.iso.org/iso/english_country_names_and_code_elements
194 L ENGUAJES DE MARCADO . XML
4.2.1 Elementos
Un elemento es un conjunto de datos del documento delimitado por etiquetas de comienzo,
"<>", y fin de elemento, "</>". Cada elemento representa un componente lógico del
documento y puede contener otros elementos. Normalmente contendrá datos de tipo
carácter como texto, tablas, etc. En el siguiente ejemplo:
1 <s >El día <date > 21/11/2000 </ date > tuvo lugar ... </s >
4.2.2 Etiquetas
Delimitan los elementos de un documento XML. La sintaxis de la etiqueta de comienzo
de elemento es: <nombreElemento>; y la sintaxis de la etiqueta de fin de elemento:
</nombreElemento>. El nombre del elemento es nombreElemento. En el siguiente
ejemplo:
el elemento doc tiene el atributo lang con valor en, que según los estándares significa
que la lengua del contenido de dicho elemento es el inglés. El elemento p, que suele
corresponder a un párrafo, tiene el atributo id con valor 1, que es un atributo de identi-
ficación de dicho elemento en el conjunto del documento.
XML permite que haya elementos vacíos, es decir, elementos que sólo constan de una
etiqueta de inicio, con una sintaxis particular, y atributos. La sintaxis de un elemento
vacío es la siguiente: <nombreElemento atrib1="va1" .../>. En el siguiente ejem-
plo:
se presenta el elemento vacío imagen que contiene el atributo fichero, cuyo valor es el
nombre de un fichero que contiene una imagen en formato gif.
4.2.3 Comentarios
Los comentarios en XML tienen la misma funcionalidad que los comentarios en los
lenguajes de programación. No forman parte del contenido del documento que será
analizado, es decir, no serán procesados por el analizador.
Un comentario es un texto que se escribe entre los símbolos de apertura de comentario
"<!--" y de cierre de comentario "-->". Por ejemplo:
La cadena "--" no puede aparecer dentro del contenido de un comentario ya que forma
parte de la secuencia de caracteres delimitadores. En XML los comentarios pueden
aparecer en cualquier punto del documento excepto dentro de las declaraciones, dentro
de las etiquetas y dentro de otros comentarios.
Veamos un ejemplo:
<saludo> y </saludo> serán reconocidos como caracteres del contenido del documento
y no como etiquetas XML. Las secciones CDATA no se pueden anidar.
4.2.5 Entidades
Una entidad permite asignar un nombre (referencia) a un subconjunto de datos (texto) y
utilizar ese nombre para referirnos a dicho subconjunto. Las entidades pueden aparecer
en dos contextos: el documento y la DTD.
La sintaxis de una entidad consta de la marca de comienzo de una referencia a una
entidad "&" y la del final ";". En el apartado 4.4.3 se verá en mayor detalle el uso y
declaración de los distintos tipos de entidades.
XML especifica 5 entidades predefinidas que permiten representar 5 caracteres espe-
ciales, de modo que se interpreten por el analizador o el procesador con el significado
que tienen por defecto en XML:
Las entidades predefinidas y las secciones CDATA permiten, por ejemplo, que dentro de
un documento XML se puedan escribir etiquetas de comienzo y de fin de elemento sin
que sean tomadas como tales. Un caso claro de aplicación sería un libro que describiera
el lenguaje XML o HTML y que estuviera escrito utilizando XML.
A través de las entidades se puede también hacer referencia a caracteres utilizando el
valor numérico de su codificación. La referencia puede hacerse con el valor decimal
del carácter utilizando la sintaxis: &#Num;, o bien con su valor en hexadecimal con la
sintaxis: &#xNum;. Por ejemplo, © es una referencia al valor decimal del signo de
copyright y © es la referencia al mismo carácter en hexadecimal8 .
Los analizadores XML no hacen nada con las instrucciones de procesamiento: las pasan
a la aplicación para que ésta decida qué hacer. El objetivo es similar al de los comen-
tarios, pero éstos van destinados a los usuarios, mientras que las instrucciones de proce-
samiento van destinadas a los programas.
Por último, están prohibidas las instrucciones de procesamiento que comiencen por
XML, salvo la del prólogo.
4.2.7 Prólogo
Los documentos XML pueden empezar con un prólogo en el que se define una declaración
XML y una declaración de tipo de documento. La declaración XML es una instrucción
de procesamiento de este tipo:
que indica la versión de XML que se está utilizando y la información sobre el tipo de
codificación de caracteres. En el ejemplo anterior la versión es la 1.0 y el código el
ASCII de 7 bits (subconjunto del código Unicode denominado UTF-8) que es el que los
analizadores manejan por defecto. Otro ejemplo podría ser:
Hay más codificaciones de caracteres además de UTF-8 y UTF-16, pero éstas son las
únicas soportadas en todos los procesadores XML.
En la declaración del tipo de documento se asocia la DTD o XSD respecto a la cual
el documento es conforme. La DTD puede estar en un fichero distinto del documento,
siendo en este caso lo que se conoce como DTD externa. Por ejemplo:
Con una DTD externa el prólogo con las dos declaraciones quedaría:
Las dos partes del prólogo son opcionales, aunque en el caso de incluir ambas la declaración
XML tiene que ir antes.
• La DTD tiene una sintaxis específica mientras que el XSD utiliza sintaxis XML.
• Un XSD soporta tipos de datos (int, float, boolean, date, . . . ) mientras que las
DTD tratan todos los datos como cadenas.
En una DTD puede haber cuatro tipos de declaraciones: tipo de elemento, atributos,
entidades y notaciones. A continuación vamos a ver cada una de ellas en más detalle.
describe el elemento <receta> que a su vez puede contener a los elementos <titulo>,
<ingredientes> y <procedimiento>.
Teniendo en cuenta la especificación anterior del elemento <receta>, el siguiente docu-
mento sería válido:
• EMPTY: indica que el elemento no tiene contenido pero sí puede tener atributos.
Por ejemplo:
En este caso el elemento articulo sólo podrá contener los elementos titulo,
resumen, cuerpo y referencias.
• Mixto: puede tener caracteres o una mezcla de caracteres y elementos. Para in-
dicar que el contenido de un elemento son caracteres de texto que serán analizados
se utiliza #PCDATA. Por ejemplo:
1 < enfasis > este texto está enfatizado </ enfasis >
1 < parrafo > A comienzos de ... < enfasis > este texto está
enfatizado </ enfasis > ... </ parrafo >
El elemento aviso debe contener un elemento titulo que podrá estar seguido de
un elemento parrafo o un elemento grafico.
El modelo de contenido del elemento aviso indica que puede contener de manera op-
cional un elemento titulo, seguido opcionalmente, y en caso de aparecer de forma
repetida, por un elemento parrafo o por un elemento grafico.
Los siguientes ejemplos son válidos de acuerdo al modelo de contenido anterior del
elemento aviso:
Ejemplo 1:
1 < aviso > < titulo > </ titulo > </ aviso >
Ejemplo 2:
1 < aviso > < grafico > </ grafico > </ aviso >
Ejemplo 3:
1 < aviso > < titulo > </ titulo > < grafico > </ grafico >
2 < grafico > </ grafico > < grafico > </ grafico >
3 </ aviso >
Ejemplo 4:
1 < aviso > < titulo > </ titulo > < parrafo > </ parrafo > </ aviso >
Ejemplo 5:
1 < aviso > < parrafo > </ parrafo > < parrafo > </ parrafo > </ aviso >
Notad que sólo se representan las etiquetas de inicio y fin de los subelementos sin entrar
en su contenido.
F UNDAMENTOS DE LA DTD 203
• #FIXED valor: el atributo tiene un valor fijo, el de una constante que aparece en la
declaración.
• NMTOKEN: solo podrá contener un name token. Un name token es una cadena de
letras, dígitos, y los caracteres punto, guión, subrayado y dos puntos. El resto de
caracteres y los espacios en blanco no pueden formar parte del mismo.
• Enumerados: es una lista de posibles valores del atributo separados por el carácter
"|". Puede haber tantos valores como se desee y deben ser de tipo name token.
• ID: es un identificador único para un elemento XML. El uso de este tipo de atri-
buto permitirá acceder a un elemento concreto del documento. Los atributos ID
deben declararse como #REQUIRED o #IMPLIED. Su valor deberá ser un nom-
bre XML, que es similar a un name token, pero con la restricción añadida de que
no puede empezar por un dígito. Un elemento sólo puede tener un atributo ID y
su valor no puede estar repetido a lo largo del documento.
fecha es un atributo obligatorio del elemento mensaje y su valor puede ser cualquier
carácter XML.
en este caso fecha es un atributo obligatorio del elemento mensaje y su valor debe ser
un name token.
Al ser fecha de tipo NMTOKEN, no puede contener espacios como en el ejemplo ante-
rior.
F UNDAMENTOS DE LA DTD 205
En una sola declaración ATTLIST se pueden declarar varios atributos. Veamos ahora
unos ejemplos de declaración de elementos y atributos:
El atributo prioridad puede tener el valor normal o urgente, siendo normal el valor
por defecto si no se especifica el atributo.
Los atributos fecha y formato son obligatorios y deben tomar uno de entre los dos tipos
de notaciones indicados en cada caso que deberán, a su vez, estar también declarados en
la DTD.
Veamos un ejemplo de cómo implementar un hipervínculo en un documento utilizando
atributos de tipo ID e IDREF. Empecemos con la declaración de elementos y atributos:
1 ...
2 <!ELEMENT capitulo ( parrafo )* >
3 <!ATTLIST capitulo referencia ID #REQUIRED>
4 <!ELEMENT enlace EMPTY>
5 <!ATTLIST enlace destino IDREF #REQUIRED>
6 ...
206 L ENGUAJES DE MARCADO . XML
1 ...
2 < capitulo referencia =" seccion -3 " > < parrafo > ... </ parrafo >
3 </ capitulo >
4 ...
5 En el capítulo < enlace destino =" seccion -3 " > podrá encontrar ...
1 ...
2 < capitulo identificacion =" seccion -1 " > < parrafo > ... </ parrafo >
3 </ capitulo >
4 < capitulo identificacion =" seccion -2 " > < parrafo > ... </ parrafo >
5 </ capitulo >
6 < capitulo identificacion =" seccion -3 " > < parrafo > ... </ parrafo >
7 </ capitulo >
8 ...
9 En los siguientes capítulos < referenciaCapitulos enlazaAvarios =
10 " seccion -1 seccion -2 seccion -3 " > podrá encontrar ...
F UNDAMENTOS DE LA DTD 207
Por ejemplo:
está declarando la entidad uned que tendrá como contenido asociado el texto "Universi-
dad Nacional de Educación a Distancia". Una vez así declarada, se podrá utilizar en el
documento XML:
1 < texto > < titulo > La & uned ; </ titulo >
2 ...
3 </ texto >
El analizador interpretará que la entidad &uned; está sustituyendo al texto que tiene
asociado. Las entidades generales analizadas, como son texto XML, también pueden
contener etiquetas.
Se puede hacer referencia a una entidad de este tipo en cualquier parte del documento.
Una misma entidad puede declarase más de una vez pero sólo se tendrá en cuenta la
primera declaración.
En las entidades generales internas su contenido asociado está hecho explícito en la
propia DTD. Sin embargo, las entidades generales también pueden ser externas; en este
caso tienen su contenido en cualquier otro sitio del sistema (un fichero, una página web,
un objeto de base de datos, . . . ).
En las entidades generales externas su definición se ubica en un recurso externo, que se
representa mediante la palabra reservada SYSTEM seguida por un Identificador Universal
de Recursos o URI (Uniform Resource Identifier).
Por ejemplo:
208 L ENGUAJES DE MARCADO . XML
está declarando la entidad introduccion que tendrá como contenido asociado el del
recurso http://www.miservidor.com/intro.xml.
El recurso que sigue a la palabra reservada SYSTEM puede estar en una ubicación local.
Por ejemplo:
Uno de los principales usos de las entidades generales externas analizadas es permitir
que un documento se forme con referencias a subdocumentos. Veamos un ejemplo:
procese el documento cómo debe tratar o qué debe hacer con la entidad en cuestión. Este
tipo de entidades se referencian por medio de un atributo de tipo ENTITY o ENTITIES.
Veamos el siguiente ejemplo:
En la DTD se declara el elemento vacío persona con dos atributos: uno obligatorio
nombre y uno opcional foto, que al ser de tipo ENTITY hará referencia a una entidad
externa no analizada. Por otro lado, en la parte de declaración de entidades, se declara la
entidad general externa csfoto que es de tipo no analizada al llevar la palabra reservada
NDATA a continuación del identificador del recurso. JPEG es el nombre de una notación
que también deberá estar declarada en la DTD. Ya en el documento se utiliza el elemento
persona con sus dos atributos con valores correctos de acuerdo a su declaración.
En el apartado 4.4.4 veremos cómo se declara una notación en la DTD y podremos
completar el ejemplo anterior.
El carácter "%" es el que diferencia la declaración de una entidad general de una enti-
dad parámetro. Su uso también es distinto. A continuación se muestra un ejemplo de
declaración y uso de una entidad parámetro interna:
Se declara la notación GIF con la definición GIF que corresponde a un tipo de imagen
que las aplicaciones deberán saber cómo procesar.
F UNDAMENTOS DE LA DTD 211
En este caso se declara la notación GIF con la definición Iexplore.exe que es el nom-
bre de una aplicación que puede procesar imágenes de ese tipo. La aplicación que pro-
cese documentos XML con esta notación podría usar el programa Iexplore.exe para ver
imágenes de tipo GIF que tengan asociada dicha notación.
Ahora podemos completar el ejemplo del apartado 4.4.3 con la declaración de nota-
ciones:
Por ejemplo:
Todos los elementos de un documento XML están dentro del elemento raíz, en este caso
del elemento peliculas. La DTD externa está en el fichero local peliculas.dtd y
además hay una declaración interna correspondiente al elemento actor.
Cuando en una DTD conviven los dos tipos de declaraciones, las declaraciones internas
prevalecen a las externas.
Veamos un ejemplo completo de una DTD. Se trata de declarar la estructura de un listín
de contactos en el que además de la información habitual (nombre, teléfono, dirección,
email), se plasmará la posible relación entre los contactos mediante enlaces:
1 <?xml version = " 1.0 " encoding = " UTF -8 "? >
2 <!DOCTYPE listin SYSTEM " listin . dtd " >
3 < listin >
4 < persona id = " ricky " >
5 < nombre > Roberto Casas </ nombre >
6 < telefono > 654783267 </ telefono >
7 < telefono > 918768760 </ telefono >
8 < email >ro . casas@direccion . com </ email >
9 < relacion rel - con = " lara ana "/ >
10 </ persona >
11 < persona id = " lara " >
12 < nombre > Lara García </ nombre >
F UNDAMENTOS DE LA DTD 213
• Todo elemento tiene una etiqueta de inicio y de final o una etiqueta de elemento
vacío.
• Los valores de los atributos van entre comillas dobles o simples (" o ’).
Por ejemplo, el siguiente texto:
1 Mi documento XML
El documento XML sí está bien construido ya que se cumplen todas las condiciones.
También el ejemplo final del apartado 4.4.5 es un documento XML bien construido.
Veamos ahora otro ejemplo:
No es un documento XML bien construido ya que la etiqueta inicio del elemento elem
está dentro del contenido del elemento p, pero su etiqueta final está fuera. Es decir, los
elementos no están anidados de forma correcta. La siguiente versión:
Sí tiene los elementos anidados correctamente y al cumplir las demás condiciones sería
un documento XML bien construido.
E SPACIOS DE NOMBRES 215
Se puede observar cómo el elemento autor (en las líneas 7, 15 y 20) podría causar
problemas de ambigüedad, ya que no es lo mismo el autor de un comentario (líneas 15
y 20) que el autor de un CD (línea 7). Los modelos de contenido de los elementos que
lo contienen son distintos. Con los espacios de nombres se eliminaría esta ambigüedad.
A cada DTD o XSD se le supone asociado su propio espacio de nombres; es decir, un
entorno en el que todos los nombres de elementos son únicos, y todos los nombres de
atributos también son únicos dentro del contexto del elemento al que pertenecen. Por lo
tanto cualquier referencia a un elemento o atributo no es ambigua. Para combinar docu-
mentos de diferente tipo tendrán que convivir en un mismo documento varios espacios
de nombres. Ahora veremos cómo se nombran y cómo se usan los espacios de nombres.
Para garantizar la singularidad de los espacios de nombres se utilizan como identifi-
cadores las URL (Localizador de Recursos Uniforme) o las URN (Nombre de Recurso
Uniforme). Esto no significa que necesariamente los espacios de nombre tengan que es-
tar en Internet, sino que se corresponde con una cadena de texto que la aplicación sabrá
identificar.
La parte prefijo es opcional y sirve de referencia del espacio de nombres a lo largo del
alcance del elemento en el que se declara. El uso de prefijo depende de si se van a
usar elementos calificados o no calificados, y puede ser cualquier cadena que comience
con un carácter alfabético, seguida por cualquier combinación de dígitos, letras y signos
de puntuación, excepto ":".
El valor del atributo puede ser una cadena cualquiera, aunque por convención suelen ser
una URI (URL, URN), y solo se requiere que sea única. No se requiere que apunte a
nada en particular ya que es simplemente una manera de identificar de forma inequívoca
un conjunto de nombres. Por ejemplo:
1 < elemento xmlns:cd =" http: // www . miweb . com / cd " >
Un nombre no calificado no usa prefijo. Por ejemplo: titulo o director. Este tipo
de nombres o están asociados a un espacio de nombres predeterminado o a ninguno.
Hay dos maneras de definir un espacio de nombres:
El elemento titulo y todos los elementos que contenga tendrán como espacio de nom-
bres la URL http://www.ejemplos.xml/ejemplo1. Al declarar el atributo xmlns
como FIXED se evita que el documento especifique cualquier otro valor. Un elemento
definido de esta manera se hace único, por lo que no genera conflictos con un elemento
que tenga el mismo nombre en otra DTD.
Para hacer referencia a un nombre de elemento que puede ser ambiguo se especifica el
atributo xmlns. Por ejemplo:
1 < titulo xmlns =" http: // www . ejemplos . com / ejemplo1 " > Ejemplo de
espacio de nombre </ titulo >
1 < libro xmlns:lib =" http: // www . ejemplos . com / ejemplo1 " >
2 ...
3 </ libro >
E SPACIOS DE NOMBRES 219
Ahora podremos hacer referencia a los elementos del modelo de contenido de libro con
el prefijo lib. Si, por ejemplo, autor es un elemento de libro podríamos referirnos a
él de la siguiente manera:
1 < libro xmlns:lib =" http: // www . ejemplos . com / ejemplo1 "
2 xmlns:libBib =" http: // www . ejemplos . com / ejemplo2 " >
3 ...
4 < lib:autor > Mario Vargas Llosa </ lib:autor >
5 < libBib:isbn > 0554123903 </ libBib:isbn >
6 ...
7 </ libro >
• Se refiere al elemento para el que se defina y los elementos que contenga, a menos
que se realice otra declaración de espacio de nombres con el mismo prefijo.
• A diferencia de los elementos, los atributos no están unidos, como opción prede-
terminada, a ningún espacio de nombres.
1 <?xml version=" 1.0 " encoding ="iso -8859 -1 " standalone=" yes "? >
2 < doc xmlns =" http: // www . ejemplos . com / ejemplo3 "
3 xmlns:nov =" http: // www . ejemplos . com / ejemplo4 " >
4 ...
5 <par nov:lang =" es " tipo =" normal "/ >
6 ...
Cuando se utilizan espacios de nombres hay que tener en cuenta que ningún elemento
debe contener dos atributos con el mismo nombre o con nombres equivalentes califica-
dos, es decir, las mismas partes locales y los mismos prefijos que corresponden al mismo
URI. Por ejemplo, dadas las siguientes definiciones de espacios de nombres:
1 < esp1:elem xmlns:esp1 =" http: // www . ejemplos . com / ejemplo5 "
2 xmlns:esp2 =" http: // www . ejemplos . com / ejemplo5 " >
3 ...
Los dos siguientes elementos no serían válidos ya que usan el mismo nombre o nombres
equivalentes:
• Usa sintaxis XML. De hecho, los XSD son documentos XML bien formados.
• Tipo simple: elementos con modelo de contenido simple y sin atributos. Sólo
pueden contener información de caracteres.
Por lo tanto, los datos de la instancia documento constituyen el espacio léxico, mientras
que el valor de los datos interpretados de acuerdo a su tipo de datos constituye el espacio
de valores. Siguiendo con el ejemplo, el valor "3.14116" de tipo xs:string es distinto
del valor de "03.14116", "3.141160", ".314116E1". En el primer ejemplo se trata de
un valor numérico que es equivalente, mientras que en el segundo caso se trata de un
tipo de cadena de caracteres que no resulta equivalente. Esta distinción es importante en
operaciones como el test de igualdad o la ordenación.
El espacio léxico es el conjunto de literales válidos o representaciones léxicas para un
tipo de datos. Cada elemento del espacio de valores puede estar vinculado con 0 o n
elementos del espacio léxico, mientras que cada elemento del espacio léxico está rela-
cionado unívocamente con un elemento del espacio de valores. Por ejemplo, "100" y
"1.0E2" son dos diferentes literales del espacio léxico del tipo xs:float que denotan al
mismo valor.
Veamos a continuación cuál es el elemento raíz de todo XSD y posteriormente cómo se
definen los demás elementos.
schema
Es el elemento raíz del XSD. Su sintaxis es la siguiente:
1 < xs:schema xmlns:xs =" http: // www . w3 . org /2001/ XMLSchema " >
F UNDAMENTOS DEL XML-S CHEMA O XSD 223
Este elemento define el espacio de nombres en el que están definidos todos los elementos
y tipos de datos de XML-Schema. Como un XSD es una instancia o documento XML
puede llevar un preámbulo:
element
Define los elementos que contendrán los documentos XML asociados al XSD. La sin-
taxis para definir un elemento de tipo simple es la siguiente:
1 < xs:element name =" nombreElemento " type =" tipoElemento "/ >
Por ejemplo, el elemento nombre del documento de la Figura 4.1 se definiría de la si-
guiente manera:
1 < xs:schema xmlns:xs =" http: // www . w3 . org /2001/ XMLSchema " >
2 < xs:element name =" nombre " type =" xs:string "/ >
3 ...
4 </ xs:schema >
1 < xs:element name =" nombre " type =" xs:string "/ >
2 < xs:element name =" descripcion " type =" xs:string "/ >
3 < xs:element name =" fecha " type =" xs:date "/ >
Veamos ahora cómo se define un elemento de tipo complejo como el elemento titulo
de la Figura 4.1:
Esta definición indica que el elemento archivo es de tipo complejo, está compuesto por
una secuencia de 1 a n ocurrencias de elementos informe. El atributo maxOccurs indica
el número máximo de ocurrencias y el valor predefinido unbounded indica que no tiene
límite.
Ahora vamos a definir el elemento de tipo complejo autor:
Esta definición describe que el elemento autor es de tipo complejo y está compuesto
por una secuencia de 1 a n elementos nombre.
Los atributos de un elemento de tipo complejo con secuencia deben ser definidos después
de la secuencia. Los atributos maxOccurs y minOccurs en la definición de elementos
permiten indicar el número máximo y mínimo de ocurrencias. Su valor por defecto es
1 (el elemento debe aparecer 1 vez). Como hemos visto en los ejemplos anteriores, el
valor unbounded asociado a maxOccurs indica que el número máximo de ocurrencias
es ilimitado.
Cuando los elementos y atributos se definen directamente dentro del elemento docu-
mento xs:schema como en el ejercicio resuelto 4, se denominan globales. Los compo-
F UNDAMENTOS DEL XML-S CHEMA O XSD 225
nentes globales se pueden referenciar en cualquier parte del esquema y en otros esque-
mas que lo importen. También se pueden utilizar como elementos raíz de un documento.
Analicemos con más detalle la siguiente definición:
En ella se hace referencia a un elemento informe definido en otra parte del XSD. Esa
referencia se puede reemplazar con la definición de dicho elemento, como se puede ver
a continuación:
Se puede ver que la definición de informe es local a archivo porque está definido
dentro de dicho elemento. Se trata por lo tanto de una definición local. Podría haber
otras definiciones de informe en otras partes del XSD. Esta definición de informe no
se puede utilizar en cualquier parte del documento, sólo como un elemento descendiente
directo de archivo. El elemento informe ya no puede ser el elemento raíz de un do-
cumento que use este XSD. En el ejercicio resuelto 5 se puede ver un ejemplo completo
de XSD con declaraciones locales. En él se puede observar que se han eliminado los
atributos ref ya que todas las definiciones son locales.
226 L ENGUAJES DE MARCADO . XML
Los dos esquemas de los ejercicios resueltos 4 y 5 permiten validar la misma instancia de
documento. Sin embargo, el 5 es menos reutilizable ya que el elemento raíz, archivo,
es el único elemento global y por lo tanto el único que puede ser utilizado en otro XSD.
En este caso se sacrifica la modularidad en favor de una descripción más acorde con la
estructura de los documentos conformes con dicho esquema.
Es posible combinar los dos tipos de definiciones según el objetivo que se pretenda. Por
ejemplo, si se quieren definir elementos con el mismo nombre pero con diferentes mode-
los de contenido en diferentes partes del XSD, habrá que utilizar definiciones locales. Si
se quiere poder reutilizar elementos ya definidos en un XSD en otro, habrá que utilizar
definiciones globales. Si se realiza un XSD recursivo, en el que un elemento se incluye
dentro de un elemento del mismo tipo como hijo (directa o indirectamente) habrá que
utilizar definiciones globales y referencias.
Supongamos que queremos definir el elemento nombre con diferentes modelos de con-
tenido en autor y asunto. Para hacerlo, al menos uno de ellos habría que definirlo de
forma local. Aunque esto es posible con XSD, puede traer problemas de ambigüedad.
attribute
Define los atributos que podrán contener los elementos. La sintaxis para definirlos es la
siguiente:
1 < xs:attribute name =" id " type =" xs:ID "/ >
2 < xs:attribute name =" confidencial " type =" xs:boolean "/ >
3 < xs:attribute name =" leng " type =" xs:language "/ >
Tipos de cadena
Hay varios tipos posibles de cadenas de caracteres:
xs:NCName Es similar a xs:Name con la restricción de que los valores deben comenzar
con una letra o con el carácter "_".
xs:ID Se deriva de xs:NCName. Su valor debe ser único en el documento ya que se
trata de un identificador único.
xs:ENTITY Se deriva de xs:NCName. Su valor debe emparejarse con una entidad ex-
terna no analizada.
11 http://www.faqs.org/rfcs/rfc1766.html
228 L ENGUAJES DE MARCADO . XML
xs:QName Soporta espacios de nombres con prefijo. Cada xs:QName contiene una tupla
{"nombre de espacio de nombre", "nombre local"}. Soporta espacio de nombres
por defecto y así el valor de la URI en la tupla será el valor por defecto. Por
ejemplo, en:
1 < xs:attribute name =" leng " type =" xs:language "/ >
1 < xs:schema xmlns:xs =" http: // www . w3 . org /2001/ XMLSchema " >
xs:hexBinary Permite codificar contenido binario como una cadena de caracteres tra-
duciendo el valor de cada octeto binario en 2 dígitos hexadecimales.
Tipos numéricos
Los tipos numéricos son los siguientes:
xs:double Igual que xs:float salvo que en este caso la precisión es de 64 bits.
xs:date Define un día concreto del calendario Gregoriano. Por ejemplo: 2003-10-21.
xs:gYear xs:gYearMonth sin la parte del mes. Por ejemplo: 2010, -1340.
xs:gDay Es un día del calendario Gregoriano (----DD). Por ejemplo: ----05 se refiere
al quinto día del mes para representar algo que ocurre ese día todos los meses.
xs:gMonth Es un mes del calendario Gregoriano (--MM). Por ejemplo: --10 se referiría
al mes de octubre en sentido de que ocurre algo todos los años ese mes.
230 L ENGUAJES DE MARCADO . XML
Tipo lista
Tipo no definido
• Por restricción.
• Por lista.
• Por unión.
Los tipos de datos se crean añadiendo restricciones a los posibles valores de otros tipos.
El propio XML-Schema usa este mecanismo. Por ejemplo, xs:positiveInteger es
una derivación por restricción de xs:integer. Las restricciones de un tipo se definen
mediante facetas. Una restricción se añade con el elemento xs:restriction y cada
faceta se define utilizando un elemento específico dentro de xs:restriction. El tipo
de dato que se restringe se denomina tipo base. Por ejemplo:
F UNDAMENTOS DEL XML-S CHEMA O XSD 231
En estas facetas se eliminan los espacios iniciales y finales, se sustituyen tab, line
feed y CR por espacios y n espacios consecutivos se sustituyen por uno. Los tipos sobre
los que pueden actuar estas facetas son: xs:ENTITY, xs:ID, xs:IDREF, xs:language,
xs:Name,xs:NCName, xs:NMTOKEN, xs:token, xs:anyURI, xs:base64Binary, xs:hex-
Binary, xs:NOTATION y xs:QName. Todas las facetas de este tipo restringen el espacio
de valores.
xs:pattern define un patrón que debe emparejarse con la cadena. Por ejemplo:
Estas facetas actúan sobre los tipos xs:float y xs:double restringiendo el espacio de
valores.
xs:pattern define un patrón que debe cumplir el valor léxico del tipo de datos.
Las mismas facetas que para los tipos numéricos reales más la siguiente:
El tipo de datos listaInteger se puede utilizar con atributos y elementos para que
acepten una lista de enteros separados por espacios, como: "1 -2345 200 33".
la semántica y facetas de los tipos miembro. Este tipo de derivación se realiza con el
elemento xs:union. Por ejemplo:
Figura 4.4: Un documento XML con dos espacios de nombres con prefijo
1 < xs:element name =" informe " form =" qualified "/ >
2 < xs:element name =" asunto " form =" unqualified "/ >
3 < xs:attribute name =" leng " form =" unqualified "/ >
Los esquemas que utilizan atributos de tipo qualified con frecuencia usan atributos de
otros espacios de nombres.
En el documento de la Figura 4.2 el espacio de nombres http://www.misitio.com/do-
csXML/archivo se define como espacio de nombres por defecto y se aplica a todos los
elementos del documento por defecto.
238 L ENGUAJES DE MARCADO . XML
12 http://www.saxproject.org/
P ROCESADORES DE DOCUMENTOS XML 239
inicio documento
inicio elemento: doc
inicio elemento: par
caracteres: Hola mundo
fin elemento: par
fin elemento: doc
fin documento
SAX está compuesto por una serie de interfaces y clases. Veamos algunas de las princi-
pales:
• ContentHandler
• ErrorHandler
• DTDHandler
• DeclHandler
• XMLReader
ContentHandler
Esta interfaz reemplaza a DocumentHandler de SAX 1.0. Contiene métodos que per-
miten que la aplicación reciba notificación de los eventos de marcado básicos. En la
aplicación debe haber una clase que implemente esta interfaz. En la Tabla 4.1 aparece la
descripción de los métodos de esta interfaz.
Por ejemplo, suponiendo que la aplicación quiere informar por la salida estándar del
comienzo y fin del análisis del documento, los métodos asociados a esos eventos podrían
ser:
Se llama a este método cuando el procesador encuentra un nuevo elemento. Sus paráme-
tros son:
URIespNombre: el espacio de nombres, si lo hay.
nombreLocal: nombre del elemento sin prefijo.
nombreBase: nombre del elemento con prefijo.
atrs: lista de atributos.
Por ejemplo, si tuviéramos el siguiente elemento:
cuando el procesador encuentre el principio del elemento b los valores de los parámetros
del método startElement contendrán:
URIespNombre: "http://www.miweb.com"
nombreLocal: "b"
nombreBase: "h:b"
atrs: estado="normal"
Si nuestra aplicación quisiera escribir los atributos de todos los elementos del docu-
mento, el método startElement podría ser el siguiente:
ErrorHandler
Esta intefaz incluye métodos para interceptar advertencias y errores. El procesador SAX
puede encontrar 3 tipos de errores: normales, fatales y advertencias:
El método del siguiente ejemplo muestra un mensaje cada vez que el procesador lanza
una advertencia:
DTDHandler
Esta interfaz será necesaria para las aplicaciones que necesitan información de las no-
taciones y entidades no analizadas. Téngase en cuenta que al no tener contenido XML
el procesador no las puede analizar. Esta interfaz permite a la aplicación localizar las
entidades no analizadas y decidir, por ejemplo, que otra aplicación se encargue de proce-
sarlas.
Los eventos de DTDHandler ocurrirán entre startDocument y startElement. La apli-
cación debería almacenar los valores devueltos por los métodos de esta interfaz y uti-
lizarlos cuando un atributo haga referencia a ellos. Veamos los métodos de esta interfaz.
hace una llamada al método notationDecl con los siguientes valores en sus argumen-
tos:
nombre: "GIF"
idPublico: NULL
idSistema: "Iexplore.exe"
1 <!ENTITY csfoto SYSTEM " csfoto . jpeg " NDATA JPEG >
hace una llamada al método unparsedEntityDecl con los siguiente valores en sus ar-
gumentos:
nombre: "csfoto"
idPublico: NULL
idSistema: "csfoto.jpeg"
nomNotacion: "JPEG"
DeclHandler
hace una llamada al método elementDecl con los siguiente valores en sus argumentos:
nombre: "receta"
modeloContenido: "(nombre,ingre+, proced)"
P ROCESADORES DE DOCUMENTOS XML 243
El método:
nombreE: "receta"
nombreA: "iden"
tipo: "ID"
valorPre: "#REQUIRED"
valor: null
y:
nombreE: "receta"
nombreA: "cocina"
tipo: "esp|fra|otr"
valorPre: "#REQUIRED"
valor: "esp"
El método:
nombre: "uned"
valor: "Universidad Nacional de Educación a Distancia"
XMLReader
1 // creamos el parser
2 XMLReader xr = XMLReaderFactory . createXMLReader ( nomParser );
3 // creamos un objeto de la clase MiAplSAX
4 MiAplSAX handler = new MiAplSAX () ;
5 // establecemos los manejadores que se van a usar
6 xr . setContentHandler ( handler );
7 xr . setErrorHandler ( handler );
8 ...
9 // Iniciamos el analizador
10 xr . parse ( docXml );
documento
elemento elemento
texto texto
elemento atributo
texto
Un objeto DOM debe ser capaz de cargar un documento XML y debe disponer de todas
las interfaces con los atributos y métodos de acuerdo con la especificación del DOM.
Los interfaces de DOM para XML son los siguientes:
Node es la interfaz básica ya que en DOM todo puede considerarse un nodo. Define
un conjunto de atributos y métodos necesarios para navegar, visitar y modificar
14 En http://www.w3.org/DOM/DOMTR se pueden encontrar los detalles de lo que cubre cada nivel.
246 L ENGUAJES DE MARCADO . XML
cualquier nodo. Sus métodos y atributos se solapan con los de las demás inter-
faces.
Comment un comentario.
En la Tabla 4.2 se pueden ver los tipos de datos que utiliza DOM.
Node
Uno de los atributos fundamentales de esta interfaz es nodeType que devuelve el tipo
del nodo activo. Existen 12 tipos de nodos y cada tipo se describe mediante un entero.
Además, hay una constante predefinida asociada a cada tipo de nodo. En la Tabla 4.3 se
presentan los tipos de nodos, su valor de atributo nodeType y su constante asociada.
P ROCESADORES DE DOCUMENTOS XML 247
Otros atributos de solo lectura de esta interfaz son: nodeName, firstChild, ownerDocu-
ment, nodeValue, lastChild, previousSibling, parentNode, nextSibling, child-
Nodes y attributes.
En la Tabla 4.4 se describen los valores de los atributos nodeName y nodeValue según
el tipo de nodo.
Tabla 4.4: Valores de los atributos nodeName y nodeValue según el tipo de nodo
Inserta un nuevo nodo hijo (secundario) antes del nodo hijo de referencia.
248 L ENGUAJES DE MARCADO . XML
1 removeChild ( nodoReferencia )
1 appendChild ( nodoNuevo )
1 hasChildNodes ()
1 cloneNode ()
Hace una copia de un nodo. Hay dos formas de copia: si el argumento del método es
true se clona el elemento y todo su contenido, pero si el argumento es false se clona
solo el elemento.
Document
Los atributos más destacados de esta interfaz son: doctype y documentElement.
doctype devuelve la información de <!DOCTYPE. Devuelve las declaraciones de las en-
tidades y notaciones como nodos hijo.
documentElement devuelve un objeto con el elemento raíz del documento, que es el
punto de partida usual en el recorrido del árbol.
Los métodos de este interfaz son: createElement(), createAttribute(), create-
TextNode(), createProcessingInstruction(), createEntityReference(), cre-
ateComment(), createDocumentFragment(), createCDATASection(), getElemen-
tsByTagName().
Los métodos que comienzan por create crean nodos con nombre, pero éstos son huér-
fanos cuando se crean, no forman parte del árbol. Para unirlos al árbol habrá que utilizar
métodos de la interfaz Node como insertBefore() o appendChild().
Por ejemplo:
El siguiente método:
1 getElementsByTagName ( nombreEtiqueta )
Obtiene una lista de todos los elementos descendientes del nodo de referencia que se
llamen como el argumento. Devuelve un objeto NodeList que en este caso es la lista
de los nodos descendientes que se llaman nombreEtiqueta en el orden en el que se
encuentran en el subárbol.
NodeList
Este interfaz permite acceder a una lista de objetos nodo. Por ejemplo, el método
getElementsByTagName() y el atributo childNodes crean un objeto de tipo NodeList.
El método item() toma un índice como argumento y devuelve el nodo que se encuentra
en dicha posición. La numeración de las posiciones empieza en 0.
El atributo length devuelve un entero largo sin signo que indica en número de nodos de
la lista.
Veamos un ejemplo de acceso a los elementos de una lista NodeList:
NamedNodeMap
Esta interfaz permite acceder a la lista de atributos de un elemento. A diferencia de
una lista de tipo Nodelist, a los elementos de una lista NamedNodeMap se accede por
sus nombres, la posición no es importante. El atributo attributes de la interfaz Node
devuelve un objeto de este tipo.
El método item() y el atributo length tienen funciones equivalentes a los del mismo
nombre del interfaz NodeList.
Los métodos getNamedItem() y removeNamedItem() recuperan y eliminan respecti-
vamente el nodo de la lista cuyo nombre se pasa como argumento.
El método setNamedItem() añade el nodo cuyo nombre se pasa como argumento.
Veamos un ejemplo de acceso a los elementos de una lista NamedNodeMap:
250 L ENGUAJES DE MARCADO . XML
Attr
Permite acceder a un atributo de un objeto elemento. El atributo name devuelve el nom-
bre del atributo. El atributo value devuelve su valor. El atributo specified devuelve
un valor booleano que indica si el atributo tiene un valor asignado (true) o si el valor está
predeterminado en la DTD (false).
Veamos dos ejemplos de uso de estos atributos:
CharacterData
Esta interfaz permite acceder y modificar datos de tipo de cadena. El atributo data
devuelve el texto del nodo como una cadena Unicode. El atributo length devuelve el
número de caracteres de la cadena.
El método subStringData() devuelve una subcadena de una cadena. Tiene el siguiente
formato:
El método appendData() añade al final del texto del nodo la cadena que se pasa como
argumento.
El método insertData() inserta una subcadena en una cadena. Tiene el siguiente for-
mato:
P ROCESADORES DE DOCUMENTOS XML 251
El método deleteData() borra una subcadena de una cadena. Tiene el siguiente for-
mato:
Element
La mayoría de los nodos de un documento son de tipo elemento o texto. Buena parte
de las operaciones que se realizan con nodos de tipo elemento las realiza la interfaz
Node; de la misma manera, la mayoría de las operaciones que se realizan con nodos de
tipo texto las realiza la interfaz CharacterData. Esta interfaz, por tanto, dispone de
varios métodos y atributos similares en funcionalidad a los existentes en otras interfaces.
Veamos algunos de los métodos más específicos:
El método getAttribute() recupera el valor del atributo que se le pasa como argu-
mento. Tanto el argumento como el valor devuelto son de tipo DOMString.
El método setAttribute() crea un atributo y establece su valor. Ambos argumentos
son de tipo DOMString.
El método removeAttribute() elimina el atributo cuyo nombre se pasa como argu-
mento. Es similar a removeNamedItem() de la interfaz NamedNodeMap.
El método setAttributeNode() establece un atributo del nodo de tipo Attr que se
le pasa como argumento. El nodo de tipo Attr puede haberse creado con el método
cloneNode() del interfaz Node o con createAttribute() del interfaz Document.
El método removeAttributeNode() borra el nodo atributo que se le pasa como argu-
mento.
Text
La mayoría de las operaciones con texto las realiza la interfaz CharacterData. La in-
terfaz Text posee solo un método, splitText() que divide un nodo de texto individual
en dos nodos. Su argumento indica el punto de división.
252 L ENGUAJES DE MARCADO . XML
4.8.1 XPath
Es un lenguaje en sí mismo que no usa la sintaxis XML. XPath proporciona una manera
de apuntar a partes de un documento XML. Forma la base del direccionamiento de do-
cumentos en otras tecnologías como XPointer y XSLT (XML Stylesheets Transformation
Language, o lenguaje de transformación basado en hojas de estilo). XPath se basa en el
concepto de notación de ruta (path), de ahí su nombre.
Para XPath un documento XML es un árbol de nodos y los operadores del lenguaje
permiten recorrer dicho árbol. El árbol asociado al documento lo crea el parser. La
sintaxis concisa de XPath se diseñó para ser utilizada en URIs y valores de atributos
XML. Con XPath se puede acceder a un elemento concreto del documento para darle un
formato diferente (utilizando la tecnología XSLT), o crear un enlace hipertexto a dicho
elemento (utilizando la tecnología XPointer), o recuperar información de elementos que
cumplen ciertos patrones (utilizando la tecnología XQL -XML Query Language-). La
especificación 1.0 de XPath se puede encontrar en http://www.w3.org/TR/xpath/.
Veamos el siguiente documento XML:
V INCULACIÓN ENTRE DOCUMENTOS 253
El árbol al que daría lugar el parser sería similar al de la Figura 4.6. Téngase en cuenta
que el nodo doc no es el nodo raíz del árbol como ocurre con DOM.
raíz
doc
cuerpo firma
• Raíz: se identifica por "/" y es el nodo raíz no el elemento raíz del documento.
• Elementos.
• Texto.
• Atributos.
• Espacios de nombres.
• Instrucciones de procesamiento.
• Comentarios.
Expresiones
El lenguaje XPath se basa en expresiones que permiten recorrer el árbol hasta llegar a un
nodo determinado. El resultado de las expresiones es un objeto de datos de uno de los
siguientes tipos:
Las expresiones XPath pueden incluir diferentes operaciones sobre distintos tipos de
operandos. Los operandos pueden ser, por ejemplo, llamadas a funciones y localizadores
(location paths). La sintaxis de un localizador es similar a la usada para describir las
rutas en Unix o Linux, pero su significado es diferente.
Supongamos el documento XML de la Figura 4.7. La siguiente expresión XPath:
/libro/capitulo/parrafo hace referencia a todos los elementos parrafo que cuel-
gan directamente de todos los elementos capitulo que cuelgan del elemento libro que
cuelga del nodo raíz /.
Una expresión XPath no devuelve los elementos que cumplen con el patrón que re-
presenta dicha expresión, sino la lista de apuntadores a los elementos que encajan en el
patrón. En el ejemplo anterior nos devolvería los apuntadores a los 4 elementos parrafo
que hay en los 2 elementos capitulo que hay en el elemento libro.
V INCULACIÓN ENTRE DOCUMENTOS 255
Un localizador siempre tiene un punto de partida llamado nodo contexto. A menos que
se indique un camino o ruta explícita, se entenderá que el localizador parte del nodo que
en cada momento se esté procesando.
Siguiendo con el ejemplo de la expresión /libro/capitulo/parrafo, un evaluador de
expresiones Xpath comienza leyendo "/" por lo que selecciona el nodo raíz, independien-
temente del nodo contexto que en ese momento exista. Cuando el evaluador de XPath
localiza el nodo raíz, éste pasa a ser el nodo contexto de dicha expresión. Después, el
analizador lee libro, lo que le indica que seleccione todos los elementos que cuelgan
del nodo contexto (en este punto raíz) que se llamen libro. Solo hay uno porque solo
puede haber un elemento raíz. A continuación el analizador lee capitulo, lo que le
indica que seleccione todos los elementos que cuelgan del nodo contexto (en este punto
el nodo libro) que se llamen capitulo. El analizador continúa leyendo la expresión
XPath y llega a parrafo que le indica que seleccione todos los elementos parrafo que
cuelgan del nodo contexto. Pero en este punto no hay un nodo contexto, sino dos. El
evaluador de expresiones recorre uno por uno los posibles nodos contexto haciendo que,
mientras evalúa un determinado nodo, ése sea el nodo contexto de ese momento. Así,
para localizar todos los elementos parrafo, se procesa el primer elemento capitulo y
de él se extraen todos los elementos parrafo que contenga. A continuación se pasa al
siguiente elemento capitulo procediendo de la misma manera. El resultado final de la
expresión es un conjunto de punteros a los nodos que encajan con el patrón buscado.
Ejes (Axes)
Los ejes permiten realizar una selección de nodos dentro del árbol. Algunos ejes son los
siguientes:
Child es el eje utilizado por defecto. Se corresponde con la barra "/", la forma larga es:
/child::. Ya se ha visto su uso en los ejemplos anteriores. Permite seleccionar
a los hijos del nodo contexto. Por ejemplo: /libro/titulo se refiere a todos los
elementos titulo del elemento libro.
descendant su signo es "//", su forma larga es: descendant::. Selecciona todos los
nodos descendientes del conjunto de nodos contexto. No contiene nodos atributo
ni espacios de nombres. Por ejemplo: /libro//parrafo selecciona todos los ele-
mentos parrafo de libro. Otro ejemplo: //parrafo//*[@href] selecciona
todos los descendientes de parrafo que tienen un atributo href.
256 L ENGUAJES DE MARCADO . XML
self el signo es ".", su forma larga es: self::. Selecciona el nodo contexto. Por
ejemplo: .//parrafo selecciona todos los elementos parrafo descendientes del
nodo contexto.
* selecciona todos los nodos de tipo principal que son: elemento, atributo o espacio de
nombres. Pero no selecciona los nodos de tipo texto, comentarios, e instrucciones
de procesamiento. Por ejemplo: //capitulo/* selecciona todos los nodos princi-
pales descendientes de capitulo, que son los 4 elementos parrafo descendientes
de los 2 elementos capitulo en el documento de la Figura 4.7.
node() selecciona todos los nodos de todos los tipos. Por ejemplo: //capitulo/node()
selecciona todos los nodos descendientes de capitulo. En este caso se selec-
cionarian los 4 elementos parrafo descendientes de los 2 elementos capitulo y
además los 6 elementos de tipo texto representados por ". . . " en el documento de
la Figura 4.7.
Predicados
Los predicados permiten restringir el conjunto de nodos seleccionados a aquellos que
cumplen ciertas condiciones. Por ejemplo, pueden seleccionar un nodo que cumple con
un patrón y con un determinado valor en un atributo.
Los predicados se incluyen dentro de un localizador utilizando corchetes. El resultado de
un predicado es un valor booleano y la selección solo se realiza cuando el valor devuelto
es verdadero. Por ejemplo:
apunta a todos los elementos parrafo de todos los elementos capitulo que tengan un
atributo llamado num con valor "1".
Los predicados se pueden suceder uno a otro teniendo el efecto de la operación lógica
AND. Por ejemplo:
V INCULACIÓN ENTRE DOCUMENTOS 257
es equivalente a:
Ambas expresiones seleccionan todos los elementos capitulo que tengan un elemento
parrafo con algún elemento que contenga un atributo href y que tengan (los elementos
capitulo) el atributo imagenes con valor si. Aplicado al documento ejemplo de la
Figura 4.7 no seleccionaría ningún elemento.
También se pueden utilizar los operadores lógicos or y not.
Hay funciones que restringen el conjunto de nodos devueltos en una expresión XPath
basándose en la posición del elemento devuelto. Algunas de estas funciones son:
id() selecciona elementos con un valor de atributo único de tipo id igual al indicado.
Por ejemplo:
id("capitulo1")/parrafo seleccionaría todos los elementos parrafo del ele-
mento con valor de atributo de tipo id igual a capitulo1. En el documento
ejemplo de la Figura 4.7 no hay ningún elemento que cumpla esa condición.
4.8.2 XPointer
XPointer es una extensión de XPath y al igual que éste no usa la sintaxis XML. Especi-
fica la sintaxis para crear identificadores de fragmentos de un documento con el objetivo
de realizar vínculos. Para la localización de fragmentos de un documento el analizador
construye y recorre la estructura de árbol de un documento XML. Al igual que XPath,
XPointer es un estándar del World Wide Web Consortium (W3C).
Recordemos cómo se enlazaría a un punto concreto de un documento HTML.
15 XML Path Language (XPath) Version 1.0 http://www.w3.org/TR/xpath
258 L ENGUAJES DE MARCADO . XML
3 <A href= " docA . htm # a1 " > Este enlace en docB . htm te llevará al
párrafo identificado como a1 de docA . htm </A>
En XML XPointer va a permitir añadir a una URI una expresión XPointer con la siguien-
te sintaxis:
1 # xpointer ( expresion )
donde expresion es una expresión XPath con algunas propiedades extra que no con-
templa el propio Xpath.
Se pueden concatenar expresiones XPointer que se evalúan de izquierda a derecha mien-
tras devuelvan un conjunto vacío de nodos. Por ejemplo:
1 documento . xml # xpointer ( id (" p1 ")) xpointer (//*[ @id =" p1 " ])
Puntos y rangos
Con XPath podemos seleccionar perfectamente un nodo principal (elementos, atribu-
tos,. . . ) pero no seleccionar partes de un nodo texto. Para poder hacerlo XPointer
dispone de los puntos y rangos.
Un punto es una posición en la información XML. Un rango es una selección contigua
de toda la información XML que se encuentra entre dos puntos determinados.
V INCULACIÓN ENTRE DOCUMENTOS 259
XPointer considera que existe un punto entre cualesquiera dos caracteres consecutivos
de texto de un documento XML, y entre cada par de elementos también consecutivos.
El fragmento de texto que existe entre dos puntos es un rango.
Por ejemplo, en <saludo> Hola! </saludo> hay 13 puntos:
La función point() permite usar los puntos en una expresión Xpointer. Se le añade un
predicado indicando qué punto en concreto se desea seleccionar. Por ejemplo:
4.8.3 XLink
XLink es un lenguaje XML cuya especificación proporciona métodos para crear enlaces
internos y externos a documentos XML, permitiendo además asociar metadatos a dichos
enlaces. Básicamente es el lenguaje de enlaces XML que supera algunas de las limita-
ciones de los enlaces HTML. XLink16 tiene sintaxis XML y soporta vínculos sencillos
(tipo HTML) pero también vínculos extendidos. Permite vincular dos documentos a
través de un tercero y especificar la forma de atravesar un vínculo. Además, los vínculos
pueden residir dentro o fuera de los documentos donde residan los recursos implicados.
En XML no existen elementos de vinculación predefinidos a diferencia de HTML que
tiene el elemento <A href=...>. XLink define un conjunto de atributos que se pueden
añadir a elementos pertenecientes a otros espacios de nombres.
Un elemento de vinculación utiliza una construcción denominada localizador para conec-
tar recursos y define atributos de vinculación estándar.
El uso de elementos y atributos XLink requiere declarar el espacio de nombres de XLink.
Por ejemplo, la declaración siguiente hará que el prefijo xlink esté disponible dentro del
elemento ejemplo:
1 < ejemplo xmlns:xlink =" http: // www . w3 . org /1999/ xlink " >
2 ...
3 </ ejemplo >
href su valor es un localizador de recurso destino, una URI, o una expresión Xpointer.
Por ejemplo: "docB.htm" es un identificador de recurso (URI), pero también se le
podría añadir una expresión XPointer, como en: "docB.htm#xpointer()".
type su valor es una cadena predefinida que determina el tipo de vínculo. Sus posibles
valores son: simple, extended, locator, arc, resource, o title. Estos tipos
indican el comportamiento que deben tener las aplicaciones cuando se encuentren
un elemento de dicho tipo.
role su valor es una cadena de caracteres que aclara el significado o da información
adicional sobre el contenido del enlace o vínculo.
16 La especificación actual es la 1.1http://www.w3.org/TR/xlink11/.
V INCULACIÓN ENTRE DOCUMENTOS 261
show su valor es una cadena predefinida que indica cómo se revela el recurso destino al
usuario. Puede tener los siguientes valores:
replace: reemplaza el documento actual por aquel al que apunta el enlace.
new: abre un nuevo navegador con el documento destino.
parsed: el contenido del texto apuntado se incluye en lugar del enlace y se procesa
como si fuera parte del mismo documento de origen.
actuate su valor es una cadena predefinida que indica cuándo se inicia un vínculo, es
decir, cuándo se procede a buscar el destino apuntado. Puede tener los siguientes
valores:
user: el vínculo se iniciará cuando el usuario pulse o dé alguna orden para seguir
el enlace.
auto: el enlace se sigue automáticamente.
to su valor es una cadena que identifica el recurso con el que se está vinculando (des-
tino).
from su valor es una cadena que identifica el recurso desde el que se está vinculando
(origen).
En la versión 1.0 de XLink los elementos se identifican por la presencia del atributo
xlink:type. Sin embargo, en la versión 1.1 los elementos se identifican por la presencia
del atributo xlink:type o del atributo xlink:href. Si un elemento tiene un atributo
xlink:type entonces debe tener uno de los siguientes valores: simple, extended,
locator, arc, resource, o title. Si un elemento no tiene el atributo xlink:type
pero sí el atributo xlink:href entonces se trata como si el valor del atributo ausente
xlink:type fuera simple.
XLink especifica dos tipos de hiperenlaces: simples y extendidos. Los enlaces sim-
ples conectan solo dos recursos, mientras que los extendidos pueden enlazar un número
arbitrario de recursos.
Un enlace simple crea un hiperenlace unidireccional del elemento origen al destino por
medio de una URI. Por ejemplo:
En este caso se enlaza al elemento con valor de atributo titulo1. Veamos un ejemplo
de cómo se declararía en una DTD un elemento de vinculación XLink:
1 <!DOCTYPE html PUBLIC " -// W3C // DTD XHTML 1.0 Strict // EN
" " http: // www . w3 . org / TR / xhtml1 / DTD / xhtml1 - strict . dtd
">
E JERCICIOS RESUELTOS 263
• La DTD XHTML Transitional es como la DTD XHTML Strict, pero las eti-
quetas en desuso están permitidas. Actualmente ésta es la DTD más popular.
1 <!DOCTYPE html PUBLIC " -// W3C // DTD XHTML 1.0 Frameset //
EN " " http: // www . w3 . org / TR / xhtml1 / DTD / xhtml1 - frameset
. dtd " >
1 <?xml version = " 1.0 " encoding = " UTF -8 "? >
2 < listin >
3 < persona sexo = " hombre " id = " ricky " >
264 L ENGUAJES DE MARCADO . XML
Escribe una DTD con respecto a la que sería válido. La DTD podría ser la si-
guiente:
3. Escribe cómo puede quedar resuelto el ejemplo ilustrativo del apartado 4.5 uti-
lizando los espacios de nombres para combinar dos DTDs. El documento que
combina varios CD y reseñas podría ser como el que sigue:
Se han definido 2 espacios de nombres, uno para los elementos de una DTD y otro
para los de la otra DTD. Por defecto, los elementos pertenecen al espacio de nom-
bres http://www.ejemplos.xml/docCD, salvo aquellos que usen el prefijo com
que pertenecen al otro espacio de nombres. Así, es posible eliminar la ambigüedad
entre los dos tipos de elemento autor.
6. Escribe cómo crearías en XML-Schema dos nuevos tipos de datos del tipo base
xs:token: uno que restrinja la longitud máxima de la cadena a 10 y otro a 255
caracteres.
7. Escribe cómo crearías en XML-Schema un nuevo tipo de datos del tipo base
xs:language que limitara los lenguajes que se pueden asociar a un elemento a
inglés y castellano.
8. Utilizando la API DOM escribe una forma de acceder a todos los atributos de un
elemento sin conocer sus nombres. Escribe además sus nombres.
1 Node aNode ;
2 NamedNodeMap atts = miElemento . getAttributes () ;
3 for ( int i = 0; i < atts . getLength () ; i ++) {
4 aNode = atts . item (i);
5 System . out . print ( aNode . getNodeName () );
6 }
E JERCICIOS RESUELTOS 269
9. Escribe un programa Java que muestre un mensaje cada vez que el procesador
SAX genera un evento.
10. Escribe un ejemplo de documento XML con diferentes valores del atributo show
de XLink.
1 <doc xmlns:xlink =" http: // www . w3 . org /1999/ xlink " >
2 ...
3 < ver xlink:type =" simple "
4 xlink:href =" http: // www . pagEjemplo . com /"
5 xlink:show =" replace " >
6 Carga la página http: // www . pagEjemplo . com /
7 </ ver >
8 <nueva - ventana xlink:type =" simple "
9 xlink:href =" http: // www . uned . es /"
10 xlink:show =" new " >
11 Abre una nueva ventana con la página de la UNED
12 </ nueva - ventana >
13 < incluir xlink:type =" simple "
14 xlink:href =" instancia . xml "
15 xlink:show =" parsed " >
16 Incluye el fichero indicado
17 </ incluir >
18 ...
19 </ doc >
2. Dado el siguiente documento XML, escribe una DTD y un XSD con respecto a
los que pueda ser válido.
5 < titulo leng = " es " > El último encuentro </ titulo >
6 < autor id =" SMarai " >
7 < nombre > Sándor Márai </ nombre >
8 <fecha - nac > 13 -4 -1900 </ fecha - nac >
9 <fecha - fall > 16 -6 -1989 </ fecha - fall >
10 </ autor >
11 < personaje id =" per1 " >
12 < nombre > El general </ nombre >
13 < descripcion > severo , triste </ descripcion >
14 </ personaje >
15 < personaje >
16 ...
17 </ personaje >
18 ...
19 </ libro >
20 ...
21 </ biblioteca >
3. Dado el documento de la Figura 4.2 escribe una DTD y un XSD con respecto a
los que sea válido.
5. Escribe un programa que imprima un documento XML que recibe como entrada
utilizando la API SAX.
6. Escribe un programa que imprima un documento XML que recibe como entrada
utilizando la API DOM. Se trata de recorrer la jerarquía del árbol DOM e ir escri-
biendo los nodos. En algunos lenguajes este recorrido recibe el nombre de Tree-
Walker. Algunos lenguajes disponen de una clase con ese nombre que implementa
este tipo de recorrido.
7. Escoger una página HTML y convertirla a XHML. Hay muchas páginas HTML
que no están bien formadas; les faltan, por ejemplo, las etiquetas de cierre. Usad
un navegador para comprobar si funciona correctamente cuando el código XHTML
no está bien formado.
Lenguajes de script
Este capítulo introduce los lenguajes de script, a los que ya se ha hecho referencia
en el capítulo 3 en el apartado dedicado a la programación con lenguajes dinámicos.
Comienza describiendo su origen, para pasar después a centrase en los dominios de apli-
cación en los que se utilizan. También se describen las principales características de
algunas herramientas y lenguajes de script, haciendo énfasis en aquellos aspectos que
más los diferencian de los lenguajes de programación tradicionales. Por ejemplo, en
los lenguajes de script se enfatiza el uso de expresiones regulares, arrays asociativos,
operadores específicos de emparejamiento de patrones, características de los registros y
unidades lógicas de entrada/salida, y programación en www. Si el lector quiere profun-
dizar en el uso de alguno de los lenguajes de script tratados deberá acudir a otras fuentes,
algunas de las cuales se citan a lo largo y al final del capítulo. En el caso de que el lector
esté interesado en la combinación de algún lenguaje de script con lenguajes de marcado,
se recomienda la lectura de este capítulo después de la lectura del capítulo 4.
5.1 Introducción
Un lenguaje de script es un lenguaje de programación que permite el control de otros
programas o aplicaciones. Una buena parte de las tareas complejas que realizan las
aplicaciones software involucran la ejecución de numerosos programas individuales que
necesitan coordinarse. Esta coordinación puede requerir el uso de estructuras condi-
cionales, repetitivas, uso de variables de determinados tipos; es decir, similares meca-
nismos y recursos que un lenguaje de programación de propósito general. Aunque para
realizar estas tareas de coordinación se podrían utilizar lenguajes de propósito gene-
ral como C o Java, los lenguajes de script presentan características que los hacen más
apropiados.
Los lenguajes de script dan más importancia a la flexibilidad y a la facilidad en el de-
sarrollo de programas que los lenguajes de programación de propósito general, aunque
también incorporan elementos de alto nivel como expresiones regulares y estructuras de
273
274 L ENGUAJES DE script
datos como arrays y ficheros. Un programa escrito en un lenguaje de script suele recibir
el nombre de script (guión). Tradicionalmente los scripts son interpretados a partir del
código fuente o de código de bytes (bytecode1 ).
Los lenguajes de script tienen dos familias de antecesores [25]. Por una parte provienen
de los intérpretes de comandos o shells de la época en la que los procesos no eran interac-
tivos sino por lotes o batch. Y por otra parte heredan características de las herramientas
para procesamiento de textos y generación de informes.
La familia de los intérpretes de comandos tiene su origen en los años sesenta. El lenguaje
más emblemático de esta familia es el Job Control Language (JCL) de IBM. Este tipo de
lenguajes permiten dar instrucciones al sistema sobre cómo tiene que ejecutar un trabajo
por lotes. Por ejemplo, indican dónde encontrar la entrada o qué hacer con la salida, fa-
cilitando así el tradicional proceso de edición-compilación-enlazamiento-ejecución. Sus
instrucciones se denominan sentencias de control de trabajos (job control statements).
Este tipo de lenguajes se suele usar en los denominados Ordenadores Centrales (Main-
frames) y es específico para cada sistema operativo. Ejemplos más recientes de este tipo
de lenguajes son el intérprete de comandos de MS-DOS (con los ficheros .bat), o los
shells sh, csh, etc. de Unix.
Con respecto a la familia de las herramientas para procesamiento de textos y generación
de informes, destacan sed y awk de Unix y RPG (Report Program Generator) de IBM.
Aunque históricamente ha habido una clara distinción entre los lenguajes de progra-
mación tradicionales, como C, y los lenguajes de script como awk o sh, a medida que
ha transcurrido el tiempo y ha evolucionado todo lo relacionado con el software, han
ido surgiendo lenguajes como Perl, Python o PHP que no solo se pueden considerar
lenguajes de script, sino que también se utilizan como lenguajes de propósito general.
Algunos autores reservan la denominación de lenguajes de script para los que controlan
y coordinan otros programas y aplicaciones. Sin embargo, esta denominación también se
usa en un sentido más amplio e incluye a los scripts de las páginas webs y a los lengua-
jes de programación que pueden incorporarse en programas escritos en otros lenguajes
para extender sus capacidades, como Tcl o Python. Estos últimos suelen denominarse
lenguajes de extensión.
La mayoría de los lenguajes de script presentan las siguientes características comunes:
bash
Cuando se trabaja con los sistemas operativos hay tareas que se ejecutan de forma perió-
dica que constan de una secuencia de instrucciones. Esta secuencia se puede almacenar
en un script para ejecutarse cuando sea necesaria como un único comando del sistema
operativo.
Un script de shell es un fichero de texto con la siguiente estructura:
1 # !/ bin / bash
2 comandos e instrucciones bash
1 nombreShellScript . sh
1 # !/ bin / bash
2 # borrar pantalla y saludar
3 clear
4 echo " Hola \ $LOGNAME "
Para recuperar el valor de una variable, se escribe su nombre precedido por el signo $ o
con la sintaxis ${nomVAR}. Por ejemplo, en las siguientes instrucciones:
$0 es el nombre del script, es decir del nombre del fichero que lo contiene.
278 L ENGUAJES DE script
$1 es el primer argumento.
$@ retorna la lista "$1" "$2" . . . "$n", es decir todos los argumentos como una serie de
palabras.
o en la forma:
1 if condición
2 then comandosCondCierta
3 [else comandosCondFalsa ]
4 fi
Hay que tener en cuenta que en Unix los programas devuelven un 0 cuando se ejecutan
correctamente y un valor distinto de 0 cuando ocurre algún error, al contrario que en C y
otros lenguajes de programación. La variable predefinida $? es la que almacena el valor
de retorno del último comando ejecutado.
Veamos un ejemplo de uso de la estructura alternativa:
1 # !/ bin / bash
2 # indica si el argumento es par
3 if [ $1 % 2 = "0" ]
4 then
5 echo " Es par "
6 else
7 echo " Es impar "
8 fi
El script anterior tiene un argumento que se almacena en la variable $1. Una vez sal-
vado en un fichero (parImpar.sh) y dándole los permisos correspondientes se podría
ejecutar con: ./parImpar.sh N, siendo N un valor numérico. bash también dispone de
una estructura case, pero debido a que en este apartado nos centramos en los aspectos
básicos, no se describe su sintaxis.
La shell bash incorpora estructuras repetitivas: while, for y until. Veamos el formato
de while:
D OMINIOS DE APLICACIÓN 279
1 while expresión
2 do
3 comandosCondCierta
4 done
A continuación se muestra un ejemplo de un script que crea 4 ficheros, los lista y final-
mente los elimina:
1 # !/ bin / bash
2 # crea los ficheros fich1 , fich2 , fich3 y fich4 , los lista y los
borra
3 VAL =1
4 while [ $VAL -le 4 ] # mientras $VAL <= 4
5 do
6 echo creando fichero fich$VAL
7 touch fich$VAL
8 VAL =‘expr $VAL + 1‘
9 done
10 ls -l fich [0 -4]
11 rm fich [0 -4]
Los operadores para comparar valores numéricos son: -eq (igual), -neq (no igual), -lt
(menor), -gt (mayor), -le (menor o igual), -ge (mayor o igual).
Los operadores de archivos de uso más común son: -e (existe), -d (existe y es directo-
rio), -f (existe y es archivo), -r (existe y tiene permiso de lectura), -s (existe y tiene una
longitud mayor que cero), -w (existe y tiene permiso de escritura), -x (existe y tiene per-
miso de ejecución), -nt (devuelve valor cierto si el fichero que representa el operando
de su izquierda es más reciente que el de su derecha), -ot (devuelve valor cierto si el
fichero que representa el operando de su izquierda es más antiguo que el de su derecha).
Por ejemplo, para comprobar la existencia de un fichero:
1 function nomSubprograma {
2 instruccionesSubprograma
3 }
Los parámetros que se pasen al subprograma también se recuperan con $1, $2 y así
sucesivamente.
280 L ENGUAJES DE script
1 comando1 | comando2
| Una barra vertical separa alternativas. Por ejemplo la expresión "esto|eso" casa con
la cadena "esto" o con la cadena "eso".
+ Indica que el carácter al que sigue debe aparecer al menos una vez. Por ejemplo la
expresión "UF+" casa con "UF", "UFF", "UFFF", etc.
? Indica que el carácter al que sigue puede aparecer como mucho una vez. Por ejemplo
la expresión "p?sicología" casa con "psicología" y con "sicología".
* Indica que el carácter al que sigue puede aparecer cero, una, o más veces. Por ejemplo
la expresión "0*33" casa con "33", "033", "0033", etc.
sed
Una de las herramientas Unix más utilizadas para el procesamiento de cadenas es sed
(stream editor), sucesor de grep. Un comando sed toma una cadena o fichero de texto
como entrada, lo lee y procesa línea a línea, según indique la expresión regular, y envía
la salida a pantalla o la redirecciona a otro fichero sin modificar el original. También
puede modificar el fichero original especificándolo expresamente.
sed permite buscar y reemplazar como un editor de textos, borrar líneas y añadir con-
tenido. La sintaxis de un comando sed de sustitución es la siguiente:
La "s" indica que es un comando de sustitución, la "g" indica que es global, es decir,
que afectará a todas las apariciones de cadenaOriginal. Si no se indica "g" solo se
sustituirá la primera aparición en cada línea. También se puede indicar un número n
para indicar que se sustituirán las primeras n apariciones en la línea. El significado es
que cada aparición de la cadena cadenaOriginal en el fichero ficheroEntrada se va
a sustituir con cadenaNueva y el resultado se escribirá en ficheroSalida. El carácter
"/" se utiliza como carácter delimitador. Si no se indica ficheroSalida el resultado se
muestra por pantalla.
La sintaxis de un comando sed de borrado de líneas es la siguiente:
La "d" al final indica que es un comando de borrado. Borra la línea que contiene
cadenaBorrada. Para borrar solo una palabra o expresión, no toda la línea, se utiliza un
comando de sustitución:
Por ejemplo, la siguiente expresión borra todos los espacios en blanco consecutivos (dos
o más):
Por defecto sed escribe el fichero que procesa entero a no ser que se indique la opción
"-n". Esta opción se puede combinar con "p" que permite mostrar la parte del fichero
que se indica. Por ejemplo:
awk
El lenguaje de programación awk, cuyo nombre viene de la letra inicial de los apellidos
de sus autores, Alfred Aho, Peter Weinberger y Brian Kernighan, se diseñó inicialmente
en 1977 como un lenguaje de programación para Unix, aunque ya se pueden encontrar
versiones para otros sistemas operativos. Fue diseñado para superar las limitaciones
de sed con el objetivo de procesar textos y cadenas. Su potencia radica en el uso que
ofrece del tipo de datos cadena, los arrays asociativos (indexados por una clave) y las
expresiones regulares, además de una sintaxis parecida a la de C.
En awk existen variables predefinidas que facilitan enormemente el procesamiento de las
cadenas de texto. Por ejemplo, la variable predefinida $0 almacena el registro que se está
procesando. La función de lectura getline almacena en esta variable el registro leído,
que por defecto es una línea. La variable RS (Record Separator) contiene el carácter o
expresión regular que indica a awk en qué punto acaba un registro y empieza el siguiente.
Por defecto el carácter separador de registros es "\n".
Cuando awk analiza un registro lo separa en campos, que por defecto son palabras (cade-
nas de caracteres separadas por espacios o tabulador "\t"), estos campos se almacenan
en las variables predefinidas $1, $2, $3, . . . según el orden de los campos en la línea. El
separador de campos se almacena en la variable FS (Field Separator) por lo que su con-
tenido se puede modificar asignándole la expresión regular que se requiera en cada caso.
La variable NF (Number of Fields) almacena el número total de campos del registro ac-
tivo. Otras variables predefinidas son NR (Number of Record) que almacena el número de
orden del registro que se está procesando, OFS (Output FS) que hace que la instrucción
print inserte en la salida un carácter de separación de campos (por defecto un espacio
en blanco), ORS (Output RS) similar a OFS pero con respecto al carácter separador entre
registros de salida (por defecto "\n") y FILENAME que almacena el nombre del fichero
abierto.
Un programa awk tiene tres secciones:
1. Bloque inicial: se ejecuta solo una vez antes de empezar a procesar la entrada. Su
sintaxis es: BEGIN operaciones.
2. Bloque central: contiene las instrucciones que se ejecutan para cada uno de los
registros de la entrada y que tienen la sintaxis: EXPREG operaciones.
284 L ENGUAJES DE script
Con el anterior formato las operaciones se ejecutan solo sobre los registros que
verifiquen la expresión regular EXPREG. Por el contrario, expresando !EXPREG las
operaciones se ejecutan en los registros que no concuerden con la expresión regu-
lar EXPREG.
3. Bloque final: se efectúa sólo una vez después de procesar toda la entrada. Su
sintaxis es: END operaciones.
Cada una de estas partes pueden aparecer varias veces en un programa awk y si ésto
ocurre se procesan en orden de aparición.
Veamos el conocido programa "Hola Mundo" en awk:
Las variables en awk pueden ser escalares si almacenan un solo valor y vectoriales si son
arrays. En awk se pueden crear arrays asociativos que se caracterizan por usar una ca-
dena como índice para referirse a un elemento dentro de un array. Los arrays asociativos
se implementan internamente en los lenguajes mediante tablas hash.
El siguiente programa permite calcular la frecuencia de las palabras de una entrada uti-
lizando arrays asociativos:
1 BEGIN {
2 FS =" [^a -zA -Z ]+ "
3 }
4 {
5 for (i =1; i <= NF ; i ++)
6 palabras [tolower( $i ) ]++
7 }
8 END {
9 for (i in palabras )
10 print i , palabras [i]
11 }
El bloque inicial (BEGIN) asigna al separador de campos, la variable predefinida FS, una
expresión regular que casa con cualquier secuencia de caracteres no alfabéticos ya que
solo se van a tener cuenta palabras. En el bloque central, para cada registro de entrada
(en este caso por defecto una línea) se procesa cada campo (en este caso cada palabra)
incrementando en 1 el número de veces del elemento del array asociativo palabras que
corresponde a la versión en minúsculas de la palabra ($i) que se está procesando. En
el bloque final, una vez que se han procesado todas las líneas, se imprime cada palabra
junto con el número de veces que ha aparecido en el fichero de entrada. El bucle for (i
in palabras) recorre el array palabras desde el primero al último elemento.
Un script awk almacenado en un fichero deberá comenzar con una línea de comentario
como la que se ve en el fichero hola.awk del siguiente ejemplo:
D OMINIOS DE APLICACIÓN 285
hola.awk sería un script linux que imprimiría "Hola mundo". La opción -f indica a
awk que lo que sigue es el programa.
En awk hay dos operadores para comprobar si una cadena casa con una expresión regu-
lar: "˜" (casa) y "!˜" (no casa). Por ejemplo, en:
1 $0 ~ /^ Actualmente \$/
2 $0 !~ /^ Actualmente \$/
la primera línea busca un registro cuyo primer campo sea la palabra "Actualmente",
mientras que la segunda hace lo contrario, busca un registro cuyo primer campo no sea
la palabra "Actualmente".
Dos expresiones regulares separadas por comas representan un rango de registros. Por
ejemplo:
busca un registro cuyo primer campo sea la palabra "Actualmente" y casa los siguientes
registros hasta llegar a uno cuyo primer campo case con "Finalmente".
En el apartado 5.4 de ejercicios resueltos se pueden encontrar más ejemplos de uso de
awk.
• Muchos juegos disponen de lenguajes de script que ayudan a determinar los even-
tos de los escenarios o el comportamiento de los actores. Un ejemplo de estos
lenguajes de script son Lua, Python, AngelScript, Squirrel.
• Microsoft Office permite que el usuario escriba macros en Visual Basic for Appli-
cations.
• GNU Emacs tiene como lenguaje de extensión una versión de Lisp. El código
básico está escrito en C, y con LISP se pueden ampliar y modificar las funcionali-
dades.
286 L ENGUAJES DE script
• JavaScript, en su uso más extendido del lado del cliente (client-side), es un lenguaje
de script dentro de los navegadores web que permite mejorar la interfaz de usuario
y las páginas web dinámicas. También existe una forma de JavaScript del lado del
servidor (Server-side Javascript o SSJS). Hay otro tipo de aplicaciones que no es-
tán relacionadas con la web en las que también se usa Javascript como lenguaje de
extensión, por ejemplo en la suite gráfica de Adobe y en aplicaciones de escritorio
(mayoritariamente widgets).
• La suite gráfica de Adobe se puede extender con JavaScript, Visual Basic (Win-
dows) y AppleScript (Mac).
Para que una aplicación pueda extenderse debe incorporar o poder comunicarse con un
intérprete de un lenguaje de script, a la vez que permitir que desde los scripts se pueda
acceder a los comandos propios de la aplicación. Además, también debe permitir que el
usuario relacione los comandos nuevos con eventos de la interfaz de usuario.
CGI
El Common Gateway Interface (CGI) es una tecnología que permite a un cliente (nave-
gador web) solicitar datos de un programa ejecutado en un servidor web. Define cómo
un servidor web pasa una petición de un usuario web a un programa y cómo se trasfieren
datos con el usuario. Por ejemplo, cuando un usuario hace una petición de una página
web, el servidor devuelve la página solicitada. Pero cuando un usuario cumplimenta un
formulario de una página web y lo envía, normalmente necesita ser procesado por un pro-
grama. El servidor pasa el formulario a un programa que procesa los datos y devuelve,
por ejemplo, un mensaje de confirmación. Este mecanismo de pasar datos entre el servi-
dor y el programa es lo que se denomina CGI y es parte del protocolo Web’s Hypertext
Transfer Protocol (HTTP). Este programa puede estar escrito en cualquier lenguaje que
288 L ENGUAJES DE script
soporte el servidor, aunque por razones de portabilidad se suelen usar lenguajes de script.
El funcionamiento de CGI sería:
1. El servidor recibe una petición del cliente que ha activado una URL que contiene
el CGI.
• El código del script PHP puede estar en cualquier parte de la página web repartido
entre anotaciones HTML.
1 <!DOCTYPE HTML PUBLIC " -// W3C // DTD HTML 4.01// EN "
2 " http :// www . w3 . org / TR / html4 / strict . dtd " >
3 <HTML>
4 <HEAD><TITLE> Primera página </TITLE></HEAD>
5 <BODY>
6 <SCRIPT type=" text / javascript " >
7 document . write (’ Hola Mundo ’) ;
8 </SCRIPT>
9 <NOSCRIPT>
10 <P>El navegador no soporta JavaScript . </P>
11 </NOSCRIPT>
12 </BODY>
13 </HTML>
290 L ENGUAJES DE script
5.3.1 Perl
A partir de lenguajes como awk, sh y RPG se desarrollaron otros, entre los que destaca
Perl, que se ha convertido en uno de los lenguajes de script de propósito general más
utilizados. Perl (Practical Extraction and Report Language) es un lenguaje de pro-
gramación de alto nivel, interpretado y dinámico desarrollado por Larry Wall en 1987.
Desde entonces ha ido evolucionando, se le han añadido nuevas funcionalidades e incor-
porado características de orientación a objetos, además de ser extensible por los usuarios.
Perl proporciona una gran potencia para la manipulación de textos, como sus antecesores
sed y awk, y añade el tratamiento de datos de longitud arbitraria. Además, también se
usa en tareas de administración de sistemas, programación en red, programación de CGI
A LGUNOS LENGUAJES DE script DESTACADOS 291
y acceso a bases de datos, entre otras. Perl es software libre3 y está disponible para una
amplia variedad de plataformas (Unix, Windows, Macintosh). Cabe destacar que aunque
hay lenguajes de propósito general, como puede ser Java, que disponen de librerías que
permiten la manipulación de cadenas y textos y otras funcionalidades, la potencia de
Perl radica en que incorpora en el propio lenguaje tipos, operadores y constructores para
estas tareas.
Perl tiene tipado dinámico por lo que los valores que se asignan a las variables son los
que determinan su tipo. Así, una misma variable escalar puede comportarse como una
cadena si su contexto (operadores, funciones,. . . ) es de cadena, o como una variable
numérica si su contexto es numérico.
En Perl hay tres tipos de variables:
• Escalares (su nombre comienza con el símbolo $). Se usan para almacenar datos
simples que son números enteros, reales o cadenas. Un ejemplo de asignación a
dos variables escalares:
1 $contador = 0;
2 $saludo = " Hola mundo ";
• Arrays o matrices (su nombre comienza con el símbolo @). Almacenan colec-
ciones de variables escalares comenzando en la posición 0. Un ejemplo de asig-
nación a dos arrays:
1 @contadores = (0 , 0, 0, 0, 0 );
2 @colores = (" amarillo " , " rojo " , " verde " , " naranja " ,
3 " violeta ");
El acceso a un elemento del array se realiza indicando el nombre del array como
un escalar y la posición entre corchetes. Por ejemplo: $contadores[1] hace
referencia al valor escalar almacenado en la posición 1 del array @contadores.
• Arrays asociativos (su nombre comienza con el símbolo %). Arrays cuya clave de
acceso es una cadena y cuyo valor es un escalar. Un ejemplo de asignación a un
array asociativo:
1 % CantidadColores = (" amarillo " , 0, " rojo " , 0, " verde " , 0,
2 " naranja " , 0, " violeta " , 0) ;
Cada elemento del array está formado por un par clave-valor. Por ejemplo, el
primer elemento del array %CantidadColores es el elemento con clave de acceso
3 GPL y Licencia artística
292 L ENGUAJES DE script
1 @colores = (" amarillo " , " rojo " , " verde " , " naranja " , " violeta ");
2 $numColores = @colores ;
En Perl no es lo mismo encerrar una cadena entre comillas dobles que simples. Las
variables escalares y arrays dentro de una cadena entre dobles comillas se sustituyen por
su valor. Por ejemplo, la instrucción $saludo= "Hola $user"; asigna a la variable
$saludo el literal formado por el texto explícito y el valor de la variable $user. También
los caracteres especiales, como "\n" o "\t", se respetan entre dobles comillas. Por el
contrario, una cadena entre comillas simples se mostrará literalmente sin tener en cuenta
los posibles caracteres especiales que pueda contener, y que se tratarán como literales.
Para anular el significado de un carácter especial dentro de comillas dobles se utiliza el
carácter "\".
Cuando se llama a un programa Perl se le pueden pasar argumentos que se guardan en el
array predefinido @ARGV. El primer argumento se almacenará en $ARGV[0], el segundo
en $ARGV[1] y así sucesivamente. Hay otro array predefinido muy importante en Perl
que es %ENV, que almacena las variables de ambiente y, como se deduce de su nombre, es
A LGUNOS LENGUAJES DE script DESTACADOS 293
Se puede observar que el nombre de la variable de ambiente, en este caso PATH, hace
de índice o clave del array asociativo. En los ejercicios resueltos 11 y 12 se pueden ver
ejemplos de recorrido y acceso al array asociativo %ENV.
Otras variables predefinidas útiles son las siguientes:
Por otra parte, hay variables que se refieren a la última expresión que casó:
$1..$9 Contiene el subpatrón entre paréntesis de la última expresión regular que casó.
Por ejemplo, $1 se refiere al primer paréntesis y así sucesivamente.
$& Contiene la última cadena que casó en una expresión regular.
$‘ Contiene la cadena precedente a la que casó en la última expresión regular.
$’ Contiene la cadena siguiente a la que casó en la última expresión regular.
$+ Contiene la cadena asociada al paréntesis que casó en la última expresión regular.
Es útil cuando no se sabe cuál de una serie de posibles patrones ha casado.
Veamos un ejemplo de algunas de estas variables:
1 $_ = ’ abcdefghi ’;
2 / def /;
3 print "$ ‘: $ &: $ ’\n";
escribiría: abc:def:ghi
El patrón que aparece entre "/" y "/" es el que se compara con el valor de la variable $_.
Perl lee los datos de la entrada estándar STDIN (el teclado) y muestra los resultados por
la salida estándar STDOUT (pantalla). Mediante el redireccionamiento y la apertura y
cierre de ficheros se podrá leer y escribir datos en ficheros. Por ejemplo, el siguiente
código:
lee líneas de la entrada estándar y las escribe en la salida estándar. La instrucción $linea
= <STDIN> lee una línea de la entrada estándar y la almacena en la variable $linea. Si
en lugar de utilizar el código anterior utilizamos el siguiente:
\f Salto de página.
Además de los modificadores "s" (sustitución) y "g" (global) que ya vimos en las expre-
siones regulares en sed, Perl dispone de otros muy útiles, por ejemplo "i". El siguiente
código:
escribe las líneas en las que aparece la palabra casa escrita en mayúscula, minúscula o
cualquier combinación de ellas. De este modo, el modificador "i" permite búsquedas
no sensibles a mayúsculas y minúsculas.
Por defecto, el emparejamiento de patrones se efectúa sobre la variable $_, pero se puede
aplicar a otra variable:
5.3.2 PHP
PHP es un lenguaje de script de propósito general de código abierto. Fue creado por
Rasmus Lerdorf en 1994 y empezó siendo el acrónimo de Personal Home Page ya que
se diseñó originalmente para la creación de páginas web dinámicas debido a que se puede
embeber en HTML. A medida que sus capacidades fueron evolucionando y ampliándose
pasó a ser el acrónimo de Hypertext Preprocessor. Actualmente la implementación que
se considera estándar es la del PHP Group4 .
Está inspirado principalmente en los lenguajes C y Perl. Aunque orientado a la progra-
mación web, también se puede utilizar desde la línea de comandos (como Perl o Python)
en su versión PHP-CLI (Command Line Interface) o como lenguaje de propósito general
con una interfaz gráfica utilizando la extensión PHP-Qt o PHP-GTK. Las versiones re-
cientes tienen soporte para orientación a objetos, aunque no es propiamente un lenguaje
orientado a objetos.
PHP se puede utilizar en la mayoría de los servidores web (Apache e Internet Infor-
mation Services (IIS) de Microsoft, entre otros), sistemas operativos (Linux, variantes
de Unix –HP-UX, Solaris y OpenBSD–, Microsoft Windows, Mac OS X, RISC OS),
así como con varios sistemas de gestión de bases de datos relacionales, bien mediante
4 http://www.php.net
296 L ENGUAJES DE script
una extensión (mysql), con una capa de abstracción PDO5 , utilizando el estándar Open
Database Connection a través de la extensión ODBC o sockets. PHP puede trabajar
como un módulo de un servidor o como un procesador CGI.
Además de generar salida HTML, PHP puede generar ficheros en otros formatos, como
XHTML, XML o PDF. PHP incluye capacidades para realizar procesamiento de cadenas
de texto que son compatibles con las de Perl. También incluye extensiones y herramien-
tas para analizar y procesar documentos XML.
El intérprete PHP ejecuta el código que se encuentra entre el delimitador de inicio <?php
y el de fin ?>. Estos delimitadores en XML y XHTML corresponden a instrucciones de
procesamiento, por lo que la mezcla de PHP y XML en un documento en un servidor
web es un documento XML bien formado.
Veamos el clásico programa "Hola mundo" en PHP embebido en HTML:
1 <! DOCTYPE HTML PUBLIC " -// W3C // DTD HTML 4.01// EN "
2 " http :// www . w3 . org / TR / html4 / strict . dtd " >
3 <HTML >
4 <HEAD >< TITLE > Primera programa PHP </ TITLE > </ HEAD >
5 <BODY >
6 <? php
7 echo ’Hola Mundo ’;
8 ?>
9 </ BODY >
10 </ HTML >
1 $colores = array(" amarillo " , " rojo " , " verde " , " naranja " ,
2 " violeta ");
5 La extensión PHP Data Objects (PDO) define una interfaz para tener acceso a bases de datos en PHP.
A LGUNOS LENGUAJES DE script DESTACADOS 297
Para crear un array asociativo hay que especificar los pares clave-valor. Por ejemplo:
1 $CantidadColores = (" amarillo " => 0, " rojo " => 0, " verde " => 0 ,}
2 " naranja " => 0, " violeta " => 0) ;
En este caso las claves de acceso al array serán los colores. También se podrían utilizar
asignaciones explícitas como en el caso anterior.
PHP consta básicamente de dos funciones para emparejamiento de patrones, una sen-
sible a mayúsculas y minúsculas y otra no, y otras dos funciones para emparejamiento
y sustitución, de nuevo una sensible a mayúsculas y minúsculas y otra no. Además, se
pueden utilizar los patrones PCRE6 (Perl-compatible Regular Expression Functions) y
POSIX, aunque está desaconsejado el uso de estas últimas funciones desde la versión
PHP 5.3.0 y en su lugar se recomienda usar las funciones de PCRE.
Para comparar un patrón con una cadena se puede utilizar la función preg_match()7
que devuelve el número de veces que coinciden. Este resultado podrá ser 0 (sin co-
incidencias) o 1, ya que preg_match() detendrá la búsqueda después de la primera
coincidencia; el valor devuelto por la función será FALSE si se produjo un error. La
función preg_match_all(), por el contrario, continuará hasta que alcance el final de la
cadena. El formato básico de la función es:
Esta función devuelve un valor entero y los dos parámetros son de tipo string. Veamos
un ejemplo de su uso:
1 echo preg_match ("/ mundo /" , "/ Hola mundo /" , $cadena ) . " <br /> ";
2 echo $cadena [0] . " <br />";
mostraría:
1
mundo
Si se necesita saber la posición en la que se produjo el casado, habrá que pasar a la fun-
ción un cuarto argumento: PREG_OFFSET_CAPTURE. En este caso, el array que almacena
la cadena que ha casado se convierte en su primera posición en un array de dos dimen-
siones: una para almacenar la cadena y la otra para almacenar la posición. El siguiente
ejemplo:
1 echo preg_match ("/ mundo /" , "/ Hola mundo /" , $cadena ,
PREG_OFFSET_CAPTURE ) . " <br /> ";
2 echo $cadena [0][0] . " <br /> ";
3 echo $cadena [0][1] . " <br /> ";
mostraría:
1
mundo
5
1 preg_match (" /(\ d +\/\ d +\/\ d +) (\ d +\:\ d +.+) /" , " 03/03/2006 15:30
PM " , $cadena );
2 echo $cadena [0] . " <br />";
3 echo $cadena [1] . " <br />";
4 echo $cadena [2] . " <br />";
mostraría:
03/03/2006 15:30PM
03/03/2006
15:30PM
Se recuerda al lector que al igual que en Perl \d representa un dígito y que el carácter "."
representa cualquier carácter. En la primera posición del array $cadena se almacena la
cadena completa que ha casado, en la siguiente posición se almacena la subcadena que
ha casado con el primer subpatrón (primer subpatrón entre paréntesis) y en la tercera la
que ha casado con el segundo subpatrón (segundo subpatrón entre paréntesis).
PHP y XML
SimpleXML
En el caso de que los elementos XML tengan nombres con caracteres no permitidos por
las normas de PHP para nombrar variables (por ejemplo un guión), se podría acceder a
dicho elemento encerrando su nombre entre llaves y comillas simples como en:
attributes() devuelve un array asociativo con todos los atributos del elemento en
forma de pares clave-valor.
children() devuelve un array con todos los hijos del nodo dado como objetos de
tipo SimpleXMLElement.
1 <? php
2 $xml = simplexml_load_file (" aviso . xml ")
3 echo $xml -> texto [0]
4 ?>
A LGUNOS LENGUAJES DE script DESTACADOS 301
Para escribir el contenido de todos los elementos hijos del nodo nota:
1 <? php
2 $xml = simplexml_load_file (" aviso . xml ");
3 foreach ( $xml -> children () as $hijo ){
4 echo " Nodo hijo : " . $hijo . " <br />";
5 }
6 ?>
1 <? php
2 $xml = simplexml_load_file (" aviso . xml ");
3 $xml -> texto [0] - > addChild (" fecha " , " 10 -03 -2011 ");
4 foreach ( $xml -> texto -> children () as $hijo ) {
5 echo " Nodo hijo : " . $hijo . " <br />";
6 }
7 ?>
1 10-03-2011
1 <? php
2 $xml = simplexml_load_file (" aviso . xml ");
3 $xml -> texto [0] - > addAttribute (" leng " , " es ");
4 foreach( $xml -> body [0] - > attributes () as $a => $b ) {
5 echo $a , ’=" ’,$b ," \" </ br >";
6 }
7 ?>
1 leng="es"
Parser XML
Esta extensión de PHP permite leer y acceder al contenido XML con un enfoque dirigido
por eventos. Cada elemento de la corriente de datos activa un evento y su correspon-
diente manejador se activa.
Para crear un nuevo analizador se utiliza la función xml_parser_create(). Por ejem-
plo:
1 $parser = xml_parser_create()
Una vez hecho lo anterior ya se puede analizar un documento XML. El documento de-
berá ser almacenado en una variable de tipo cadena con la función file_get_contents(),
por ejemplo:
Una vez que se ha acabado de analizar el documento XML conviene eliminar el analiza-
dor para liberar memoria:
1 xml_parser_free( $parser );
DOM
DOMNode representa un nodo del árbol DOM. La mayoría de las clases DOM derivan
de ésta.
en http://www.php.net/manual/es/book.dom.php
304 L ENGUAJES DE script
Para empezar a trabajar con un documento DOM primero hay que crear un objeto de
tipo DOMDocument, por ejemplo:
A partir de aquí se puede utilizar el objeto $doc para leer el documento, escribirlo y
modificarlo cambiando, añadiendo o eliminando nodos.
Para procesar un nodo suele ser necesario determinar de qué tipo de nodo se trata. Para
ello se utiliza la propiedad nodeType del objeto DOMNode que devuelve una constante
predefinida indicando su tipo. Algunos de los valores más comunes son:
Para crear un documento XML es necesario crear nodos y añadirlos al árbol DOM. Al-
gunos de los métodos más comunes para crear nodos pertenecen a la clase DOMDocument:
Una vez creado un nodo se añade como nodo hijo a un nodo ya existente del árbol
utilizando el método appendChild(). Por ejemplo:
En el apartado 5.4 hay ejemplos de programas PHP que procesan documentos XML.
Este script tiene dos argumentos que son ficheros o directorios e indica si el
primero es más reciente o más antiguo que el segundo.
3. Escribe un script bash para comprobar si un fichero que se pasa como argumento
existe. En caso de que no exista, hay que crearlo.
8 if [ -f $1 ]
9 then
10 echo se ha creado el fichero $1
11 else
12 echo no se puede crear el fichero $1
13 fi
14 fi
5. Escribe un script bash para realizar una copia de seguridad. En el nombre del
fichero resultante de la copia deberá constar la fecha en la que se realiza.
6. Escribe un comando sed para eliminar las líneas de un fichero que comienzan con
el carácter #.
8. Escribe un comando sed que muestre un bloque de texto que comience con una
línea que contenga la palabra "INICIO" y termine con una línea que contenga
"FIN".
9. Escribe un script en awk que tenga como entrada una lista de líneas de texto nu-
meradas, siendo este número el primer campo de cada línea, y las muestre orde-
nadas por dicho número. Se trata por lo tanto de ordenar las líneas de entrada.
La forma más sencilla de hacerlo es utilizar un array asociativo que tenga como
índice la propia línea, por lo que se ubicarán en el array en orden de numeración.
1 {
2 if ( $1 > max )
3 max = $1
4 lineas [ $1 ] = $0
5 }
6 END {
7 for (x = 1; x <= max ; x ++)
8 if (x in lineas )
9 print lineas [x]
10 }
En el caso de que haya líneas con números repetidos la más reciente sobrescribe a
la anterior.
10. Escribe un script en awk que copie todas las líneas de entrada en la salida, excepto
aquellas que contengan @include nombreFichero en cuyo caso se reemplazá
dicha línea por el contenido del fichero indicado.
1 {
2 if ( NF == 2 && $1 == " @include ") {
3 while ((getline linea < $2 ) > 0)
4 print linea
5 close( $2 )
6 } else
7 print
8 }
308 L ENGUAJES DE script
Se usa la instrucción getline para leer desde un fichero. En concreto se usa con el
formato getline var < nombreFichero para leer un registro de nombreFichero
y almacenarlo en la variable var.
11. Escribe un programa Perl que visualice todas las variables de ambiente y sus va-
lores.
La función keys devuelve todas las claves del array asociativo que tiene como
argumento.
12. Escribe un programa Perl que visualice todas las variables de ambiente en orden
alfabético junto con sus valores.
La función sort devuelve el array que recibe como argumento ordenado alfabéti-
camente sin alterarlo, por lo que el resultado hay que almacenarlo en otro array.
Si el array contiene números, los ordena en orden ascendente. Se puede utilizar
con todos los tipos de arrays.
13. Escribe un programa Perl que cuente todas las apariciones de la palabra "casa" en
un fichero o conjunto de ficheros.
14. Sea un fichero noticias.xml que contiene numerosas noticias descritas por el
elemento <DOC>. Estas noticias tienen asociada una categoría que corresponde
al valor del atributo CODE del elemento CAT (ver ejemplo). Se pide escribir un
E JERCICIOS RESUELTOS 309
1 <?xml version=" 1.0 " encoding =" ISO -8859 -1 "? >
2 < NEWSREPOSITORY COMMENT =" CorpusX " DATE =" 19/09/2002 " >
3 < DOC SOURCE =" Madrid , 31 dic " LNG =" sp " AUTHOR =" Pepe " DOCNO
="1" DATE =" 2000/01/01 " >
4 < CAT CODE =" 13000000 " SCHEME =" IPTC " / >
5 < TITLE > El ... </ TITLE >
6 < SUMMARY > <P > Los ... </P >
7 </ SUMMARY >
8 < BODY >
9 <P > ... </P >
10 <P > ... </P >
11 </ BODY >
12 </ DOC >
13 < DOC ... > ... </ DOC >
14 </ NEWSREPOSITORY >
15. Escribe el código de un CGI interactivo en Perl para crear una página web que
multiplique el valor de dos operandos tecleados por el usuario, devolviendo el
resultado en otra página web generada automáticamente.
En primer lugar, veamos cómo podría ser el código HTML para el formulario en
el que se piden los operandos y se lanza la petición:
1 <HTML>
2 <HEAD>
3 <TITLE> Multiplicador </TITLE>
4 </HEAD>
5 <BODY>
E JERCICIOS RESUELTOS 311
6 <FORM action="/ cgi - bin / multiplicador . pl " method=" post " >
7 <P> <INPUT name=" ope1 " size=4 > Primer operando <BR>
8 <INPUT name=" ope2 " size=4 > Segundo operando </P>
9 <P> <INPUT type=" submit " value=" multiplicar " > </P>
10 </FORM>
11 </BODY>
12 </HTML>
El usuario debe introducir el valor de los dos operandos en los campos de texto
y hacer clic en el botón "multiplicar". Entonces, el navegador cliente envía una
petición al servidor para la URI /cgi-bin/multiplicador.pl. El script Perl
usa esos valores para realizar la multiplicación y generar una nueva página web.
16. Escribe un script en PHP embebido en una página web para obtener una lista de
los usuarios que están utilizando un servidor junto con algunas estadísticas.
1 <HTML >
2 <HEAD >
3 <TITLE > Información de <? php echo $host = chop( ’
hostname ’) ?>
4 </ TITLE >
5 </ HEAD >
6 <BODY >
7 <H1 > <? php echo $host ?> </H1 >
8 <PRE >
9 <? php echo ’ uptime ’, "\n" ’who ’ ?>
10 </ PRE >
11 </ BODY >
12 </ HTML >
312 L ENGUAJES DE script
17. Crea un formulario utilizando HTML y PHP para solicitar a un usuario: nombre,
apellidos, dirección de correo electrónico y comentarios. Los datos del formulario,
una vez cumplimentado, deberán mostrarse al usuario.
1 <HTML>
2 <HEAD>
3 <TITLE> Datos personales </TITLE>
4 </HEAD>
5 <BODY>
6 <FORM ACTION=" FormularioPersonal . php " METHOD= POST >
7 Nombre <INPUT TYPE=text NAME=" nombre " SIZE=22 > <BR>
8 Apellidos <INPUT TYPE=text NAME=" apellidos " SIZE=50 >
<BR>
9 e - mail <INPUT TYPE=text NAME=" email " SIZE=70 > <BR>
10 Comentarios <TEXTAREA TYPE=text NAME=" comentarios "
FILAS =6 COLS=40 > </TEXTAREA><BR>
11 <INPUT TYPE= submit NAME=" enviar " > <BR>
12 </FORM>
13 </BODY>
14 </HTML>
1 <HTML >
2 <HEAD >
3 <TITLE > Datos procesados </ TITLE >
4 </ HEAD >
5 <BODY >
6 // mostrar los datos introducidos
7 <? php echo}
8 print " Nombre : $nombre <BR > \n";
9 print " Apellidos : $apellidos <BR > \n";
10 print "e - mail : $email <BR > \n";
11 print " Comentarios : $comentarios <BR > \n";
12 ?>
13 </ BODY >
14 </ HTML >
En algunos casos puede resultar necesario eliminar los espacios que pueda haber
antes y después de los caracteres que formen una cadena, por ejemplo en una
E JERCICIOS RESUELTOS 313
contraseña, un correo electrónico, etc. Para ello PHP dispone de funciones pre-
definidas. La función trim() elimina los espacios extra de comienzo y fin de una
cadena. Si en el script anterior utilizamos esta función quedaría:
1 <HTML >
2 <HEAD >
3 <TITLE > Datos procesados </ TITLE >
4 </ HEAD >
5 <BODY >
6 // mostrar los datos introducidos
7 <? php echo}
8 $nombre = trim( $nombre );
9 $apellidos = trim( $apellidos );
10 $email = trim( $email );
11 $comentarios = trim( $comentarios );
12 print " Nombre : $nombre <BR > \n";
13 print " Apellidos : $apellidos <BR > \n";
14 print "e - mail : $email <BR > \n";
15 print " Comentarios : $comentarios <BR > \n";
16 ?>
17 </ BODY >
18 </ HTML >
18. Escribe una función PHP que determine si un documento XML almacenado en la
cadena $doc es válido.
19. Dado un documento XML similar al del ejercicio resuelto 14, se pide añadir un
nuevo elemento DOC a los ya existentes. Utiliza PHP y la extensión DOM.
1 <! DOCTYPE html PUBLIC " -l // W3C // DTD XHTML 1.0 Strict // EN "
2 HTML xmlns =" http :// www . w3 . org / TR / xhtml1 / DTD / xhtml1 - strict .
dtd " >
3 <HTML xmlns = " http :// www . w3 . org /1999/ xhtml " " xml : lang =" es "
lang =" es " >
314 L ENGUAJES DE script
4 <HEAD >
5 <TITLE > Añadir un elemento nuevo usando DOM </ TITLE >
6 <LINK > rel =" stylesheet " type =" text / css " href =" common .
css "/>
7 </ HEAD >
8 <BODY >
9 <H1 > Añadir un elemento nuevo usando DOM </H1 >
10 <PRE >
11
12 <? php
13
14 // Carga el fichero XML
15 $docu = new DOMDocument () ;
16 $docu -> preserveWhiteSpace = false ;
17 $docu -> load (" NEWSREPOSITORY . xml ");
18 $docu -> formatOutput = true ;
19
20 // Accede a la raíz }
21 $newsrepositoryElements = $docu ->
getElementsByTagName (" NEWSREPOSITORY ");
22 $newsrepository = $newsrepositoryElements -> item (0) ;
23
7. Escribe un script que permita validar fechas. Tendrá como entrada una fecha en
formato string y devolverá la misma fecha indicando si es o no válida. Se permiten
como entrada los siguientes formatos de fechas: "15/1/1998", "15/01/2001", "12-
09-98", "22-3-08". Cuando el año se especifique con dos dígitos, se referirá a
fechas entre 1960 y 2059. Escribe el script en PHP.
11. Escribe un script que implemente un analizador de textos que permita obtener
las siguientes estadísticas de un fichero de texto que se pase como argumento de
entrada:
Además, de cada palabra se desea tener un registro de las oraciones donde aparece
con el objetivo de poder realizar búsquedas rápidamente. Sin embargo, dado que
esta asociación puede resultar en un ingente uso de memoria, para este registro se
filtrarán aquellas palabras de longitud menor que cuatro caracteres.
En http://en.wikipedia.org/wiki/Comparison_of_computer_shells se pueden
encontrar tablas comparativas de diferentes interpretes de comandos o shells.
En http://sed.sourceforge.net/sedfaq.html hay una FAQ muy útil sobre la he-
rramienta sed, con varias referencias a manuales de libre acceso, y se pueden encontrar
numerosos ejemplos de uso de sed en http://sed.sourceforge.net/sed1line.txt.
Además, en http://www.gnu.org/software/sed/manual/sed.html#sed-Programs
se puede encontrar un manual de sed.
El lector que quiera profundizar en awk puede encontrar una buena fuente en [2]. En
http://www.gnu.org/software/gawk/manual/gawk.html está disponible el manual
de usuario de awk.
En http://www.perl.org/ se encuentra el sitio de Perl con las versiones para diferen-
tes plataformas y una amplia documentación sobre el lenguaje.
En http://www.php.net/manual/en/index.php hay un manual de PHP en inglés y
desde esta misma página se puede enlazar a la versión en español. Desde ahí se puede
acceder a http://www.php.net/manual/es/refs.xml.php donde se pueden encon-
trar extensiones para manipular documentos XML con las APIs XML de PHP 5. Una
referencia bastante completa para el lenguaje PHP es [10].
Capítulo 6
En este capítulo se pretende dar una visión general sobre aspectos pragmáticos de los
lenguajes de programación, aquellos que resultan clave en la elección de un determinado
lenguaje frente a otro, a la hora de crear una programa informático o de estudiar la
interoperabilidad entre aplicaciones escritas en diferentes lenguajes de programación.
La idea es presentar los criterios fundamentales que permitan definir o pensar en un buen
lenguaje de programación, ya sea para seleccionarlo como herramienta de desarrollo o
pensando en el diseño de un nuevo lenguaje.
6.1 Introducción
La pragmática se define como la rama de la lingüística interesada en la manera en que
el contexto influye en la interpretación del significado. Cuando se habla de lenguajes de
programación, la pragmática se encarga de las técnicas y tecnologías empleadas durante
la construcción de programas. Por tanto, se refiere a la relación entre el lenguaje de
programación y sus usuarios, los programadores.
A la hora de abordar el desarrollo de un gran proyecto informático, aparte de aspectos
relativos a la ingeniería del software como la planificación del proyecto, el análisis de
requisitos, etc., se considera algo fundamental el conocimiento y comprensión de la
herramienta de trabajo que, en este caso, no es otra que el lenguaje de programación
utilizado.
319
320 A SPECTOS PRAGMÁTICOS DE LOS LENGUAJES DE PROGRAMACIÓN
sería importante identificar esas características que lo hacen el más adecuado para el
desempeño de una determinada tarea.
Existe un conjunto de principios de diseño comúnmente aceptados que hacen de un
lenguaje de programación un buen lenguaje ([11], [18]). No todos los lenguajes de
programación incluyen todos estos principios; la razón es que ciertos lenguajes están
orientados hacia un dominio específico y priman, por ejemplo, la usabilidad en ese con-
texto frente a la inclusión de otros principios que podrían limitar, de alguna forma, esa
usabilidad.
En general, en este apartado nos centraremos en los lenguajes de alto nivel, o de tercera
generación. Como se ha visto a lo largo de los capítulos anteriores, existen multitud de
lenguajes de alto nivel que, en principio, podrían utilizarse para realizar cualquier tarea
que un programador necesitara llevar a cabo. La realidad, sin embargo, es que ciertos
lenguajes se utilizan más en ciertos contextos que otros, y un programador debería ser
capaz de escoger el lenguaje que más se adecúe a sus necesidades, siempre teniendo
en cuenta factores que pueden influir en la decisión, como la capacidad del equipo de
trabajo de asumir el aprendizaje de un nuevo lenguaje, etc. Por poner un ejemplo, en
general el lenguaje C se utiliza en la programación de sistemas, donde son necesarias
ciertas capacidades de bajo nivel como en el caso del desarrollo de sistemas operativos.
Java y PHP, por el contrario, son lenguajes comúnmente utilizados para el desarrollo de
aplicaciones web. Así, es raro encontrar aplicaciones web programadas en C, aunque
podría haberlas.
A continuación se presentan algunos principios de diseño relacionados con los lenguajes
de programación. Es difícil que un lenguaje de programación los incluya todos, sin
embargo la presencia de unos u otros puede ser de ayuda a la hora de evaluar el lenguaje.
6.2.2 Fiabilidad
Un programa se considera fiable si hace lo que se espera de él. Los lenguajes que fomen-
tan la escritura de programas fiables deberían permitir detectar y corregir rápidamente
los errores que se producen. Por ejemplo, la sentencia goto es una característica no fiable
de un lenguaje. La permisividad de C al poder especificar una asignación en cualquier
parte hace que en ocasiones se sustituya un == (comparación por igualdad) por = (asig-
nación) en una condición de un if por ejemplo. Como los símbolos son muy parecidos
este error puede pasar inadvertido hasta después de muchas revisiones del código.
Además, un lenguaje fiable debe permitir al programador recuperarse de errores que
se puedan producir en ejecución. Por ejemplo, el programador puede no desear que el
programa termine anómalamente simplemente porque no existe un fichero, porque en su
lugar el programa podría preguntar al usuario dónde se encuentra éste o si quiere crearlo
en ese momento. Lo mismo sucede si en una entrada el usuario especifica un valor no
válido (una letra cuando se esperaba un número o un cero como denominador de una
división), el comportamiento deseado puede ser avisar del error cometido al usuario y
pedirle que introduzca una entrada válida.
6.2.3 Ortogonalidad
La ortogonalidad se refiere al comportamiento homogéneo de las características de un
lenguaje. ¿Un símbolo o una palabra reservada tienen siempre el mismo significado,
independiente del contexto en el que se utilicen? ¿Tiene el lenguaje un pequeño número
de características básicas que interactúan de forma predecible, sin importar cómo se
combinen en un programa?
Si un lenguaje es ortogonal quiere decir que las características que presenta pueden com-
binarse entre ellas comportándose de igual manera en cualquier circunstancia. Un ejem-
plo se encuentra en los conceptos de tipo y función. Un tipo describe la estructura de
los elementos de datos y una función es un subprograma que recibe un número finito
de valores de parámetro y devuelve un único valor hacia el subprograma que la invoca.
En un lenguaje ortogonal, los tipos son independientes de las funciones, y no se aplican
restricciones a los tipos de parámetros que pueden ser pasados o al tipo de valor que
puede ser devuelto. Así, idealmente podríamos ser capaces de pasar una función a una
función, y recibir una función de regreso. Un ejemplo claro de falta de ortogonalidad se
da en el lenguaje Pascal, donde no se permite que las funciones devuelvan registros, lo
que rompe la ortogonalidad en cuanto a la combinación de tipos de datos y funciones.
Uno esperaría que una función pudiera devolver cualquier tipo de dato sin excepciones.
6.2.4 Generalidad
La generalidad se refiere a que se dejan en el lenguaje sólo las características necesarias,
y el resto se construyen a partir de éstas combinándolas sin limitación de una manera
previsible.
322 A SPECTOS PRAGMÁTICOS DE LOS LENGUAJES DE PROGRAMACIÓN
Como ejemplo de carencia de generalidad se tiene, por ejemplo, el tipo de unión libre
en Pascal, un registro que puede tener un campo que se denomina campo variante, y
que varía dependiendo de su uso. En un registro de esta clase el campo variante puede
funcionar como un puntero, mientras que en otro momento durante la misma ejecución
puede ser usado como un tipo entero, con lo que su valor estaría disponible para opera-
ciones aritméticas, etc. Esta característica no es general, porque la ubicación en memoria
relacionada con las variables de campo variante no se trata de manera uniforme, por lo
que pueden aparecer efectos no previsibles.
6.2.5 Notación
Con frecuencia los lenguajes de programación toman de las matemáticas muchos de sus
símbolos. Por ejemplo, para especificar una expresión aritmética se usan los operadores
conocidos +, −. Esta notación es consistente con los conocimientos que tenemos a
priori (antes de aprender el lenguaje) y ayuda al programador a entender el lenguaje.
Por tanto, en general, la notación debería basarse en conocimiento que ya existe. Sin
embargo, en ocasiones es complicado utilizar determinados símbolos, porque el sistema
podría no soportarlos. Es el caso de símbolos como φ (conjunto vacío) o ∈ (pertenencia
a un conjunto).
6.2.6 Uniformidad
La idea de lenguaje uniforme es que nociones similares deberían verse y comportarse de
la misma manera. Si un lenguaje exige que toda sentencia acabe con un punto y coma, se
dice que es uniforme. Un ejemplo de no uniformidad lo podemos observar en el lenguaje
Pascal. En este lenguaje en el cuerpo de un enunciado for sólo se permite una sentencia.
Para incluir más de una sentencia es necesario encerrarlas entre un begin y un end. Sin
embargo, en el cuerpo de un enunciado repeat ...until se pueden incluir múltiples
sentencias.
6.2.7 Subconjuntos
En ocasiones es útil poder construir subconjuntos funcionales de un lenguaje. Un sub-
conjunto de un lenguaje es una implementación de sólo una parte del mismo (por ejem-
plo, C++ sin objetos). Los subconjuntos son útiles en ámbitos académicos, porque per-
miten focalizar el aprendizaje en unas características dejando de lado otras hasta que las
primeras se han aprendido. También tiene utilidad para desarrollar incrementalmente
el lenguaje. Java, por ejemplo, ha evolucionado dos veces desde la versión inicial. La
versión actual (cuarta versión del lenguaje) incluye características que no estaban pre-
sentes en versiones anteriores, sin embargo programas escritos en la nueva versión son
compatibles con código de versiones anteriores y pueden utilizar dicho código.
P RINCIPIOS DE DISEÑO DE LOS LENGUAJES 323
6.2.8 Portabilidad
La portabilidad es una característica por la cual un programa escrito en un determi-
nado lenguaje puede ejecutarse en diferentes máquinas sin tener que reescribir el código
fuente. Se trata entonces de la facilidad para ejecutar programas en distintos entornos
lógicos o físicos.
Para conseguir la portabilidad son de vital importancia las organizaciones de estándares,
como ANSI o ISO. Estas organizaciones fijan definiciones de lenguajes a las que se
pueden adherir los constructores de compiladores para asegurar que el mismo programa
podrá ser compilado en diferentes arquitecturas. Los lenguajes pensados para ser eje-
cutados por un intérprete o por medio de una máquina virtual habitualmente suelen ser
portables.
6.2.9 Simplicidad
Un lenguaje de programación para considerarse simple debería contener la menor can-
tidad posible de conceptos. Además deberían evitarse aquellos conceptos que pueden
llevar al programador a errores (= y ==), que son difíciles de entender al leer el código,
o que son difíciles de traducir por parte de un compilador.
Un lenguaje de programación debe esforzarse en la simplicidad sintáctica y semántica.
Simplicidad semántica implica que el lenguaje contiene un mínimo número de conceptos
y estructuras. Estos conceptos deben resultar naturales para un programador, de rápido
aprendizaje y comprensión, tratando de minimizar errores de interpretación. Para ello es
deseable tener un número mínimo de conceptos diferentes, con las reglas de combinación
lo más simples y regulares posibles. Esta claridad semántica y de conceptos representa
un factor determinante para la selección de un lenguaje. Por otro lado, la simplicidad
sintáctica requiere que la sintaxis represente cada concepto de una única forma y que el
código resulte tan legible como sea posible.
6.2.10 Abstracción
Los lenguajes de programación, como se ha visto, pueden clasificarse por su nivel de
abstracción. La abstracción es un principio por el cual se aísla toda aquella información
que no resulta relevante a un determinado nivel de conocimiento.
Una característica fundamental para la reutilización de código es el permitir crear y car-
gar librerías durante el proceso de desarrollo de un programa. Un buen lenguaje de
programación debería ofrecer mecanismos para abstraer patrones recurrentes. La pro-
gramación orientada a objetos, por ejemplo, introduce un nivel más de abstracción que
324 A SPECTOS PRAGMÁTICOS DE LOS LENGUAJES DE PROGRAMACIÓN
6.2.11 Modularidad
Su objetivo principal es resolver un problema más o menos complejo, dividiéndolo en
otros más sencillos, de modo que luego puedan ser enlazados convenientemente y nos
den la solución del problema original. Cada subproblema se representará mediante uno
o varios módulos según su complejidad. La idea es que estos módulos sean indepen-
dientes, es decir, que se puedan modificar o reemplazar sin afectar al resto del programa
o que puedan ser reutilizados dentro de otros programas.
La modularidad permite desarrollar los programas como unidades independientes. Estas
unidades interaccionan unas con otras de alguna manera (APIs, librerías, etc.). Los
lenguajes que soportan el concepto de módulo (también llamados paquetes, o unidades)
son más escalables, porque diferentes módulos pueden evolucionar de manera separada.
Los módulos deben tener interfaces bien definidas para que puedan utilizarse desde otros
módulos.
servicios web para intercambiar datos dentro de una red. La interoperabilidad se con-
sigue, en este caso, mediante la adopción de estándares abiertos. Las organizaciones
OASIS y W3C son las responsables de la arquitectura y reglamentación de los servicios
web.
La interoperabilidad de los Servicios Web se puede dividir en dos categorías básicas: in-
teroperabilidad SOAP (Simple Object Access Protocol) e interoperabilidad WSDL (Web
Services Description Language). SOAP es un protocolo estándar que define cómo dos
objetos en diferentes procesos pueden comunicarse por medio de intercambio de datos
XML, mientras que WSDL es un formato XML que se utiliza para describir servicios
Web. Como vemos, en la actualidad XML tiene un papel muy importante como formato
de intercambio de información, ya que permite la compatibilidad entre sistemas para
compartir información de una manera segura, fiable y fácil.
Por otro lado, y también dentro del mundo de los servicios web, REST (Representa-
tional State Transfer) es un modelo de arquitectura software para generar aplicaciones
cliente-servidor. Si bien el término REST originalmente se refería a un conjunto de prin-
cipios de arquitectura, en la actualidad se usa en un sentido más amplio para describir
cualquier interfaz web que utilice XML y HTTP, pero sin las abstracciones adicionales
de los protocolos basados en patrones de intercambio de mensajes como el protocolo de
servicios web SOAP.
A continuación se enumeran algunas aproximaciones desarrolladas a lo largo del tiempo
para tratar de conseguir interoperabilidad entre aplicaciones. En algunos casos existen
especificaciones estándar definidas para interconectar aplicaciones nativas sobre distin-
tas plataformas.
El lenguaje XML, descrito en detalle en el capítulo 4, se describe como un medio para lo-
grar la interoperabilidad de sistemas, debido al hecho de que provee medios de autodes-
cripción para representar los datos usados y compartidos entre aplicaciones [4]. Aunque
no ofrece una facilidad real de computación distribuida como las ofrecidas por Corba o
COM+, provee soporte para lograr interoperabilidad entre sistemas desarrollados inde-
pendientemente.
Un típico ejemplo de código embebido es cuando se desea acceder a una base de datos
desde dentro de un programa. En este caso, un código SQL embebido intercala sus ins-
trucciones en el código de un programa escrito en un lenguaje de programación al que
se denomina lenguaje anfitrión, y que puede ser un lenguaje como FORTRAN, COBOL,
C, . . . Estas instrucciones se ejecutan en el momento en que se ejecuta el programa in-
vitado de acuerdo a su lógica interna. En este contexto, el intercambio de información
con el Sistema de Gestión de Base de Datos (SGBD) se realiza a través de variables del
lenguaje, a las que se denomina variables huéspedes; por ejemplo, el resultado de las
consultas se asignaría a variables del programa declaradas con ese fin.
La forma de construir un programa con SQL embebido varía dependiendo del lenguaje y
el SGBD utilizados. Por ejemplo, en el caso de que se deseara embeber un códido SQL
en un programa escrito en C y accediendo a una base de datos Oracle, se ha definido un
lenguaje especial para soportar este tipo de programación: lenguaje Pro*C.
Otro claro ejemplo de uso de lenguajes embebidos es el caso de las páginas web dinámi-
cas. En este caso se introducen códigos en algún lenguaje de programación dentro del
código HTML, de modo que el navegador o un servidor web, dependiendo de si ejecuta-
mos en el lado del cliente o del servidor, además de mostrar el contenido del documento
HTML siguiendo la semántica de las etiquetas HTML, ejecuta el código embebido e
intercala la salida producida por el código en el código HTML final.
Hay numerosos ejemplos de lenguajes web embebidos, entre los que destacan, entre
otros:
• Código CSS (Cascading Style Sheets) embebido en HTML, SVG (Scalable Vector
Graphics) y otros lenguajes XML. Se trata de hojas de estilo en cascada. Con
ellas se describe cómo se va a mostrar un documento en la pantalla o cómo se
va a imprimir; incluso cómo va a ser pronunciada la información presente en ese
documento a través de un dispositivo de lectura.
– ASP (Active Server Pages). Se trata de una tecnología desarrollada por Mi-
crosoft para la creación dinámica de páginas web y ofrecida junto a su servi-
dor Internet Information Server.
– JSP (JavaServer Pages). Esta tecnología Java permite generar contenido
dinámico para web, en forma de documentos HTML, XML o de otro tipo.
Las JSPs permiten la utilización de código Java mediante scripts. Además,
es posible utilizar algunas acciones JSP predefinidas mediante etiquetas. En
este modelo, y de forma similar al caso de las páginas ASP, es posible usar
330 A SPECTOS PRAGMÁTICOS DE LOS LENGUAJES DE PROGRAMACIÓN
1 <html >
2 <body >
3 <p > & nbsp ; </p >
4 < div align = " center " >
5 < center >
6 < table border = "0" cellpadding = "0" cellspacing
7 = "0" width = " 460 " bgcolor = "# EEFFCA " >
8
9 <tr >
10 <td width = " 100% " >< font size = "6" color
11 = " #008000 " >& nbsp ; Date Example </ font > </ td >
12
13 </tr >
14 <tr >
15 <td width = " 100% " ><b >& nbsp ; Current Date
16 and time is :& nbsp ; < font color = "# FF0000 " >
17
18
19 <%= new java . util . Date () %>
20 </ font > </b > </ td >
21 </tr >
22 </ table >
23 </ center >
24 </div >
25 </ body >
26 </ html >
C RITERIOS DE SELECCIÓN DE LENGUAJES 331
Por otro lado, los diferentes paradigmas en los que se pueden clasificar los lenguajes de
programación también pueden suponer un criterio claro de selección, ya que un deter-
minado lenguaje de programación puede fomentar el uso de determinados paradigmas o
disuadir del uso de otros.
332 A SPECTOS PRAGMÁTICOS DE LOS LENGUAJES DE PROGRAMACIÓN
2. Enumera las principales diferencias de PHP frente a ASP y JSP como lenguajes
embebidos en páginas web.
335
336 B IBLIOGRAFÍA