Vous êtes sur la page 1sur 63

TEMA 1: PARADIGMAS DE LA COMPUTACIÓN

“Si la forma en que nos comunicamos influye en la forma que pensamos y viceversa, entonces la forma en que
programamos influye en lo que pensamos sobre los programas y viceversa”.

Antes de la década de los 40, se programaba cableando, y es en dicha década cuando Von Neumann plantea el uso
de códigos para determinar las acciones de los ordenadores, naciendo asi el lenguaje ensamblador. Pero dependía de
cada ordenador y era difícil de entender.
Al principio los lenguajes seguían reflejando la arquitectura Von Neumann: un área de memoria donde se
almacenaban tanto a los programas como a los datos de los mismo, y por separado había una unidad de
procesamiento.

Los lenguajes modernos, al aumentar el nivel de abstracción y utilizar nuevas arquitecturas en paralelo, se hacen
independientes de la máquina y solo describen el procesamiento en general.

“Un lenguaje de programación es un sistema notacional para describir computaciones en una forma legible tanto
para el ordenador como para el programador”. Un lenguaje de programación es una notación especial para
comunicarse con el ordenador y la computación incluye todo tipo de operaciones, siendo los lenguajes más
interesantes, desde el punto de paradigma, son los lenguajes de propósito general.

La evolución de los lenguajes de programación se ha organizado en cinco generaciones:

- Primera: Se incluyen lenguajes máquina, en los que los datos y las operaciones se describen mediante ceros y
unos.

- Segunda: Incluye a los lenguajes ensambladores, cuya traducción a lenguaje maquina es muy sencilla.

- Tercera: Incluye a los lenguajes de alto nivel como Pascal, Fortran, C o Java. Para su traducción a lenguaje
maquina se necesitan compiladores o intérpretes.

- Cuarta: Agrupa a lenguajes de propósito específico, como SQL.

- Quinta: Se incluyen lenguajes que se utilizan en primer lugar, en el área de la Inteligencia Artificial, como
Prolog y Haskell.

1.1 ABSTRACCIÓN EN LOS LENGUAJES DE PROGRAMACIÓN


1.1.1 Abstracciones de datos

- Básicas.

Se refieren a la representación internas de los datos de tipo atómico que ofrece el lenguaje, junto a sus
operaciones estándar. Otro tipo de abstracción básica es el uso de nombres simbólicos para referenciar las
localizaciones de memoria que contienen los datos del programa (variable).

- Estructuradas.

Son el mecanismo de abstracción para colecciones de datos. Una estructura típica es el array, que reúne
datos como una secuencia de elementos.

- Unitarias.

Se refieren a la agrupación, como única unidad, de datos y operaciones sobre ellos. Introducen el concepto
de encapsulado de datos, mecanismo muy útil para reunir códigos relacionados entre sí en localizaciones
específicas dentro del programa.
Estas abstracciones se asocian con los tipos abstractos de datos, lo que se conoce como interfaz.

Las clases de los lenguajes orientados a objetos son un mecanismo conceptualmente más cercano a las
abstracciones unitarias.
Dos de las características más importantes de estas abstracciones de datos son:

- Capacidad de reutilización.
- Interoperabilidad.

1.1.2 Abstracciones de control

- Básicas.

Son sentencias individuales que permiten modificar el control del flujo de la ejecución de un programa,
como la sentencia de asignación o el goto de Fortran.

- Estructuradas.

Agrupan sentencias más simples para crear una estructura con un propósito común, como los bucles o las
sentencias condicionales.

Otro mecanismo para estructurar el control es el subprograma, que necesita una declaración con un nombre
y un conjunto de acciones a realizar que se abstraen bajo dicho nombre. En segundo lugar, es necesario que
el subprograma sea llamado o invocado en el punto en que las acciones deben ejecutarse. Una llamada a
un subprograma es un mecanismo que requiere el almacenamiento del estado del programa en el punto de
llamada en el entorno de ejecución.

Un mecanismo de abstracción muy cercano al subprograma es el de función, que es un subprograma que


devuelve un estado tras ser invocado.

- Unitarias.

Permiten agrupar una colección de subprogramas como una unidad en si misma e independiente del
programa. Son idénticas a las abstracciones de datos unitarias, simplemente varía el enfoque, que en esta
ocasión se orienta más a las operaciones que a los datos.

Si un lenguaje de programación solo necesita describir computaciones, entonces solo necesita mecanismos
suficientes para describir todos los cálculos que puede llevar a cabo una máquina de Turing.

Como resumen:

Abstracción De datos De control


Tipos atómicos Asignación
Básica
Variables Goto
Bucles
Estructurada Tipos estructurados Condicionales
Subprogramas
Módulos
Unitaria
Paquetes
1.2 PARADIGMAS DE COMPUTACIÓN
Inicialmente los lenguajes de programación se basaron en el modelo de computación Von Neumann, que se conocen
como lenguajes imperativos, porque sus instrucciones representan ordenes, siendo estos lenguajes la mayoría de los
existentes.

Hay cuatro paradigmas de computación principales:

- Paradigma imperativo: La computación puede ser paralela o ser no determinista e independiente del orden.

- Paradigma funcional: Basado en abstracciones matemáticas, usa la noción de función según se plantea en el
lambda calculo.

- Paradigma lógico: Basado en abstracciones matemáticas, se basa en la lógica simbólica.

- Paradigma de la programación orientada a objetos: Facilita la reutilización de programas y su ampliación,


siendo más natural la elaboración de código que se quiere ejecutar. Los programas están formados por pequeñas
piezas de código, cuyas interacciones están controladas y se cambian fácilmente.

1.2.1 Programación orientada a objetos

Este paradigma se basa en la idea de que un objeto se puede describir como una colección de posiciones de
memoria junto con todas las operaciones que pueden cambiar los valores de dichas posiciones.
En la mayoría de lenguajes orientados a objetos, los objetos se agrupan en clases que representan a todos los
que tienen las mismas propiedades. Tras la declaración de una clase, se pueden crear objetos concretos a partir
de la misma, mediante la instanciación de la clase.

1.2.2 Programación funcional

La computación en este paradigma se fundamenta en la evaluación de funciones o en la aplicación de funciones


a valores conocidos, por lo que también se denominan lenguajes aplicativos. El mecanismo básico es la
evaluación de funciones con las siguientes características:

- La transferencia de valores como parámetros de las funciones que se evalúan.


- La generación de resultados en forma de valores devueltos por las funciones.

Las operaciones repetitivas se representan mediante funciones recursivas.

1.2.3 Programación lógica

Un programa está formado por un conjunto de sentencias que describen lo que es verdad o conocido con respecto
a un problema, en vez de indicar la secuencia de pasos que llevan al resultado.
En Prolog, un programa es un conjunto de sentencias, denominadas clausulas, de la forma: a :- b, c, d. que es
una afirmación que se entiende como “a es cierto, o resoluble si b, a continuación, c y finalmente d son ciertos
o resolubles en este orden”.
TEMA 2: PRINCIPIOS DE DISEÑO DE LOS LENGUAJES
2.1 DESCRIPCIÓN DE LOS LENGUAJES DE PROGRAMACIÓN
Los lenguajes de programación deben describirse de manera formal, completa, precisa e independiente de la máquina
y de la implementación.
Los elementos fundamentales para la definición de un lenguaje de programación son los siguientes:

- Léxico: Es el conjunto de las “palabras” o unidades léxicas que son las cadenas de caracteres significativas del
lenguaje, también denominados tokens. También son unidades léxicas los identificadores, operadores y
símbolos de puntuación.

- Sintaxis: Conlleva la descripción de los diferentes componentes del lenguaje y de sus combinaciones posibles.
Para ello se utilizan las gramáticas libres de contexto.

- Semántica: Expresa los efectos de la ejecución en un contexto determinado. La semántica es la parte más difícil
en la definición de un lenguaje. Entre los sistemas de notación para definiciones semánticas formales se
encuentran:

- Semántica operacional: El significado de una construcción es una descripción de su ejecución en una


maquina hipotética.

- Semántica denotacional: Asigna objeto matemáticos a cada componente del lenguaje.

- Semántica axiomática: Modela el significado con un conjunto de axiomas que describen a sus
componentes junto con algún tipo de inferencia del significado.

2.1.1 Traducción de los programas para su ejecución

Para la ejecución de los programas escritos en un lenguaje de programación, es necesario disponer de un


traductor, un programa que acepta como entrada los programas del lenguaje (interprete) y los ejecuta o
transforma en una forma adecuada para su ejecución (compilador).

En el caso del interprete, la ejecución de un programa se realiza en un paso. El intérprete produce la ejecución
del programa sobre los datos de entrada. La compilación es un proceso de dos pasos: el código fuente de la
entrada se convierte en un nuevo programa o código objeto que es el que puede ser ejecutado.

El lenguaje del código objeto debe ser a su vez traducido por un ensamblador que será linkado con otros códigos
objeto. Otro caso posible es aquel en el que un pseudo-interprete no produce un programa objetivo, sino que
traduce el programa fuente a un lenguaje intermedio que posteriormente es interpretado.

Las fases que tanto un intérprete como un compilador deben llevar a cabo son:

1. Un analizador léxico debe identificar los tokens del programa, ya que inicialmente el programa se
entiende como una secuencia de caracteres.

2. Un analizador sintáctico identifica las estructuras correctas que definen las secuencias de tokens.

3. Un analizador semántico asigna el significado de forma suficiente para su ejecución o la obtención del
programa objetivo.

Estas fases exigen el mantenimiento de un entorno o ambiente de ejecución, que administra el espacio de
memoria para los datos del programa y registra el avance de la ejecución.
Los interpretes son menos eficientes que los compiladores ya que estos permiten la optimización del código en
análisis previos a la ejecución del programa.

Hay otro aspecto que influye en la selección de un intérprete o un compilador: las propiedades del lenguaje que
pueden ser determinadas antes de su ejecución (estáticas) y las que no (dinámicas). En un lenguaje que solo
tenga asignación estática, se puede utilizar un ambiente totalmente estático, y en caso contrario usar uno
totalmente dinámico. Una posición intermedia es el ambiente basado en pilas.

Un último aspecto por considerar con respecto a la traducción es el relacionado con la recuperación de errores,
que favorece la fiabilidad. Los errores se clasifican de acuerdo con la fase de traducción en que se encuentran:

- Errores léxicos: Suelen estar limitados al uso de caracteres ilegibles o no admitidos.

- Errores sintácticos: Se refieren a tokens que faltan en expresiones o expresiones mal formadas.

- Errores semánticos: Pueden ser:

- Estáticos: Detectados antes de la ejecución.


- Dinámicos: Detectados durante la ejecución.

- Errores lógicos: Son también cometidos por el programador y producen un comportamiento erróneo o no
deseable del programa.

La pragmática de los lenguajes de programación se ocupa de aspectos como la especificación de mecanismos


para activación o deshabilitación de opciones de optimización, depuración y otras facilidades pragmáticas, que
suelen incluirse en los traductores.

2.2 DISEÑO DE LOS LENGUAJES DE PROGRAMACIÓN


2.2.1 La eficiencia

Este principio se refiere a que el diseño debe permitir al traductor la generación de código ejecutable eficiente.
La eficiencia se organiza en tres principios:

- Eficiencia de traducción: Estipula que el diseño del lenguaje debe permitir el desarrollo de un traductor
eficiente y de tamaño razonable. Hay traductores que se diseñan para que los programadores omitan la
verificación de errores con reglas muy difíciles de verificar en tiempo de traducción, lo que influye
negativamente en otro principio de diseño: la fiabilidad que aparece ligada a la verificación de errores.

- Eficiencia de implementación: Es la eficiencia con que se puede escribir un traductor, que a su vez
depende de la complejidad del lenguaje.

- Eficiencia de la programación: Relacionada con la rapidez y facilidad para escribir programas o capacidad
expresiva del lenguaje, que se refiere a la facilidad para escribir procesos complejos de forma que el
programador relacione de manera sencilla su idea con el código.
Relacionado con este concepto, está el de Azúcar Sintáctico, para descubrir aquellas estructuras sintácticas
que no añaden expresividad al lenguaje, pero que facilitan la escritura de los programas.

La fiabilidad exige de tiempo adicional para pruebas, recuperación de errores o agregación de nuevas
características y depende tanto de los recursos disponibles como de su consumo.
2.2.2 La regularidad

Se refiere al comportamiento de las características del lenguaje. Las irregularidades durante el diseño de un
lenguaje son por causa de las prioridades de diseño. Sin embargo, si una irregularidad no puede justificarse
entonces probablemente sea un error de diseño.

La regularidad se divide en tres propiedades, y si viola alguna de ellas, el lenguaje se puede clasificar como
irregular.

- Generalidad: Se consigue cuando el uso y la disponibilidad de los constructores no están sujetas a casos
especiales y cuando el lenguaje incluye solo a los constructores necesarios y el resto se obtienen por
combinaciones de constructores relacionados.

- Ortogonalidad: Ocurre cuando los constructores del lenguaje pueden admitir combinaciones significativas
y en ellas, la interacción entre los constructores o con el contexto, no provocan restricciones ni
comportamientos inesperados.

- Uniformidad: Se refiere a que lo similar se ve similar y lo diferente, diferente, lo que implica la


consistencia entre la apariencia y el comportamiento de los constructores.

2.2.3 Principios adicionales

- Simplicidad.

Se refiere sintácticamente a que cada concepto del lenguaje se presente de forma única y legible y
semánticamente que contiene el menor número posible de conceptos y estructuras con reglas sencillas de
combinación.

- Expresividad.

Es la facilidad con la que un lenguaje de programación permite expresar procesaos y estructuras complejas,
siendo uno de los mecanismos más expresivos, la recursividad. Una expresividad muy alta entra en
conflicto con la simplicidad ya que el ambiente de ejecución es muy complejo.

- Extensibilidad.

Es la propiedad relacionada con la posibilidad de añadir nuevas características a un lenguaje o añadir


palabras clave y constructores al traductor. Esta es una propiedad que se considera prioritaria. La tendencia
es permitir que el usuario defina nuevos tipos de datos, nuevas operaciones sobre ellos y que su tratamiento
sea como si se hubieran definido en el lenguaje desde el principio. Esto es muy útil para definir nuevos
tipos de datos, como el de matrices, y que las operaciones entre ellas se puedan escribir de forma similar a
las operaciones entre enteros.

Otro aspecto de extensibilidad importante es la modularidad, que es la capacidad de disponer de bibliotecas


y agregar nuevas, y se corresponde con la posibilidad de dividir un programa en partes independientes que
puedan enlazarse para su ejecución. Los módulos pueden ser reutilizados o cambiados sin que afecten al
resto del programa (escalabilidad), Además es necesario que tengan interfaces bien definidas para que
interactúen entre ellos.

- Capacidad de restricción.

Se refiere a la posibilidad de que un programador utilice solo un subconjunto de constructores mínimo y


por lo tanto solo necesite un conocimiento parcial del lenguaje. Asi el programador no necesita aprenderlo
todo y el traductor puede implementar solo el subconjunto determinado porque la implementación para
todo el lenguaje sea muy costosa e innecesaria.
Otro aspecto relacionado es la eficiencia: porque un programa no utilice ciertas características del lenguaje,
su ejecución no debe ser más ineficiente.

Otra ventaja es el desarrollo incremental del lenguaje.

- Consistencia entre la notación y las convenciones.

Un lenguaje debe incorporar notaciones y cualquier otra característica que se hayan convertido en
estandares, como lo son el concepto de programa, funciones y variable.

- Precisión.

Es la propiedad que exige una definición precisa del lenguaje, de forma que el comportamiento de los
programas sea predecible. La precisión ayuda a la confianza en los traductores, porque el programa se
comporta igual en cualquier máquina.

- Portabilidad.

Se consigue si la definición del lenguaje de programación es independiente de una maquina en particular.

- Seguridad.

Pretende evitar los errores de programación y permitir su descubrimiento. Por lo tanto, está muy relacionada
con la fiabilidad y la precisión. Sin embargo, compromete a la capacidad expresiva del lenguaje y a su
concisión, pues se apoya en que el programador especifique todo lo que sea posible en el código.

- Interoperabilidad.

Se refiere a la facilidad que tienen diferentes tipos de sistemas en general para trabajar conjuntamente de
manera efectiva, sin comunicaciones previas, para intercambiar información útil y con sentido. Kay tres
tipos de interoperabilidad:

- Semántica: Cuando los sistemas intercambian mensajes entre sí, interpretando el significado y el
contexto de los datos.

- Sintáctica: Cuando un sistema lee datos de otro, mediante una representación compatible.

- Estructural: Cuando los sistemas pueden comunicarse e interactuar en ambientes heterogéneos.

Para hacer posible la interoperabilidad, se debe establecer la representación de la información a


intercambiar y además utilizar un protocolo de comunicación.

En ocasiones se puede escribir dentro de un programa, código en otro lenguaje de programación, con objeto
de simplificar la programación.

TEMA 3: PROGRAMACIÓN FUNCIONAL


Otros modelos de cálculo, como el lambda cálculo se apartan del hardware para acercarse al software, haciendo
hincapié en la descripción del programa más que en la descripción de la máquina que va a ejecutarlo. Partiendo de
las siguientes funciones:

- Función identidad id(x) = x que recibe un parámetro x y lo devuelve.


- Función suma s(x, y) = x + y que recibe dos parámetros numéricos x e y, y devuelve la suma de ambos.

Algunas de las ideas fundamentales que sustentan el lambda calculo son:


- Las funciones no necesitan ser nombradas de forma explícita.

- x → x (dado x, devuelve x)
- x, y → x + y (dados x e y, devuelve x + y)

- El nombre de los parámetros es irrelevante. x → x y z → z representan la misma función identidad.

