Apuntes sobre Paradigmas de Programación

Anuncio
Apuntes sobre Paradigmas de Programación
Por Dr. Ignacio Ponzoni
Correcciones 2010: Dra. Jessica A. Carballido
Departamento de Ciencias de la Computación
Universidad Nacional del Sur
Prólogo
Existen varias maneras de clasificar los lenguajes. Una de las formas más empleadas
consiste en agruparlos según el paradigma de programación que soportan. Para
comprender esta clasificación debemos comenzar por establecer el significado de la
palabra “paradigma”.
Este vocablo, surgido en la Grecia Clásica, tiene 2500 años de antigüedad y proviene
de la conjunción de los términos: para, ‘lado por lado’, y deigma, ‘tal como se muestra’.
Originariamente, esta expresión se utilizaba durante la exhibición de productos en el
mercado. Posteriormente adquirió el significado de ‘ejemplo’. En esa época, era común
enseñar a través de casos particulares, luego un paradigma era un ejemplo “bien
establecido” para el aprendizaje de un concepto específico. Hoy en día, se sigue hablando
de casos paradigmáticos para expresar un ejemplo que reúne las características
sobresalientes de una familia de entidades. Esta última constituye la definición humanística
de la palabra paradigma.
En el campo de las ciencias, tiene una acepción algo diferente. Aquí, el vocablo
refiere a un ejemplo tomado como punto de partida para el desarrollo de una teoría o
método. A modo de ilustración, se puede afirmar que el movimiento de los planetas fue el
paradigma usado para establecer las Leyes de la Mecánica. Luego, un paradigma bien
elegido usualmente conduce a una teoría o metodología exitosa.
En Ciencias de la Computación, y en otras ramas del conocimiento, un paradigma es
un conjunto coherente de métodos, más o menos efectivo, para el manejo de un tipo de
problemas, también llamado dominio de aplicación. A manera de ejemplo, la democracia,
oligarquía y dictadura pueden constituir paradigmas de gobierno.
Usualmente, un paradigma puede ser caracterizado mediante un principio básico,
fácil de formular. Para el caso anterior, en una dictadura la autoridad se concentra
usualmente en una única persona, en una oligarquía el poder es ejercido por una pequeña
elite y en democracia recae sobre todos los ciudadanos. Por otra parte, para llevar a la
práctica este principio, es necesario establecer un conjunto de métodos y técnicas.
Prosiguiendo con nuestro ejemplo, una democracia requiere de un sistema electoral que
permita establecer los gobernantes. Asimismo, estas técnicas deben ser soportadas por una
serie de conceptos, a fin de asegurar un funcionamiento apropiado de las mismas. Luego,
el sistema electoral requerirá de conceptos tales como elección, representación, ballotage,
etc. Es importante notar, que al igual que los métodos, estos conceptos tienen su raíz en los
principios fundamentales que guían al paradigma.
Volviendo al tema que nos ocupa, varios paradigmas de programación han sido
establecidos desde los inicios de la computación. De los cuales, rescatamos como más
prominentes a cuatro de ellos: el imperativo, el funcional, el lógico y el orientado a objetos.
Durante los primeros años de la carrera, los alumnos han adquirido sólidamente los
conceptos inherentes a cada enfoque. Por tal motivo, en la práctica de Lenguajes de
Programación trataremos de concentrar esfuerzos para alcanzar una visión más global de
los distintos paradigmas, trazando paralelos y estableciendo distintos puntos de
comparación entre los diferentes tipos de lenguajes. Por otra parte, y con el fin de mejorar
la comprensión de los tópicos expuestos, se presentará con cada paradigma un lenguaje
que lo soporta. En tal sentido, diremos que un lenguaje soporta un paradigma si provee
mecanismos que facilitan su aplicación de manera eficiente y natural.
El espíritu de estas notas es introducir a los estudiantes en los aspectos esenciales de
cada paradigma. Lógicamente, un análisis exhaustivo de los temas escapa al alcance del
curso. No obstante, se recomienda la lectura de los libros y artículos citados en este
trabajo. El apunte está organizado en tres partes. La primera, describe el paradigma
funcional empleando Standard ML como lenguaje de referencia. El segundo, presenta el
paradigma lógico, utilizándose Prolog como caso de estudio. Finalmente, el paradigma
orientado a objetos es ilustrado a través de Smalltalk. En todos los casos, el énfasis esta
puesto en las características que soportan el paradigma. Sin embargo, con frecuencia los
lenguajes pierden parte de su pureza al incorporar prácticas propias de otros paradigmas,
las cuales son introducidas para satisfacer requerimientos de eficiencia y algunas
cuestiones de índole comercial. Esto no debe confundirse con la denominada “integración
de paradigmas”. Esta nueva tendencia persigue la integración de los distintos conceptos y
métodos de manera apropiada, a partir de una propuesta conceptualmente clara. La idea
es integrar, no mezclar.
Desde un punto de vista pedagógico, se espera que este apunte resulte de fácil
lectura, liberando a los alumnos de la necesidad de tomar notas. De este modo, se busca
agilizar el dictado de la práctica. Asimismo, se descuenta la colaboración de los
estudiantes, quienes con sus comentarios y sugerencias pueden ayudar a mejorar este texto.
Dr. Ignacio Ponzoni
Bahía Blanca, 6 de abril de 2001
TABLA DE CONTENIDOS
1. INTRODUCCIÓN
1
Breve reseña histórica
1
2. CONCEPTOS BÁSICOS
2
2.1 OBJETOS
2.2 FUNCIONES PRIMITIVAS
2.3 FORMAS FUNCIONALES
2.4 OPERACIÓN DE APLICACIÓN
2
2
3
3
3. PROPIEDADES DE LOS LENGUAJES FUNCIONALES
3
3.1 SEMÁNTICA DE VALORES
3.2 TRANSPARENCIA REFERENCIAL
3.3 FUNCIONES COMO VALORES DE PRIMERA CLASE
3.4 CURRYING
3.5 EVALUACIÓN PEREZOSA (LAZY EVALUATION)
3
3
4
4
5
4. ML: UN CASO DE ESTUDIO.
7
4.1 PRINCIPALES CARACTERÍSTICAS DE ML
Unit
Booleanos
Enteros
Cadenas
Números Reales
Tipos de Datos Estructurados
Tuplas (Producto Cartesiano)
Listas (Secuencia)
Registros (Producto Cartesiano)
7
8
8
9
9
9
9
9
9
9
9
10
10
10
11
4.2.2 IDENTIFICADORES, LIGADURAS Y DECLARACIONES
Inferencia de Tipos
11
12
Sobre el software empleado por la Cátedra
Interacción con ML
4.2. ELEMENTOS BÁSICOS DE ML
4.2.1 EXPRESIONES BÁSICAS, VALORES Y TIPOS
Tipos de Datos Simples
Declaraciones Locales
4.2.3 PATTERN MATCHING
4.2.4 DEFINICIÓN DE FUNCIONES
Tipo Función
Funciones Definidas por el Usuario
Restricciones de Tipo
Funciones Definidas mediante Cláusulas
Declaraciones Locales
Referencias a Variables No Locales
Funciones Curried
Formas Funcionales
Composición
Aplicar a Todos
Operador de Inserción
4.2.5 POLIMORFISMO Y SOBRECARGA
4.2.6 DEFINICIÓN DE TIPOS
Abreviación de Tipo
Definición de Tipo
Definición de un Tipo de Dato Abstracto
4.2.7 DEFINICIÓN DE MÓDULOS
Structures
Signatures
Functors
4.2.8 MANEJO DE EXCEPCIONES
Declaración de Excepciones
Activación de Excepciones
Excepciones predefinidas
Manejadores de excepciones
REFERENCIAS
12
13
15
15
16
17
17
18
18
19
19
20
20
20
20
21
21
21
23
23
24
24
Error! Bookmark not defined.
26
26
27
27
28
28
PARADIGMA FUNCIONAL
1. INTRODUCCIÓN
Los lenguajes imperativos permiten a los programadores expresar algoritmos, los
cuales son descripciones precisas de “cómo” resolver determinados problemas. El modelo
de computación subyacente usa un estado, conformado por las variables manipuladas por
el programa, el cual es modificado repetidamente a través de sentencias de asignación. La
computación termina al alcanzarse un estado final. A grandes rasgos, este modelo puede
ser implementado almacenando el estado en la memoria principal y traduciendo las
operaciones sobre el estado en instrucciones de la máquina.
Como apreciará el lector, este modelo está fuertemente influenciado por la
arquitectura de von Neumann. Justamente, esta última característica constituye la principal
ventaja, como también la mayor desventaja, de los lenguajes imperativos. Por un lado,
estos lenguajes pueden ser implementados de manera muy eficiente debido a que el mapeo
con el hardware resulta bastante directo. Como contrapartida, el paradigma imperativo
restringe el modelo mental del programador a la estructura de la máquina. La persona
debe pensar en función de nombres de locaciones de memoria (variables) cuyos contenidos
deberán ser modificados hasta alcanzar la solución (estado final del computo). De este
modo, se produce un alejamiento entre el planteo natural de la solución de un problema y
su implementación a través del lenguaje.
Esto último motivó el desarrollo de modelos alternativos que no reflejasen la
arquitectura de von Neumann. La idea central de estos nuevos enfoques consistió en
especificar “qué” se desea computar, en lugar de “cómo” la computación debe ser llevada
a cabo. Los modelos que persiguen este objetivo se denominan declarativos. Básicamente,
podemos distinguir dos paradigmas que responden a esta filosofía: el paradigma funcional
y el paradigma lógico. En este apunte nos concentraremos en el primero de ellos, el cual
ilustraremos mediante el uso del lenguaje funcional ML.
Breve reseña histórica
Sin duda alguna, Lisp fue el primero de los lenguajes funcionales. La versión original
de Lisp, desarrollada por John McCarthy hacia fines de los años 50, estaba orientada a la
computación simbólica. Este lenguaje introdujo varios conceptos novedosos para la época.
En particular, la implementación a través de intérpretes, la cual es común a la mayoría de
estos lenguajes.
APL y SNOBOL4 surgieron en la década del 60. El primero, diseñado por Kenneth
Iverson, fue ampliamente utilizado en ambientes científicos dada su versatilidad para el
manejo de problemas numéricos. El segundo, concebido por Ralph Griswold para la
manipulación de textos, provee facilidades para el trabajo con cadenas de caracteres.
A principios de los años 70, John Backus presenta una fuerte crítica a los lenguajes
imperativos. En dicho trabajo, introduce un lenguaje funcional puro al que denomina FP.
En realidad, FP constituye una familia de lenguajes, donde cada uno provee un conjunto
de funciones y formas funcionales. En estos años también surge un dialecto de Lisp
conocido como Scheme. Este, a diferencia de su antecesor, adopta reglas de alcance
estático.
2
Apuntes sobre Paradigmas de Programación
Paradigma Funcional
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
En los años 80 surgen los primeros lenguajes funcionales modernos. El primero de
ellos, denominado ML, fue propuesto por Robin Milner. ML es fuertemente tipado y brinda
soporte para conceptos tales como encapsulamiento y modularidad. Este lenguaje será
tomado como caso de estudio durante el presente cuatrimestre.
Miranda, presentado por A. Turner en 1986, y Haskell, diseñado A. Davie en 1992,
cierran el panorama actual dentro de los lenguajes funcionales. Ambos resumen el estado
del arte en lo que refiere al paradigma funcional.
2. CONCEPTOS BÁSICOS
El paradigma funcional (ó aplicativo) se fundamenta en la evaluación de
expresiones construidas a partir de la combinación de funciones. Una función, al igual que
en matemática, mapea valores tomados de un conjunto, denominado dominio, en valores de
otro conjunto, conocido como rango o codominio. Básicamente, los lenguajes funcionales
se caracterizan por cuatro componentes:
•
Un conjunto de Objetos1: son los objetos miembros de los dominios y rangos de
las funciones.
•
Un conjunto de Funciones Primitivas: son aquellas predefinidas por el lenguaje.
•
Un conjunto de Formas Funcionales: son mecanismos que permiten combinar
funciones y/o objetos con el fin de construir nuevas funciones.
•
Una operación de Aplicación: es un mecanismo predefinido por el lenguaje para
aplicar la función a sus argumentos.
2.1 Objetos
El conjunto de objetos provistos por un lenguaje funcional depende de las
características del mismo. Por ejemplo, FP es un lenguaje no tipado donde un objeto puede
ser un átomo, una secuencia de objetos ó el elemento ⊥ (indefinido). En cambio, Standard
ML es fuertemente tipado y sus objetos deben ser de algún tipo predefinido (boolean,
string, integer, real) o de un tipo definido por el programador.
2.2 Funciones Primitivas
Las funciones predefinidas por los lenguajes dependen del dominio de aplicación del
mismo. Por ejemplo, la primera y más pura versión de Lisp contaba sólo con cinco
funciones primitivas (ATOM, EQ, CAR, CDR y CONS) destinadas al manejo de listas. En
contraste, APL, ideado para el rápido desarrollo de prototipos en aplicaciones numéricas,
posee una gran cantidad de funciones vinculadas a la operación de matrices. Por otra
parte, SNOBOL4, otro lenguaje surgido en la década del 60, fue diseñado para el
procesamiento de texto y provee principalmente funciones para la manipulación de cadenas
de caracteres.
1
Resulta importante aclarar que el término objeto posee diferentes significados según el contexto.
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
3
Paradigma Funcional
2.3 Formas Funcionales
Una función se dice de primer orden si ninguno de sus argumentos y/o resultado es
una función. Las formas funcionales, también llamadas funciones de alto orden, son
aquellas cuyos argumentos y/o resultado son funciones. Un ejemplo, familiar al lector, es la
composición. Las funciones de alto orden constituyen junto con las de primer orden la base
para la construcción de funciones más poderosas. En tal sentido, el conjunto de formas
funcionales provistas por un lenguaje puede tener un fuerte impacto en el poder expresivo
del mismo. En contrapartida, el exceso en el número de funciones primitivas puede ir en
detrimento de la sencillez del lenguaje sin aumentar su expresividad. Nuevamente, existen
notorias diferencias entre un lenguaje y otro. En FP existen ocho formas funcionales,
mientras que APL sólo provee tres. En tal sentido, Backus [1978] opina que las funciones
de alto orden provistas por APL resultan insuficientes y difíciles de usar.
2.4 Operación de Aplicación
La operación de aplicación de funciones constituye el principal mecanismo de
control de los lenguajes funcionales. Una función puede llamar a otra. Más aún, una
función puede ser recursiva, ya sea de manera directa o indirecta. La aplicación puede ser
definida de distintas formas. Por ejemplo, en algunos lenguajes, tales como Miranda,
Gofer y Standard ML, se soporta la aplicación parcial (currying). Además, existen otros
aspectos (como por ejemplo la evaluación perezosa) que determinan la semántica de la
operación de aplicación. En las siguientes secciones se profundizará en estos temas.
3. PROPIEDADES DE LOS LENGUAJES FUNCIONALES
Los lenguajes funcionales puros poseen varias propiedades que los distinguen. En
esta sección se presentan algunas de sus características más relevantes.
3.1 Semántica de Valores
Desaparece la noción de estado, subyacente al Modelo de von Neumann, dado que
no existen los conceptos de locación de memoria (cuya abstracción son las variables) y de
modificación de las mismas (sentencia de asignación). Luego, el ambiente de ejecución
asocia variables a valores únicamente (no a locaciones de memoria). Por otra parte, una
variable dentro de un ambiente no puede cambiar su valor. Esto favorece fuertemente la
demostración formal de programas.
3.2 Transparencia Referencial
Un sistema se dice referencialmente transparente si, en un determinado contexto, la
semántica del sistema puede ser determinada a partir de la semántica de sus partes, sin
importar las computaciones previas ni el orden de evaluación. Los lenguajes funcionales
puros poseen transparencia referencial. En ellos, una función computa valores basándose
únicamente en sus parámetros de entrada, también llamados argumentos. Por otra parte,
la ejecución de una función no produce efectos colaterales.
4
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
3.3 Funciones como Valores de Primera Clase
Los lenguajes funcionales tratan a las funciones como valores de primera clase. Es
decir, ellas pueden ser pasadas como argumentos, retornadas como resultados de otras
funciones, y compuestas a partir de otros valores (funciones). Esto genera un fuerte
impacto en el estilo de programación, contribuyendo entre otras cosas a la regularidad y
flexibilidad de estos lenguajes.
3.4 Currying
La aplicación funcional constituye el mecanismo de control fundamental de los
lenguajes funcionales. En general, una aplicación tiene la forma:
e e’
donde e y e’ son expresiones.
Esto establece que la expresión e será aplicada a la expresión e’. Esta simple frase
posee fuertes implicaciones. Por ejemplo, asuma la existencia de una función doble, con
dominio en los enteros, que retorna el valor de su argumento multiplicado por 2. Luego,
doble 4*5
es una aplicación funcional tal que e= doble y e’=4*5. El resultado de esta aplicación es
40. En términos generales, la secuencia es:
•
•
•
evaluar e, obteniendo una función f (en nuestro caso resulta trivial, f es doble)
evaluar e’, obteniendo un argumento x (en nuestro caso x es 20)
aplicar la función f a x.
Sin embargo, hay un caso especial en lo que concierne a la aplicación funcional. Este surge
al definir una función de manera curried, como por ejemplo:
- fun add x y = x + y; (dos argumentos, x e y, y no (x,y), que sería una tupla)
val add = fn : int -> int -> int
Esta función puede ser aplicada:
- add 3 5;
val it = 8: int
Notar que la diferencia está en el tipo de add que es: int -> (int -> int) (recordar que el
operador -> asocia a derecha).
Lo que sucede al hacer add 3 5 es:
- (add 3) 5;
val it = 8 : int
ya que add es una función que toma un entero y produce una función que a su vez, toma un
entero y produce un entero. Esto es, add 3 produce una función que luego es aplicada con
el argumento 5. En general, una aplicación del tipo f x y z se resuelve como (((f x) y) z). De
esta manera, también podemos hacer:
5
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
- val suma3 = add 3;
obteniendo a partir de esta aplicación parcial una nueva función llamada suma3 que recibe
un argumento de tipo entero y retorna el mismo incrementado en 3 unidades.
La idea principal del currying consiste en que permite reducir funciones con
múltiples argumentos a funciones de menos argumentos. En particular, la más importante
utilidad de las funciones curried es que si se las aplica parcialmente, obtenemos nuevas
funciones.
El currying permite además evaluar un número variable de argumentos. Cada
argumento es manejado en secuencia a través de una aplicación funcional. Cada
aplicación reemplaza una de las variables ligadas, resultando en una función
“parcialmente evaluada” que puede ser aplicada al siguiente argumento. Esto es lo que se
conoce como aplicación parcial o parametrización parcial según el autor. Como dijimos
previamente, en general, una función f (x,y,x) puede ser escrita en forma curried como g x
y z, lo que corresponde a (((g x) y) z). En este caso, g x retorna una función que será
aplicada a y. La función resultante de esta última aplicación será luego aplicada a z.
Veamos un ejemplo: sea f x y z = 3*x+y*z
fxyz
↓
gyz
↓
g’ z
↓
valor
f 2 1 4 = 3*x+y*z
↓
g 1 4 = 3 * 2 + y*z
↓
g’ 4 = 3 * 2 + 1 * z
↓
10
Note que luego de cada aplicación parcial se obtuvo una nueva función con “un argumento
menos” que la anterior hasta llegar a una función de “un argumento”, que en realidad es
la función cuyo resultado no constituye una nueva función. La evaluación de esta última
función (g’ en el ejemplo) da como resultado un valor entero.
3.5 Evaluación Perezosa (Lazy Evaluation)
Una manera simple de aplicar una función es evaluar primero sus argumentos y
luego evaluar la función. Por ejemplo, en:
mult (fact 3) (fact 4)
las aplicaciones (fact 3) y (fact 4) deben ser evaluadas en primer término, en un orden
arbitrario, reduciendo la expresión a:
mult 6 24
Posteriormente, la función mult es evaluada con argumentos 6 y 24, dando como resultado
final 144. Esta manera de reducir la expresión original se conoce como reducción de
orden aplicativo.
6
Apuntes sobre Paradigmas de Programación
Paradigma Funcional
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Una alternativa es reducir las expresiones yendo de afuera hacia adentro. De este
modo, las subexpresiones recién son evaluadas cuando sus valores son necesarios. Este
enfoque, se denomina reducción de orden normal. Volviendo al ejemplo anterior:
mult (fact 3) (fact 4)
es reducida en primer lugar a: (fact 3) * (fact 4)
y luego a: 6*24
Para obtener finalmente el valor 144.
El lector acostumbrado a la programación imperativa puede pensar que la reducción
de orden aplicativo resulta más natural. Sin embargo, esta presenta algunas desventajas.
Suponga que se necesita implementar una función cond con tres argumentos, donde el
primer argumento, b, es un valor booleano. Si b es true, cond devuelve el segundo
argumento, en caso contrario retorna el tercero:
cond b x y = if b then x else y
Si todos los argumentos de cond son evaluados antes de aplicar la función, existen dos
problemas:
•
•
Uno de los argumentos de cond es evaluado innecesariamente.
Si la evaluación innecesaria de un argumento no termina, entonces la expresión
completa no finalizará.
Como un ejemplo de esto último, considere la siguiente definición de la función factorial:
fac n = cond (n=0) 1 (n*fac(n-1))
Si el tercer argumento de cond es siempre evaluado, la función fac nunca termina.
Estos problemas conducen al concepto de evaluación perezosa (lazy), la cual usa
reducción de orden normal. La idea es evaluar los argumentos de una función sólo cuando
es indispensable, y evaluarlos una única vez para aquellos que estén repetidos. En
contraste, la reducción de orden aplicativo siempre evalúa sus argumentos, esto se conoce
como evaluación ansiosa (eager). Para el ejemplo anterior, si n es igual 0 la expresión
(n*fac(n-1)) jamás será reducida por la evaluación perezosa, pero sí por la evaluación
ansiosa. Los lenguajes basados en esta última se denominan de semántica estricta. En
contrapartida, los lenguajes que usan evaluación perezosa poseen semántica no estricta,
tal es el caso de Miranda. Standard ML emplea evaluación ansiosa dado que tiene
asociada la semántica call-by-value para los argumentos. Sin embargo, presenta otros
aspectos de diseño del lenguaje que son “perezosos” (tal es el caso de las expresiones “ifthen-else” y “handle” entre otras). Por otro lado, existe una versión conocida como Lazy
ML, que soporta evaluación perezosa.
Costo Computacional
Lamentablemente, la evaluación perezosa tiene asociado un alto costo computacional.
Un problema es que la evaluación de orden normal puede evaluar la misma expresión más
7
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
de una vez. Esto no sucede con la evaluación de orden aplicativo. A manera de ilustración,
considere la siguiente función:
doble x = x + x
La evaluación de orden normal aplica doble 23*45 como sigue:
doble 23*45 ⇒ 23*45 + 23*45 ⇒ 1035 + 23*45 ⇒ 1035 + 1035 ⇒ 2070
mientras que la evaluación de orden aplicativo sólo efectúa las siguientes reducciones:
doble 23*45 ⇒ doble 1035 ⇒ 1035 + 1035 ⇒ 2070
Lo que resulta poco eficiente. Este problema puede resolverse con la técnica de reducción
de grafos de Peyton Jones [1987], la cual evalúa las expresiones a lo sumo una vez.
Otro problema es que la evaluación de los argumentos es más sencilla cuando se
realiza antes de la llamada, y por lo tanto, la postergación de estas evaluaciones ocasiona
un significativo overhead en la evaluación perezosa. No obstante, algunas optimizaciones
pueden bajar este costo.
Como ya se mencionó, no todos los lenguajes funcionales soportan la evaluación
perezosa. No obstante, muchos proveen algunas funciones predefinidas con semántica
perezosa. Por ejemplo, como se dijo en la sección anterior, en Standard ML existen
primitivas evalúan su segundo argumento sólo si es necesario (ejemplos adicionales son
“ándalos” y “orelse”). Características similares son provistas por lenguajes imperativos,
tal es el caso de C y Ada.
4. ML: UN CASO DE ESTUDIO.
En esta sección se describe un lenguaje, denominado Standard ML, que soporta el
paradigma funcional. El objetivo es introducir a los alumnos en la programación funcional
mediante la implementación de pequeños programas destinados a profundizar y afianzar
los conceptos vertidos en clase. Cabe destacar que nuestra meta no es formar expertos en
ML sino lograr que los estudiantes capturen la esencia del paradigma.
Varias razones motivaron la elección de este lenguaje. En primer lugar, ML permite
ejemplificar las principales características de la programación funcional. Por otro lado, es
uno de los lenguajes funcionales más difundidos. De hecho, buena parte de la bibliografía
actual (entre ellos Ghezzi[1998], Sethi[1992] y Watt[1990]) emplean ML para presentar
este paradigma. Finalmente, ML provee un fuerte soporte para aspectos tales como:
modularidad, encapsulamiento, tipos de datos abstractos, manejo de excepciones, etc.
4.1 Principales Características de ML
ML es un lenguaje funcional dentro de la tradición de ISWIM de Landin [1966], que
es un acrónimo de la frase en inglés “If you See What I Mean” (si entiendes lo que digo).
ISWIN está basado en Lisp y en el cálculo lambda. ML (Meta Language) fue creado por
Robin Milner en los años setenta, y con el tiempo surgieron varios dialectos. En 1983 se
inició un esfuerzo de normalización que dio como resultado Standard ML. La más reciente
8
Apuntes sobre Paradigmas de Programación
Paradigma Funcional
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
contribución al lenguaje fue el sistema de módulos desarrollado por Dave MacQueen
[1986]. De aquí en adelante diremos simplemente ML para referirnos a su versión
estándar. A continuación se enumeran las principales características del lenguaje:
•
Soporta el paradigma funcional. Las funciones son valores de primera clase: ellas
pueden ser pasadas como argumentos y retornadas como resultados. El principal
mecanismo de control en ML es la aplicación funcional.
•
Fuertemente tipado. Esta característica garantiza que un programa no incurra en
un error de tipos en tiempo de ejecución.
•
Sistema de tipos polimorfo. Esto contribuye a la flexibilidad del lenguaje.
•
Soporta tipos de datos abstractos. Nuevas declaraciones de tipos pueden ser
definidas por el programador, junto con una familia de funciones para la
manipulación de variables de dichos tipos. Los detalles de la implementación quedan
ocultos al usuario del tipo.
•
Alcance estático. Las referencias a identificadores se resuelven en tiempo de
compilación.
•
Mecanismos para el manejo de excepciones. Esto aumenta la confiabilidad de los
programas escritos en ML.
•
Soporta programación modular. En ML un programa es implementado como una
colección de “structures” interdependientes vinculados mediante “functors”. La
compilación separada de módulos es soportada a través de la importación y
exportación de “functors”.
En las siguientes subsecciones sólo se introducirán los aspectos más relevantes del
lenguaje. El lector interesado en profundizar sus conocimientos sobre ML puede consultar
el libro de Lawrence Paulson [1996] y los reportes de Robert Harper [1993] y Mads Tofte
[1997], en este último se brinda una minuciosa descripción del sistema de módulos de ML.
Sobre el software empleado por la Cátedra
En Lenguajes de Programación se adoptará como versión oficial el ML Works,
versión 2.0. La versión utilizada por la cátedra es de distribución gratuita.
Interacción con ML
La implementación de ML es interactiva, con una forma de diálogo “leer-evaluarmostrar” común a las implementaciones de otros lenguajes funcionales, por ejemplo Lisp.
En otras palabras, cada vez que una expresión es entrada, ML la analiza, compila y
ejecuta. Por ejemplo:
MLWorks> 3+2;
val it : int = 5
ML emplea el prompt “MLWorks>” para indicar que espera el ingreso de una expresión. El
usuario entra una expresión, por ejemplo “3+2”. Luego, se muestra el resultado de la
evaluación, “5”, precedido por su tipo, “int”. En este caso, como el resultado no es ligado
a una variable, el compilador lo liga automáticamente al identificador it.
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
9
Paradigma Funcional
4.2. Elementos Básicos de ML
Se brindará a continuación una descripción detallada de los principales aspectos
referidos al lenguaje. Las distintas funciones primitivas y formas funcionales provistas por
ML serán introducidas gradualmente.
4.2.1 Expresiones básicas, valores y tipos
Las expresiones en ML denotan valores de la misma manera que un numeral denota
un número. Cada expresión tiene asociado un tipo. El lenguaje provee un conjunto básico
de tipos predefinidos, más una colección de constructores para la definición de tipos de
datos estructurados.
Tipos de Datos Simples
Unit
El tipo unit consiste de un único valor, denotado como (). Este tipo es empleado
cuando no interesa el valor de una expresión, o cuando una función no tiene argumentos.
En tal sentido, unit se asemeja al tipo void de C.
Booleanos
El tipo bool consiste de los valores true y false. Las funciones not, andalso y orelse
son provistas como primitivas. Las funciones andalso y orelse realizan evaluaciones en
corto circuito.
Enteros
El tipo int está formado por el conjunto de enteros, positivos y negativos. Los enteros
se escriben de la manera usual excepto que los negativos van precedidos del caracter “~”.
Los operadores aritméticos +, -, *, div y mod, están disponibles, donde div y mod son la
división entera y resto respectivamente. También se proveen las operaciones relacionales
<, <=, >, >=, = y <>, las cuales toman como “argumentos” dos expresiones de tipo int y
retornan un valor de tipo bool.
Cadenas
El tipo string está constituido por un conjunto de secuencias finitas de caracteres. Las
strings se denotan de la manera tradicional (entre comillas). Para indicar una string
formada por una comilla se escribe “\””. ML provee dos primitivas para el manejo de
valores de este tipo. La primera, denominada size, retorna la longitud de una string
(medida en cantidad de caracteres). La segunda, permite concatenar dos strings y se
denota con el caracter ^. Por ejemplo:
MLWorks> "Lenguajes"^"Funcionales";
val it : string = "LenguajesFuncionales"
Números Reales
10
Apuntes sobre Paradigmas de Programación
Paradigma Funcional
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
El tipo correspondiente a los números en punto flotante es conocido en ML como
real. Estos números se escriben de la forma usual: un entero seguido de un punto y uno o
más decimales. También puede indicarse el exponente. Por ejemplo:
MLWorks> 3.14159;
val it : real = 3.14159
MLWorks> 3E2;
val it : real = 300.0
MLWorks> 3.14159E2;
val it : real = 314.159
Las funciones aritméticas predefinidas para los reales son: ~, +, -, *. Es importante
aclarar que no se pueden mezclar tipos en estas operaciones. Por ejemplo, un real sólo
puede ser sumado a otro real, y no a un entero. Esto constituye una diferencia importante
con respecto a los lenguajes que efectúan coerciones, por ejemplo Pascal. Los operadores
relacionales provistos son los mismos que para enteros. La función ‘/’ denota la división de
números reales. Por último, las funciones real y floor permiten la conversión explícita de
tipos entre enteros y reales. La primera toma por argumento un número entero y devuelve
el real correspondiente, mientras que floor trunca un real al mayor entero menor que él.
Esto completa la presentación de los tipos de datos simples predefinidos por ML. En
la siguiente subsección se describen los tipos de datos estructurados.
Tipos de Datos Estructurados
Tuplas (Producto Cartesiano)
El tipo σ*τ es el tipo de los pares ordenados cuyas primeras y segundas componentes
tienen tipo σ y τ respectivamente. Un par ordenado se escribe como (e1, e2), donde e1 y e2
son expresiones. Generalizando, podemos definir n-tuplas, con n ≥ 2, escribiendo las
expresiones separadas por comas y entre paréntesis. Por ejemplo:
MLWorks> (1, false, 17, "lenguajes");
val it : (int * bool * int * string) = (1, false, 17, "lenguajes")
Dos tuplas son iguales si cada una de sus componentes coincide en valor y posición.
Si se comparan dos tuplas con diferente tipo se produce un error de tipos.
Listas (Secuencia)
El tipo τ list consiste de una secuencia finita de valores de tipo τ. Por ejemplo, el tipo
int list refiere a listas de enteros, mientras que el tipo bool list list corresponde a listas de
listas de valores booleanos. Existen dos notaciones para las listas. La primera esta basada
en la siguiente caracterización recursiva: una τ list está vacía, o contiene un valor de tipo τ
seguido de una τ list. De acuerdo con esta visión, una lista vacía se representa mediante la
palabra reservada nil , mientras que una lista no vacía se denota como e :: l, siendo e una
expresión de tipo τ y l una τ list. El operador :: se pronuncia “cons”. En la otra notación,
denominada “forma comprimida”, se listan los elementos separados por comas y cerrados
entre corchetes. Por ejemplo:
MLWorks> (3 :: nil) :: (4 :: 5 :: nil) :: nil;
11
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
val it : int list list = [[3], [4, 5]]
Note que ML siempre muestra las listas en el formato comprimido.
La función length permite obtener la longitud de una lista. Por otra parte, dos listas
son iguales si tienen la misma longitud, y son iguales componente a componente.
Una cuestión interesante es: ¿cuál es el tipo de nil? Dada una lista vacía, no hay
manera de saber si esta es una lista de enteros, de booleanos o de cualquier otra cosa. ML
resuelve este problema asignando el tipo ‘a list a nil, donde ‘a es una variable cuyos
posibles valores pertenecen a una colección de tipos.
Un tipo que involucra variables de tipos (como ‘a) se denomina politipo. Por otra
parte, una instancia de un politipo se obtiene sustituyendo sus variables de tipos por tipos,
incluyendo politipos. Por ejemplo: int list y (int * ‘b) list son instancias del politipo ‘a list.
Luego, nil es tratado como un objeto polimorfo que puede ser considerado de diferentes
tipos según el contexto. Finalmente, un tipo que no contiene variables de tipos se denomina
monotipo. Este tema será profundizado en la sección de definición de funciones.
Registros (Producto Cartesiano)
El último tipo compuesto, considerado en esta sección, es el tipo record. Este es
similar a los registros de Pascal. Un registro consiste de un conjunto finito de campos
etiquetados, cada de los cuales contiene un valor de algún tipo. Los valores de los registros
se escriben como un conjunto de ecuaciones de la forma l = e, donde l es una etiqueta y e
una expresión. Las componentes de un registro se seleccionan por sus campos y no por sus
ubicaciones dentro del mismo. Nuevamente, la igualdad se define componente a componente.
Son ejemplos de registros:
MLWorks> {nombre="Jorge", edad=18};
val it : {edad: int, nombre: string} = {edad=18, nombre="Jorge"}
MLWorks> {nombre="Jorge", edad=18} = {edad = 3*6, nombre="Jor"^"ge"};
val it : bool = true
4.2.2 Identificadores, Ligaduras y Declaraciones
En ML todos los identificadores deben ser declarados antes de ser utilizados (los
nombres tales como + y size están pre-declarados por el compilador). El sistema mantiene
un ambiente con todas las ligaduras que el programa crea. Los identificadores pueden ser
empleados en distintas maneras en ML. En esta sección, se presentará solamente lo
concerniente a las ligaduras entre valores y variables.
Una variable es ligada a un valor como sigue:
MLWorks> val x = 4*5;
val x : int = 20
Para establecer una ligadura, ML evalúa el lado derecho de la ecuación y asocia dicho
valor al identificador de la izquierda. En el ejemplo anterior, x es ligado a 20.
Múltiples identificadores pueden ser ligados simultáneamente, usando la palabra
“and” como separador:
12
Paradigma Funcional
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
MLWorks> val x = 17;
val x : int = 17
MLWorks> val x = true and y = x;
val x : bool = true
val y : int = 17
Nótese que y recibe el valor 17, no true. Cuando se realizan ligaduras múltiples estas
son evaluadas en paralelo. Primero todas sus partes derechas son evaluadas y luego son
ligadas a sus partes izquierdas. Otra manera de combinar ligaduras es empleando el punto
y coma como sigue:
MLWorks> val x = 17; val x = true and y = x;
val x : int = 17
val x : bool = true
val y : int = 17
En este caso, ML evalúa primero la declaración más a la izquierda, produciendo un
ambiente E, y luego evalúa la declaración de la derecha (con respecto al entorno E),
generando un nuevo ambiente, E’.
Inferencia de Tipos
Como ya se ha mencionado, ML es un lenguaje fuertemente tipado donde todos los
identificadores deben ser declarados antes de ser utilizados. Sin embargo, y a diferencia
con otros lenguajes como Pascal, el tipo de una variable no es especificado explícitamente
en su declaración. Luego, la pregunta es: ¿cómo determina el lenguaje el tipo de una
variable? La respuesta es sencilla, ML realiza inferencia de tipos. En pocas palabras,
cada vez que se declara una variable, se establece una ligadura entre ella y un valor.
Luego, el tipo de la variable será el tipo al cual pertenece el valor. Si el valor es simple
(entero o booleano) está tarea es directa, si el valor es compuesto el tipo se determina a
partir de los constructores de tipos. Más adelante, en la sección de declaración de
funciones, se retomará este tema.
Declaraciones Locales
ML también permite efectuar declaraciones locales. Estas asisten en la declaración de
otras construcciones. Por ejemplo:
MLWorks> local
val x = 10
in
val u = x*x + x*x
val v = 2*x + (x div 5)
end;
val u : int = 200
val v : int = 22
La ligadura de x es local a las ligaduras de u y v, en este sentido x está disponible sólo
durante la evaluación de las ligaduras de u y v, pero no después. Como consecuencia,
únicamente u y v quedan declaradas. También es posible localizar una declaración a una
expresión usando la palabra let:
13
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
MLWorks> let
val x = 10
in
x*x + 2*x + 1
end;
val it : int = 121
La declaración de x es local a la expresión que sigue al in, y por lo tanto no es visible fuera
de ella.
4.2.3 Pattern Matching
En secciones anteriores se han presentado varios tipos de datos estructurados, entre
ellos las tuplas. Sin embargo, no se indicó hasta ahora cómo recuperar una componente de
una tupla. Dada la siguiente declaración:
MLWorks> val tup1 = ("jorge", 12, true, 24);
Puede resultar de interés obtener la segunda componente del tup1. Para ello, existe un
mecanismo, denominado pattern matching, que permite descomponer valores compuestos.
El pattern matching es un concepto soportado por muchos lenguajes funcionales actuales,
entre ellos ML. Esta técnica consiste en aparear componentes con identificadores no
ligados. Por ejemplo:
MLWorks> val tup1 = ("Jorge", 12, true, 24);
val tup1 : (string * int * bool * int) = ("Jorge", 12, true, 24)
MLWorks> val (primero, segundo, true, cuarto) = tup1;
val cuarto : int = 24
val primero : string = "Jorge"
val segundo : int = 12
(1)
La parte izquierda de la segunda ligadura (1) es un patrón (pattern), que es construido
usando variables, constantes y constructores de tipos. En definitiva, un pattern es una
expresión especial que puede contener variables no ligadas previamente. Nótese, que un
caso particular de pattern es una variable, por ejemplo:
MLWorks> val x = 7;
val x : int = 7
MLWorks> val y = x;
val y : int = 7
La sentencia en negrita es un caso simple de pattern matching entre variables.
El pattern matching también puede aplicarse sobre listas. Abajo se muestran varios
patrones de esta clase:
MLWorks> val lista1 = ["Jorge", "Marcos", "Laura"];
val lista1 : string list = ["Jorge", "Marcos", "Laura"]
MLWorks> val [x1, x2, x3] = lista1;
val x1 : string = "Jorge"
14
Paradigma Funcional
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
val x2 : string = "Marcos"
val x3 : string = "Laura"
Este tipo de matching resulta útil cuando conocemos a priori la longitud de la lista. Sin
embargo, ¿qué sucedería si lista1 fuera nil? Una solución consiste en efectuar la siguiente
descomposición inductiva:
MLWorks> val lista1 = ["Jorge", "Marcos", "Laura"];
val lista1 : string list = ["Jorge", "Marcos", "Laura"]
MLWorks> val cabeza :: cola = lista1;
val cabeza : string = "Jorge"
val cola : string list = ["Marcos", "Laura"]
Por otro lado, puede ocurrir que sólo sea de interés recuperar la cabeza de la lista, y
no la cola. En tal caso, ML brinda un wildcard pattern que permite realizar el matching sin
crear una ligadura entre la cola y una variable. Por ejemplo:
MLWorks> val lista1 = ["Jorge", "Marcos", "Laura"];
val lista1 : string list = ["Jorge", "Marcos", "Laura"]
MLWorks> val cabeza :: _ = lista1;
val cabeza : string = "Jorge"
El pattern matching también puede efectuarse sobre registros a través de los nombres
de sus campos. El siguiente ejemplo ilustra ese tipo de matching:
MLWorks> val registro1 = {edad = 25, nombre = "Jorge"};
val registro1 : {edad: int, nombre: string} = {edad=25, nombre="Jorge"}
MLWorks> val {nombre = u, edad = v} = registro1;
val u : string = "Jorge"
val v : int = 25
Algunas veces puede resultar conveniente utilizar un record wildcard, el mismo se utiliza
como sigue:
MLWorks> val registro1 = {edad = 25, nombre = "Jorge"};
val registro1 : {edad: int, nombre: string} = {edad=25, nombre="Jorge"}
MLWorks> val {nombre = u, ... } = registro1;
val u : string = "Jorge"
Existe una importante restricción para el uso de record wildcard: debe ser posible
determinar en tiempo de compilación el tipo del pattern record completo. En otras
palabras, debe ser factible la inferencia de todos los campos y sus tipos, a partir del
contexto. Un análisis más profundo de este tema escapa al alcance del curso. No obstante,
el lector interesado en record matching puede consultar el trabajo de R. Harper [1993].
Una última forma de pattern matching soportada por ML es el denominado layered
pattern, el cual permite anidar patrones. Por ejemplo:
MLWorks> val x = ( ("casa", true), 17 );
val x : ((string * bool) * int) = (("casa", true), 17)
MLWorks> val (tup as (tizq,tder), der) = x;
val der : int = 17
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
15
Paradigma Funcional
val tder : bool = true
val tizq : string = "casa"
val tup : (string * bool) = ("casa", true)
Por último, cabe destacar que los pattern poseen una significativa limitación: deben ser
lineales, es decir, una variable no puede aparecer más de una vez en el pattern. Esto
excluye la posibilidad de escribir patrones de la forma (x, x) para la identificación de
partes simétricas.
4.2.4 Definición de Funciones
Hasta ahora se han presentado algunas de las funciones predefinidas por el lenguaje, tales
como las funciones aritméticas y las operaciones relacionales. En esta sección se
introducirá el concepto de ligadura funcional que es la herramienta mediante la cual ML
permite definir nuevas funciones.
Para comenzar, se analizarán los aspectos generales relativos a las funciones en ML.
Las funciones son “aplicadas” sobre un argumento. Este último puede ser un valor simple
o compuesto, incluso otra función (funciones de alto orden). Sintácticamente, esto se indica
en notación prefija, primero el nombre de la función y luego el argumento. Por ejemplo,
size “estudio”. Las funciones sólo tienen un argumento. Múltiples argumentos se pasan a
través de tuplas. De este modo, la función append recibe una tupla formada por las dos
listas a concatenar. Sin embargo, para algunas funciones (usualmente las predefinidas) el
lenguaje adopta la notación infija. Por ejemplo, la expresión e1+ e2 en realidad significa
“aplicar la función +” a la dupla (e1, e2). Él usuario también tiene la posibilidad de
escribir funciones infijas. Sin embargo, por razones de tiempo esta alternativa no será
tratada en el curso. Nótese que el uso de dos notaciones distintas perjudica la uniformidad
del lenguaje.
La aplicación de una función puede ser sintácticamente más compleja en ML que en
los lenguajes de programación procedurales. En estos últimos, las funciones sólo pueden
ser designadas a un identificador y por lo tanto la aplicación de una función es siempre de
la forma f(e1, e2, ..., en), donde f es un identificador. En cambio, ML no tiene esa
restricción. Las funciones son valores y pueden ser empleados arbitrariamente en
expresiones. La aplicación de una función tiene la forma e e’. Primero se evalúa e,
obteniéndose una función f. Luego se evalúa e’ obteniéndose un valor v. Finalmente se
aplica f a v. En el caso más simple e es un identificador, por ejemplo size, y de esta manera
la evaluación resulta sencilla. Pero en general, e puede ser muy complejo y requerir varias
computaciones hasta obtener la función f como un valor.
Tipo Función
Una pregunta que el lector puede hacerse es: ¿cómo se puede garantizar en una
aplicación e e’ que la evaluación de e retornará una función y no otro valor, como por
ejemplo un booleano? La respuesta es simple. Las funciones son valores, y todos los
valores en ML tienen un tipo asociado. Luego, basta con asegurar que el tipo de e
corresponda a un valor del tipo función.
El tipo función está integrado por funciones. Un valor de este tipo tiene la forma
σ→τ, pronunciado como “de σ a τ”, donde σ y τ son tipos. Una expresión de este tipo
devuelve una función que puede ser aplicada a un valor de tipo σ, la cual retorna valores
de tipo τ. Los tipos de σ y τ son llamados los tipos del dominio y rango de la función
respectivamente. De este modo, una aplicación e e’ es legal si e tiene tipo σ→τ y e’ tiene
16
Paradigma Funcional
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
tipo σ. Luego, el tipo de la expresión completa e e’ es τ. Lo anterior, permite comprender
como infiere ML el tipo de una expresión. Sin embargo, como se verá más adelante, existen
situaciones en que el mismo no puede ser inferido.
Dado que las funciones son valores en ML. Estas pueden ser ligadas a variables.
Para ilustrar lo anterior, considere las siguientes declaraciones:
MLWorks> val longitud = size;
val longitud : string -> int = fn
MLWorks> longitud "hola";
val it : int = 4
El identificador size es ligado a la variable longitud. La aplicación longitud “hola” es
procesada evaluando longitud para obtener una función, luego dicha función es aplicada a
“hola” obteniéndose el valor 4.
Una función es un objeto complejo. Su estructura es invisible al programador. De
hecho, a las funciones no se les puede aplicar el pattern matching como a los demás
objetos. Más aún, no es posible determinar si dos funciones son iguales. Luego, se dice que
el tipo función no admite igualdad. De todos los tipos introducidos hasta el momento, el
tipo función es el único que no define la igualdad entre dos de sus valores.
Funciones Definidas por el Usuario
Para declarar nuevas funciones el programador establece ligaduras mediante la
palabra fun, seguida por el nombre de la función y sus argumentos. Si existe más de un
parámetro, estos se pasan a través de una tupla ó, como veremos más adelante, se define
una función curried. Son ejemplos de definiciones:
MLWorks> fun doble x = 2*x;
val doble : int -> int = fn
MLWorks> doble 4;
val it : int = 8
MLWorks> fun factorial n = if n = 0 then 1 else n * factorial (n-1);
val factorial : int -> int = fn
MLWorks> factorial 5;
val it : int = 120
MLWorks> fun f1(x,y) = x*3 - y*2;
val f1 : (int * int) -> int = fn
MLWorks> f1 (5, 2);
val it : int = 11
Cuando una función definida por el usuario es aplicada a un valor, este es ligado al
argumento generándose un nuevo ambiente en el cual se evaluará el cuerpo de la función.
Por ejemplo, para la función doble, x será ligado a 4. Una vez finalizada la evaluación de
doble, se restablece el ambiente anterior a la aplicación. En la función f1, definida arriba,
el argumento es una dupla. Otras funciones podrían tener argumentos más complejos aún,
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
17
Paradigma Funcional
construidos a partir de registros y listas. En todos estos casos ML utiliza el pattern
matching para ligar los argumentos con sus valores.
Restricciones de Tipo
En la definición de la función factorial se empleó la expresión condicional, la cual
tiene la forma if e then e1 else e2. El primer argumento, e, debe ser booleano. Es
importante destacar que la cláusula else no es opcional. La razón es que este “if” es una
expresión condicional y no una sentencia como en Pascal. Si el else fuera omitido, y la
evaluación de e fuera false, la expresión condicional no tendría valor. Además, las
expresiones e1 y e2 deben ser del mismo tipo. Para comprender esta aseveración basta con
analizar el siguiente caso:
if x = y then true else 16
Si x e y están ligados al mismo valor, la expresión vale true, pero si x e y tienen ligados
valores distintos dicha expresión vale 16. ¿Cuál es el tipo de la expresión?, ¿ bool o int?.
Luego, es claro que e1 y e2 deben ser del mismo tipo.
Cuestiones como la anterior, pueden ser fácilmente manejadas en tiempo de
compilación mediante un chequeo de los tipos de e1 y e2. Sin embargo, existen situaciones
donde resulta imposible determinar el tipo de una expresión durante la compilación.
Suponga la siguiente declaración de función:
MLWorks> fun suma (x, y) = x + y;
Dado que el operador + está sobrecargado. El compilador no puede distinguir si se trata de
una función de tipo (real*real) → real o (int*int) → int. Existen dos alternativas para la
implementación del compilador. Algunos muestran un error de tipos derivada de la
presencia de un operador sobrecargado. Otros, tal es el caso del empleado en este curso,
asumen el tipo int por defecto.
En el primer caso, resulta necesario indicar el tipo de al menos uno de los
argumentos. Para el ejemplo:
MLWorks> fun suma (x, y:int) = x + y;
De este modo, el tipo de la función queda bien establecido. Esto se conoce como restricción
de tipos. Note que “:int” sólo declara el tipo de y, los tipos de x y suma son inferidos del
contexto.
Para la otra alternativa, únicamente es necesario especificar el tipo cuando este no
coincide con el definido por defecto. Por ejemplo, si se desea definir la suma de reales:
MLWorks> fun suma (x, y: real) = x + y;
Funciones Definidas mediante Cláusulas
Algunas funciones requieren una definición por partes, de acuerdo al valor de su
argumento. Por ejemplo, la función esvacia puede definirse como:
MLWorks> fun esvacia(nil) = true
| esvacia(_::_) = false;
val esvacia : 'a list -> bool = fn
18
Paradigma Funcional
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Este tipo de expresiones se conocen como funciones definidas por cláusulas. Estas
funciones deben estar definidas para todos los valores del dominio. Las cláusulas son muy
útiles para la definición de funciones recursivas. Por ejemplo, podemos definir la función
concatenar como:
MLWorks> fun concatenar (nil, L2) = L2
| concatenar (cabeza :: cola, L2) = cabeza :: concatenar(cola,L2);
val concatenar : ('a list * 'a list) -> 'a list = fn
(1)
(2)
El tipo de la función concatenar es un politipo, esto es un tipo que involucra una variable
de tipo ’a. Dado que nil es de tipo ’a list, de (2) ML puede inferir que cabeza es de tipo ‘a y
cola es de tipo ’a list. Además, dada la definición del constructor ::, por (2) resulta que
concatenar es de tipo ’a list. Finalmente por (1), L2 también debe ser de tipo ’a list. Nótese,
que la función concatenar es en sí una función polimorfa. Es decir, presenta un
comportamiento uniforme para listas con distintos tipos de componentes.
Declaraciones Locales
Una característica a tener en cuenta es que las funciones pueden declarar variables y
funciones locales empleando el constructor let visto anteriormente. Por ejemplo:
MLWorks> fun multxpi (x) =
let
val c = 3.14
in
x*c
end;
val multxpi : real -> real = fn
Referencias a Variables No Locales
Las funciones no están restringidas al uso de parámetros y variables locales, ellas
pueden referir a cualquier variable visible dentro de su ambiente de referenciamiento. Por
ejemplo, dada la siguiente secuencia de definiciones:
MLWorks> val c = 2.5;
val c : real = 2.5
MLWorks> fun multx (x) = x*c;
val multx : real -> real = fn
MLWorks> multx (2.0);
val it : real = 5.0
MLWorks> val c = 8.17;
val c : real = 8.17
MLWorks> multx (2.0);
val it : real = 5.0
Se aprecia que una vez declarada multx, las nuevas ligaduras del identificador c no alteran
la definición de la función.
19
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
Funciones Curried
ML permite que el usuario defina funciones curried. Por ejemplo, se puede definir
una función que multiplique dos enteros en forma curried:
MLWorks> fun producto (x:int) (y:int) = x*y;
val producto : int -> int -> int = fn
Esta función tiene tipo int -> int -> int. Luego, se puede definir la función multx5 como:
MLWorks> val multx5 = producto (5);
val multx5 : int -> int = fn
Note que producto difiere de la siguiente función:
MLWorks> fun producto2 (x, y:int) = x*y;
val producto2 : (int * int) -> int = fn
El tipo de producto2 es (int * int) -> int.
A modo de ilustración considere el siguiente ejemplo:
Sea fun f cond x y z = if cond then (x – y)*z else x*(z - x)*y;
luego, analicemos la siguiente evaluación
f false 2 4 3 = if cond then (x – y)*z else x*(z - x)*y; fn: boolean -> (int -> (int -> (int -> int)))
↓
g 2 4 3 = x*(z - x)*y;
fn: int -> (int -> (int -> int))
↓
g’ 4 3 = 2*(z - 2)*y;
fn: int -> (int -> int)
↓
g’’ 3 = 8*(z - 2);
fn: int -> int
↓
8
int
A la derecha de cada evaluación se indica el tipo de la función*. Es fácil ver que la
aplicación parcial trata a las funciones como de un único argumento, mientras que el
resultado de cada aplicación es una función.
Formas Funcionales
Las funciones en ML son valores; ellas pueden ser pasadas como argumentos y
devueltas como resultado de otras funciones. Como se definió anteriormente, una función
que emplea otras funciones como argumentos y/o resultado se denomina forma funcional o
función de alto orden. En ML existen varias formas funcionales: la composición, el
operador “aplicar a todos” (denominada map) y los operadores de “inserción” a
izquierda y derecha (conocidos como foldl y foldr respectivamente).
*
Note que la notación de tipos de funciones curried en ML no emplea los paréntesis. Aquí son incluidos
para facilitar la comprensión del lector.
20
Paradigma Funcional
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Composición
La composición permite construir una nueva función a partir de la composición de otras
dos. Por ejemplo:
MLWorks> fun comp (f, g) (x) = f(g(x));
val comp : (('a -> 'b) * ('c -> 'a)) -> 'c -> 'b = fn
Para que la composición sea válida los tipos de f y g deben ser compatibles. Es decir, si g
tiene tipo σ→ϖ, entonces f debe tener dominio ϖ, dado que el valor devuelto por g es el
argumento de f. En particular, si f tiene tipo ϖ→τ, comp será de tipo σ→τ.
Aplicar a Todos
El operador “aplicar a todos” toma como argumento una función f y una lista L. El valor
retornado es una lista cuya componente yi es el resultado de aplicar f al i-ésimo elemento
de L. Por ejemplo:
MLWorks> map length [[], [1,2,3],[3]];
val it : int list = [0, 3, 1]
Note, que map es una función curried.
Operador de Inserción
El operador de inserción a derecha permite generalizar una función binaria a n-aria. Esta
función puede definirse como:
MLWorks> fun foldr (f, s, nil) = s
| foldr (f, s, (cabeza :: cola)) = f(cabeza, foldr (f, s, cola));
val foldr : ((('a * 'b) -> 'b) * 'b * 'a list) -> 'b = fn
Por ejemplo, se puede sumar una lista de números como sigue:
MLWorks> foldr (op +, 0, [1, 2, 3, 4]);
val it : int = 10
De forma similar puede definirse el operador de inserción a izquierda foldl.
Nótese que en Standard ML foldr y foldl están predefinidas de manera curryed, lo cual
constituye una distinta implementación de la anteriormente mostrada. Piense como
realizaría tal implementación.
4.2.5 Polimorfismo y Sobrecarga
En las secciones anteriores se establecieron los conceptos de politipo, como aquel
tipo que contiene variables de tipo, y monotipo. También se definieron como funciones
polimorfas, a aquellas que trabajan sobre una clase de tipos de manera uniforme. Por
ejemplo, para la función concatenar no importa el tipo de las componentes de las listas.
Sólo interesa que el argumento esté formado por dos listas cuyos componentes sean del
mismo tipo. En ML el tipo de una función polimorfa es siempre un politipo. Esta clase de
polimorfismo se denomina polimorfismo paramétrico dado que el tipo esta parametrizado
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
21
Paradigma Funcional
por las variables de tipos. Nótese que el polimorfismo no está limitado a las funciones. Por
ejemplo, nil es un objeto polimorfo.
Este concepto puede ser contrastado con otro, conocido como sobrecarga. Esta es
una noción mucho más ad hoc de polimorfismo dado que mira más el nombre de la función
que su estructura de definición. A modo de ilustración considérese la función de suma, +.
La expresión 3 + 2 hace referencia a la suma entre enteros, 3 y 2, mientras que 3.0 + 2.0
implica la suma de dos reales, 3.0 y 2.0. En principio, esta situación puede parecer similar
a la concatenación de listas, pero esta semejanza es sólo aparente: la función concatenar
empleada sobre listas de distintos tipos es siempre la misma, en cambio el algoritmo para
la suma de enteros difiere del empleado para reales. Por lo tanto, en el caso de la suma, se
está en presencia de dos funciones distintas que comparten un mismo nombre. Luego, se
dice que el operador de suma está sobrecargado.
4.2.6 Definición de Tipos
El sistema de tipos en ML es extensible. El programador puede definir nuevos tipos
usando alguna de las siguientes formas: abreviación de tipo, definición de tipo de dato y
definición de tipo de dato abstracto.
Abreviación de Tipo
La forma más elemental de definir un tipo es la abreviación, la cual liga una
expresión de tipo a un identificador de tipo. Por ejemplo:
MLWorks> type parenteros = int*int;
eqtype parenteros = (int * int)
MLWorks> type 'a par = 'a * 'a;
eqtype 'a par = ('a * 'a)
MLWorks> type parbooleanos = bool par;
eqtype parbooleanos = (bool * bool)
La segunda definición corresponde a un politipo. Luego, es posible definir variables
y funciones polimorfas de tipo ‘a par, como también definir subtipos del mismo, por ejemplo
parbooleanos.
Definición de Tipo
La definición de tipos consiste de un nombre (y quizás algunos parámetros de tipo) y
un conjunto de constructores de valores del tipo. Por ejemplo:
MLWorks> datatype color = Red | Blue | Yellow;
datatype color =
Blue |
Red |
Yellow
val Blue : color
val Red : color
val Yellow : color
MLWorks> Red;
val it : color = Red
22
Paradigma Funcional
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
No todos los tipos definidos por el programador necesitan tener constantes como
constructores. Suponga la siguiente definición:
MLWorks> datatype dinero = sindinero | centavos of int | pesos of int | cheque of string*int;
datatype dinero =
centavos of int |
cheque of (string * int) |
pesos of int |
sindinero
val centavos : int -> dinero
val cheque : (string * int) -> dinero
val pesos : int -> dinero
val sindinero : dinero
MLWorks> fun contarcentavos (sindinero) = 0
| contarcentavos (centavos(cant)) = cant
| contarcentavos (pesos(cant)) = 100*cant
| contarcentavos (cheque(banco,cant)) = 100*cant;
val contarcentavos : dinero -> int = fn
Analizando este último ejemplo, surge una duda: ¿cuándo dos valores de tipo dinero son
iguales? ML define como iguales a aquellos valores del mismo tipo, construidos de la
misma forma y cuyas componentes son iguales. Las evaluaciones de las siguientes
expresiones ilustran lo anterior:
MLWorks> sindinero = sindinero;
val it : bool = true
MLWorks> sindinero = centavos(5);
val it : bool = false
MLWorks> centavos(5) = centavos(2+3);
val it : bool = true
MLWorks> cheque("MoneyBank", 500) = cheque("GoldBank", 500);
val it : bool = false
Los tipos de datos también pueden ser recursivos. De esta manera, se puede definir un tipo
arbolbinario como sigue:
MLWorks> datatype arbolbinario = vacio | hoja | nodo of arbolbinario*arbolbinario;
datatype arbolbinario =
hoja |
nodo of (arbolbinario * arbolbinario) |
vacio
val hoja : arbolbinario
val nodo : (arbolbinario * arbolbinario) -> arbolbinario
val vacio : arbolbinario
También se pueden especificar tipos de datos paramétricos. Un ejemplo es el tipo
predefinido τ list, el cual corresponde a la siguiente definición:
datatype ‘a list = nil | :: of ‘a * ‘a list;
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
23
Paradigma Funcional
Sin embargo, si el programador ingresa está definición, ML Works retornará un error de
compilación debido a que el operador “::” está reservado.
Definición de un Tipo de Dato Abstracto
Finalmente, ML soporta la definición de tipos de datos abstractos (TDA). Un tipo de
dato abstracto está formado por un tipo de dato y un conjunto de funciones que permiten
manipular valores de dicho tipo. La principal característica de los TDA es que permiten
definir nuevos tipos, ocultando la representación interna de los mismos. De este modo, el
usuario puede operar las variables únicamente a través de las operaciones implementadas
dentro del TDA. A continuación se muestra la declaración de un TDA:
MLWorks> abstype color = blend of int * int * int
with val blanco = blend (0,0,0)
and rojo = blend (15,0,0)
and azul = blend (0,15,0)
and amarillo = blend (0,0,15)
fun mezclar (parte1: int, blend(ro1, az1, am1), parte2: int, blend(ro2, az2, am2)) =
if parte1 < 0 orelse parte2 < 0 then blanco
else let val tp = parte1+parte2; val rop = (parte1*ro1 + parte2*ro2) div tp
and azp = (parte1*az1 + parte2*az2) div tp
and amp = (parte1*am1 + parte2*am2) div tp
in blend(rop, azp, amp)
end
end;
type color = color
val amarillo : color = _
val azul : color = _
val blanco : color = _
val mezclar : (int * color * int * color) -> color = fn
val rojo : color = _
La representación interna de un valor de tipo color no es visible fuera del TDA.
Únicamente se puede acceder a la interface del TDA (bloque comprendido entre el with y el
end). A diferencia con los tipos de datos (datatype), el constructor de datos blend queda
oculto. Un programa usuario del TDA sólo puede definir una variable de tipo color y
ligarle algunos de los valores definidos en la interface (blanco, rojo, azul o amarillo), pero
no puede construir un nuevo valor empleando blend. La única manera de generar nuevos
valores es a través de la función mezclar.
4.2.7 Definición de Módulos
En la mayoría de los lenguajes modernos los programas son concebidos a partir de
la integración de distintos módulos, cada uno de los cuales define sus propias estructuras
de datos y operaciones relacionadas. ML se vale de dos constructores para la definición de
módulos: las “structures” que permiten combinar declaraciones de tipos, valores y otras
structures relacionadas, y las “signatures” que especifican una clase de structures
mediante el listado de los nombres y tipos de cada componente.
Las signatures y structures de ML guardan similitud con las partes de definición e
implementación de módulos de Modula-2. Finalmente, ML también provee “functors”, los
cuales son structures parametrizados con otras structures.
24
Paradigma Funcional
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Structures
Para introducir este constructor nos valdremos de un ejemplo. Suponga que se desea
definir un módulo para el manejo de números complejos. Dicha structure puede definirse
como sigue:
structure Complejo =
struct
type t = real*real;
val cero = (0.0, 0.0);
fun suma ((x, y), (x’, y’)) = (x+x’, y+y’) : t;
fun resta ((x, y), (x’, y’)) = (x-x’, y-y’) : t;
fun producto ((x, y), (x’, y’)) = (x*x’ – y*y’, x*y’ + y*x’) : t;
fun reciproco (x, y) = let
val p = x*x + y*y
in
(x/p, ∼y/p) : t
end;
fun division (z, z’) = producto (z, reciproco z’);
end;
Las distintas declaraciones efectuadas dentro del módulo son encerradas entre las
palabras reservadas struct y end. En toda parte del programa en donde la structure
Complejo sea visible, sus componentes pueden ser accedidos mediante una notación que
recuerda a los registros de Pascal. Por ejemplo, el número cero definido dentro del módulo
puede ser referenciado externamente como Complejo.cero, y la función suma como
Complejo.suma. Dentro del cuerpo de la structure las componentes son conocidas por sus
identificadores, en los casos anteriores cero y suma. El tipo de un número complejo se
denomina Complejo.t, de este modo es posible declarar variables de este tipo.
En este punto, es posible utilizar las funciones definidas dentro del módulo sobre
cualquier dupla de reales. Por ejemplo:
- val a = (0.0, 1.0);
> val a : (real * real) = (0.0, 1.0)
- val b = (2.3, 1.0);
> val b : (real * real) = (2.3, 1.0)
- val c = Complejo.suma(a, b);
> val c : (real * real) = (2.3, 2.0)
- val d = Complejo.suma(c, (1.5, 2.1));
> val d : (real * real) = (3.8, 4.1)
Es importante notar que la representación interna del tipo Complejo.t no está oculta dentro
del módulo.
Signatures
Una signature especifica la información que ML necesita para integrar las distintas
unidades que conforman un programa de manera segura. En sí, una signature es una
descripción de las componentes de una structure. Una vez que efectuamos la declaración
de la structure Complejo, ML responde mostrando su correspondiente signature:
25
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
structure Complejo =
struct
type t = (real * real)
val cero : (real * real) = (0.0, 0.0)
val division : ((real * real) * (real * real)) -> (real * real) = fn
val producto : ((real * real) * (real * real)) -> (real * real) = fn
val reciproco : (real * real) -> (real * real) = fn
val resta : ((real * real) * (real * real)) -> (real * real) = fn
val suma : ((real * real) * (real * real)) -> (real * real) = fn
end
Los identificadores struct y end encierran el cuerpo de la signature. La misma sólo muestra
los nombres y tipos de las componentes definidas en la structure, junto con los
identificadores correspondientes a los tipos declarados.
La signature inferida por ML no siempre es la mejor a los propósitos del
programador. De hecho, muchas veces un módulo contiene la declaración de tipos y
valores privados, los cuales deseamos que permanezcan ocultos dentro de la structure.
Para nuestro ejemplo, reciproco es una función auxiliar que podría ser privada. Luego, el
usuario puede restringir la visión definiendo una signature específica para el módulo.
Volviendo a nuestro ejemplo, la definición de una nueva signature se realizaría así:
- signature ARITMETICA =
sig
type t;
val cero : t;
val resta : t * t -> t;
val division : t * t -> t;
val producto : t * t -> t;
val suma : t * t -> t;
end ;
Luego, esta signature es asociada a Complejo en el encabezamiento de la structure:
structure Complejo : ARITMETICA =
struct
type t = real*real;
val cero = (0.0, 0.0);
fun suma ((x, y), (x’, y’)) = (x+x’, y+y’) : t;
fun resta ((x, y), (x’, y’)) = (x-x’, y-y’) : t;
fun producto ((x, y), (x’, y’)) = (x*x’ – y*y’, x*y’ + y*x’) : t;
fun reciproco (x, y) = let
val p = x*x + y*y
in
(x/p, ∼y/p) : t
end;
fun division (z, z’) = producto (z, reciproco z’)
end;
Note que la signature inferida por ML muestra el tipo de los argumentos como real * real
mientras que ARITMETICA sólo emplea el tipo t.
26
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
Alternativamente, se puede asociar una structure con una signatura diferente a la sugerida
por ML de la siguiente manera:
structure NewComplejo: ARITMETICA = Complejo;
Es importante aclarar que una misma signature puede ser asociada a más de una structure,
por ejemplo, es posible definir un módulo para el manejo de fracciones como sigue:
structure Fraccion : ARITMETICA =
struct
type t = int*int;
val cero = (0, 1);
M
end;
4.2.8 Manejo de Excepciones
Muchas veces, una función aparentemente bien definida, puede tener un
comportamiento inesperado bajo circunstancias poco frecuentes. Por ejemplo, considere
una base de datos relativa a la Universidad y una función alumno que dado un número de
libreta devuelve el nombre del mismo. ¿Qué ocurriría si se pasa como parámetro un
número de libreta que no corresponde a ningún alumno?
Una alternativa para estas situaciones es agregar chequeos antes y/o después de
efectuar ciertas operaciones. En el caso anterior, se puede verificar que el número este
dentro de un rango preestablecido. Sin embargo, la incorporación de estos tests “ensucia”
el código, tornándose más difícil su seguimiento. Por otra parte, no siempre es fácil
determinar que eventos extraordinarios pueden suceder y en que lugares ocurrirán.
Declaración de Excepciones
Con el fin de tratar estos casos, el lenguaje provee mecanismos para el manejo de
excepciones. En ML existe un tipo predefinido, denominado exn, cuyos valores se conocen
como valores de excepción. Este tipo se asemeja a los tipos de datos (datatype), pero a
diferencia de estos, nuevos constructores (denominados constructores de excepciones)
pueden ser sumados al mismo mediante la declaración:
exception nomexcep
o
exception nomexcep of tipo
donde nomexcep es el nombre de la excepción. Por ejemplo, se puede declarar:
exception Divisionx0
exception PalProhibida of string
Luego, son ejemplos de valores de excepciones: Divisionx0, PalProhibida(“combate”). Note
que si bien Divisionx0 es una constante, también es posible definir constructores que sean
funciones, por ejemplo, PalProhibida es de tipo string → exn. Esta capacidad de definir
excepciones como funciones brinda la posibilidad de que el manejador reciba, a través de
los argumentos de la excepción, información que le permita decidir la secuencia de
acciones a realizar para recuperarse de la falla.
27
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
Las excepciones pueden ser declaradas a nivel local usando let, incluso dentro de
una función recursiva. De este modo, puede suceder que diferentes excepciones tengan el
mismo nombre. También se pueden declarar excepciones al nivel del programa principal,
pero en este caso es necesario asegurarse de que las excepciones sean monomórficas. Esta
restricción responde a ciertos aspectos imperativos subyacentes en la definición de ML que
no serán tratados en esta materia.
Activación de Excepciones
El alcance de una excepción (raise) crea un paquete de excepción conteniendo un
valor de tipo exn. Si Ex es una expresión de tipo exn, y la evaluación de Ex arroja un valor
e, luego
raise Ex
evalúa a un paquete de excepción que contiene a e. Los paquetes no son valores de ML y
las únicas operaciones que reconocen son raise y handle. El tipo exn actúa como mediador
entre los paquetes y los valores de ML.
Durante la evaluación, los paquetes de excepción se propagan bajo reglas de
llamadas por valor. Si una expresión Ex retorna un paquete de excepción que es el
resultado de la aplicación f(Ex) para cualquier función f. Luego, f(raise Ex) es equivalente
a raise Ex.
Una excepción puede ser activada mediante la expresión:
raised exp
La siguiente función interes calcula el cociente entre dos valores reales (c y d ) y
activa la excepción Divisionx0 si d es igual a 0 ó ValorNegativo si es d es negativo:
fun interes (c,d) = if d = 0 then raise Divisionx0
else if d < 0 then raise ValorNegativo else c/d;
El comportamiento normal de interes es devolver el cociente c/d, pero si d es igual a
0 ó d es menor que 0, las excepciones Divisionx0 ó ValorNegativo se activarán
respectivamente.
Excepciones predefinidas
Fallas en el “pattern-matching” puede ser alcanzadas mediante las excepciones
predefinidas Match y Bind. Una función alcanza la excepción Match cuando es aplicada a
un argumento que no hace “matching” con su pattern. La excepción Bind es alcanzada
cuando en una declaración del tipo val P = E, el valor de la expresión E no hace “patternmatching” con P. Otras excepciones presentes en la librería de estándar son:
Overflow Es alcanzada por operaciones aritméticas cuyos resultados están fuera de
rango.
Es alcanzada cuando se produce una división por cero.
Div
Domain Es alcanzada por algunas funciones matemáticas con restricciones de dominio,
por ejemplo, si se aplica la función logaritmo a un argumento negativo.
Es alcanzada si k no corresponde a un caracter.
Chr(k)
Subscript Es alcanzada cuando un índice está fuera de rango. Las operaciones sobre
listas, strings y arreglos pueden alcanzar esta excepción.
28
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
Size
Es alcanzada si se intenta crear un lista, string ó arreglo de tamaño negativo ó
muy grande.
Manejadores de excepciones
La ligadura entre una excepción y su manejador es dinámica. Si f invoca a g, g
invoca a h y h activa una excepción excep, luego se buscan manejadores para excep
siguiendo la cadena h, g y f. El primer manejador encontrado captura la excepción.
Básicamente, el manejador de excepción chequea si el resultado de una expresión es
un paquete de excepción. Si es así, el paquete contiene un valor de tipo exn, el cual puede
ser examinado por casos. En general, una expresión que tiene un manejador de excepción
utiliza el constructor case de la siguiente forma:
E handle P1 => E1 | ... Pn => En
Si E retorna un valor del tipo esperado, el manejador devuelve ese valor. En cambio, si E
retorna un paquete de excepción, el contenido c de dicho paquete es apareado con los Pi
del case, i= 1...n. De este modo, si Pj es el primer patrón que es apareable con c, el
manejador devolverá el valor obtenido de evaluar Ej. En caso de que ningún Pi sea
apareable con c, la excepción es propagada hasta que sea capturada por otro manejador o
alcanzar la excepción Match.
Por ejemplo, dada la siguiente función:
fun calculainteres (c,d) = interes (c,d) handle Divisionx0 => 0.0 | ValorNegativo => 0.0;
Suponga que la evaluación de calculainteres(2.0,4.0) retornará el valor 0.5, en cambio la
expresión calculainteres(3.0,0.0) tomará el valor 0.0 devuelto por el manejador.
REFERENCIAS
Backus J., “Can Programming be Liberated from the von Neumann Style? A Functional
Style and its Algebra of Programs”, Comm. ACM, 21(8), 613-641, 1978.
Ghezzi C. y Jazayeri M., “Programming Language Concepts”, 3rd Ed., John Wiley & Sons,
1998.
Harper R., “Introduction to Standard ML”, School of Computer Science, Carnegie Mellon
University, Estados Unidos, 1993.
Hudak P., “Conception, Evolution and Application of Functional Programming
Languages”, ACM Computing Surveys, 21 (3), 359-411, 1989.
Landi P.J, “The Next 700 Programming Languages”, Comm. ACM. 9 (3), 157-166, 1966.
Mac Queen D., “Modules for Standard ML”, in Standard ML, Edimburgh University
Internal Report ECS-LFCS-86-2, 1986.
Paulson, L.C., “ML for the Working Programmer”, 2nd edition, Cambridge University
Press, 1996.
Peyton Jones S.L., “The Implementation of Functional Programming”, Englewood Cliffs
NJ, Prentice Hall, 1987.
Sethi R., “Lenguajes de Programación: Conceptos y Constructores”, Addison-Wesley
Iberoamericana, 1992.
29
Apuntes sobre Paradigmas de Programación
Dr. Ignacio Ponzoni – Dra. Jessica A. Carballido
Paradigma Funcional
Tofte M., “Essentials of Standard ML Modules”, Department of Computer Science,
University of Copenhagen, Dinamarca, Reporte Técnico, 1997.
Ullman J.D., “Elements of ML Programming”, 2 nd edition, Prentice-Hall, 1998.
Watt D.A., “Programming Language Concepts and Paradigms”, chapter 13, Prentice Hall,
1990.
Descargar