- Toda función que requiera dos argumentos puede ser reinterpretada como una función que requiera un único
argumento y que devuelva una función que aceptará un único argumento. Proceso que se conoce como
currificación.

- x → (y → x + y). Dado x, devuelve una función que dado y, devuelve x + y.

3.1 PROGRAMAS COMO FUNCIONES


Un programa funcional contiene las definiciones de diversas funciones que realizan el computo deseado sobre los
argumentos de entrada.
En la arquitectura Von Neumann el concepto de variable hace referencia al contenido de una posición de memoria
que puede variar.

Ejemplo: Se tiene la función:


doble(x) = 2 * x

Se pueden escribir ecuaciones en las que se utilice la función doble aplicada a una variable x que representa un valor
real:
sea x tal que doble(x) = 4

El concepto de variable sirve para hacer referencia al valor de uno de los parámetros de la función, no una localización
en memoria que pueda ser modificada. Esto se conoce como concepto de programación pura.

No desaparece el concepto de constante, ya que una constante puede ser vista como una función que siempre devuelve
un mismo valor.

Ejemplo: La función dos(x) = 2 siempre devuelve 2 con independencia del valor de x.

Dado que en programación funcional no existe el concepto de variable, no existe el concepto de asignación y en
consecuencia desaparece el concepto de bucle, que deben ser resueltos mediante recursión.

Otra consecuencia de la ausencia del concepto de variable es la ausencia de un estado del cómputo. De esta forma,
el resultado de una función depende únicamente de los valores de sus argumentos, siendo independientes de cualquier
calculo que se haya realizado con anterioridad a la llamada a la función. Esta propiedad se conoce como transparencia
referencial.
En programación funcional, todas las funciones son transparentes referencialmente, lo que simplifica la semántica
de los programas. El entorno en el que los programas se ejecutan asocia nombres a valores que nunca van a variar a
lo largo del cómputo, lo que se denomina semántica de valor.

Dado que los resultados no dependen del orden en el que se evalúe el resto del programa, la paralelización del
cómputo se facilita bastante.

Las funciones pueden devolver funciones y, para mantener la coherencia, debería poder recibir funciones como
parámetros de entrada. Esto se expresa diciendo que las funciones son ciudadanos, valores, objetos o entidades de
primera clase.
Ejemplo: Se suponen las siguientes dos funciones:

| suma3 x = x + 3
| funCuadrado f x = f x * f x

suma3 toma un número y devuelve el resultado de sumar 3 a dicho número. funCuadrado toma una función f y un
valor x y devuelve el resultado de elevar al cuadrado el resultado de aplicar f a x. Por tanto, se puede hacer:

funCuadrado suma3 5

que devuelve 64.

El tipo de funciones que reciben otra función como parámetro de entrada se denominan funciones de orden superior.

3.2 EVALUACIÓN PEREZOSA


En los lenguajes imperativos tradicionales, cuando se realiza una llamada a una función, lo primero que se hace es
evaluar los parámetros de la misma. Esto se conoce como evaluación impaciente o estricta, y presenta un
inconveniente: si no posible evaluar en tiempo finito alguno de sus argumentos, la ejecución del programa podría
interrumpirse o prolongarse indefinidamente.

Ejemplo: Si se programa la siguiente función.

| int decide (boolean x, int y){


| if x return 1
| else return y
|}

y se realiza la llamada:
decide (true, 1/0)

Si la evaluación de los parámetros se realiza de forma estricta, la ejecución se detendrá con error. La función decide
no necesita conocer el valor del parámetro y para devolver el resultado, ya que como x es true, la función podría
devolver el valor 12 con independencia del valor que se haya pasado al parámetro y.

Esta función es no estricta, ya que no necesita conocer el valor de todos sus parámetros para devolver el resultado.
Todos los lenguajes modernos presentan una forma de evaluación no estricta para algunas de sus operaciones
conocida como evaluación en cortocircuito.
Gracias a la evaluación no estricta, se puede retrasar la evolución de alguna expresión hasta que su valor concreto
sea necesario para continuar el computo, lo que es útil si la evaluación de la expresión conlleva un coste importante.

Se acuñó el término memoización para referirse al proceso de almacenar el valor de una expresión la primera vez
que se evalúa, lo que se conoce como paso por necesidad.
Para evaluar un parámetro compuesto, como una lista, sería necesario retrasar todos aquellos pasos necesarios para
realizar esa evaluación al completo. De esta forma, la evaluación ira paso a paso y se podrá detener en el momento
en el que no sea necesario continuar.

La capacidad de retrasar la evaluación de los parámetros de las funciones junto con la memoización se conoce con
el nombre de evaluación perezosa, ya que se evalúa lo justo y necesario para seguir con el cálculo y solo lo hace una
vez.
La evaluación perezosa permite en muchas ocasiones realizar una programación separando las funciones:

- Productoras: Van produciendo resultados intermedios paso a paso.


- Consumidoras: Consumen los resultados de las productoras.
3.3 INTRODUCCIÓN AL LENGUAJE HASKELL
3.3.1 Tipos de datos predefinidos en Haskell

Haskell es un lenguaje sensible a mayúsculas y minúsculas (case sensitive). En concreto, los identificadores de
tipo siempre han de comenzar con una letra mayúscula, mientras que los identificadores de funciones y los de
variables comienzan con letras minúsculas.

- Tipos atómicos: Son aquellos tipos cuyos valores no pueden descomponerse en otros.

- Enteros: Hay dos tipos diferentes de enteros, que pertenecen a una misma clase denominada Integral.

- Int: Representa a los enteros ligados a la arquitectura del computador. Son muy eficientes, pero están
acotados.

- Integer: Representa el conjunto de los numero enteros sin ningún tipo de limite. Es menos eficiente
que Int, pero son muy útiles cuando se trabaja con algoritmos que maneje números muy grandes.

- Reales: Hay dos tipos que pertenecen a la clase RealFloat.

- Float: Reales de simple precisión.


- Double: Reales de doble precisión.

- Caracteres: Se implementan sobre ASCII-7 y su identificador de tipo es Char.

- Booleanos: Representan los valores True y False del algebra de Boole, siendo Bool su identificador.

- Tipos estructurados: Son aquellos tipos cuyos valores pueden descomponerse en otros más simples.

- Tupla: Es una secuencia finita de valores de tipos no necesariamente iguales. La forma de construir una
tupla es encerrar estos valores entre paréntesis y separarlos mediante comas.

La tupla (4, “vacío”, False) es del tipo (Int, [Char], Bool)

- Lista: Es una colección de valores de un mismo tipo. El tipo se expresa encerrando entre corchetes
cuadrados el tipo de sus elementos. Las listas de tipo [Char] representan cadenas de caracteres, que pueden
escribirse tanto encerradas entre comillas dobles (“lista”) como en forma de lista con todos sus caracteres
separados por comas ([‘l’, ‘i’, ‘s’, ‘t’, ‘a’]).
Las listas deben ser entendidas como una estructura recursiva con dos constructores básicos: la lista vacía
representada como [] y el constructor infijo (:) que añade un elemento a la cabeza de la lista y asocia a la
derecha.

[‘l’, ‘i’, ‘s’, ‘t’, ‘a’] es una forma abreviada de escribir ‘l’ : ‘i’ : ‘s’ : ‘t’ : ‘a’ : []

- Listas definidas por extensión: Se escriben todos sus componentes entre corchetes cuadrados.

Ejemplo:

- [1, 2, 3] lista formada por los numero 1, 2 y 3.


- [5…87] lista formada por los números del 5 al 87.
- [1, 3…10] lista formada por los números impares hasta el 10
- [10…] lista infinita que comienza en 10.
- [1, 3…] lista infinita formada por los impares consecutivos.
- Listas definidas por comprensión o intensión: Se describen los elementos que la componen en lugar
de detallar todos ellos.

3.3.2 Definición de funciones

- Identificadores vs operadores: Las funciones pueden definirse de dos maneras:

- Mediante un identificador de función: Debe comenzar obligatoriamente con una letra minúscula y a
continuación una secuencia de caracteres que pueden ser letras, dígitos, apostrofes o subrayados. Se utilizan
de forma prefija.

- Mediante un símbolo de operador, que es una secuencia de uno o más caracteres:

: ! # $ % & * + . / < = > ? @ \ ^ | -


Salvo las siguientes combinaciones:

:: = .. @ \ | -> <- =>

Se utilizan de forma infija.

Al trabajar con funciones definidas mediante operadores hay que tener en cuenta dos factores:

- Precedencia: La aplicación de funciones definidas con un identificador tiene más precedencia


que cualquier símbolo de operador.

- Asociatividad: Se dice que un operador cualquiera representado por #:

- Asocia a la izquierda: Si x # y # z se debe interpretar como x # (y # z).


- Asocia a la derecha: Si x # y # z se debe interpretar como (x # y) # z.
- No asocia: Si no se admite una expresión como x # y # z.

Haskell permite definir las reglas de precedencia y asociatividad de los operadores mediante una
declaración utilizando la siguiente sintaxis:

asociatividad precedencia lista_de_operadores

donde:

- lista-de-operadores es una lista de símbolos de operador separados por comas.

- precedencia es un numero entre 0 y 9 representando la precedencia de los operadores de la


lista.

- asociatividad: Ha de ser:

- infixr: Indica que los operadores de la lista asocian a la derecha.


- infixl: Indica que los operadores de la lista asocian a la izquierda.
- infix: Indica que los operadores de la lista no asocian.

- Definición ecuacional y encaje de patrones.

La definición de una función se realiza mediante una serie de ecuaciones con el siguiente formato:

identificador <patron1> <patron2> … <patronn> = <expresión>


donde cada expresión <patron1> a <patronn> representa uno de los n (aridad de la función) argumentos de
entrada de la función y se denomina patrón.
Si una función se debe definir con más de una ecuación:

- Todas las ecuaciones deben definirse juntas.


- Todas las ecuaciones deben tener la misma aridad.
- Solo se aplicará la definición de una de las ecuaciones.

La forma de selecciona que definición se va aplicar se denomina encaje de patrones, donde se evalúan los
argumentos de la función que sean necesarios para comprobar si los valores de dichos argumentos pueden
encajar con los patrones de las diferentes ecuaciones.

La evaluación perezosa es la encargada de evaluar los argumentos hasta que es posible determinar si encaja
con el patrón de una de las ecuaciones. Si un argumento encajase con el patrón de dos o más ecuaciones,
se aplicará siempre la primera de ellas según el orden textual en el que se hayan escrito en el programa.
En caso contrario se detendría la ejecución del programa.

Los patrones básicos que pueden ser utilizados en la definición de las funciones son:

- Patrones constantes: Representa un dato de cualquier tipo de datos y solo encaja con un argumento
de entrada que coincida exactamente con dicha constante.

Ejemplo:

| f 1 = True
| f 2 = False

f toma un valor entero y devuelve True si dicho valor es 1 y False si el valor es 2. Para cualquier otro
valor de entrada, se generaría un error indicando que no se puede calcular el valor de la función.

- Patrones variable: Es el tipo más básico de patrón que encaja con cualquier argumento de entrada.
Una variable se representa mediante un identificador de variable y puede ser referenciado dentro de la
definición de función.

Ejemplo:

| suma x y = x + y

donde x e y son patrones variable que encajarían con cualquier valor que se pase a la función siempre
que sean de los tipos adecuados.

- Patrones anónimos: Se representan mediante el símbolo _ y encajan con cualquier argumento de


entrada con independencia de su tipo. Su uso es útil cuando no es necesario conocer el valor de uno
de los argumentos de la función para devolver el resultado.

Ejemplo:

| siempre2 x = 2

no necesita conocer el valor de x, por lo que podría ser reescrita asi:

|siempre2 _ = 2
- Patrones para listas: Cuando se quiere trabajar con listas, se pueden utilizar los siguientes tipos de
patrones:

- []: Es una lista vacía.


- [x]: Encaja con una lista de un único elemento al cual se referencia como x.
- (x:xs): Encaja con una lista de al menos un elemento referenciado como x y una cola
referenciada como xs.

Ejemplo: Función que suma todos los elementos de una lista:

| suma [] = 0
| suma (x:xs) = x + suma xs

- Patrones para tuplas: La forma de definir patrones es referenciar todos los elementos de la tupla
encerrados entre paréntesis.

(x, y, z)

representa una tupla formada por tres elementos los cuales se referencian como x, y, y z
respectivamente, mientras que:

(a, _ )

representa una tupla de dos elementos, el primero de los cuales se referencia como a y cuyo
segundo elemento encajara con cualquier valor de cualquier tipo.

- Patrones con nombre: Todo patrón no anónimo puede ser nombrado para referenciar el argumento
completo

Ejemplo: Se quiere definir una función que reciba una lista y devuelva la misma lista, pero con el
primer elemento duplicado.

| duplicaCabeza (x:xs) = x:x:xs

sin embargo, es posible renombrar todo el patrón x:xs con un nombre y referenciarlo con ese nombre.

| duplicaCabeza a@ (x:xs) = x:a

- Patrones aritméticos: Se utilizan para valores enteros y tiene la forma:

(n + k)

donde k es una constante natural. Este patrón solo encajara con un número entero mayor o igual que
k, asociando a la variable n el valor de dicho número menos k.

- Expresiones case.

Es posible utilizar patrones en cualquier punto de una expresión utilizando una expresión case, que emplea
una sintaxis muy similar a las sentencias case presentes en múltiples lenguajes de programación:

case expresión of
patron1 -> resultado1
patron2 -> resultado2

patronn -> resultadon
- Funciones definidas a trozos: guardas.

Si solo se pudiera definir una función a base de ecuaciones mediante encaje de patrones, no sería posible
definir una función como el valor absoluto:

x si x >= 0
|x|
-x si x < 0

Para distinguir si el valor de dicha variable es positivo o negativo, se pueden definir guardas dentro de una
función de la siguiente forma:

identificador <lista_de_patrones>
| guarda1 -> expresion1

| guardan -> expresión

Cada línea conteniendo una guarda debe presentar una indentación inicial con respecto a la línea de
definición de la función, para que no se confundan con la definición de otra función.
Cada guarda es una expresión de tipo Bool y las expresiones han de ser del mismo tipo, que será el tipo del
calor devuelto por la función. a la hora de evaluar la función, se evalúan las guardas por orden textual y la
primera que devuelva True será la que se aplique.

Ejemplo: La función valor absoluto puede definirse como:

abs x
| x >= 0 = x
| x < 0 = -x

Es posible utilizar la constante otherwise como una guarda para indicar que esa condición deberá ser
utilizada si no se hubiera cumplido ninguna otra guarda previa.

Ejemplo: La función valor absoluto se puede reescribir como:

abs x
| x >= 0 = x
| otherwise = -x

- Definiciones locales.

Es posible definir subfunciones dentro de una función. Al igual que con las guardas, deben estar indentadas
y si se definen varias subfunciones, todas ellas han de conservar la misma indentación. la forma de definir
subfunciones es la siguiente:

definición_de_la_funcion
where
subfuncion1

subfuncionn

Las subfunciones solo pueden aparecer al final de una definición de función y solo pueden ser utilizadas
dentro del cuerpo de esa función.
Ejemplo:

f x y = (a + 1) * (c – 1)
where
a = div (x + y) 2
c = mod (x + y) 2

al igual que las expresiones case permiten utilizar patrones en cualquier punto de una expresión, es posible
utilizar definiciones locales dentro de cualquier expresión utilizando la sintaxis:

let definición in expresión

Ejemplo: La siguiente expresión:

let a x = x + 1 in a 100

devuelve 101.

- Funciones anónimas (expresiones lambda).

Permiten introducir definiciones de funciones dentro de cualquier expresión.

Ejemplo: La función:

| esPar x = mod x 2 == 0

que indica si un número es par o no.


Si se quiere utilizar esta función dentro de una expresión, pero sin tener que definirla se haría lo siguiente:

(\x -> mod x 2 == 0) 4

obteniendo como resultado True.

- La función error.

Al ser invocada, esta función detiene completamente la evaluación y muestra por pantalla la cadena de
caracteres que se le ha pasado como parámetro.

Ejemplo:

| divide a 0 = error “No se puede dividir por 0”


| divide a b = a / b

3.3.3 El tipo de las funciones

Las funciones también tienen un tipo que depende de los tipos de sus parámetros de entrada y de salida. El tipo
de las funciones puede definirse usando la siguiente sintaxis:

identificador :: expresionDeTipo

donde expresionDeTipo es una lista de los tipos de todos los parámetros de entrada y el tipo devuelto por la
función separados todos ellos por operadores ->.

Aunque no es necesario definir explícitamente el tipo de una función, si se desea incluir la definición de tipo,
esta deberá situarse siempre antes de cualquier definición ecuacional de la función.
Para cualquier función, el intérprete de Haskell permite averiguar el tipo inferido para una función de la siguiente
forma:

:t identificador

- Polimorfismo.

En algunas ocasiones las funciones tienen sentido para varios tipos de datos. Las funciones que para
cualquier parámetro de entrada x, devuelve, precisamente x, se denominan funciones polimórficas. Para
representar su tipo, Haskell introduce las variables de tipo, que denotan cualquier tipo de datos. Asi, la
función identidad, id, se escribe como:

id :: a -> a

Si alguno de los tipos solo admitiera a una clase de tipos, se incluyen esas restricciones al comienzo de la
expresión de tipo separándolas de la lista de tipos de los parámetros por el símbolo =>.

Ejemplo: El operador + tiene el siguiente tipo:

(+) :: Num a => a -> a -> a

Para cualquier tipo a perteneciente a la clase Num, + recibe dos parámetros de dicho tipo y devuelve un
resultado del mismo tipo.

- Currificación de funciones y aplicación parcial.

Las funciones en Haskell se pueden currificar ya que toda función f con n parámetros de entrada es, en
realidad, una función f´ con un único parámetro de entrada que devuelve una función con n – 1 parámetros
de entrada.
Si se tienen más de dos parámetros de entrada, es posible generalizar la currificación de las funciones
entendiendo que toda función con n parámetros de entrada es una función con m parámetros de entrada
(siendo m < n), cuya definición es la misma que la función original, pero sus n – m parámetros se tratan
como constantes

Ejemplo:

| suma3 :: Float -> Float


| suma3 x = suma 3 x

lo que puede abreviarse como:

| suma3 :: Float -> Float


| suma 3 = suma 3

Las funciones definidas como operadores también pueden beneficiarse de la aplicación parcia, en cuyo
caso se denominan secciones de un operador. Para usar un operador con uno de sus argumentos es necesario
recordar que todo operador binario definido de forma infija puede ser utilizado de forma prefija sin más
que encerrar su símbolo de operador entre paréntesis. Así las secciones del operador / serian:

- (/): Toma dos valores y los divide.


- (/ 5): Toma un valor y lo divide por 5.
- (5 /): Toma un valor y devuelve el resultado de dividir 5 entre dicho valor.
- Funciones de orden superior.

Es una función que tiene alguna función entre sus parámetros de entrada.

Ejemplo: La función:

aplica f x y = f x y

es una función que recoge tres parámetros de entrada y devuelve el resultado de aplicar el primero de ellos
al segundo y al tercero.
Como hasta ahora no se tiene ninguna información sobre qué tipos han de tener dichos parámetros, la única
información que se tiene es que el tipo más genérico para la función f es:

f :: a -> b -> c

El primer parámetro de la función f es el segundo de aplica, al igual que el segundo de f es el tercero de


aplica. Llamando x al tipo de la función f, se tiene que:

aplica :: x -> a -> b -> c

Por último, se sustituye x por el tipo de f, pero por ser una función, y dado que el operador -> asocia por la
derecha, se encierra entre paréntesis para indicar que es un único parámetro de entrada y no tres:

aplica :: (a -> b -> c) -> a -> b -> c

que se debe leer como que aplica recibe una función f, la cual necesita dos parámetros de entrada de los
tipos a y b y devuelve y valor del tipo c, y dos valores a y b para devolver finalmente un valor del tipo c.

3.3.4 Tipos de datos avanzados

- El valor indefinido: Bottom.

Se denota como ⊥ y representa el valor que tienen aquellas expresiones cuya evaluación produce un error
o no termina.

- Sinónimos de tipos: Type.

Consiste en la definición de un nuevo nombre para un tipo ya existente, sin que en realidad se cree un
nuevo tipo de datos, ya que ambos nombres representarán el mismo tipo de datos y a todos los efectos serán
totalmente equivalentes.

Ejemplo:

| type Carácter = Char


| type String = [Char]

En la primera definición se asigna el nombre Carácter al tipo de datos predefinido Char, mientras que en la
definición de la línea 2 se indica que el tipo String es una lista de elementos tipo Char. Asi se puede
reescribir como:

| type Carácter = Char


| type String = [Carácter]
Ejemplo: Definición de un tipo de datos para almacenar números complejos en forma de tupla de dos
números reales:

| type Complex = (Double, Double)


| a :: Complex
| a = (3.4, 2.5)

- Definición de nuevos tipos de datos: data.

- Tipos enumerados: Consisten en una enumeración de todos los valores que pertenecen a dicho tipo, por
tanto, han de tener una cantidad finita de elementos.

Ejemplo: Tipo enumerado para almacenar las luces de un semáforo:

| data Semáforo = Rojo | Amarillo | Verde


| deriving (Eq, Ord, Show, Read)

Mediante el uso de la palabra reservada deriving, Haskell permite indicar que el nuevo tipo deriva de alguna
de las clases de tipos ya existentes y, de este modo, sobrecargar operaciones ya definidas en esas clases de
forma que puedan ser utilizadas en el nuevo tipo de datos. Las clases de tipos más usadas son:

- Eq: Si un tipo deriva de esta clase es posible comparar valores de dicho tipo mediante los operadores
de igualdad.

- Ord: Que un tipo derive de esta clase implica que se induce un orden en los valores del tipo.

- Show: Permite que el intérprete pueda mostrar un valor de ese tipo. Si un tipo deriva de esta clase,
el nombre del valor será la representación textual que el intérprete mostrará por pantalla.

- Read: Permite el paso inverso al de la clase Show. Convierte cadenas de caracteres en elementos del
tipo.

Ejemplo: El tipo de datos Bool se define como un tipo enumerado.

| data Bool = False | True

A la hora de utilizar un tipo de datos enumerado en una función, se pueden emplear tanto, patrones
constantes:

| not :: Bool -> Bool


| not True = False
| not False = True

como variables o indefinidos:

| (&&), (||) :: Bool -> Bool -> Bool


| False && _ = False
| True && x = x
| False || x = x
| True && _ True
- Unión de tipos: Permite agrupar en un único tipo elementos de dos tipos diferentes:

Ejemplo:

| data BooleanoEntero = Booleano Bool | Entero Integer

Asi, sería posible definir una lista de elementos del tipo BooleanoEntero, salvando la restricción de que las listas
solo pueden tener elementos del mismo tipo.

| listaMixta :: [BooleanoEntero]
| listaMixta = [Entero 0, Booleano True, Booleano False, Entero -1]

Cuando se quiera utilizar un tipo de datos que sea unión de tipos, en el patrón se deberá incluir el identificador
de tipo de datos que se pretenda utilizar en cada momento.

| negación :: BooleanoEntero -> BooleanoEntero


| negación (Entero x) = Entero (-x)
| negación (Booleano x) = Booleano (not x)

- Producto cartesiano: Los elementos de un tipo construido mediante el producto cartesiano están formados por
tantas componentes como tipos se estén incluyendo en el producto.

Ejemplo: Es posible definir un tipo de datos Asignatura para almacenar la información sobre el nombre de una
asignatura, el curso en el que se imparte y la dirección de su página web:

| data Asignatura = Asig String Integer String

El constructor de tipo (Asig) puede considerarse como una función que recibe los valores de cada uno de los
tipos que intervienen en el producto cartesiano y devuelve un valor del tipo producto.

Asig :: String -> Integer -> String -> Asignatura

Cuando se utiliza un tipo de datos producto en un patrón, será necesario encerrar todo el patrón entre paréntesis
y se deberá incluir el constructor de tipo.

Ejemplo:

| esDePrimero (Asig _ x _) = x == 1

Recibe un valor de tipo Asignatura y devuelve un valor de tipo Bool que indica si dicha asignatura es de primer
curso o no.

data también ofrece la posibilidad de nombrar cada una de las componentes del tipo utilizando la siguiente
notación:

| data Asignatura = Asig {nombre :: String, curso :: Integer, web :: String}

Esta notación es más cercana a los tipos de datos registro comúnmente presentes en lenguajes imperativos,
obteniendo una serie de ventajas.

- Los datos pueden ser definidos sombrando los campos.

Ejemplo:

| tlp = Asig {nombre = “Teoría de los lenguajes de programación”, curso = 2, web =


|“http://www.lsi.uned.es/tlp”}
- Haskell crea automáticamente una serie de funciones que permiten acceder a cada una de las componentes
directamente.

Ejemplo:

nombre :: Asignatura -> String


curso :: Asignatura -> Integer
web :: Asignatura -> String

lo que permite reescribir la función esDePrimero como:

| esDePrimero x = curso x == 1

- Es posible crear nuevos datos a partir de otros ya existentes tan solo modificando algunas de sus
componentes:

Ejemplo:

| lpp = tlp {nombre = “Lenguajes de programación y procesadores”, web = “http://www.lsi.uned.es/lpp”}

- Tipos de datos recursivos: Un tipo de datos será recursivo siempre que el tipo que está siendo definido aparece
en su propia definición. El típico ejemplo es la definición de los naturales mediante inducción.

Ejemplo:

El cero es natural.
Si n es natural, entonces su sucesor también lo es.

Cero :: Natural
Sucesor :: Natural -> Natural
| data Natural = Cero | Sucesor Natural

Por lo tanto, se puede escribir:

| uno = Sucesor Cero


| dos = Sucesor uno
| tres = Sucesor dos

Si el tipo de datos Natural derivase de las clases Eq y Ord, se podría escribir:

| mayor (Sucesor _) Cero = True


| mayor Cero _ = Falso
| mayor (Sucesor x) (Sucesor y) = mayor x y

- Tipos polimórficos: Son aquellos que pueden contener valores de cualquier tipo. El tipo genérico es:

| data TipoMixto a b = Tipo1 a | Tipo2 b

Ejemplo: El tipo BooleanoEntero se escribiría como:

| type BooleanoEntero = TipoMixto Bool Integer


3.3.5 Ejemplos de funciones: Trabajando con listas

- Contar los elementos de una lista.

En Haskell esta función viene predefinida como length. La forma de programar esta función sería:

- Una lista vacía no tiene elementos.


- Una lista que tiene, al menos, un elemento, tiene un elemento más que su propia cola.

Asi pues, el código de la función seria:

| length [ ] = 0
| length (x:xs) = 1 + length xs

- Concatenar listas.

Dadas dos listas xs e ys, se desea obtener una lista formada por la concatenación de ambas de forma que
primero aparezcan los elementos de xs y después los de ys. Esto se realiza mediante la función (++).

- Si la primera de las listas es vacía, entonces el resultado de la concatenación será la segunda lista.
- Si la primera lista tiene un primer elemento, entonces ese elemento será el primero de la lista
concatenación, mientras que la cola concatenación será la concatenación de la cola de la primera lista
con la segunda lista.

El código de (++) es:

| [ ] ++ ys = ys
| (x:xs) ++ ys = x : (xs ++ ys)

Una extensión es la concatenación de una lista de listas, y se realiza mediante la función concat. La forma
de realizar esta función es:

- El caso en el que la lista de listas es vacía, la concatenación de todas sus listas también será vacía.
- Si la lista de listas no es vacía, entonces tiene una primera lista xs y una lista de listas yss como cola.
Por lo tanto, el resultado de la concatenación de xs y las listas que forman parte de la lista de listas yss
será concatenar xs con el resultado de concatenar todas las listas de yss.

Siendo el código de concat el siguiente:

| concat [ ] = [ ]
| concat (xs:yss) = xs ++ (concat yss)

- Combinar listas.

Se realiza mediante la función zip, con el siguiente funcionamiento:

- Si las dos listas tienen un primer elemento, el primer elemento de la lista resultante seria la tupla
formada por ambos elementos, mientras que la cola de la lista resultante será el resultado de aplicar
zip a las colas de ambas listas.

- Si alguna de las listas no tuviera primer elemento, entonces el resultado debería ser la lista vacía.

Asi el código de zip es:

| zip (x:xs) (y:ys) = (x,y) : (zip xs ys)


| zip _ _ = [ ]
La longitud de la lista resultado siempre será la longitud de la más pequeña de las listas de entrada.

Ahora es posible hacer justo lo contrario: partiendo de una lista de tuplas, separarla en dos listas, la primera
con los primeros elementos de las tuplas y la segunda con los segundos. De esto se encarga la función
unzip, que tiene el siguiente funcionamiento:

- Si la lista de tuplas fuese vacía, entonces se deberá devolver la tupla compuesta por dos listas vacías.

- Si la lista de tuplas no es vacía, se tendrá una cabeza de lista formada por la tupla (a,b) y un resto de
lista xs, pero no se tiene acceso a las listas que forman los primeros y segundos elementos de las tuplas
de xs. Para ello se introducen parámetros acumuladores que actuaran de forma similar a como lo harían
las variables en un programa imperativo. Teniendo esto en cuenta, el proceso será el siguiente:

1. Se crea una función unzipAux que reciba dos parámetros: la lista de entrada y un parámetro
acumulador en el que se ira construyendo la salida.

2. Si la lista de entrada no es vacía, entonces tiene una cabeza (a,b) y un resto xs. Se tiene una
tupla cuyo primer elemento es la lista (as) de los primeros elementos de salida y cuyo segundo
elemento (bs) es la lista de los segundos elementos de la salida. Para construir el resultado será
necesario volver a aplicar unzipAux a la cola de la lista de entrada (xs) y al resultado de modificar
adecuadamente el acumulador concatenando la lista as con [a] y la lista bs con [b].

3. La función unzip debe llamar a unzipAux dando el acumulador un valor inicial que permita ir
construyendo este parámetro de forma adecuada.

El código es el siguiente:

| unzip xs = unzipAux xs ([ ], [ ])
| where
| unzipAux [ ] acumulador = acumulador
| unzipAux [ ] ((a,b) : xs) (as, bs) = unzipAux xs (as ++ [a], bs ++ [b])

- Aplicar una función a los elementos de una lista.

Esto lo realiza la función map devolviendo una nueva lista de la misma longitud que xs, pero conteniendo
los resultados de aplicar f a los respectivos elementos de xs. La construcción se justifica como:

- Aplicar map sobre una lista vacía produce una lista vacía.
- Si la lista tiene un primer elemento x, se aplicará la función f a x y el resultado será el primer elemento
de la lista resultado, mientras que su cola será el resultado de volver a aplicar map a f y a la cola de la
lista de entrada.

Por tanto, el código es:

| map _ [ ] = [ ]
| map f (x:xs) = (f x) : (map f xs)

Una extensión de map seria aplicar una función con dos parámetros de entrada a dos listas, para producir
una tercera lista cuyo primer elemento sería el resultado de aplicar la función a los primeros elementos de
ambas listas, el segundo sería el resultado de aplicar la función a los segundos elementos de ambas listas y
asi sucesivamente. Esta función se llama zipWith y su construcción es la siguiente:

- Si se quiere aplicar la función f a dos listas cuyos primeros elementos son a y b respectivamente (y
sus colas son as y bs), entonces el primer elemento del resultado será f a b y la cola será el resultado
de aplicar zipWith a f y a las colas de las listas de entrada.
- Si alguna de las listas no tuviera al menos un elemento, entonces el resultado debería ser la lista
vacía.

Por lo que el código de zipWith queda:

| zipWith f (a:as) (b:bs) = (f a b) : (zipWith f as bs)


| zipWith _ _ _ = [ ]

Una variante seria generar una lista de forma que cada elemento sea la aplicación de una determinada
función sobre el elemento precedente. De esto se encarga la función iterate, cuya construcción es:

- La función necesita de una función f y un primer elemento x.


- La cabeza de la lista resultado será x y la cola de dicha lista será una lista cuyo primer elemento
deberá ser f x y cada siguiente elemento se obtiene aplicando f al anterior.

Por tanto, el código de la función es:

| iterate f x = x : (iterate f (f x ))

- Filtros sobre listas.

Filtrar sirve para producir una nueva lista cuyos elementos pertenezcan a la lista original y que además
cumplan una cierta propiedad. Haskell ofrece las funciones take y drop que permiten coger o descartar los
n primeros elementos de una lista. Su funcionamiento se basa en:

- Si la lista es vacía, la lista resultante también.

- Si se quieren coger o descartar 0 o menos elementos de cualquier lista, se presenta otro caso base.
En el caso de take, el resultado deberá ser una lista vacía. En el caso de drop, el resultado será
precisamente la lista de entrada.

- Si la lista tiene al menos un elemento y el número de elementos que se desea coger o descartar es 1
o más, el resultado será coger o descartar ese primer elemento y aplicar recursivamente la función
correspondiente, decrementando en 1 el número de elementos a coger o descartar sobre la cola de la
lista.

El código de estas funciones es:

| take _ [ ] = [ ]
| take n (x:xs)
| | n <= 0 = [ ]
| | otherwise = x : take (n – 1) xs
| drop _ [ ] = [ ]
| drop n (x:xs)
| | n <= 0 = (x:xs)
| | otherwise = x : drop (n – 1) xs

También se puede considerar como es posible filtrar una lista atendiendo a alguna propiedad de sus
elementos. Esto lo hace la función filter:

| filter _ [ ] = [ ]
| filter p (x:xs)
| | p x = x : (filter p xs)
| | otherwise = filter p xs
Otras dos funciones bastante comunes permiten coger o descartar los primeros elementos de una lista que
cumplan cierta propiedad. Estas son takeWhile y dropWhile y su funcionamiento es:

- Si la lista sobre la que operan es vacía, ambas devolverán la lista vacía.

- Si la lista tiene un primer elemento, las funciones tendrán primero que comprobar si el elemento
cumple la propiedad para cogerlo o descartarlo.

- Si el elemento no cumpliera la propiedad, takeWhile devolvería directamente la lista vacía mientras


que dropWhile devolvería la lista completa.

El código de estas funciones es:

| takeWhile _ [ ] = [ ]
| takeWhile p (x:xs)
| | p x = x : takeWhile p xs
| | otherwise = [ ]
| dropWhile _ [ ] = [ ]
| dropWhile p (x:xs)
| | p x = x : dropWhile p xs
| | otherwise = (x:xs)

- Listas por comprensión.

La estructura general es la siguiente:

[expresión | cualificador1, …, cualificadorn]

Donde:

- expresión: Define la forma de los elementos de la lista resultado.

- cualificador: Define propiedades de los elementos y están separados por comas. Pueden ser de 3
tipos:

- generadores: Son expresiones que permiten generar una lista.

- filtros: Son expresiones booleanas utilizadas para filtrar aquellos elementos de un generador que
cumplen una determinada condición.

- Definiciones locales: Se utilizan para definir elementos locales dentro de la definición de la


lista. Se introducen mediante la palabra reservada let y quedan definidas para todos los
cualificadores situados a su derecha.

- Plegado de listas.

| función [ ] = caso_base
| función (x:xs) = combinar x (función xs)

Por lo tanto, es posible escribir una función de orden superior que aplique este esquema a una función
combinar y a un valor trivial caso_base dados. Esta función se denomina foldr y su definición es:

| foldr f z [ ] = z
| foldr f z (x:xs) = f x (foldr f z xs)
Esta función pliega una lista de derecha a izquierda, ya que el valor trivial se devuelve al ser aplicada a la
lista vacía. En algunas ocasiones es posible que se necesite plegar la lista del revés, de izquierda a derecha.
Para ello existe la función foldl, que define como:

| foldl f z [ ] = z
| foldl f z (x:xs) = foldl f (f z x) xs

El resultado devuelto por ambas funciones al aplicarse sobre los mismos parámetros de entrada coincidirá
siempre que se cumplan las siguientes condiciones:

- La función f es asociativa, por lo que será posible cambiar el orden de aplicación de los paréntesis.
- El valor z es un elemento neutro de f, por lo que el orden en el que sea evaluado no influye.
- La lista de entrada es finita.

TEMA 4: PROGRAMACIÓN LÓGICA


Prolog es una herramienta practica de programación lógica que proporciona un entorno potente y flexible a partir de
los mecanismos básicos de unificación y de resolución con una estrategia predefinida para la gestión de fórmulas
que son clausulas y retroceso automático. Las principales características de la programación Prolog son:

- Su semántica declarativa permite al programador orientar sus trabajos hacia “cuál es el problema” y su
semántica procedimental permite utilizar características de control de la ejecución para conseguir mayor
eficiencia.

- Es un lenguaje de muy alto nivel ya que es un lenguaje de especificación lógica que es directamente
computable. Una comparación entre la programación declarativa y la programación imperativa es la siguiente:

Programación
Característica Programación declarativa
imperativa
Como resolver un
Idea básica Qué se conoce del problema
problema
Nivel de abstracción Limitado Elevado
Semántica Básica Bien formalizada
Variables Asignación Unificación
Variables locales Si Si, todas
Variables globales Si No, aunque simulables
Estructura de datos Arrays Listas
No en programación lógica (PL)
Tipado de datos Si
Si en programación funcional (PF)
Secuencial, Bifurcación Sin orden, Bifurcación por unificación,
Estructuras de control
Iteración, Recursión Recursión
Combinación con otros
Si Si
paradigmas
Si. En PL es el modelo de Horn.
Semántica declarativa Difícil de construir
En PF es el modelo inicial (algebra)
PL: SLD-resolución y unificación
Semántica operacional Menos difícil de aportar
PF: Reescritura y emparejamiento

Las características más importantes propias a Prolog que no aparecen en otros lenguajes son:

- Los programas y datos se representan mediante clausulas definidas.


- La unificación es una equiparación incluida en el sistema.
- Prolog incluye un mecanismo de inferencia automática con vuelta atrás.
- Permite un estilo no determinista de programación.
- Soporta la recursión y maneja estructuras de datos.
El proceso de inferencia es complejo; las inferencias se efectúan con una estrategia dirigida por el objetivo, primera
en profundidad y con vuelta atrás.

El proceso de paso de parámetros en Prolog es general, en dos direcciones y se basa en la unificación como operación
de comparación.

La gran ventaja que supone Prolog en la programación se debe a su semántica declarativa. Actualmente los entornos
Prolog disponibles ya incorporan mecanismos basados en la compilación o técnicas de paralelismo, para conseguir
una mayor eficiencia computacional, permitiendo la programación de problemas interesantes con una programación
declarativa y poco convencional.

4.1 ESPECIFICACIÓN DE PROGRAMAS


El lenguaje de Prolog está constituido por símbolos lógicos y símbolos no lógicos para representar explícita o
implícitamente los objetos y sus particularidades en el dominio del problema.

Los elementos básicos de formulación de problemas son:

- Términos: Representan objetos.


- Átomos: Representan propiedades o relaciones entre objetos que se construyen con símbolos no lógicos.

La combinación de relaciones entre objetos y sus propiedades mediante los símbolos lógicos da lugar a las formulas
o clausulas.

Se llaman marco conceptual a la descripción de los símbolos no lógicos necesarios para la formulación de un
problema. Los elementos que aparecen en un problema se formulan mediante los términos definidos por:

- Todo símbolo de variable es un término, como: X, Y, Variable, Var_1.


- Todo símbolo de constante es un término, como: manuel, francisco, m30, 1_5.
- Todo símbolo de función con argumentos que son términos es un término, como: mas (2, 3).

Las propiedades y relaciones entre objetos se formulan con los literales, símbolo no lógico de predicado con n
argumentos que son términos. Hay tres tipos:

- Símbolo de predicado 0-ario: Permite la representación de hechos simples o proposicionales.


- Símbolos de predicado 1-ario: Permiten la representación de propiedades de objetos.
- Predicados con un numero de argumentos mayor que 1: Expresan relaciones entre objetos.

Ejemplo: Supóngase que se llega a un restaurante y se revisa la carta. En ella se encuentra la información de la
siguiente tabla:

Paella
Entrantes Gazpacho
Potaje
Besugo
Pescados
Bacalao
Filete
Carnes
Pollo
Tarta Helada
Postres Nueces con miel
Naranja

Para formular este conocimiento se utilizan los siguientes símbolos no lógicos que forman el marco conceptual:

- Constantes: Representan los objetos concretos y bien conocidos del dominio y son secuencias de letras sin el
carácter blanco y que comienzan por una letra minúscula.
| paella, gazpacho, potaje, besugo, bacalao, filete, pollo, tarta_helada, nueces_con_miel, naranja

- Predicados: Representan relaciones o propiedades que permiten clasificar y discriminar los diferentes tipos
de objetos del problema y deben comenzar con una letra minúscula y las variables con mayúscula.

entrante(X) (X es un plato de entrante)


plato_de_pescado(X) (X es un plato de pescado)
plato_de_carne(X) (X es un plato de carne)
postre(X) (X es un plato de postre)
menu (X, Y, Z) (Los platos X, Y, Z forman un menu)

4.1.1 Reglas y programas

A partir de la representación formal de los elementos y sus propiedades y relaciones, es necesario disponer de
estructuras de representación más complejas. Para ello se utilizan los símbolos lógicos del lenguaje que permiten
combinaciones de literales, denominadas reglas o Clausulas de Horn. Las reglas Prolog están formadas por una
cabeza, parte izquierda o conclusión de la regla, y un cuerpo, parte derecha o condiciones de la regla, separadas
ambas partes por el símbolo :- y acabando en un punto.

La lectura declarativa de la regla:

Q :- P1, P2, …, PN

en la siguiente:

Si son ciertas las condiciones P1 y P2 y … y PN entonces es cierta la conclusión Q. Lo que corresponde


con la formula lógica P1 ^ P2 ^ … ^ PN → Q, o bien con (¬(P1) v ¬(P2) v … v ¬(PN) v Q).

También se denominan clausulas definidas, porque solo contienen un literal afirmado en su representación con
conectivas logicas. también puede asociarse un significado procedimental que se entiende por:

El objetivo Q se puede resolver si se puede resolver primero el objetivo P1 y a continuación el objetivo P2,
… y en último lugar el objetivo PN.

En cuanto al tipo de los argumentos objetivo, este se reconoce en el programa por su sintaxis:

- Variables: Empiezan en mayúscula.


- Constantes: Empiezan por minúscula o entre comillas simples.
- Números: Se incluyen los naturales y los reales.

Ejemplo: Siguiendo con el de la carta de un restaurante se formaliza el conocimiento relativo a los platos
ofrecidos y clasificados como se ha indicado, con las siguientes reglas Prolog.

% La carta

entrante (paella).
entrante (gazpacho).
entrante (potaje).

plato_de_carne (filete).
plato_de_carne (pollo).

plato_de_pescado (bacalao).
plato_de_pescado (besugo).
postre (tarta_helada).
postre (nueces_con_miel).
postre (naranja).

La línea 1 comienza con el símbolo % y construye un comentario.

Además, se sabe que “un plato de carne es un plato fuerte” y “un plato de pescado es un plato fuerte” lo cual se
expresa mediante:

plato_fuerte (X) :- plato_de_carne (X).


plato_fuerte (X) :- plato_de_pescado (X).

Cuya lectura es: “Para todo X si X es un plato de carne (o pescado) entonces X es un plato fuerte”.
A continuación, se define un menú como la composición de un entrante, un plato fuerte y un postre:

|menu (X, Y, Z) :- entrante (X), plato_fuerte (Y), postre (Z).

Lo cual debe ser entendido como “para cualquier tipo de valores X, Y, Z tal que el primero es un entrante, el
segundo un plato fuerte y el tercero es un postre, los tres forman un menu”.

Por tanto, el programa del ejemplo completo es:

% La carta

entrante (paella).
entrante (gazpacho).
entrante (potaje).

plato_de_carne (filete).
plato_de_carne (pollo).

plato_de_pescado (bacalao).
plato_de_pescado (besugo).

postre (tarta_helada).
postre (nueces_con_miel).
postre (naranja).

% Plato principal

plato_fuerte (P) :- plato_de_pescado (P).


plato_fuerte (P) :- plato_de_carne (P).

% Composición de una comida

menu (E, P, D) :- entrante (E), plato_fuerte (P), postre (D).

Las variables que aparecen en las reglas se han de asumir cuantificadas universalmente, es decir, representan a
cualquier termino u objeto y no es necesario que se declaren previamente. Las variables que aparecen en una
regla Prolog son locales a esa regla, es decir, son independientes de las de otras reglas y por lo tanto pueden
representar elementos diferentes.

p (X) :- a (X, Y), q (Y).


q (Y) :- c (Y).

La variable Y de la regla 1 no se corresponde en ningún aspecto con la variable Y de la regla de la línea 2.


En ningún momento se hace declaración de los valores posibles de los argumentos ni de su tipo.

Además de la conectiva lógica de conjunción, también es posible utilizar la disyunción en la parte derecha de la
regla, para hacer más concisa la programación. La parte de condiciones puede contener entre paréntesis la
disyunción de dos o más literales u objetivos separados por ; . Asi la regla p :- q ; r. se lee “p es cierto si q o r lo
son”, representando por lo tanto la formula lógica q v r → p.

Ejemplo: Volviendo al ejemplo de la carta del restaurante, la regla:

| menu (X, Y, Z) :- entrante (X), (plato_de_carne (Y) ; plato_de_pescado (Y)), postre (Z).

compacta a cualquiera de las siguientes alternativas:

menu (X, Y, Z) :- entrante (X), plato_de_carne (Y), postre (Z).


menu (X, Y, Z) :- entrante (X), plato_de_pescado (Y), postre (Z).

menu (X, Y, Z) :- entrante (X), plato_fuerte (Y), postre (Z).

plato_fuerte (X) :- plato_de_carne (X).


plato_fuerte (X) :- plato_de_pescado (Y).

En Prolog la coma es más prioritaria que el punto y coma por lo que la regla:

| p :- q , r ; s.

se entiende como: p si q o r o bien si s. Es decir (q ^ r) v s → p.

4.1.2 Extracción de respuestas

La obtención o extracción de conocimiento expresados en el programa se realiza a partir de una consulta, que
es una secuencia de literales u objetivos acabada en un punto y comienza con el símbolo ?-. Supone la ejecución
o petición de prueba en el entorno Prolog de la existencia de objetos que verifican la conjunción de los objetivos
de la consulta. Una consulta se puede resolver si se puede resolver cada objetivo de la misma.

Cuando se hace una consulta, Prolog responderá con todos los valores posibles.

El significado declarativo de los programas determina cuando un literal es verdadero o para que valores es
verdadero. Como las variables de una regla se entienden cuantificadas universalmente, cualquier especificación
de una variable de una regla en un objeto del dominio del problema da lugar a una formula llamada instancia de
la regla. Por lo tanto, una instancia de una regla es la regla resultado de sustituir todas las apariciones de una
variable X por un término cualquiera t.

Las variables Prolog son logicas y representan un elemento u objeto genérico en vez de una posición de
memoria.

De esta forma, una consulta es cierta en un programa cuando existe un hecho en el programa que es una instancia
de la consulta, o cuando existe una instancia de una regla del programa cuya cabeza es sintácticamente idéntica
a la consulta y son ciertos cada uno de los literales de las condiciones de la instancia.

4.1.3 Guía metodológica para la especificación

La programación en Prolog comienza por la representación de sus conocimientos en términos de literales y


clausulas o reglas y hechos.
Ejemplo: Especificación de un problema a partir de la representación de las relaciones de parentesco en una
familia. Se sabe que:

- Hay una serie de individuos que forman la familia.


- Que pueden ser hombre o mujeres.
- Que entre algunos de ellos se verifica la relación ser hijo o hija (hijo-ja).
- Dos personas son padres de una tercera si esta es hijo-ja de los dos (padres).
- Que un padre (madre) es un hombre (mujer) que verifica la relación padres.
- Que los abuelos son los que a su vez son padres de los de esa persona.
- Que un abuelo/a es un hombre/mujer que verifica la relación abuelos.
- Que alguien es nieto/nieta si es un hombre/mujer que tiene abuelos.

Y sea un caso concreto de familia la formada por el matrimonio Miguel y Estefanía que tienen dos hijos
Francisco y Gloria. Estos están casados con Teresa y Manuel respectivamente. Aránzazu y Bruno son hijos de
Teresa y Francisco, y Diego y Paula son hijos de Manuel y Gloria.

Una metodología aconsejada es ajustarse a las dos fases siguientes:

1. Descripción del Marco Conceptual: En esta fase hay que realizar lo siguiente:

- Descripción de los elementos u objetos del Dominio.


- Descripción de las propiedades de los objetos.
- Descripción de las posibles relaciones entre los objetos.

Ejemplo: En el de la familia se tiene:

- Dominio: {miguel, estefania, teresa, francisco, gloria, manuel, aranzazu, bruno, diego, paula}.

- Propiedades:

- hombre (X) (X es un hombre)


- mujer (X) (X es una mujer)

- Relaciones básicas:

- progenitores (X, Y, Z) (X e Y son los padres de Z)


- hijo_ja (X, Y) (X es hijo o hija de Y)
- hijo (X, Y) (X es hijo de Y)
- hija (X, Y) (X es hija de Y)
- padres (X, Y, Z) (X e Y son padres de Z)
- padre (X, Y) (X es el padre de Y)
- madre (X, Y) (X es la madre de Y)
- abuelos (X, Y, Z) (X e Y son los abuelos de Z)
- abuelo (X, Y) (X es el abuelo de Y)
- abuela (X, Y) (X es la abuela de Y)
- nieto (X, Y) (X es el nieto de Y)
- nieta (X, Y) (X es la nieta de Y)

2. Formulación: En esta fase los conocimientos deben representarse en términos de:

- Afirmaciones.
- Formulas que representan la interacción entre literales: conclusión si conjunción de objetivos.
Ejemplo:

% Afirmaciones

hombre (miguel).
hombre (francisco).
hombre (manuel).
hombre (bruno).
hombre (diego).

mujer (estefania).
mujer (teresa).
mujer (gloria).
mujer (aranzazu).
mujer (paula).

% Relaciones básicas

progenitores (miguel, estefania, francisco).


progenitores (miguel, estefania, gloria).
progenitores (francisco, teresa, aranzazu).
progenitores (francisco, teresa, bruno).
progenitores (gloria, manuel, diego).
progenitores (gloria manuel, paula).

% Relaciones causales

hijo_ja (X, Y) :- progenitores (Y, Z, X).


hijo_ja (X, Y) :- progenitores (Z, Y, X).
hijo (X, Y) :- hombre (X), hijo_ja (X, Y).
hija (X, Y) :- mujer (X), hijo_ja (X, Y).

padres (X, Y, Z) :- hijo_ja (X, Z), hijo_ja (Y, Z), dif (X, Y).
padre (X, Y) :- hombre (X), padres (X, Z, Y).
madre (X, Y) :- mujer (X), padres (X, Z, Y).

abuelos (X, Y, Z) :- padres (U, V, Z), (padres (X, Y, U) ; padres (X, Y, V)).
abuelo (X, Y) :- hombre (X), abuelos (X, Z, Y).
abuela (X, Y) :- mujer (X), abuelos (X, Z, Y).

nieto (X, Y) :- hombre (X), abuelo (Y, X).


nieto (X, Y) :- hombre (X), abuela (Y, X).
nieta (X, Y) :- mujer (X), abuelo (Y, X).
nieta (X, Y) :- mujer (X), abuela (Y, X).

La programación recursiva es uno de los pilares de la programación Prolog, por lo que una definición recursiva
es una definición correcta.

Ejemplo: La definición de antepasado es recursiva:

% antepasado (X, Y) significa que X es antepasado de Y

antepasado (X, Y) :- hijo_ja (Y, X).


antepasado (X, Y) :- hijo_ja (Y, Z), antepasado (X, Z).
Ejemplo: Calculo del factorial de un numero natural. Dicho cálculo se define por la función:

1 𝑠𝑖 𝑛 = 0
𝑓𝑎𝑐𝑡(𝑛) = { }
𝑛 ∗ 𝑓𝑎𝑐𝑡(𝑛 − 1) 𝑠𝑖 𝑛 > 0

Toda función puede ser representada en términos predicativos utilizando un nuevo símbolo de predicado con
un argumento más que la función, en el que se representara el resultado. Asi, la función fact se puede representar
con el predicado factorial X, Y) que indica que el número Y es factorial del número X.

En Prolog, la aritmética disponible se expresa mediante el predicado predefinido is, con notación infija, esto es
Z is X Operado_con Y, de forma que Operado_con representa a una función aritmética.
La aritmética se puede entender también en forma predicativa, es decir, Prolog “conoce” las tablas de las
operaciones sobre los números.

Ejemplo: El listado del programa del factorial es:

factorial (0, 1).


factorial (N, M) :- I is N – 1, factorial (I, A), M is N * A.

Ejemplo: El de los planetas. En él se conoce que la distancia entre el sol y algunos planetas es la siguiente, cuya
información se formaliza con las reglas distancia (Planeta, Distancia).

Planeta Millones de Millas


mercurio 36
venus 67
tierra 93
marte 141
júpiter 484
saturno 886
urano 1790
neptuno 2800
pluton 4600

También se sabe que algunos planetas tienen satélites son:

Planeta Numero de satélites


tierra 5
saturno 1
urano 9

Hechos que se formalizan con la regla satélite (Planeta, Numero).

Un conocimiento adicional es que un planeta puede tener vida si está al menos a 93 Mm del sol y tiene al menos
un satélite, lo que se formaliza en Prolog como:

| tiene_vida (Planeta) :- distancia (Planeta, D), D >= 93, satélite (Planeta, S), S > 0.

4.2 COMPUTACIÓN LÓGICA


Es el proceso que resuelve la consulta, o lista de objetivos, a partir del programa y se basa en la manipulación de las
reglas del programa de acuerdo con una estrategia predefinida y en la comparación de objetivos vía la unificación
para obtener las instancias que se identifiquen sintácticamente con las conclusiones.

Responder una consulta en un programa es determinar si la consulta es una consecuencia lógica del programa.
La programación lógica pura, si tiene propiedades matemáticas como la corrección, completitud, transparencia y no
existencia de efectos colaterales. La resolución SLD que utiliza Prolog es completa y correcta teóricamente, pero en
la practica la introducción de algunos aspectos de control hacen necesario definir la corrección de una forma
independiente.

El procedimiento de resolución de Prolog es en profundidad y con vuelta atrás, por lo que se obtienen todas las
soluciones de una consulta en su árbol de computación lineal.

4.2.1 Unificación

- Unificación simple.

Dos objetivos (átomos o literales positivos) se unifican si existe un unificador, formado por un conjunto de
pares <Variable, Término>, que los identifica sintácticamente.

- Dos literales u objetivos se unifican si tienen igual símbolo de predicado, igual número de
argumentos y estos son unificables uno a uno.

- Una variable se unifica con cualquier otro termino pasando a tomar ese valor.

- Una constante solo se unifica con cualquier otra constante idéntica sintácticamente.

- Dos términos compuestos se unifican si tienen igual símbolo de función, igual número de argumentos
y estos son unificables uno a uno.

- Unificación general: El algoritmo más conocido es el propuesto por Robinson:

- Entrada: A, B literales a unificar.

- Algoritmo:

Sea S = { } la sustitución vacía


MIENTRAS S (A) no sea S (B) HACER
Determinar símbolo más a la izquierda de S (A) que sea distinto de S (B) en la misma posición.
Calcular a partir de esos símbolos, los términos u1 y u2 mínimos que comienzan por ellos.

SI ni u1 ni u2 son variables o uno es una variable contenida en el otro


ENTONCES
acabar, pues A y B no son unificables.
SI NO
Determinar una variable x en u1 (o en u2) y el subtérmino b en la misma posición en
u2 (resp. en u1) y hacer S U {<x, b>}.
FIN SI
FIN MIENTRAS

- Salida: S es el unificador máxima generalidad (umg) de los literales A y B.

Se dice que una regla o clausula permite deducir un objetivo si existe una unificación que identifica este objetivo
con la conclusión de la regla o si existe una instancia de la regla del programa que infiere exactamente al
objetivo. Entonces el objetivo será resoluble si se pueden resolver cada una de las condiciones de la instancia
de la regla.
4.2.2 Algoritmo de resolución

- Inicio: La lista de objetivos es la que forma la consulta.

- Selección…

- De un objetivo a resolver: El primero más a la izquierda de la lista de objetivos actual.


- De una regla o clausula para resolver el objetivo seleccionado: La primera del programa cuya conclusión
o parte izquierda se unifica con el objetivo.

Una vez realizada la selección:

- El modelo de computación recuerda todas las reglas que podrían haber sido seleccionadas en este
momento.

- El objetivo es reemplazado en la lista de objetivos por la parte derecha de la regla seleccionada.

- Se aplica a la lista de objetivos resultante el unificador que hace posible unificar el objetivo resuelto
con la parte izquierda de la regla.

- Repetición: Se continua el proceso de selección hasta que se cumple una de las siguientes condiciones:

- Se vacía la lista de objetivos (ÉXITO) y se devuelve el unificador como respuesta.


- No puede ser resuelto un objetivo (FRACASO LÓGICO).

En ambos casos hay vuelta atrás al punto inmediatamente anterior en el que quedan reglas para seleccionar
y resolver el objeto.

Es muy útil simular el proceso de ejecución mediante un árbol de búsqueda, donde:

1. Cada nodo tiene asociada una etiqueta que indica la lista de objetivos a resolver.

2. Cada nodo tiene un nodo descendiente para cada una de las reglas que permiten unificar su parte
izquierda con el primer objetivo de la lista. La lista que etiquetará a estos nodos será la del nodo antecesor,
en la que se ha sustituido el primer objetivo por la parte derecha de la regla correspondiente. Prolog
construye el árbol y recorre todas sus ramas de izquierda a derecha y cada vez que acabe de recorrer una
rama volverá al nodo anterior con una rama alternativa sin haber sido visitada aún.

3. A cada conexión entre nodos del árbol, se asocia una etiqueta con la identificación de la regla
seleccionada para resolver el primer objetivo del nodo anterior y la unificación realizada.

El espacio de búsqueda de soluciones es completo, es decir, en el aparecen todas las opciones a seleccionar en
cada momento del proceso y Prolog lo recorre en su totalidad, luego una consulta puede responder con varias
soluciones.
El control automático de la computación se realiza en los puntos de selección:

- Al seleccionar un objetivo en la lista.


- Al seleccionar una regla que resuelva o unifique o resuelva su parte izquierda con el objetivo.

4.2.3 Control de la ejecución

El control es interno y no modificable. El programador solo puede cambiar el orden de las reglas o el orden de
los objetivos en la parte derecha de las reglas, pero nunca en el momento de ejecución. Sin embargo, hay algunos
mecanismos no lógicos que permiten la variación de la forma de ejecución.
El predicado predefino corte (!) permite reducir el espacio de búsqueda de un objetivo haciendo más eficaz la
ejecución. Permite suprimir el número de elecciones en las fases de selección facilitando asi la recuperación de
la memoria o bien eliminando ramas infinitas o alternativas que se sabe que no llevan a ninguna solución.

El corte elimina todas las opciones de la rama en que se encuentra a partir del nodo anterior a su aparición. Con
este predicado, pues, es posible implementar, por ejemplo, como “obtener la primera solución de…”.

Otro punto en el que es muy útil la utilización del corte aparece cuando los problemas son de solución única, y
se sabe a priori, por lo que no hace falta buscar más soluciones una vez que se ha obtenido la primera de ellas.

Sin embargo, el corte puede producir problemas no esperados y es conveniente utilizarlo con cuidado, aunque
es una de las mejores formas de conseguir programas efectivos y eficientes, al eliminar opciones en la ejecución
que desde el principio son conocidas como inútiles.

El predicado corte también permite escribir el programa general que define la negación por defecto: algo no es
cierto cuando no es posible demostrarlo vía resolución Prolog. O lo que es lo mismo, “no (X) es falso cuando
es posible resolver “X” y cierto en caso contrario”.

no (X) :- call (X), !, fallo.


no (X).

call (X) es un predicado predefinido en algunas implementaciones que permite parametrizar la resolución Prolog
del objetivo X (la variable “X” ha de estar unificada con un objetivo en el momento de su ejecución, daría error
en otro caso).

fallo es un predicado al que nunca se define en el programa y, por lo tanto, siempre es falso lógicamente. En
algunas implementaciones ya se encuentra implementada la negación con el predicado not (X), e incluso el
predicado fallo como fail por su utilidad en la programación.

4.2.4 Estructura de datos: La lista

La lista se comporta como un término funcional especial ya que dispone de un procedimiento específico para
unificación entre listas.
Una lista de elementos u objetos se representa con símbolos de inicio y fin de lista [ y ] y todos sus elementos
separados por comas. Recursivamente se define una lista como:

- La lista vacía (sin elementos) es una lista.


- Un elemento seguido de una lista es una lista.

Dos listas son unificables si es posible unificar sus primeros elementos (cabezas) y sus listas restantes (colas o
restos) son, a su vez, listas unificables. El tratamiento predefinido de las listas genéricas se realiza usando una
notación especial:

[Cabeza_lista | Cola_lista]

Ejemplo:

- [1, 2, 3] y [X | R] se unifican con el umg {<X, 1>, <R, [2, 3]>}.


- [1, 2 | R] y [X, Y, Z, U] se unifican con el umg {<X, 1>, <Y, 2>, <R, [Z, U]>}.
- [aranzazu, manuel, quico] y [paula] no se unifican.
- [aranzazu, [manuel, quico], [nadie_mas]] y [M, H, R] se unifican con el umg
{<M, aranzazu>, <H, [manuel, quico]>, <R, [nadie_mas]>}.

Según la definición recursiva de lista, acceder a los elementos de una lista es un proceso dividido en cómo
acceder al primer elemento y como acceder a los elementos del resto de la lista. Por tanto, el programa de cómo
acceder a un elemento es:
pertenece_a (X, [X | R]).
pertenece_a (X, [Y | R]) :- dif (X, Y), pertenece_a (X, R).

Otra operación útil entre listas es concatenar dos listas: dadas las listas L1 y L2 obtener la lista L3 que contenga
los elementos de L1 y L2 concatenados.

concatenar ([ ], L, L).
concatenar ([X | R1], L2, [X | R3]) :- concatenar (R1, L2, R3).

Otro problema relativo a las listas es como invertir una lista. Como este problema es de solución única, puede
optimizarse con el corte para evitar el desarrollo de nodos que no llevan a una solución en el espacio de
búsqueda. Para resolver este problema de forma distinta, se utiliza una lista auxiliar L2 donde se guardan la
parte de lista invertida hasta ese momento. Cuando se ha acabado de recorrer la lista, en L2 se tiene el resultado.
El programa completo es:

invertir ([ ], L, L).
invertir ([X | R], L2, L3) :- invertir (R, [X | L2], L3)

4.3 TÉCNICAS AVANZADAS DE PROGRAMACIÓN LÓGICA


4.3.1 Indeterminismo

El indeterminismo depende del tipo de problema y de la forma de sus soluciones, y exige un análisis complejo
del programa y de sus refinamientos sucesivos. El primer aspecto a tener en cuenta es el relativo a la corrección
del programa.

También se ha de estudiar la forma de ejecución del programa, ya que la inferencia incorpora aspectos
procedimentales a la semántica declarativa del programa, al entender los programas como una secuencia
ordenada de reglas y cada parte derecha de una regla como una secuencia ordenada de objetivos, y por lo tanto
diferentes secuencias de objetivos pueden tener efectos beneficiosos o nocivos para la eficiencia e incluso para
la corrección del programa.

En general, en la relación a la poda de alternativas en el árbol o espacio de búsqueda, pueden identificarse tres
tipos de soluciones:

- Solución declarativa: Incorporando al programa el conocimiento pertinente.


- Solución ad-hoc: Utilizando el predicado predefinido de corte en el programa.
- Solución temporal: Incorporando el corte en la consulta.

La eficiencia eliminando alternativas exige un primer análisis para el refinamiento del programa, comprobando
la correspondencia del programa con el conocimiento disponible del problema y su forma de uso y
posteriormente un análisis de la solución más adecuada.

4.3.2 Eficiencia con estructuras de datos

Hay que tener en cuenta que la operación sobre los argumentos de los predicados u objetivos consiste en el uso
de términos funcionales extendidos lógicamente con la sintaxis especifica del entorno Prolog.

Las funciones en los argumentos de objetivos con un número determinado de elementos permiten aglutinar
entidades e incluso facilitan la metaprogramación. Un ejemplo del uso de términos funcionales es la solución
de Kowalski para el problema de los bloques, consiguiendo una elevada sintonía con la especificación
conceptual del problema en el que se describen entidades de diferente naturaleza.
Ejemplo: El problema de los bloques consiste en encontrar una planificación o secuencia de movimientos de un
brazo de robot en un escenario formado por tres bloques y tres posiciones en el escenario. Los bloques se pueden
apilar. El brazo del robot solo puede cambiar la posición de los bloques de uno en uno. Los datos del problema
consisten en la configuración inicial del escenario y la configuración final que se quiere alcanzar.

En el marco conceptual se identifican tres tipos de objetos o elementos: bloques, posiciones y estados:

- Un estado denota la situación de los bloques en el escenario con los predicados u objetivos en (X, Y) y
libre (X, Y), que indican que el objeto X esta sobre el Y, y que X no tiene nada encima.

- Se utiliza la función llevar (X, Y, Z) que representa el movimiento del robot de llevar X desde Y hasta Z.

- La función resultado (llevar (X, Y, Z), E), denota el estado resultante de llevar un bloque X de Y a Z, en
el estado E.

- Los meta-predicados necesarios son cierto (P, E), que indica que P ocurre en el estado E y el que indica
que un cierto estado E es posible, posible (E).

El marco conceptual anterior permite formular el problema de los bloques como sigue:

% Condiciones en el estado 0 o inicial

cierto (en (bloque_A, bloque_B), 0).


cierto (en (bloque_A, pos1), 0).
cierto (en (bloque_A, pos3), 0).
cierto (libre(pos2), 0).
cierto (libre(bloque_A), 0).
cierto (libre(bloque_C), 0).

% Condiciones para cambiar bloques de sitio

cierto (en (X, Z), resultado (llevar X, Y, Z), W)).


cierto (libre (Y), resultado (llevar (X, Y, Z), W)).

% Condición de invarianza

cierto (U, resultado (llevar (X, Y, Z), W)) :- cierto (U, W), dif (U, en (X, Y)), dif (U, libre (Z)).
posible (0).
posible (resultado (llevar (X, Y, Z), W)) :- posible (W), manipulable (X), dif (X, Z), cierto (libre (X), W),
cierto (libre (Z), W), cierto (en (X, Y), W).
manipulable (bloque_A).
manipulable (bloque_B).
manipulable (bloque_C).

La consulta es la descripción del estado final deseado, y que ha de ser posible de alcanzar desde el inicial.

?- cierto (en (bloque_A, bloque_B), W), cierto (en (bloque_B, bloque_C), W), cierto (en (bloque_C, pos3),
W), posible (W).

Las listas en Prolog están implementadas de forma muy eficiente, pero son exigentes en tiempo y memoria, por
lo que deben usarse solo cuando son necesarias: siempre que a priori no se conozca el número de elementos que
tendrá la lista de elementos en tiempo de ejecución.
En caso de que se conozca que el número de elementos de una lista va a ser fijo, es mejor utilizar un predicado
aglutinante. Debería utilizarse una función, pues de este modo se gastará menos memoria para el mismo número
de elementos y se dispondrá de un acceso a los elementos en tiempo constante.
4.3.3 Gestión dinámica
La memoria de trabajo es dinámica, es decir, puede ser modificada en tiempo de ejecución utilizando predicados
predefinidos, que en general aparecen para añadir una clausula (assert), eliminar clausulas (retract), listar las
cláusulas de memoria y otros.

Ejemplo: Para calcular los numero de Fibonacci sería útil recordar resultados intermedios o soluciones ya
encontradas o calculadas. En:

fib (0, 1).


fib (1, 1).
fib (N, F) :- N > 1, N1 is N – 1, N2 is N1 -1, fib (N1, F1), fib (N2, F2), F is F1 + F2.

se realiza un número muy elevado de llamadas recursivas, pero si se programa guardando en la memoria de
trabajo los resultados intermedios:

fib (X, Y) :- lfib (X, Y), !.


fib (X, Y) :- A is X – 1, fib (A, M), B is X – 2, fib (B, N), Y is M + N, assert (lfib (X, Y)).
initfib :- assert (lfib (0, 1)), assert (lfib (1, 1)).

entonces el programa “recuerda” los cálculos realizados para las sucesivas llamadas.

4.3.4 Uso eficiente de la recursión

En términos de la programación lógica, se tiene una forma de iteración cuando no hay que recordar resultados
intermedios para finalmente componer el resultado por unificación. Esto ocurre cuando la llamada recursiva es
la última de la cláusula o regla y no hay alternativas pendientes anteriores a la llamada del objetivo a resolver.

Una técnica de programación relativa al uso eficiente de la recursión es la utilización de un parámetro de


acumulación o argumentos para guardar resultados intermedios cuando estos no son estrictamente necesarios
para la resolución de un objetivo.

En general el comportamiento determinista o iterativo de un programa recursivo se da, cuando la llamada


recursiva es la última de la regla y esta a su vez es de solución única.

En cualquier caso, retrasar la llamada recursiva, siempre que sea posible, es útil para evitar alternativas que no
llevan a soluciones.

4.3.5 Meta-programación

Se entiende como metaprograma, al programa que describe relaciones entre entidades del programa subyacente
o programa objeto. Un programa objeto podrá ser ejecutado directamente o ejecutado a partir de llamadas del
programa meta, por lo tanto, con control externo.

La metaprogramación es una técnica utilizada para:

- Describir el conocimiento del problema original en forma de programa objeto.


- Especificar su manipulación o método de inferencia asociado a través del programa meta.

Para ello Prolog aporta un conjunto de predicados predefinidos que permiten acceder al estado de la
computación en un momento dado:

- var (termino): termino es una variable.


- ground (termino): termino no contiene variables.

O predicados cuyos argumentos son a su vez objetivos Prolog:


- freeze (X, Objetivo): Mientras sea cierto que var (X), no se resuelve objetivo.
- call (objetivo): Siempre es verdad y produce la ejecución de objetivo.

Prolog soporta algunos predicados predefinidos como:

setOf (Variable, Objetivo (Argumentos), Lista)

que almacena en una lista todos los valores, que, asignados a una variable, hacen cierto un objetivo.

TEMA 5: SINTAXIS DE LOS LENGUAJES DE


PROGRAMACIÓN
5.1 ESTRUCTURA LÉXICA DE LOS LENGUAJES DE PROGRAMACIÓN
La estructura léxica se encarga de las cadenas de caracteres en un lenguaje. EL análisis léxico identifica las secuencias
de tokens y tras el análisis sintáctico, son las que forman la estructura sintáctica de un programa.

Las clases más generales de tokens son:

- Palabras claves: Tienen un significado especial en el lenguaje y forman parte de la sintaxis.


- Palabras reservadas: Son palabras que no pueden redefinirse como identificadores.
- Literales: Números o cadenas de letras.
- Símbolos: Como + o ;.
- Identificadores: Cadenas definidas por el programador que no pueden ser palabras reservadas.

En el caso de los identificadores, su longitud puede estar predefinida en algunos lenguajes, ser arbitraria o solo ser
significativos los primeros caracteres. Para evitar esta ambigüedad, se utiliza el principio de subcadena de mayor
longitud que determina que un token es la cadena más larga posible de caracteres hasta que haya un delimitador
(símbolo especial que indique sin ambigüedad su final).

La forma habitual de describir formalmente los tokens de un lenguaje es mediante expresiones regulares.

5.1.1 Formalización de las expresiones regulares

El objetivo de las expresiones regulares es representar a los lenguajes sobre un alfabeto , basándose en
lenguajes primitivos y operadores de composición. Dado un alfabeto , las expresiones regulares sobre él se
construyen mediante las siguientes reglas:

- Las siguientes expresiones son expresiones regulares primitivas:

-  (lenguaje vacío).
-  (palabra vacía).
- a, con a  .

- Sean α y  dos expresiones regulares, entonces son expresiones regulares derivadas:

- α +  (union).
- α .  o α (concatenación).
- α* (cierre).
- (α) (expresión regular entre paréntesis).
La precedencia, de mayor a menor, de los operadores es la siguiente:

1. () paréntesis.
2. * cierre.
3. . concatenación.
4. + union.

Sea exp una expresión regular sobre  y sean α y  dos expresiones regulares también sobre . El lenguaje
descrito por exp, denominado L (exp), se define recursivamente como:

- Si exp =  entonces L () := .


- Si exp =  entonces L () := {}.
- Si exp = a, con a   entonces L (a) := {a}.
- Si exp = α +  entonces L (α + ) := L (α)  L ()
- Si exp = α .  entonces L (α . ) := L (α)L ()
- Si exp := α* entonces L (α*) := L (α)*.
- Si exp := (α) entonces L ((α)) := L (α).

Dos expresiones regulares son equivalentes si describen el mismo lenguaje. Se pueden establecer las siguientes
equivalencias y propiedades:

α + ( + ) = (α + ) +  α* =  + α . α*
α . ( . ) = (α . ) .  * = 
α+=+α * = 
α . ( + ) = α .  + α .  α* α* = α*
(α + ) .  = α .  +  .  αα* = α*α
α.=αyα+=α (α*)* = *α
α.= (α* + *)* = (α**)* = (α + )* = (α*)*α*
Si   L (α) entonces α +  = α (α)*α = α (α)*

La clase de lenguajes que se pueden representar mediante una expresión regular es equivalente a la clase de
lenguajes regulares.

5.1.2 Las expresiones regulares en la programación

Las expresiones regulares son patrones que permiten presentar y encontrar combinaciones de caracteres dentro
de cadenas de texto. Están compuestas por una secuencia de caracteres que permiten especificar características
determinadas de los ejemplos del patrón.

Sim. Significado
\ Indica que el siguiente carácter es especial
\n Encuentra una nueva línea
\d Representa un digito del 0 al 9
\D Representa cualquier carácter que no sea un digito del 0 al 9
\w Cualquier carácter alfanumérico.
\W Cualquier carácter no alfanumérico
\s Representa un espacio en blanco
\S Representa cualquier carácter que no sea un espacio en blanco
\b Marca el inicio y el final de una palabra
^ Encuentra el comienzo de una línea
$ Posición de final de una cadena o una línea
* Encuentra 0 o más coincidencias del carácter que le precede
+ Encuentra 1 o más coincidencias del carácter que le precede
? Encuentra 0 o 1 coincidencias del carácter que le precede
. Encuentra una coincidencia de cualquier carácter salvo del principio de línea
| Indica varias opciones
(x) Encuentra coincidencias con x y recuerda el patrón para su uso posterior
{n} Encuentra coincidencia si el carácter que le precede aparece exactamente n veces
{n, } Encuentra coincidencia si el carácter que le precede aparece al menos n veces
{n, m} Encuentra coincidencia si el carácter que le precede aparece entre n y m veces
[xyz] Agrupa caracteres en grupos o en clases.
[n – m] Rango entre n y m
[^xyz] Encuentra coincidencias con caracteres que NO aparecen en el grupo

5.2 GRAMÁTICAS LIBRES DE CONTEXTO Y BNF


Una gramática libre de contexto está formada por un alfabeto de símbolos no terminales, otro alfabeto de símbolos
terminales, un símbolo no terminal denominado símbolo inicial S y un conjunto de reglas.

Las reglas están formadas por un único símbolo no terminal que forma la parte izquierda de la regla, a continuación,
el metasímbolo →, y finalmente la parte derecha de la regla. El lenguaje que define una gramática es el conjunto de
todas las cadenas de símbolos terminales para las cuales existe una derivación aplicado las reglas de la gramática.
El metasímbolo de disyunción | sirve para agrupar reglas con la misma parte izquierda.

Una derivación es una secuencia que comienza con el símbolo inicial y en la que en cada paso se sustituye un símbolo
no terminal de la cadena en curso por la parte derecha de una regla. El metasímbolo que indica cada paso de
derivación es =>.

En la notación BNF la representación de las reglas se hace con símbolos y metasímbolos que son:

Sim. Significado
: := Separación entre la parte izquierda y derecha de una regla
| Alternativa (solo se puede elegir uno de los elementos)

5.3 ESTRUCTURA SINTÁCTICA: ÁRBOLES SINTÁCTICOS


Se genera durante el proceso de análisis sintáctico y representa gráficamente el proceso de reemplazo o sustitución
dentro de una derivación.
Los no terminales están en los nodos interiores y los terminales en las hojas. En los arboles de sintaxis abstracta se
eliminan todos los nodos intermedios que no aportan información semántica, dejando únicamente los símbolos
terminales.

5.4 AMBIGÜEDAD, ASOCIATIVIDAD Y PRECEDENCIA


La ambigüedad aparece cuando dos derivaciones diferentes llevan a arboles sintácticos diferentes, para una misma
palabra del lenguaje de la gramática.
Hay criterios o restricciones que se pueden aplicar a las derivaciones, para que desaparezca la ambigüedad.

Las gramáticas ambiguas no expresan claramente las estructuras sintácticas de un lenguaje, por ello es necesario
aportar un criterio para eliminar ambigüedades, que puede definirse de forma independiente a la gramática o
revisándola añadiendo o modificando reglas.

5.5 DIAGRAMAS SINTÁCTICOS


Para facilitar la comprensión de reglas BNF complejas se utilizan las facilidades de los metasímbolos de las
expresiones regulares en lo que se denominó notación EBNF (Extended BNF) que sirve para simplificar la notación
BNF sin añadir más expresividad.
Sim. Significado
: := Separación entre la parte izquierda y derecha de una regla
| Alternativa (solo se puede elegir uno de los elementos)
{} Repetición (los elementos pueden repetirse 0 o más veces)
[] Opción (los elementos que incluyen pueden utilizarse o no)
() Agrupación (sirven para agrupar los elementos que incluyen)

Para diferenciar ( ) del metasímbolo de agrupación, la convención es encerrarlos entre comillas simples. La notación
EBNF no permite la recursión, es decir, un no terminar no puede aparecer al mismo tiempo en la parte izquierda y
derecha de una misma regla.

Para representar las reglas EBNF se usan los diagramas sintácticos en los que los símbolos terminales están
encerrados en círculos y los no terminales en rectángulos. Estos símbolos se unen mediante flechas dirigidas que
indican el orden en el que se pueden presentar en una regla. Para una regla determinada, una cadena valida debe
definir un cambio en el diagrama.

Ejemplo: Para las siguientes reglas EBNF

<exp> : := <termino> {(+ | -) <termino>}


<termino> : := <factor> {(* | /) <factor>}
<factor> : := ‘(‘ <exp> ‘)’ | <num>
<num> : := <digito> {<digito>}
<digito> : := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

Se usan los siguientes diagramas sintácticos:

TEMA 6: SEMÁNTICA BÁSICA


Hay varias formas posibles de especificar informalmente un lenguaje:

- Mediante un manual de referencia del lenguaje. Puede ser impreciso.


- Mediante el uso de un traductor para definir la semántica. Puede haber preguntas que puedan no ser contestadas
Otras desventajas de estas aproximaciones son que es difícil depurar los errores del traductor y que este suele ser
dependiente de la máquina. Entre las aproximaciones existentes, la semántica denotacional que describe la semántica
de un lenguaje mediante funciones, es la más utilizada.

6.1 ATRIBUTOS, LIGADURAS Y FUNCIONES SEMÁNTICAS


El significado de un identificador queda determinado por las propiedades o atributos asociados al mismo. Los
atributos más importantes son:

- Valores: Representan cualquier cantidad almacenada.

- Localizadores: Lugares donde se almacenan los valores y pueden entenderse como direcciones en la memoria
o de forma más abstracta.

Al proceso de asignación de un atributo a un identificador se denomina vinculo.

Un atributo puede clasificarse según el tiempo en que se está calculando o bien vinculando a un identificador, lo que
se conoce como tiempo de vinculación. Hay dos tipos de vínculos:

- Vínculos estáticos: Son aquellos que vinculan un atributo a un identificador en tiempo de traducción.

- Vínculos dinámicos: La vinculación se realiza durante la ejecución del programa.

Los lenguajes funcionales tienen más vínculos dinámicos que los imperativos.

Para hacer que el análisis de los atributos y sus vínculos sea independiente del traductor, se toma por convenio que
el tiempo de vinculación de un atributo es el tiempo más corto de los que las reglas del lenguaje permiten al atributo
estar vinculado.

Un atributo estático puede vincularse:

- En tiempo de traducción durante el análisis gramatical o semántico.

- En tiempo de linkado durante el encadenamiento del programa con las bibliotecas.

- En tiempo de carga durante la carga del programa para su ejecución.

- En tiempo de definición del lenguaje.

- En tiempo de implementación del lenguaje.

Un atributo dinámico puede vincularse en tiempo de ejecución, en la entrada o en la salida de un procedimiento o


bien en la entrada o salida de todo el programa.

El traductor debe hacer posible que se mantenga la consistencia de los vínculos durante la traducción y durante la
ejecución. Para mantener esta información se utiliza una tabla de símbolos, en la que se almacena la forma de vincular
los atributos a los identificadores. La tabla de símbolos de un traductor se gestiona de forma diferente según se trate
de un intérprete o un compilador:

- Compilador: Solo puede procesar atributos estáticos, ya que la ejecución del programa es posterior a la
compilación.

- La asignación de memoria para almacenar el vínculo de los identificadores con localizaciones de


almacenamiento, se realiza por separado, en el ambiente o entorno.
- Los vínculos entre las localizaciones de almacenamiento y los valores forman el denominado estado del
cómputo que supone una abstracción de la memoria de un ordenador real.

- Interprete: Se procesan tanto los atributos estáticos como los dinámicos, por lo que se combinan la tabla de
símbolos y el ambiente. Luego en un intérprete, el ambiente produce y almacena la asignación de los
identificadores a sus atributos estáticos (labor de la tabla de símbolos) asi como a la localización de sus valores.

6.2 DECLARACIONES, BLOQUES Y ALCANCE


Las declaraciones pueden establecer vínculos de forma implícita o explicita. En C, int x; establece implícitamente
que el tipo de datos de x es integer, aunque la localización exacta de x durante la ejecución está vinculada de manera
implícita. Sin embargo, int x = 0; vincula o liga de manera explícita a 0 como valor inicial de x.

En el caso de C y C++ se distingue entre declaraciones que vinculan a todos los atributos potenciales, que se
denominan definiciones, o las que vinculan solo a algunos atributos, que son las declaraciones propiamente dichas.
Las declaraciones suelen estar asociadas a constructores específicos del lenguaje. A cada constructor especifico o
estándar, se le denomina bloque, que es una secuencia de declaraciones seguidas por una secuencia de sentencias y
rodeado por marcadores. En C, los bloques se denominan sentencias compuestas y aparecen en el cuerpo de las
definiciones de una función y en cualquier otra parte de un programa en que podría aparecer una sentencia ordinaria.

Se pueden escribir declaraciones externas y globales fuera de cualquier sentencia compuesta. Las que se encuentran
dentro de un bloque se denominan declaraciones locales al bloque y al resto no locales.

En los lenguajes orientados a objetos, la clase es un constructor básico. En estos lenguajes a las funciones que no
devuelven valores se las denomina métodos.
Dependiendo del tipo de declaración se vinculan varios atributos a los identificadores. Cada uno de estos vínculos
tienen una determinada posición en el programa que especifican el alcance o ámbito.

Es posible referirse al alcance de una declaración siempre que el alcance de todas las ligaduras establecidas en dicha
declaración sea el mismo, pero debe evitarse el uso del concepto de alcance referido a los identificadores, porque un
mismo identificador puede estar involucrado en varias declaraciones diferentes.

En la mayoría de los lenguajes actuales estructurados en bloques, en los que los bloques pueden anidarse, el alcance
de un vínculo queda limitado al bloque en que aparece su declaración. Esta regla se conoce por alcance léxico.

En C hay una regla para indicar que el alcance de una declaración comienza en la propia declaración (regla conocida
como declaración antes de uso) con lo que el alcance de una declaración se extiende desde que dicha declaración
aparece hasta el final del bloque en el que se encuentra.

Otra característica de la estructura de bloques es que las declaraciones en los bloques anidados toman precedencia
sobre las declaraciones anteriores. La visibilidad de un vínculo es la region del programa en el que el vínculo es
aplicable. Es una subregión de la zona de alcance (si hay bloques anidados, pueden existir declaraciones que tapen
otras anteriores temporalmente, dentro del bloque anidado, lo que se denomina ocultamiento de alcance).

Las declaraciones locales, en el interior de una declaración de clase, generalmente tienen un alcance que incluye toda
la clase, con lo que deja de aplicarse la regla de declaración antes de uso.

6.3 LA TABLA DE SÍMBOLOS


Para permitir los bloques anidados y los alcances locales de los vínculos, la tabla de símbolos se implementa mediante
una pila, añadiéndose los vínculos en las declaraciones y eliminándose al finalizar el bloque que las contenía.
Si la tabla de símbolos se construye procesando el programa en el orden en el que se ha
escrito, tras procesar la declaración de variable en el cuerpo de p (línea 6), la tabla de símbolos
se representa como:

La tabla de símbolos al procesar el bloque anidado en p (línea 8) es:

Tras procesar el bloque anidado (línea 9) se restaura el vínculo de y, y se vuelve al estado de la primera figura. Una
vez terminado el proceso de p (línea 11) se restaura el vínculo de x, quedando la situación representada como:

Tras procesar las declaraciones de la función q (línea 15), la tabla de símbolos queda como:
Finalmente, la tabla de símbolos al procesar el main es:

Las tablas de símbolos también pueden procesar las declaraciones según aparecen durante la ejecución del programa,
lo que se conoce como regla de alcance dinámico.

La tabla de símbolos dinámica se inicia con main, pero las declaraciones globales deben
ser procesadas antes de dicha ejecución, ya que main debe conocer todas las declaraciones
anteriores. El aspecto de la tabla de símbolos antes de la llamada a q (línea 19) es:

Al seguir con la ejecución, desde main se llama a q y hay que procesar su cuerpo. Tras procesar las declaraciones de
q (línea 13), la tabla de símbolos es:
Con la regla de alcance dinámico, cada una de las llamadas a una función puede tener una tabla de símbolos diferente
en su entrada, dependiendo de la trayectoria de ejecución de dicha llamada.

Desde q se llama a p y la tabla de símbolos al llegar a la línea 8 es:

En general, la regla del alcance dinámico es compleja de implementar y produce diferentes problemas:

- La semántica de una función puede cambiar según avanza la ejecución bajo el alcance dinámico.
- Dado que las referencias a variables no locales no pueden predecirse antes de la ejecución, tampoco pueden
definirse los tipos de datos de esas variables.

El alcance dinámico es una opción aceptable para los lenguajes muy dinámicos, los interpretados y en los que los
que los programas no suelen ser muy grandes como APL, Snobol y Perl.
La tabla de símbolos como una tabla única para todo el programa, con inserciones de un alcance a la entrada y
eliminaciones a la salida, es solo apropiada para lenguajes simples. Si el lenguaje permite definir estructuras más
complejas, como clases en Java o C++, o estructuras en C, entonces cada estructura tendrá asociada sui propia tabla
de símbolos. Se sigue utilizando la pila, pero para anidar las tablas, en lugar de los vínculos.

Cada una de las declaraciones struct en el código debe contener


declaraciones adicionales de los campos de datos de cada struct y deben
ser accesibles siempre que las variables struct estén en el alcance.

Cuando se llega a la línea 15, la tabla de símbolos es:


6.4 ASIGNACIÓN, TIEMPO DE VIDA Y AMBIENTE
El ambiente se encarga de mantener los vínculos de los identificadores con las localizaciones de memoria, y se puede
construir estáticamente (Fortran), dinámicamente en tiempo de ejecución (LISP) o una mezcla de ambos (C, C++,
Ada, Algol o Java).

En general, en un lenguaje con estructura de bloques:

- Los identificadores de las variables globales se asignan estáticamente porque su significado es fijo en el
programa.

- Los identificadores de las variables locales se asignan dinámicamente cuando la ejecución llega al bloque.

En el ambiente también se utiliza una pila para vincular las localizaciones a las variables.

Durante la ejecución, el ambiente gestiona las asignaciones de las variables a


localizaciones. Si se representa gráficamente este proceso con una pila, entonces el
ambiente después de la entrada en A en la línea 3 es:

El ambiente después de la entrada en B es:

En la línea 7, a la salida del bloque B, el ambiente es el mismo que después de la entrada en A, ya que se han
eliminado las asignaciones de localizaciones del bloque B. Cuando el proceso entra en el bloque C (líneas 8 y 9),
asignan sus variables a las localizaciones de memoria liberadas por las variables del bloque B. Finalmente, a la
entrada del bloque D (líneas 11 y 12), el ambiente es:

La gestión de las localizaciones para las llamadas a subprogramas es más compleja pues debe resolverse
dinámicamente. Se hace mediante los registros de activación.

Se denomina objeto al área de almacenamiento asignada en el ambiente tras el procesamiento de una declaración.
Las constantes globales y los tipos de datos no son objetos, porque sus declaraciones no dan como resultado una
asignación de almacenamiento. Las variables y los parámetros si lo son.
El tiempo de vida o extensión de un objeto es la duración de su asignación en el ambiente. No necesariamente debe
coincidir con su accesibilidad (la variable x del bloque A se extiende a lo largo del bloque B, aunque en B no es
accesible) ni con su alcance (podría mantenerse la ligadura con la localización mas allá de su alcance en lenguajes
con ambientes totalmente dinámicos).

Cuando en un lenguaje de programación existen los punteros, cuyo valor almacenado es una referencia a un área de
almacenamiento asignada en el ambiente, es necesaria una extensión del ambiente.

Cada tipo de asignación (también llamado clase de almacenamiento) tiene su propia zona de memoria en el ambiente
Hay tres tipos de asignaciones:

- Asignación automática: Es la que se lleva a cabo con las variables y los parámetros.

- Asignación estática: Es la que se lleva a cabo para las variables globales.

- Asignación dinámica: Hay lenguajes que permiten al programador reservar memoria y por tanto realizar
vinculaciones de localización. Esto implica que el ambiente debe tener una zona de memoria específica para
permitirlo llamada montículo.

Algunos lenguajes permiten modificar la clase de almacenamiento, mediante palabras reservadas. Una variable static
en C, pasa a ser de asignación estática, en lugar de automática.

6.5 VARIABLES Y CONSTANTES


Una variable queda completamente especificada a partir de sus atributos, entre los que se encuentran:

- Identificador.
- Localización.
- Valor.
- Otros atributos como el tipo de datos o el tamaño.

La variable puede cambiar de valor almacenado durante la ejecución a traces de la sentencia de asignación x = e,
donde x es un identificador de variable y e, una expresión que se evalúa a un cierto valor, que a su vez se copia en la
localización de x. Si la expresión e es a su vez una variable (caso de x = y), entonces la parte izquierda representa a
la localización de x y la parte derecha al valor contenido en la localización de y.

En algunos lenguajes, se usa la asignación por compartición, copiándose las localizaciones en vez de los valores.
Otra alternativa es la asignación por clonación, que consiste en utilizar una localización nueva, copiar en ella el valor
de y, y vincular a x con la nueva localización. En ambos casos, esta interpretación de la asignación se conoce por
semántica de puntero, para distinguirla de la más habitual, denominada semántica de almacenamiento.

Una constante es una entidad del lenguaje que tiene un valor fijo mientras existe en un programa. Se especifica
mediante un identificador, un valor y otros atributos, entre los que no se encuentra la localización. Las constantes
por tanto tienen semántica de valor.
Las constantes admiten la siguiente clasificación:

- Constantes estáticas: Son aquellas cuyo valor se puede calcular antes de la ejecución, que a su vez se clasifican
en constantes que:

- Se calculan en tiempo de traducción, también denominadas constantes en tiempo de compilación.


- Se calculan en tiempo de carga o al principio de la ejecución del programa, que son las constantes estáticas
propiamente dichas.

- Constantes dinámicas: Son aquellas cuyo valor solo se puede computar en tiempo de ejecución.
Las constantes que se pueden calcular en tiempo de compilación no necesitan ocupar memoria y mejoran la eficiencia
de un programa.

TEMA 7: TIPOS DE DATOS


Los tipos de datos son abstracciones que permiten trabajar con los datos en los programas de una manera cercana a
la información que representan, y, por tanto, alejada de la representación binaria de fondo.

7.1 TIPOS DE DATOS


- Tipo abstracto de datos: Representa una abstracción de la realidad con independencia de la implementación concreta
que se realice del mismo. Se representa mediante el comportamiento semántico de las operaciones que se pueden
realizar a los elementos de dicho tipo.

- Estructura de datos: Es la representación computacional concreta de la organización de los datos del tipo abstracto
en términos de los tipos de datos primitivos que ofrece el lenguaje.

- Tipo de datos: Es el resultado de dotar a una estructura de datos de las operaciones definidas en el tipo abstracto de
datos.

Cualquier estructura del lenguaje susceptible de ser tipada suele estar acompañada de una declaración de tipo, que
es la sentencia por la que se define el tipo de una variable, contante, función, parámetro, … etc. Una vez declarado
el tipo, quedan fijadas las operaciones permitidas con ese elemento.
Gracias a las declaraciones de tipos de los diferentes elementos del programa, es posible realizar la llamada
verificación de tipos, que es el proceso por el cual el intérprete o compilador comprueba que los programas utilicen
de forma correcta los datos.

Aquellos lenguajes en los que se hace necesario expresar los tipos de datos de forma explícita y que realizan una
estricta verificación de tipos en tiempo de traducción se denominan lenguajes fuertemente tipados (tipificados). A
favor de este tipo de lenguajes hay varias razones:

- Mejora la eficiencia de ejecución.


- Mejora la eficiencia de la traducción.
- Mejora la legibilidad de los programas.
- Detección prematura de errores de tipo y mejora de la capacidad de escritura y por tanto la seguridad y
confiabilidad.

Algunos lenguajes como C permiten que se realicen operaciones sobre variables que no pertenecen a su tipo abstracto
de datos, lo que dificulta la legibilidad de los programas. Este tipo lenguajes se conocen como lenguajes débilmente
tipados.

Otros lenguajes como Prolog no poseen ninguna información sobre los tipos, por lo que se admiten cualquier
operación entre tipos. Este tipo de lenguajes se conoce como lenguajes no tipados.

La inferencia de tipos es el proceso por el cual el intérprete o el compilar decide el tipo de una expresión, a la vista
de los tipos de los operandos y la semántica de los operadores utilizados.

Los lenguajes ofrecen la posibilidad de definir nuevos tipos de datos más complejos basándose en los tipos básicos,
que se denominan tipos definidos por el usuario. Esto se realiza mediante los constructores de tipos. Por ejemplo, el
constructor de arrays de Java:

- Si el nuevo tipo se usa directamente para declarar una variable, se habla de un tipo anónimo.
- Si se le da un nombre al nuevo tipo se habla de definición de tipo.
Cada lenguaje debe incluir una serie de reglas para comprobar si dos tipos posiblemente provenientes de diferentes
declaraciones de tipo son o no equivalentes. Este problema se conoce como equivalencia de tipos.

Los mecanismos ofrecidos por un lenguaje para la creación de nuevos tipos, más los algoritmos de verificación,
inferencia y equivalencia de tipos se conocen de forma conjunta como sistema de tipos del lenguaje. Asi, los
lenguajes fuertemente tipados poseen un sistema de tipos estático (que puede ser aplicado en tiempo de compilación
sin que el programa se esté ejecutando) y garantiza que los programas peligrosos o bien son rechazados en tiempo
de compilación o bien son capaces de generar un error de ejecución antes de que se produzca una corrupción de los
datos, pero este tipo de lenguajes también pueden rechazar los programas seguros.
Asi, los programas correctos son un subconjunto de los programas seguros. Por lo tanto, el principal reto de un
sistema de tipos estático es minimizar el conjunto de programas seguros incorrectos.

7.1.1 Tipos de datos atómicos

Un tipo de datos se considera atómico si no es posible separarlo en diferentes elementos más sencillo.

Salvo los números reales, los tipos atómicos son tipos discretos, ya que dado un valor es posible definir cuál es
su sucesor y/o su predecesor. Comparando entre Java y Haskell, los tipos atómicos son:

- Enteros.

Java ofrece los tipos byte (1 Byte), short (2 Bytes), int (4 Bytes) y long (8 Bytes) para almacenar
números enteros.

En Haskell hay dos tipos de enteros: Int (acotados por el compilador y la maquina) e Integer (sin
acotar).

Las operaciones que se definen son las aritméticas básicas: suma, resta, multiplicación, división entera
y modulo.

- Reales.

Un número real se representa como: mantisa * 10exponente

Tanto Java como Haskell ofrecen dos tipos de datos que almacenan reales, los cuales pueden ser:

Lenguaje Java haskell


Simple precisión (32 bits)
Mantisa: 23 bits
float Float
Exponente: 8 bits
Signo: 1 bit
Doble precisión (64 bits)
Mantisa: 52 bits
double Double
Exponente: 11 bits
Signo: 1 bit

Además de las operaciones aritméticas básicas, se suelen implementar la raíz cuadrada, el logaritmo
neperiano y la exponenciación.

- Caracteres.

En Java existe el tipo char, que esta almacenado en forma de un entero de dos bytes de longitud. Los
datos se almacenan siguiendo el estándar UNICODE. En Haskell está el tipo Char, que sigue el
estándar ASCII-7.
Las operaciones que suelen implementarse para trabajar con caracteres son:

Operación Java haskell


¿Es un espacio? isWhiteSpace isSpace
¿Es un digito? is digit
¿Es una letra? isLetter isAlpha
¿Está en mayúsculas? isUpperCase isUpper
¿Está en minúsculas? isLowerCase isLower
Transformar a mayúsculas toUpperCase toUpper
Transformar a minúsculas toLowerCase toLower

- Booleanos.

En Java se denominan boolean, mientras que en Haskell Bool y permiten almacenar y operar con
valores lógicos.
Las operaciones entre booleanos son las comunes del Algebra de Boole.

- Tipos enumerados.

A pesar de ser tipos atómicos, son tipos que pueden ser definidos por el usuario. Se definen mediante
una enumeración de los elementos que los componen, junto a las operaciones que se pueden realizar
sobre dichos elementos. Por tanto, una de sus características más importantes es que el conjunto de
posibles valores de los datos ha de ser finito.

- Rangos.

Son tipos definidos por el usuario. Toman los valores de un subconjunto de elementos contiguos de
un tipo discreto, que se definen dando el menor y el mayor de los elementos que conforman el rango.

Ni Java ni Haskell permiten de forma directa la creación de rangos, sino que se debe utilizar un tipo
de datos predefinido y controlar directamente que los datos no se salgan del rango seleccionado.
Pascal y Ada si permiten la definición de este tipo de datos.

7.1.2 Tipos de datos estructurados

Son aquellos cuyos valores pueden descomponerse en otros más simples.

- Arrays.

Representan estructuras matriciales de una o varias dimensiones, en las que es posible acceder
directamente a cualquiera de sus elementos sin más que indicar los índices del elemento deseado en
todas las dimensiones del array.

En Java, conjunto de índices ha de ser siempre un rango de enteros positivos que comienzan en 0. No
tienen tamaño prefijado, ya que el tamaño del array se define en el momento de su creación y se
conserva como una propiedad del mismo.
Para conocer el tamaño de un array en Java se usa la propiedad .length.

Los arrays no tienen por qué ser unidimensionales, sino que pueden tener cualquier número de
dimensiones.

En los lenguajes funcionales puros como Haskell, el concepto de array como tabla que permite un
acceso directo a cualquier de sus componentes son existe. En su lugar, se pueden utilizar listas para
simular arrays, aunque ello implica que los accesos a los componentes no se realizan en tiempo
constante.
- Funciones.

Se consideran tipos de datos en Haskell, no en Java. El tipo de una función viene dado por la tupla
formada por los tipos de los parámetros y el tipo de devolución.

- Producto cartesiano.

El producto cartesiano de dos conjuntos U y V se define como los pares ordenados (u, v) en los que
u  U y v  V.

Sobre este tipo se define una serie de funciones de proyección, que dado un elemento del producto
cartesiano permiten obtener cada uno de los elementos de los conjuntos originales que lo forman.

En Haskell es posible construir este tipo de datos de forma pura mediante el uso de tuplas.

En otros lenguajes como C o Pascal, este tipo de datos se construye mediante registros.

En lenguajes orientados a objetos como Java, es posible crear tipos producto cartesiano mediante el
uso de clases.

- Tipos recursivos.

Son aquellos en el que el tipo que está siendo definido aparece en su propia definición.

En Java, los tipos recursivos deben implementarse mediante clases, mientras que en Haskell es posible
realizarlo de una forma más sencilla en una línea de código.

- Union de tipos.

No es posible clasificar una union de tipos como un tipo atómico o estructurado sin tener en cuenta
los tipos que intervienen. Si uno de ellos es estructurado, entonces el tipo resultante también lo será,
mientras que, si todos los tipos son atómicos, el resultado será atómico.

Las uniones pueden ser discriminadas si cada elemento del tipo debe ser identificado con el tipo
concreto al que pertenece o indiscriminadas en caso contrario.

En Haskell es posible crear uniones discriminadas mediante el uso de la construcción data.

En lenguaje Java no permite la creación de tipos mediante la union de otros, aunque existe un
mecanismo similar a la union discriminada, aunque mucho más potente, que es la herencia de clases
propia de los lenguajes orientados a objetos.

7.2 EQUIVALENCIA DE TIPOS DE DATOS


El mecanismo de equivalencia de tipos es el responsable de decidir si dos tipos son o no iguales.

La equivalencia estructural decide que dos tipos son equivalentes si se han construido exactamente de la misma
forma, es decir, aplicando los mismos constructores de datos a los mismos tipos sobre los que se han construido.
La forma de comprobar la equivalencia estructural consiste en sustituir en la declaración de una variable el nombre
del tipo por su definición.

Por ejemplo, en C, hay equivalencia estructural entre tipos struct, pero:

- El nombre de los campos debe ser el mismo.


- Los campos deben estar declarados en el mismo orden.
La equivalencia en nombre decide que dos tipos son equivalentes únicamente si tienen el mismo nombre. A veces se
permite que, aun no llamándose igual, un tipo y su alias sean considerados equivalentes en nombre. Si dos variables
se declaran de un mismo tipo anónimo en la misma sentencia, también se consideran tipos equivalentes en nombre.

7.3 CONVERSIÓN DE TIPOS DE DATOS


En muchas ocasiones es necesario trabajar de forma conjunta con enteros y reales, por lo que dentro del sistema de
tipos se hace necesario algún mecanismo de conversión de tipos de datos.

Java realiza una conversión automática entre los tipos de enteros cuando dicha conversión es posible. Esto se conoce
como conversión implícita de tipos.

En contraposición esta la conversión explicita (forzada) en la que la conversión de tipos se delega en el programador.
Este tipo de conversión puede realizarse precediendo la expresión con el tipo deseado para el resultado:

x = (double) (3 + 2.5)

como hacen C y Java, o bien utilizando el propio tipo como si de una función se tratase

x = double (3 + 2.5)

Mientras que la conversión implícita puede dar lugar a comportamientos inesperados, la conversión explicita carga
la tarea en el programador. Existe un punto intermedio en el que el lenguaje realiza una conversión implícita siempre
que los datos no puedan corromperse, como Java, que solo permite este tipo de conversiones de un tipo a otro que lo
extienda.

TEMA 8: CONTROL: EXPRESIONES Y SENTENCIAS


8.1 EVALUACIÓN DE EXPRESIONES
Una expresión es una estructura que representa un valor concreto de uno de los tipos de datos que dicho lenguaje
contempla. Las expresiones básicas están formadas por:

- Constantes: Representan un valor concreto.


- Identificadores: Referencian un valor.
- Funciones.
- Operadores.

Aquellos operadores que solo pueden aplicarse a un único operando se denominan operadores unarios, mientras los
que se aplican a dos operandos se denominan binarios.
Las funciones reciben una serie de parámetros de entrada y devuelven un único valor de salida.

8.1.1 Notaciones infija, prefija y postfija

En la expresión 6 + 2 * 4 se encuentran los operadores binarios + y * que están situados entre los dos operandos
a los que se aplican. Esta forma de escribir una expresión se conoce como notación infija.

Esta misma expresión puede escribirse en notación prefija sin más que anteponer el operador binario a sus dos
operandos (+ 6 * 2 4).

También puede escribirse en notación postfija si los dos operandos a los que se aplica cada operador preceden
a dicho operador (6 2 4 * +)

Las notaciones prefija y postfija presentan una ventaja sobre la notación infija: no existe ambigüedad en el orden
en el que se deben aplicar los operadores, por lo que no es necesario utilizar paréntesis.
La notación postfija se puede realizar mediante una maquina pila, en la que los operandos se van apilando, los
operadores desapilan los operandos necesarios y apilan el resultado.
Los lenguajes ofrecen una serie de operadores predefinidos que han de escribirse mediante notación infija,
mientras que las funciones se escriben en notación prefija.

8.1.2 Evaluación de una expresión

Antes de definir las distintas evaluaciones hay que tener claros dos conceptos:

- Efecto colateral: Al invocar una función, se producen modificaciones en variables que no son pasados
como argumentos.

- Transparencia referencial: La misma invocación a una función con los mismos valores en los argumentos
siempre produce el mismo resultado. Este principio se cumple si no existen efectos colaterales.

La mayoría de los lenguajes de programación establecen que los operandos o parámetros han de ser evaluados
antes de aplicar los operadores o funciones. Esto se conoce como evaluación impaciente o estricta. Si la
evaluación de los parámetros no produce ningún tipo de efecto colateral, el orden en el que se realice la
evaluación de los parámetros es indiferente. Sin embargo, si hay efectos colaterales, el orden sí que tiene
importancia.

En contraposición a la evaluación estricta esta la evaluación no estricta o diferida, en la que la evaluación de los
parámetros de una función se difiere hasta que dicha evaluación sea necesaria.

Que se cumpla la transparencia referencial permite el uso de una estrategia de evaluación denominada
evaluación normal en la que las funciones comienzan a ser evaluadas antes de que se realice la evaluación de
sus parámetros. Si no hay efectos colaterales, la evaluación normal y la impaciente de una misma expresión han
de devolver el mismo resultado.

Si, además de la evaluación normal, la evaluación de los parámetros compuestos de las funciones se retrasa
hasta tener toda la información suficiente para evaluar la función, la evaluación pasa a denominarse evaluación
perezosa.

La evaluación en cortocircuito es aplicable en algunos operadores, implica que no todos los operandos son
evaluados, solo los estrictamente necesarios, como AND (u OR), que se sabe si es cierta o falsa sin tener que
evaluar todos lo operandos, ya que si un lado es falso (verdadero) no hace falta evaluar el otro. Los operadores
que no apliquen este, pudiendo hacerlo, son conocidos como operadores ansiosos.

8.2 SENTENCIAS CONDICIONALES


Son sentencias que permiten elegir la ejecución o no de unos bloques de instrucciones u otros en función del resultado
de evaluar expresiones logicas.

8.2.1 Sentencias if-then-else

La forma básica de las sentencias if-then-else es la siguiente:

if (e) then s1 [else s2]

aunque en muchos lenguajes se omite la palabra clave then.

Su semántica es la siguiente:

- Tanto s1 como s2 pueden ser una única sentencia o una secuencia de sentencias.
- En primer lugar, se evalúa la expresión e, que ha de ser booleana.
- Si el valor de la expresión es verdades se ejecutará s1.
- Si el valor de la expresión es falso y está presente la parte opcional, se ejecutará s2. Si no está presente,
no se hace nada.

En muchas ocasiones, cuando se anidan dos sentencias if-then-else se presenta un problema denominado el
problema del else antiguo. Algunos lenguajes como C o Pascal optan por aplicar la llamada regla del
anidamiento más cercano como criterio para eliminar ambigüedades, que establece que un else se debe asociar
al if más cercano.

En Java la parte then de una sentencia if-then-else no puede acabar en una sentencia if-then-else sin su parte
else. Sí puede incluirse si se mete dentro de un bloqueo de otras sentencias que tengan un terminador explícito.
Por tanto:

if (e1) if (e2) s1 else s2

se trata de una sentencia if-then sin else en la que se ejecuta una sentencia if-then-else si la expresión es cierta.

La forma de forzar que se interprete de la otra forma seria encerrar la sentencia if-then-else sin parte else dentro
de un bloque:

if (e1) {if (e2) s1} else s2

8.2.2 Sentencias case

Se usa una expresión de algún tipo discreto (entero, carácter), y los bloques de instrucciones se asocian a los
distintos valores de la expresión.

switch (e) {
case valor1: s1; break;
case valor2: s2; break;
case valor3a:
case valor3b:
case valor3c: s3; break;
default sd; break;
}

La semántica de esta sentencia es la siguiente:

- Se evalúa e, que no tiene por qué ser booleana.


- Se compara el resultado de la evaluación con valor1. Si coincide se ejecuta s1.
- Si no coincide, se compara con valor2. Si coincide se ejecuta s2.
- Si no coincide se compara con valor3a, valor3b y valor3c y si coincide con alguno, se ejecuta s3.
- Si no coincide con ninguno se ejecuta sd.

Esta construcción es azúcar sintáctico pues se puede crear una construcción similar utilizando sentencias if-then
else:

if (e = = valor1) s1
else if (e = = valor2) s2
else if (e = = valor3a || e = = valor3b || e = = valor 3c) s3
else sd;
8.3 BUCLES
Permiten la ejecución repetida de un bloque de instrucciones, mientras una expresión lógica se evalúa a cierto.

La forma más comúnmente ofrecida por los lenguajes de programación es el bucle while y su forma general en Java
es:
while (e) s

que ejecuta la secuencia s mientras la expresión e sea verdadera.


Muchos lenguajes ofrecen diferentes variaciones sobre esta construcción como el bucle do-while, que fuerza la
ejecución del cuerpo del bucle al menos una vez:

do s while (e)

Otra versión muy común de bucle son los bucles for de Java, cuya sintaxis es:

for (si; e; sa) s;

que es equivalente a:

si;
while (e) {
s;
sa;
}

donde:

- si es la sentencia de inicialización.
- e es la condición de terminación del bucle.
- sa es la sentencia de actualización del bucle.
- s es el cuerpo del bucle.

Y la semántica es:

- Se inicializa una variable de control.


- Se evalúa la condición de salida y si es falsa se termina le ejecución del bucle.
- Si la condición es verdadera se ejecuta el cuerpo del bucle y la sentencia de actualización, que es una
actualización de la variable de control inicializada.

Algunos lenguajes ofrecen la posibilidad de interrumpir la ejecución del cuerpo del bucle dentro del interior del
mismo utilizando la sentencia break, pero complica la semántica de los bucles y otros lenguajes como Pascal no lo
permiten.

8.4 EXCEPCIONES
Son instrucciones de control no explicitas (al contrario que los bucles que si lo son), pues su cometido es controlar
aquellos eventos que tengan lugar durante la ejecución del programa que violen las restricciones semánticas del
lenguaje. Asi, cuando ocurre una excepción, se dice que esta se lanza y en ese momento una parte del código del
programa llamada manejador de excepciones entrara en ejecución para capturar dicha excepción e intentar contener
el error de forma que la ejecución normal del programa se vea afectada lo menos posible.

Si un lenguaje no incorpora mecanismos de control de excepciones, la ejecución de los programas se detendrá, lo


que no es deseable desde el principio de diseño de confiabilidad, ya que un programa debería ser capaz de recuperarse
ante determinados errores.
Hay dos tipos de excepciones:

- Excepciones asíncronas: Pueden suceder en cualquier punto de la ejecución del programa con independencia
del código del mismo.

- Excepciones síncronas: Errores que pueden surgir como respuesta a la ejecución de los programas.

Si un lenguaje incluye manejadores de excepciones, la tarea de prever todas las posibles excepciones en todos los
posibles puntos donde estas pueden surgir se simplifica y pasa por considerar los siguientes puntos:

1. Definir las posibles excepciones que puedan surgir.


2. Definir los manejadores de excepciones para controlar dichas excepciones y los mecanismos para que se pase
el control de la ejecución al manejador de excepciones y se lo devuelva al programa.

8.4.1 Definición de excepciones

Java diferencia dos tipos de excepciones:

- Excepciones comprobadas: Representan un error del cual el programa puede recuperarse. Se denominan
comprobadas porque el compilador de Java comprueba si existe un manejador que las trate. Si el
programador quiere definir las suyas propias, las hace heredar de la clase Exception.

- Excepciones no comprobadas: Representan errores de programación, como acceder a un componente


inexistente de un array. El compilador de Java no comprueba que exista un manejador para su tratamiento.
Si el programador quiere definir las suyas propias, las hace heredar de la clase RuntimeException.

8.4.2 Definición de manejadores de excepciones y control de flujo

Los manejadores de excepciones de Java se realizan mediante bloques try-catch-finally. Las sentencias que
puedan provocar una excepción deben ser encerradas dentro de un bloque try {…} al que le deben seguir tantos
bloques catch (excepción) {…} como excepciones diferentes se quieran capturar. Por último, se puede incluir
un bloque finally {…} que encerrara una serie de sentencias que se ejecutaran, en cualquier caso, incluso si no
se produce ninguna excepción.

Si una excepción comprobada se puede lanzar en un método y no es capturada en dicho método, debe indicarse
en la cabecera del método con throws. Se podría añadir esta sentencia a la cabecera del main, en cuyo caso la
excepción abortaría la ejecución del programa.

Las sentencias en el bloque try posteriores al lanzamiento de la ejecución no se ejecutarán. Las sentencias catch
se evalúan por orden, y se aplican las reglas de tipo-subtipo de las relaciones de herencia.

El mecanismo de las excepciones introduce una penalización en la ejecución.

TEMA 9: CONTROL: SUBPROGRAMAS Y AMBIENTES


9.1 SUBPROGRAMAS
Este concepto se refiere a bloques de sentencias que reciben una serie de parámetros de entrada y realizan una serie
de operaciones sobre los mismo. Su ejecución se difiere hasta que en otro punto del programa son llamados de forma
adecuada.

Si tras finalizar la ejecución se devuelve un valor se denominan funciones y su invocación es una expresión y no una
sentencia. Si no se devuelve un valor se suelen denominar procedimientos (Pascal) o métodos (Java) y su invocación
es una sentencia. Una función no debería producir ningún efecto colateral, mientras que un procedimiento si podría
hacerlo.
Muchos lenguajes no hacen distinción explicita en este sentido entre funciones y procedimientos y permiten la
creación de funciones que producen efectos colaterales.

Para definir un subprograma es necesario realizar su declaración, que consiste en lo siguiente:

- Interfaz: Contiene la definición del identificador del subprograma y la lista de parámetros de entrada y el tipo
de valor devuelto (si el subprograma devuelve uno).

- Cuerpo: Consiste en el bloque de sentencias que se ejecutarán cuando el subprograma sea invocado.

En Java no existe una gran diferencia entre declarar métodos o funciones, solo en el tipo del valor devuelto, que en
el caso de los métodos es void y no requieren de un return. En otros lenguajes sí que existe diferencia en las
declaraciones, como en Pascal, que utiliza la palabra reservada Procedure para la creación de procedimientos y
Function para la creación de funciones. En los lenguajes funcionales solo se permite la declaración de funciones que
siempre han de devolver un resultado.

9.1.1 Semántica

El significado de los identificadores viene determinado por el entorno o ambiente, que se encarga de gestionar
los vínculos de dichos identificadores con las posiciones de memoria donde se encuentran sus valores o código.

Los subprogramas pueden contener identificadores locales, por lo que necesitan que se asigne memoria para
ellos. Esta memoria se denomina registro de activación y se dice que el subprograma está activado si se está
ejecutando bajo las pautas dictadas por su registro de activación.
Cuando se invoca un subprograma, se activa su registro de activación. Cuando hay varios subprogramas activos,
hay varios registros de activación en memoria, uno por cada subprograma de ejecución. Para resolver referencias
no locales, los registros de activación suelen tener un enlace al ambiente local.

Ejemplo: Considérese el siguiente programa en java

1 static int x;
2 static void incrementa () {
x++;
4}
5 static void suma (int i) {
incrementa ();
int z = x + y;
System.out.println (z);
9}
10 public static void main (String [] args) {
x = 1;
suma (3);
13 }

En la línea 1 se declara la variable x, la cual se asigna en el registro de activación del ambiente global, por lo
que el estado de los registros de activación al comenzar la ejecución del método main (línea 10) es:

En la línea 12 se invoca al método suma, por lo que el control pasa a la línea 5. En dicho método se utiliza el
parámetro y, y una variable local z (línea 7), por lo que durante su ejecución, el estado de los registros de
activación es:
Al comenzar el método incrementa, ya que este no añade ningún identificador adicional, se añadiría un registro
de activación que no incluyen nuevos identificadores.

Dado que incrementa se llama desde suma, la activación de incrementa debe guardar información sobre la
activación de suma, para que al terminar la ejecución de incrementa (línea 4) se devuelva el control de la
ejecución a la siguiente instrucción tras la llamada (línea 7).
Sin embargo, incrementa necesita acceder a la variable global x declarada en la línea 1, la cual se encuentra en
el registro de activación del ambiente global. Esto se conoce con el nombre de referencia no local y requiere
que durante la ejecución de un subprograma se tenga información sobre el registro de activación del
subprograma que lo rodea.

Bajo la regla del alcance léxico y, dado que el método suma no rodea al método incrementa, toda referencia no
local realizada en el cuerpo de incrementa se referirá al ambiente global, que es el que rodea a incrementa.

Esta distinción entre ambientes se realiza de la siguiente manera:

- Ambiente de definición: Es aquel en el que se encuentra la definición de un subprograma. Se determina


en tiempo de compilación.

- Ambiente de invocación: Es aquel desde el que se invoca a un subprograma. Se determina en tiempo de


ejecución.

Asi, las referencias no locales se refieren siempre al ambiente de definición, por lo que un subprograma nunca
podrá comunicarse con su ambiente de invocación. Esto se resuelve mediante el paso de parámetros.

Para diferenciar entre los identificadores de los parámetros utilizados dentro del cuerpo del subprograma y los
valores que se les asigna en las diferentes llamadas, a los primeros se les denomina parámetros formales y a los
segundos, parámetros actuales.

9.1.2 Paso de parámetros

- Paso por valor.

Es el mecanismo más utilizado. Las expresiones utilizadas al definir los parámetros actuales son evaluadas
previamente a la ejecución del mismo y los valores resultado se asignan a los parámetros formales para que
puedan ser referenciados dentro del cuerpo del subprograma.

- Paso por referencia.

Este mecanismo requiere que los parámetros actuales sean variables que tengan asignada una dirección
dentro de la memoria. Asi, en lugar de pasar al cuerpo del subprograma el valor de la variable, lo que se le
está pasando es la dirección de dicha variable, por lo que todas las modificaciones que se efectúen sobre el
parámetro formal dentro del cuerpo del subprograma se estarán efectuando, en realidad sobre la variable
utilizada como parámetro actual.
- Paso por resultado, por copia-restauración o por copia de entrada y salida.

Este mecanismo requiere que los parámetros actuales sean variables. En el cuerpo del programa los
parámetros formales son tratados como variables y, al igual que en el mecanismo de paso por valor, el valor
del parámetro actual es copiado a la variable que representa el parámetro formal. Dicha variable es utilizada
en el interior del cuerpo del subprograma pudiendo ser modificada y, al terminar la ejecución del cuerpo,
el valor final de la variable del parámetro formal es copiado nuevamente a la variable del parámetro actual.

Una variación de este mecanismo consiste en no inicializar la variable parámetro formal con el valor de la
variable usada como parámetro actual y solo realizar la restauración del valor final. Esta variante se conoce
con el nombre de paso por resultado.

- Paso por nombre.

La expresión utilizada como parámetro actual en la llamada al subprograma se sustituye directamente en


las apariciones del parámetro formal dentro el cuerpo del mismo. Este es un mecanismo de paso de
parámetros utilizado en una evaluación no estricta.

La ventaja que ofrece este mecanismo es que, si un parámetro del subprograma no se utiliza, no se pierde
tiempo en realizar su evaluación, pero si se utiliza varias veces, deberá ser evaluado en múltiples ocasiones.
Una forma de solucionar esto es recurrir a la memoización (proceso de almacenar el valor de una expresión
la primera vez que se evalúa) para evitar repetir cálculos ya realizados. El paso de parámetros por nombre
más la memoización dan lugar al paso por necesidad.

Este mecanismo es el usado para el paso de parámetros en los lenguajes puramente funcionales.

9.2 AMBIENTES DE EJECUCIÓN


- Ambientes totalmente estáticos.

Se usa para lenguajes sin anidamiento ni recursión de subprogramas.

Las referencias no locales serán siempre a variables globales y, en consecuencia, toda la gestión de la memoria
se puede realizar de forma estática, por tanto, el registro de activación de un subprograma será siempre el mismo.
Asi, cada registro de activación y el ambiente de un programa en Fortran77 con diferentes subprogramas tendrán
la siguiente estructura:

- Ambientes basados en pilas con recursión sin anidamiento.

La forma de resolver la recursión es estructurar los registros de activación en una pila de registros de activación.
En cada uno de estos registros de activación será necesario almacenar la misma información ya almacenada en
un registro de activación para un ambiente estático.

Sin embargo, también es necesario almacenar la información sobre donde comienza el registro de activación
del subprograma que actualmente se está ejecutando. Esta información se debe almacenar externamente a la pila
de registros de activación, normalmente en un registro que se conoce con el nombre de puntero de ambiente
(ep).
Además, cada registro de activación deberá almacenar adicionalmente la dirección de inicio del registro de
activación del subprograma invocador, de forma que se pueda restaurar el valor del puntero de ambiente al salir
del subprograma y volver al invocador. Esta información se conoce como enlace de control.

- Ambientes basados en pilas con recursión y anidamiento.

Se hace necesario que un subprograma también almacene la información de cuál es su ambiente de definición,
lo que se conoce como enlace de acceso.
Asi, a la hora de resolver una referencia no local a una variable, se deberá seguir el enlace de acceso para buscar
la variable dentro del ambiente de definición del subprograma en ejecución. Este proceso se denomina
encadenamiento de accesos y el número de enlaces de acceso que se deben seguir hasta encontrar la variable se
denomina profundidad de anidamiento entre el ambiente de acceso y el ambiente de definición de la variable.

- Ambientes totalmente dinámicos.

En este tipo de lenguajes no es posible que la gestión del ambiente se base en pilas, porque el enlace de acceso
de un subprograma apuntaría a su ambiente de definición, el cual podría haber desaparecido en el momento en
el que el subprograma sea invocado.
La forma de solucionar este problema es no eliminar del ambiente el registro de activación de un subprograma
mientras existan referencias a objetos definidos localmente en él.

Los ambientes dinámicos necesitan mecanismos para recuperar de forma automática la memoria que ya no es
accesible, como los conteos de referencias o la recolección de basura.

Vous aimerez peut-être aussi