Anda di halaman 1dari 48

Apuntes de

Alberto Ruiz, (aruiz@um.es)


January 6, 2007
1 Introducci on
Lo mejor sera olvidarse de este documento e irse directamente a la p agina web de la comunidad Haskell, donde podemos
encontrar toda la informaci on y material que podamos necesitar: tutoriales, ejemplos de c odigo, etc:
www.haskell.org
La programaci on funcional es sobre todo una forma m as divertida de programar. Su caracterstica m as importante es la
transparencia referencial: lo que se ve es lo que hay; nada depende de un estado de la m aquina. No hay variables
ni asignaciones sino aplicaci on de funciones sobre el resultado de la aplicaci on de funciones, etc., donde los argumentos
son datos normales u otras funciones (son rst class citizens del lenguaje). En lugar de la iteraci on se usa bastante la
recursi on apoyada en listas.
Esto contrasta mucho con los lenguajes imperativos, sobre cuyo c odigo difcil razonar, ya que consiguen su objetivo como
un efecto colateral de las asignaciones de memoria de un lado para otro. Aqu damos deniciones casi matem aticas, en las
hay verdaderas igualdades, porque podemos sustuir libremente un lado de la igualdad por otro en todo el c odigo (equational
reasoning) sin cambiar el resultado del programa. Curiosamente, esto tambi en es bueno para los optimizadores de c odigo
y para la paralelizaci on. Adem as es posible simplicar autom aticamente algunas expresiones. Es muy interesante leer
el paper Why functional programming matters (aunque utiliza una sintaxis ligeramente distinta a la de Haskell, pero
igualmente muy clara y comprensible).
Hay muchos lenguajes con inspiraci on funcional, cada uno con sus peculiaridades. El propio lenguaje de programaci on del
entorno de c alculo cientco Mathematica, caracterizado por un notable poder expresivo, utiliza construcciones funcionales
y transformaciones de estructura denidas mediante pattern matching.
Haskell es un lenguaje desarrollado por la comunidad, de c odigo abierto, multiplataforma, con varias implementaciones
que compiten ayud andose entre s. Hay un n ucleo estable del lenguaje y muchas extensiones m as o menos experimentales,
que si dan resultado tienden a incorporarse en versiones futuras. Est a orientado tanto al campo acad emico como a su
uso en aplicaciones reales, y es una plataforma ideal para experimentaci on e investigaci on en aspectos avanzados de la
computaci on.
Uno de sus mayores ventajas es que se comprueba en tiempo de compilaci on la consistencia de tipos, de manera que se
reducen muchsimo los errores en tiempo de ejecuci on. (Si se hacen bien las cosas se pueden evitar todos.) Y entonces
ocurre un fen omeno muy curioso, que es una de las razones por las que el lenguaje engancha: si el c odigo compila,
entonces lo m as seguro es que funcione bien!.
En los lenguajes imperativos, por muy estricta que sea, la comprobaci on est atica no puede vericar que el orden de las
operaciones es el correcto. En cambio, como en la programaci on funcional no hay orden, la consistencia est atica es una
garanta mucho m as fuerte de correcci on. Incluso ayuda cuando has entendido mal el algoritmo, porque eso a veces
conduce a que los tipos del programa son inconsistentes. Esto lo explican mejor en Why Haskell just works.
Otra caracterstica importante es el polimorsmo: el lenguaje est a dise nado para que las funciones sean capaces de admitir
todos los tipos de dato que admitan las operaciones utilizadas. Por eso, si escribimos una funci on de ordenaci on, funcionar a
1
con cualquier tipo que admita comparaciones. Es como si estuvieramos programando todo el rato con templates parecidas
a las de C++, pero con un verdadero control est atico de consistencia de tipos y la posibilidad de compilar realmente esas
funciones polim orcas.
Adem as, el sistema es capaz de inferir el tipo m as general de las funciones que aparecen en el programa. Normalmente,
para documentar el c odigo se suele poner la signatura de las funciones m as importantes. En cualquier caso, el compilador
deduce autom aticamente el tipo m as general de una funci on (y, como veremos m as adelante, se lo podemos preguntar al
int erprete con :t fun).
La sintaxis est a orientada a la claridad del c odigo, lo que permite que muchos algoritmos (todos?) se expresen de forma
muy concisa. De hecho, podramos armar que si el c odigo queda bonito seguro que es correcto. Y si queda feo seguro
que se puede mejorar. Esa tremenda concisi on y elegancia a veces hace pensar (epecialmente a programadores expertos en
otro tipo de lenguajes, en los que hay que estar pendiente de millones de detalles t ecnicos al escribir el c odigo) que se trata
de un pseudolenguaje de juguete. Sin embargo, el hecho de que un algoritmo pueda escribirse de forma enga nosamente
simple no signica que no haya un control muy estricto sobre el. Un c odigo sencillo que pasa todos los tests de consistencia
es improbable que sea incorrecto. Un mismo concepto de programaci on se puede expresar de muchas formas distintas, a
gusto del programador, pero casi todo es syntactic sugar que internamente se transforma en lambda c alculo con un cierto
sistema de tipos.
La evaluaci on es no estricta (lazy), lo que signica que las deniciones no se eval uan si no es estrictamente necesario para
obtener el resultado nal. Esto nos permite entrar en un mundo nuevo de t ecnicas de programaci on mucho m as potentes y
modulares.
Un concepto distintivo del lenguaje es de las clases de tipos. Esto no tiene nada que ver con la orientaci on a objetos (que
tambi en se puede conseguir mediante ciertas t ecnicas de programaci on). Es una forma sistem atica de abordar la sobrecarga
de funciones de forma no jer arquica.
La entrada/salida y la secuenciaci on de acciones se trata mediante un enfoque que tambi en es novedoso, basado en la clase
de tipos monad, una abstracci on que curiosamente resulta muy util tambi en en otros ambitos. De hecho se podra decir
que puedes escribir c odigo en estilo imperativo mejor que en los lenguajes imperativos. . .
Finalmente, la modularizaci on de los programas se consigue de forma muy sencilla, mediante la declaraci on explcita
de los smbolos que exporta cada m odulo. Los tipos abstractos de datos se consiguen sencillamente no exportando sus
constructores sino funciones que los crean manteniendo los invariantes deseados.
El poder expresivo del lenguaje facilita la existencia de bibliotecas muy interesantes (parsec, quickcheck, etc.) y, por
supuesto, podemos utilizar cualquier biblioteca externa (esencialmente a trav es de C) mediante el Foreign Function Inter-
face.
Los tutoriales de la p agina web nos explican en detalle el lenguaje as que aqu solo pondremos ejemplos que nos den una
idea de c omo se hacen las cosas. Dos muy buenos son: A gentle introduction to Haskell y Yet another Haskell tutorial. Hay
m as material en Learning Haskell. Aqu tenemos la denici on completa del lenguaje y las bibliotecas: Haskell Report.
Finalmente es muy interesante leer The history of Haskell (cuando est e online). Mientras tanto se puede echar un vistazo
a esta retrospectiva.
Por supuesto no todo son ventajas. A veces se dice que no es tan r apido como C y en algoritmos de procesamiento intensivo
de datos, por supuesto esto es verdad. El precio que se paga por ascender en la capacidad de abstracci on es una p erdida de
eciencia, a veces bastante apreciable, en comparaci on con los bucles de C. Sin embargo, no siempre lo m as importante
es el tiempo de ejecuci on. Tambi en hay que contabilizar el tiempo de desarrollo, de depuraci on, mantenimiento, y exten-
sibilidad de una aplicaci on (lo que explica el exito de lenguajes como perl, python, ruby, etc.). En algunas aplicaciones,
pensemos por ejemplo en la criptografa, la correcci on del c odigo es m as importante que la velocidad. Adem as, casi siem-
pre el 90% del tiempo de ejecuci on es responsabilidad de un 10% del c odigo. Por lo que si necesitamos reescribir alg un
proceso crtico en C, o utilizar una biblioteca optimizada externa (igual que lo que se hace en cualquier otro lenguaje,
cuando se engancha con blas+lapack, con OpenGL o con IPP), podemos hacerlo sin ning un problema. En cualquier caso,
coincido con la idea de que la ansiedad por las prestaciones conduce a la optimizaci on prematura. Mejorar la eciencia
del c odigo ejecutable no debera interferir siempre en la escritura de los algoritmos. Es una tarea de los dise nadores de
compiladores.
Un problema m as serio es el de razonar sobre la complejidad espacial de los algoritmos, debido a la evaluaci on no estricta.
Pero hay herramientas de proling que nos ayudan y nos dicen donde es conveniente, por ejemplo, forzar una evaluaci on
(seq) para que el problema se arregle.
2
En resumen, las decisiones de dise no de este lenguaje no tienen como m axima prioridad aprovechar al m aximo la potencia
de c alculo del computador, sino ayudar al programador humano a escribir correctamente algoritmos cada vez m as com-
plejos. Sobre esa base hay que juzgarlo. Y, ciertamente, no merece la pena aprender (o simplemente conocer) un lenguaje
que no cambia tus ideas sobre la programaci on. . . Aunque al nal no se utilice en los proyectos reales, muchos de estos
conceptos pueden ayudarnos a programar mejor en cualquier otro lenguaje.
2 Compilador
Yo destacara el compilador ghc (Glorious GlasgowHaskell Compiler), que siempre tiene las ultimas mejoras del lenguaje.
Incluye un int erprete (ghci), lo cual es muy pr actico para experimentar con el c odigo. Otra alternativa muy buena es el
int erprete Hugs; y tambi en hay otros compiladores m as o menos experimentales.
2.1 Instalaci on
Todas las distribuciones de linux permiten instalar una versi on bastante actualizada de ghc (or Hugs) mediante los re-
spectivos administradores de paquetes. Pero yo recomendara la versi on binaria m as reciente de ghc (actualmente la 6.6),
que se puede instalar muy f acilmente como usuario local. Elegimos la versi on generic Linux que encontramos en:
http://haskell.org/ghc/download ghc 66.html#x86linux
desempaquetamos el tar.bz2 en cualquier sitio, hacemos ./congure y make-inplace. A nadiendo a PATH la ruta donde lo
hemos dejado, el compilador, el int erprete, y alguna otra utilidad m as que luego veremos, funcionar an perfectamente. Para
comprobarlo nos vamos a cualquier otro directorio y en un terminal escribimos:
linux> ghc -V
The Glorious Glasgow Haskell Compilation System, version 6.6
linux>
2.2 Hello World
Para empezar a familiarizarnos con el sistema vamos a escribir el tpico programa que imprime Hello World!. En un
editor preparamos el siguiente programa (m as adelante explicaremos la diferencia entre print y la funci on que usamos
aqu):
main = putStrLn "Hola amigos"
Ahora lo compilamos y comprobamos su funcionamiento:
linux> ghc hello.hs -o hello
linux> ./hello
Hola amigos
La compilaci on produce los archivos hello (el ejecutable), hello.o (c odigo objeto), y hello.hi (un interface que
le viene bien para la compilaci on separada).
Una forma mejor de compilar es la siguiente:
linux> ghc hello.hs --make -Wall -O
De esta forma se comprueba si hay que recompilar m odulos y adem as incluye las directivas necesarias para usar tranquila-
mente todas las bibliotecas del sistema (podramos hacer lo mismo con un makele tpico y ghc -c, etc. En realidad muchas
veces viene bien tener un makele y tambi en usar --make, sobre todo cuando combinamos Haskell y C)
La opci on -Wall no suele ser necesaria ya que avisa de cosas que no suelen importar mucho, por ejemplo que un nombre
oculta a otro, o que una funci on no tiene signatura (lo cual es normal, ya que el compilador la deduce el solito), o que una
funci on denida mediante pattern matching no cubre todos los casos (no es exhaustiva), lo que a veces s es importante (se
nos ha olvidado) y a veces no (puede ser intencionado, ya que ese caso no puede producirse nunca).
3
La opci on -O consigue un c odigo que normalmente se ejecuta m as r apido, pero, sobre todo, puede tener el efecto de que la
complejidad espacial de ciertas funciones sea la que realmente esperamos. Sin ella podran implementarse ingenuamente
y ser intratables.
Ahora vamos a probar el int erprete. en un terminal tecleamos ghci. Debera salir algo como:
linux> ghci
___ ___ _
/ _ \ /\ /\/ __(_)
/ /_\// /_/ / / | | GHC Interactive, version 6.6, for Haskell 98.
/ /_\\/ __ / /___| | http://www.haskell.org/ghc/
\____/\/ /_/\____/|_| Type :? for help.
Loading package base ... linking ... done.
Prelude> 2+2
4
Prelude>
Podemos probar el programilla anterior desde el int erprete:
Prelude> :l hello.hs
k, modules loaded: Main.
Prelude Main> main
Hola amigos
Prelude Main> D
linux>
Un proceso tpico es escribir las funciones en un chero, probarlas en en int erprete, si hay que mejorarlas se hace :r eload,
hasta que todo vaya bien, y luego se termina ya el programa real con entrada/salida, opciones, etc. Por cierto, tambi en
podemos ejecutar un programa fuente directamente mediante la utilidad runhaskell:
linux> runhaskell hello.hs
Hola amigos
Y tambi en usando #!/usr/bin/runhaskell para indicar un int erprete a bash, como cualquier lenguaje de scripts
(perl, python, ruby, etc.). Por ejemplo, el siguiente gui on invierte el orden de todas las lneas recibidas por la entrada
est andard:
#! /home/brutus/apps/ghc-6.6/bin/i386-unknown-linux/runhaskell
f = reverse
main = interact (unlines . map f . lines)
Para ver si funciona se lo aplicamos a el mismo:
linux> cat script.hs | ./script.hs
lleksahnur/xunil-nwonknu-683i/nib/6.6-chg/sppa/suturb/emoh/ !#
esrever = f
)senil . f pam . senilnu( tcaretni = niam
linux>
Algunos comandos de unix pueden expresarse de manera muy simple en Haskell:
http://haskell.org/haskellwiki/Simple unix tools
2.3 Bibliotecas
La suite ghc incluye un conjunto muy amplio de bibliotecas, algo as como la biblioteca est andar de Haskell, junto con
algunas otras tambi en muy utiles (OpenGL, etc.) Para buscar en ellas podemos usar la documentaci on html generada
autom aticamente por la herramienta Haddock (el doxygen de Haskell):
4
/home/brutus/apps/ghc-6.6/share/html/index.html
o usar una herramienta muy pr actica llamada Hoogle, a la que le puedes preguntar por el nombre (aproximado) o la sig-
natura (aproximada) de la funci on y te devuelve funciones que podran ser la que buscas, con enlaces a la documentaci on:
http://www.haskell.org/hoogle/
Hoogle puede instalarse en refox como otro motor de b usqueda m as.
Aqu est an denidas, con ejemplos de uso, las funciones m as utilizadas: Tour of the standard prelude.
Y esto es un motor en la web capaz de varias cosas: lambdabot.
2.4 Eciencia
Es conveniente que nos hagamos una idea de la eciencia que podemos conseguir en un ejemplito sencillo comparando
con C. Consideremos el siguiente programa que calcula una especie de sucesi on de Fibonacci m odulo 13:
#include <stdio.h>
int main(int argc, char
*
argv[]) {
int a = 1, b = 1, n, j, z;
n = atoi(argv[1]);
for(j = 3; j<=n; j++) {
z = (a + b)%13;
a = b;
b = z;
}
printf("%d\n",b);
return 0;
}
Lo ejecutamos con un valor grande del argumento:
linux> gcc -O3 fibo.c -o fiboc
linux> time ./fiboc 10000000
10
real 0m0.268s
user 0m0.240s
sys 0m0.004s
Ahora vamos a escribir algo parecido en Haskell aunque l ogicamente de forma recursiva (aunque luego veremos algunas
alternativas curiosas):
import System(getArgs)
main = do
args <- getArgs
let n = read (args!!0)
print $ fib n 1 1
fib 2 _ b = b
fib n a b = fib (n-1) b (a|+|b)
a |+| b = mod (a+b) 13
El tiempo requerido es aproximadamente el doble que el de la versi on en C anterior:
linux> ghc --make -O2 fibo.hs -o fiboh
[1 of 1] Compiling Main ( fibo.hs, fibo.o )
5
Linking fiboh ...
linux> time ./fiboh 2 10000000
10
real 0m0.564s
user 0m0.536s
sys 0m0.000s
Podemos armar que la p erdida de eciencia no es excesiva, sobre todo si tenemos en cuenta que est a trabajando con
Integer de tama no ilimitado! (muy poquito m as lentos que el tipo Int de la m aquina, lo cual me sorprende, debo
comprobarlo. . . )
De todas formas, podemos esperar que escribiendo el c odigo sin ning un cuidado, el tiempo de c alculo aumente alrededor
de 10x en relaci on a C. Normalmente, esto se puede mejorar bastante, a veces sacricando la claridad (lo cual no tiene
ning un sentido, en mi opini on). En la p agina shootout hay una comparativa de muchos lenguajes (de la que en realidad
no puede concluirse nada, ya que la gracia est a precisamente en cambiar los pesos de lo que consideras importante en la
puntuaci on para que tu lenguaje favorito gane). Lo curioso es que teniendo en cuenta unicamente la velocidad de ejecuci on
Haskell no sale muy mal parado.
3 Ejemplos Sencillos
En esta secci on mostramos algunos trocitos de c odigo Haskell para hacernos una idea de c omo funciona. En esta p agina
hay una colecci on de ejemplos de programaci on sencillos y explicados: Haskell Wiki: 99 Haskell exercises.
3.1 Alternativas sint acticas
Nuestro primer ejemplo es la tpica funci on factorial, que tiene una implementaci on recursiva tpica, muy similar a la que
escribiramos en C:
fact n = if n==0
then 1
else n
*
fact (n-1)
La evaluaci on de la funci on consigue el resultado esperado:
Main> f 4
24
La aplicaci on de funciones se hace sin par entesis, poniendo los argumentos a continuaci on y tiene la m axima precedencia
en las expresiones con operadores. Es interesante observar que el scope se consigue con la indentaci on (layout), de manera
intuitiva (o si se desea, explcitamente con llaves y punto y coma).
Tambi en podemos denir la funci on dando diferentes deniciones para distintos (constructores de) datos:
fact 0 = 1
fact n = n
*
fact (n-1)
Tambi en podemos denirla a trozos:
fact n | n==0 = 1
| n<0 = error("fact de negativo")
| otherwise = n
*
fact (n-1)
donde hemos tratado de detectar un valor de entrada malo:
Main> f (-4)
***
Exception: fact de negativo
O mediante la construcci on case:
6
fact n = case n of
0 -> 1
n -> n
*
fact (n-1)
Esto tambi en funciona, si no queremos usar la indentaci on:
fact n = case n of {0->1;n->n
*
fact(n-1)}
Y otra forma m as sera usar la notaci on de listas de datos enumerables:
fact n = product [1..n]
Donde la notaci on anterior construye una lista, que incluso puede ser innita:
Main> sum (take 5 [1..])
15
En realidad, sum y product son casos particulares del concepto fold: la reducci on de una lista mediante una cierta
operaci on binaria (que podemos pensar que sustituye a la coma que separa los elementos de la lista), y con un cierto valor
inicial (que podemos pensar que sustituye a la lista vaca del nal). En el preludio hay varias versiones de fold. Por
ejemplo, podramos escribir:
fact n = foldr (
*
) 1 [1..n]
En The evolution of Haskell programmer podemos encontrar muchas m as formas de escribir la funci on factorial, con
comentarios humorsticos.
3.2 Operadores y list comprehensions
Ahora vamos a escribir una funci on que nos dice si un n umero es perfecto. Para ello denimos un operador para que luego
quede m as claro el c odigo, y los divisores de un n umero los expresamos como una list comprehension (crear una lista
sacando elementos de otra (u otras), y que cumplan ciertas condiciones):
infixl 5 <| -- Definimos un nuevo operador
n <| m = mod n m == 0 -- n es divisible por m
isPerfect n = sum (divis n) == n
where
divis n = [x | x <- [1..n-1], n <| x]
-- la lista de los x entre 1 y n-1
-- que dividen exactamente a n
Parece que funciona:
Main> take 3 (filter isPerfect [1..])
[6,28,496]
Con una list comprehension tambi en podemos generar f acilmente tripletas pitag oricas:
triplets n = [(y,x,z)| z <- [2..], x<-[1..z], y <- [1..x-1], xn + yn == zn]
Si n = 2 encontramos innitas soluciones. Para valores m as grandes equivaldra a refutar el ultimo teorema de Fermat:
*
Main> triplets 2
[(3,4,5),(6,8,10),(5,12,13),(9,12,15),(8,15,17),(12,16,20),(15,20,25),
(7,24,25),(10,24,26),(20,21,29),(18,24,30),(16,30,34),Interrupted.
*
Main> triplets 3
Interrupted.
7
En realidad una list comprehension no es m as que syntactic sugar de una combinaci on del filter y map, por lo que
podramos haber escrito tambi en (es cuesti on de gustos):
divis n = filter (n <|) [1..n-1]
En esta versi on hemos seccionado el operador <|, lo que no es m as que la aplicaci on parcial de uno de los argumentos,
creando una funci on de un solo argumento. Por ejemplo:
Prelude> map (3
*
) [1..5]
[3,6,9,12,15]
Prelude> filter (>=7) [1..10]
[7,8,9,10]
Prelude> map ((3+).(2)) [1..7]
[4,7,12,19,28,39,52]
En el ultimo caso hemos aplicado la funci on que resulta de componer la elevaci on al cuadrado y la suma de 3 sobre una
lista de n umeros. Lo mismo puede hacerse deniendo sobre la marcha una funci on (el smbolo \ signica ):
Prelude> map (\x -> x2+3) [1..7]
[4,7,12,19,28,39,52]
El signo menos (-) sirve para dos cosas: restar y cambiar el signo, por lo que a veces hay que poner un par entesis en el
paso de argumentos: f 3 (-2). Sin par entesis, f 3 -2 signica (f 3) - 2. Por esa raz on, seccionar la resta por la
izquierda es mejor hacerlo con substract.
Prelude> map (2-) [1..5]
[1,0,-1,-2,-3]
Prelude> map (subtract 3) [1..5]
[-2,-1,0,1,2]
3.3 Recursi on
Uno de los algoritmos tpicos que Haskell permite implementar de manera muy elegante es quicksort:
qsort [] = []
qsort (x:xs) = qsort a ++ [x] ++ qsort b
where a = [ y | y <- xs , y < x ]
b = [ y | y <- xs , y >= x ]
O m as directamente:
qsort [] = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs)
Parece que funciona bien:
Main> qsort [1,9,8,2,3,7]
[1,2,3,7,8,9]
Reconozco que solo cuando lo he visto escrito as he comprendido bien el algoritmo.
Otra forma de ordenar es mezclar dos sublistas ordenadas:
mergeSort [] = []
mergeSort [a] = [a]
mergeSort xs = merge (mergeSort a) (mergeSort b)
where
(a,b) = splitAt (length xs quot 2) xs
merge [] b = b
merge a [] = a
merge (a:as) (b:bs)
8
| a < b = a: merge as (b:bs)
| otherwise = b: merge (a:as) bs
Tambi en funciona:
Main> mergeSort [1,9,8,2,3,7]
[1,2,3,7,8,9]
En CodeCodex: Merge sort podemos comparar otros lenguajes (no s e si de forma absolutamente imparcial. . . ). La unica
versi on m as corta es la de Python, pero porque al parecer la funci on merge est a disponible de manera predenida (!).
Otro ejemplo muy elegante de denici on recursiva es la generaci on de permutaciones de una lista:
import Data.List(delete)
permus [x] = [[x]]
permus xs = [x:ps | x <- xs, ps <- permus (delete x xs)]
Que, por supuesto, funciona con listas de cualquier tipo base:
Main> permus [1,2,3]
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
Main> unwords (permus "cosa")
"cosa coas csoa csao caos caso ocsa ocas osca osac oacs oasc scoa scao
soca soac saco saoc acos acso aocs aosc asco asoc"
Se puede mejorar la eciencia con alguna funci on auxiliar, tal y como propuso el autor de esta implementaci on, Sebastian
Sylvan, aparecida en la lista Haskell-Cafe. (El problema es que delete quita el elemento buscando uno igual, no
quita lo que hay en una determinada posici on de la lista, que en este caso sera bastante m as eciente si la lista contiene
elementos m as complejos que un n umero). Adem as fallara si hay elementos repetidos.
3.4 Correcursi on
Haskell permite deniciones correcursivas, en las que las llamadas recursivas no tienen por qu e aplicarse a un caso m as
simple del problema. Si se hacen las cosas bien pueden funcionar porque la evaluaci on es no estricta.
Un ejemplo tpico de esto es una Criba de Erat ostenes sobre una lista de n umeros innita:
primos = sieve [2..]
where
sieve (p:ns) = p : sieve [x | x <- ns , x mod p /= 0]
Se va generando la lista soluci on en t erminos de elementos de ella misma, de manera sana, sin desparramar en consumo
de memoria.
El funcionamiento parece correcto:
Main> primos
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,
73,79,83,89,97,101,103,107,109,113,12Interrupted.
Ejercicio: reescribe sieve usando map y filter.
Realmente tenemos los primos en una lista innita, de manera que la primera vez que pedimos un elemento tarda un rato
en calcularlo, pero la segunda vez contin ua calculando a partir de lo que ya tiene. Esto se llama memoization:
*
Main> :set +s
*
Main> primos!!1000
7927
(2.41 secs, 39956832 bytes)
*
Main> primos!!1001
7933
(0.00 secs, 0 bytes)
9
*
Main> sum (take 1000 primos)
3682913
(0.00 secs, 0 bytes)
(Para que el int erprete ghci nos d e informaci on de tiempo y espacio requerido por la computaci on usamos la opci on
:set +s. Observa tambi en que la versi on compilada es m as r apida.)
Como los elementos de la lista innita de primos est an ordenados, podemos comprobar f acilmente si un elemento est a o
no en ella:
esPrimo n = n elem (fst $ span (<=n) primos)
(usamos la funci on elem de forma inja para facilitar la legibilidad). Funciona bien:
Main> esPrimo 1001
False
Observa que en ning un momento hemos puesto un lmite a los n umeros con los que podemos trabajar.
En el siguiente ejemplo nos enfrentamos al caso tpico de una doble recursi on que si se programa ingenuamente se vuelve
intratable:
fibo 0 = 1
fibo 1 = 1
fibo n = fibo (n-1) + fibo (n-2)
As, por ejemplo:
Main> fibo 35
14930352
(8.93 secs, 1435761932 bytes)
Como alternativa podemos fabricar una lista innita con los elementos de la sucesi on, donde cada elemento se calcula a
partir de los anteriores (el operador !! extrae el elemento deseado de una lista, empezando por cero):
fibos = 1:1:[fibos!!(i-1) + fibos!!(i-2) | i <-[2 ..] ]
fibonacci = (fibos!!)
Esto ya es mucho m as eciente:
Main> fibonnaci 35
14930352
(0.01 secs, 0 bytes)
Otra posiblidad que no indexa explicitamente los elementos es usar zip, que toma dos listas y devuelve una lista de tuplas:
fibos = 1:1:[a+b | (a,b) <- zip fibos (tail fibos)]
Las tuplas son combinaciones de dos o m as datos del tipo que deseemos. Se usan mucho para devolver m as de un resultado
en las funciones. En el caso m as usual de 2-tuplas, las funciones fst y snd nos permiten sacar el primero y el segundo
elemento de la tupla, aunque suele quedar m as elegante desestructurarlas con pattern matching.
Ahora bien, si se va a operar inmediatamente con los elementos de la tupla, es mejor usar zipWith, que cambia la tupla
por la aplicaci on de una funci on cualquiera:
fibos = 1:1: zipWith (+) fibos (tail fibos)
Y otra posibilidad m as es usar list comprehensions paralelas:
{-# OPTIONS -fglasgow-exts #-}
fibos = 1:1:[ x+y | x<- fibos | y <- tail fibos]
10
Esta construcci on no est a en el estandard Haskell98, es una extensi on del lenguaje, que igual que otras tambi en muy utiles
se consigue con la opci on -fglasgow-exts en la lnea de ordenes o indic andolo en la primera lnea del c odigo fuente.
3.5 Polimorsmo
La siguiente funci on obtiene una estimaci on robusta de la localizaci on de una masa de datos que no es unimodal:
-- el algoritmo de estimacion robusta de localizacion
-- inventado por Pedro E Lopez de Teruel
import Data.List(sortBy,transpose,minimumBy)
on f g = \x y -> f (g x) (g y)
robustLocation :: Ord b => (a -> a -> b) -> [a] -> [(a,b)]
robustLocation dis xs = mins where
mins = map (minimumBy (compare on snd)) dst
dst = transpose ds
ds = map getdis xs
getdis p = sortBy (compare on snd) [(p, dis p y) | y<-xs]
Sirve para cualquier tipo de dato y cualquier funci on de distancia. Por ejemplo, podemos probarlo con simples n umeros:
dist x y = abs (x-y)
dat = [1,2,3,11,12,13,14,15::Double]
main = print $ robustLocation dist dat
Conseguiramos algo como:
*
Main> main
[(1.0,0.0),(1.0,1.0),(2.0,1.0),(12.0,2.0),(13.0,2.0),(11.0,8.0),(11.0,9.0),(11.0,10.0)]
A partir de ah es inmediato elegir el elemento en la posici on deseada, o analizar la estructura de distancias para elegirlo
autom aticamente. Por ejemplo, de 13 a 11 se pasa de distancia 2 a distancia 8, el mayor incremento en la secuencia,
por lo que la estimaci on nal podra ser 13. En una secci on posterior utilizaremos este estimador para la fabricaci on de
clasicadores. En ese caso los datos ser an vectores y la funci on de distancia algo como dist x y = norm (x-y).
El siguiente algoritmo consigue estimaciones robustas de modelos para explicar un conjunto de datos. La idea se en-
tiende f acilmente observando el c odigo (aunque por brevedad no se muestran algunas funciones auxiliares). Se incluyen
comentarios aclaratorios sobre los argumentos que apareceran en la documentaci on html generada automaticamente.
-- | version basica, no adaptativa del algoritmo RANSAC
ransac :: ([a]->t) -- modelador
-> (t -> a -> Bool) -- criterio de inlier
-> Int -- numero de objetos necesarios para estimar cada modelo
-> Int -- numero de repeticiones
-> [a] -- datos de entrada, que incluyen outliers
-> (t,[a]) -- resultado, en forma de (modelo, inliers)
ransac estimator isInlier n t dat = (result, goodData) where
result = estimator goodData
goodData = inliers bestModel
bestModel = maximumBy (compare on (length.inliers)) models
models = take t (map estimator (samples n dat))
inliers model = filter (isInlier model) dat
(La versi on adaptativa es un poco m as larga y liosa, por lo que no la incluyo aqu.)
Ahora podemos usar ransac para lo que deseemos, simplemente deniendo la funci on de estimaci on b asica y lo que se
considera inlier. Por ejemplo, para estimar homografas escribimos algo parecido a:
11
isInlierHomog t h (dst,src) = norm (vd - vde) < t
where vd = vector dst
vde = inHomog $ h <> homog (vector src)
estimateHomography corresp = -- appropriate definition
niceHomography umbral = ransac estimateHomography (isInlierTrans umbral) 4
y para estimar matrices fundamentales hacemos algo an alogo:
isInlierFund t f (x,x) = epipolarQuality f x x < t -- defined elsewhere
estimateFundamental corresp = -- appropriate definition
niceFundamental umbral = ransac estimateFundamental (isInlierFund umbral) 8
Por supuesto, habra que usar ransac adaptativo y, si se desea, recalcular el modelo nal con los inliers encontrados.
Tambi en hay que tener cuidado con la normalizaci on, etc. Todos esos detalles no se incluyen aqu.
3.6 Cast explcito
En Haskell no hay typecasting autom atico. Debemos transformar siempre los tipos explcitamente:
mean list = sum list / fromIntegral (length list)
la funci on length devuelve un entero, que no puede operarse con la divisi on:
Prelude> sum [1,2,3] / length [1,2,3]
<interactive>:1:0:
No instance for (Fractional Int)
arising from use of / at <interactive>:1:0-27
Possible fix: add an instance declaration for (Fractional Int)
In the expression: (sum [1, 2, 3]) / (length [1, 2, 3])
In the definition of it:
it = (sum [1, 2, 3]) / (length [1, 2, 3])
observa que, a pesar de que la lista parece de enteros, el sistema deduce que lo que contiene es alg un tipo Fractional
1
ya que hay una divisi on, pero no puede hacer nada con el denominador, que es un int. La funci on fromIntegral est a
sobrecargada y convierte cualquier cosa entera en cualquier cosa fraccional.
Observa que las constantes num ericas son smbolos polim orcos (sobrecargados?):
Prelude> :set + t
Prelude> :m + Complex
Prelude Complex> 2.3 + 5.4
7.7
it :: Double
Prelude Complex> 2.3 + (5.4::Float)
7.7
it :: Float
Prelude Complex> 2.3 + (5.4::(Complex Double))
7.7 :+ 0.0
it :: Complex Double
Prelude Complex> (2.3::Double) + (5.4::Float)
<interactive>:1:17:
Couldnt match expected type Double against inferred type Float
In the second argument of (+), namely (5.4 :: Float)
1
Para no agobiar a usuario, y solo en el int erprete, a los n umeros cuyo tipo queda ambiguo se les da un tipo por omisi on. Para los de punto otante se
asume Double y para los enteros Integer (con tama no ilimitado).
12
In the expression: (2.3 :: Double) + (5.4 :: Float)
In the definition of it: it = (2.3 :: Double) + (5.4 :: Float)
Normalmente, el uso en otro lugar hace que el tipo de un objeto polim orco quede especicado y casi nunca hay que hacer
anotaciones explcitas de tipos.
3.7 Estructuras de Datos
Adem as de los tipos de datos b asicos (Integer, Bool, Double, etc. y los tipos estructurados (listas y tuplas entre
otros), el programador puede denir sus propios tipos de datos de forma muy sencilla, mediante lo que se conoce como
(algebraic datatypes), en los que cada tipo tiene uno o m as constructores, y cada uno contiene cero o m as datos de
cualquier otro tipo. Veamos algunos ejemplos.
El siguiente tipo puede representar vectores del espacio 3D. Es una especie de registro con 3 campos double:
data Vector3 = Vec Double Double Double
p:: Vector3
p = Vec 2.5 8 (-3.2)
Podemos aprovecharlo para la denici on de un cuaterni on:
data Quaternion = Quat Double Vector3
Alternativamente podemos usar una notaci on m as parecida a la de los registros, donde se da un nombre a los campos, que
son funciones de acceso a ellos:
data Pixel = Pt { pixRow :: Int, pixCol :: Int}
p = Pt 3 5 -- simple definition
q = Pt { pixRow = 3, pixCol = 5} -- explicit definition
r = p {pixRow = pixCol q + 7 } -- record update
Los tipos de datos pueden tener sus componentes parametrizados:
data Tripleta a = Tripleta a a a -- parametric type
x = Tripleta False True True :: Tripleta Bool
x = Tripleta 2 3 57 :: Tripleta Int
-- z = Tripleta False 7 8 doesnt typecheck
Puede haber varios par ametros distintos:
data Raro a b = Raro a [b]
x = Raro 8.7 [4,3,2,-5] :: Raro Double Int
En este caso coincide el nombre del tipo y el del constructor. Eso se hace muchas veces y no hay ning un problema, ya que
el contexto siempre indica si nos referimos a un tipo o a un valor.
Los tipos pueden tener varios constructores alternativos y ser recursivos:
-- un arbol binario con un cierto tipo de objeto en los nodos y en las hojas.
data Tree a = Hoja a | Nodo a (Tree a) (Tree a)
x :: Tree Bool
x = Hoja False
13
y :: Tree Int
y = Nodo 6 (Hoja 5) (Nodo 8 (Hoja 1) (Hoja 2))
Los tipos predenidos tambi en se podran denir as (aunque por eciencia, en los tipos num ericos se haga internamente
de manera m as eciente):
data Bool = False | True
data Int = ... -5 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 ...
-- [a] is syntactic sugar for something like
-- data List a = Nil | Cons a (List a)
-- data [] a = [] | a : ([] a)
Se puede denir constructores de datos binarios mediante operadores que empiezan por :. Por ejemplo:
data Complex a = a :+ a
El tipo Maybe, disponible en la biblioteca est andar, sirve para contener datos opcionales o que fueden fallar:
data Maybe a = Just a | Nothing
safedivision :: Int -> Int -> Maybe Int
safedivision _ 0 = Nothing
safedivision a b = Just (a div b)
Como veremos en el captulo de monads, ser a muy sencillo combinar tipos Maybe para transferir autom aticamente valores
v alidos o Nothing en una cadena de operaciones que pueden fallar.
3.8 Abstracci on y modularizaci on
El siguiente ejemplo muestra varios conceptos muy importantes de Haskell. Se trata de un m odulo que suministra el tipo
pila:
module Stack (Stack, empty, push, pop, top, isEmpty) where
data Stack a = Stk [a]
empty = Stk []
push x (Stk xs) = Stk (x:xs)
pop (Stk (x:xs)) = Stk xs
top (Stk (x:xs)) = x
isEmpty (Stk xs) = null xs
-- sacado del Haskell wiki, OOP vs type classes
-- newtype can be used instead of data
Podemos hacer varias observaciones:
El ejemplo muestra la forma de denir un m odulo. Se le da un nombre, una lista de smbolos a exportar y a continuaci on
las deniciones que deseemos. Si no hay lista de exportaci on todos los smbolos ser an visibiles. Las signaturas se deducen
del c odigo.
Este ejemplo tambi en ilustra la forma de crear tipos de datos abstractos. El m odulo exporta el tipo Stack pero no su
constructor Stk, que solo es visible dentro del m odulo, en su implementaci on. El usuario solo puede trabajar con las
funciones exportadas: push, pop, etc.
La implementaci on es extraordinariamente concisa, y su correcci on pr acticamente salta a la vista. Se hace en base a
una lista, a nadiendo y eliminando elementos por la cabeza, lo que es eciente (otras estructuras de datos (p.ej. una deque)
14
necesitaran una implementaci on m as cuidadosa).
La pila denida es polim orca. Funcionar a con cualquier tipo base: n umeros, listas, tuplas, arboles o incluso otras pilas
de pilas. A pesar de ello, podemos compilar el c odigo y guardarlo en una biblioteca de manera que posteriormente pueda
usarse con toda tranquilidad y agilidad.
Dado que estamos en un entorno funcional, hay que insistir en que las operaciones con la pila generan nuevas pilas, no
mutamos el estado de una pila. A pesar de esto, no se generan copias, sino que se van a nadiendo y quitando elementos a
la lista interna, que ser a parcialmente compartida por las diferentes pilas que vayan surgiendo. Por ejemplo, si queremos
usar una pila para invertir una lista, metiendo todas los elementos y luego sac andolos, podramos hacerlo as:
invierte obj = result where
pila = foldr push empty obj
result = saca pila []
where saca p l | isEmpty p = l
| otherwise = saca (pop p) (top p:l)
Parece que funciona:
*
Main> invierte [1..5]
[5,4,3,2,1]
*
Main> invierte "Alberto"
"otreblA"
Es sorprendente el poder expresivo de un mecanismo de modularizaci on y abstracci on tan sencillo.
3.9 Closures
La aplicaci on parcial de funciones es un mecanismo sint actico muy pr actico, como podemos apreciar en la siguiente
situaci on.
Nuestro interface con la GSL nos proporciona una sencilla funci on de integraci on num erica, que vamos a usar con una
precisi on predenida y sin preocuparnos mucho del error obtenido.
import GSL
quad f a b = fst $ integrateQAGS 1E-9 100 f a b -- with typical parameters
La probamos con la funci on f(x) = x
2
entre 0 y 1:
*
Main> :t quad
quad :: (Double -> Double) -> Double -> Double -> Double
*
Main> quad (2) 0 1
0.3333333333333333
Podemos calcular mediante la siguiente integral:
=

1
0
4
1 + x
2
dx
deniendo la funci on sobre la marcha:
*
Main> quad (\x -> 4/(1+x2)) 0 1
3.141592653589793
*
Main> pi
3.141592653589793
(Por supuesto tambi en podramos denir f x = 4/(1+x2) y hacer quad f 0 1)
Lo interesante es que con la aplicaci on parcial podemos denir muy f acilmente una funci on de integraci on 2D, pr acticamente
igual que una denici on matem atica:
15
quad2 f a b g1 g2 = quad h a b
where h x = quad (f x) (g1 x) (g2 x)
Vamos a probarla integrando una funci on de dos variables que nos da el volumen de una esfera de radio r:
volSphere r = 8
*
quad2 (\x y -> sqrt (r
*
r-x
*
x-y
*
y))
0 r (const 0) (\x->sqrt (r
*
r-x
*
x))
main = do
print $ volSphere 2.5
print $ 4/3
*
pi
*
2.5
**
3 -- para comparar
Funcionar a como es debido porque compila bien (typechecks). Pero para tranquilizar al lector desconado podemos
ejecutar el programa:
*
Main> main
65.44984694978736
65.44984694978736
Y ya que estamos en el int erprete preguntamos por el tipo que tiene:
*
Main> :t quad2
quad2 :: (Double -> Double -> Double)
-> Double
-> Double
-> (Double -> Double)
-> (Double -> Double)
-> Double
LLama bastante la atenci on que podemos usar desde Haskell una funci on de C de manera mucho m as c omoda que en el
mismo C. Podemos experimentar con ella en el int erprete, usar la aplicaci on parcial del integrando para expresar claramente
una integral doble, y, lo que es m as importante, los par ametros adicionales de las funciones se suministran de manera
natural mediante aplicaci on parcial, en lugar de usar punteros void a los que se hace un cast dentro de la funci on.
Otro ejemplo tpico que ha surgido en varias aplicaciones de vision: el usuario pincha con el rat on y queremos el punto
caliente m as cercano para hacer algo con el. La funci on esencial es algo como:
on f g = \x y -> f (g x) (g y)
closest [] p = p
closest hp p = minimumBy (compare on dist p) hp
where dist (Pixel a b) (Pixel x y) = (a-x)2+(b-y)2
Sin embargo, la aplicaci on parcial no solo es util desde el punto de vista sint actico, ahorrando el trabajo de escribir
funciones auxiliares con la estructura de argumentos requerida por la funci on que la usa. Es mucho m as que eso, debido al
concepto de closure, que intentaremos explicar con un ejemplo. En todo caso, la wikipedia:closure lo explica muy bien.
Consideremos la siguiente funci on, que tiene dos argumentos, uno de los cuales podemos considerar como un par ametro,
y el otro como el argumento real:
fun a x = 3
*
z -- algo simple
where z = work a -- algo muy costoso
La funci on se calcula muy f acilmente a partir de z, que requiere una computaci on muy costosa work que solo depende
de a. Por ejemplo, a podra ser una base de datos y z el resultado de un cierto proceso de aprendizaje. Si vamos a usar
muchas veces fun a, con el mismo a, sobre diferentes x, no tiene sentido estar recalculando z en cada llamada. Por
ejemplo, si hacemos:
f = fun 5
a = map f [1..1000]
b = map (fun 7) [1..1000]
16
queremos que z se calcule dos veces, no 2000. Que este tipo de optimizaci on se produzca de forma autom atica depende
un poco del compilador. Si queremos estar seguros, compilamos con -O y escribimos la funci on explcitamente as:
fun a x = g
where g x = 3
*
z -- sencillo
z = work a -- costoso
Cada aplicaci on parcial fun 5, fun 7, etc., formar a una closure con su propio z precalculado de forma autom atica. En
otros lenguajes habra que separar articialmente (a veces es interesante hacerlo, pero no siempre) la construcci on de los
z y pasarlos como par ametro a la funci on principal.
Este concepto se usa extensivamente en una secci on siguiente, donde las m aquinas de aprendizaje fabrican clasicadores,
capturando internamente los ejemplos, funciones de kernel, sus par ametros, etc., de forma autom atica.
En C++ se puede conseguir algo parecido con objetos funci on. Se dene de una clase con operador () cuyo constructor
la inicializa con los argumentos necesarios. Sin embargo es como matar moscas a ca nonazos; se necesitan muchas deni-
ciones auxiliares y no es muy pr actico generar funciones sobre la marcha. De hecho existen bibliotecas que tratan de dotar
a C++ de esta capacidad, y lo consiguen, pero con menos claridad y elegancia.
3.10 Evaluaci on no estricta (lazy)
De forma parecida a la evaluaci on shortcut de expresiones booleanas en C o de los pipes de Unix, donde un programa
consume la salida de otro, en Haskell solo se eval uan las expresiones que son estrictamente necesarias para obtener el
resultado necesario (normalmente forzado por una operaci on de entrada salida). Esto tiene la desventaja de una cierta
p erdida de eciencia, pero en muchsimas aplicaciones compensa con creces al permitir una mayor claridad y modularidad
del c odigo. Por ejemplo, permite separar la generaci on de candidatos a resolver un problema de la comprobaci on de las
soluciones, como se observa a continuaci on.
Hay un m etodo muy sencillo y curioso para calcular la raz de un n umero N. A partir de una estimaci on a la mejoramos
con
1
2
(a + N/a) (creo que es el m etodo de Newton-Raphston). Converge muy r apidamente. Veamos como podemos
programarlo:
En primer lugar escribimos la funci on que mejora una cierta aproximaci on:
apro n a = 0.5
*
(a + n/a)
y la probamos a ver qu e tal funciona:
*
Main> :t apro
apro :: (Fractional a) => a -> a -> a
*
Main> apro 5 2
2.25
Ahora construimos la lista innita de aproximaciones (empezando por n/2, un valor inicial no especialmente peor que
otro):
rs n = iterate (apro n) (n/2)
Y comprobamos que funciona:
*
Main> :t rs
rs :: (Fractional a) => a -> [a]
*
Main> rs 5
[2.5,2.25,2.236111111111111,2.2360679779158037,
2.23606797749979,2.23606797749979 C
Por supuesto tenemos que interrumpir el programa. Ahora vamos a escribir una funci on que dada una lista de elementos,
me devuelve el primero que se repite:
fixedPoint (a:b:xs) | a == b = a
| otherwise = fixedPoint (b:xs)
17
Vemos que funciona:
*
Main> :t fixedPoint
fixedPoint :: (Eq t) => [t] -> t
*
Main> fixedPoint [1,2,3,4,5,5]
5
Finalmente combinamos la generaci on con la comprobaci on de que hemos terminado:
raiz n = fixedPoint (rs n)
*
Main> raiz 7
2.6457513110645907
*
Main> sqrt 7
2.6457513110645907
*
Main> sqrt 7 - raiz 7
0.0
*
Main> sqrt 2 - raiz 2
2.220446049250313e-16
Esto se puede mejorar un poco. En primer lugar, fixedPoint usa una comparaci on de igualdad estricta, que no tiene
mucho sentido en n umeros de coma otante. Por otro, recibe una lista. Quiz a sera mejor que recibiera una funci on y
obtuviera el punto jo de ella con una funci on de comparaci on gen erica:
fix comp f a = fp (iterate f a)
where fp (a:b:xs) | comp a b = a
| otherwise = fp (b:xs)
Vemos que el tipo de fix es una funci on que recibe la funci on de comparaci on, la que genera las aproximaciones y
devuelve una funci on, que a partir del valor inicial devuelve el punto jo:
*
Main> :t fix
fix :: (t -> t -> Bool) -> (t -> t) -> t -> t
Nuestra funci on raiz anterior es el punto jo de las aproximaciones apro (dado n) que empiezan en un cierto valor:
*
Main> fix (==) (apro 2) 1
1.414213562373095
Podemos escribirlo as:
raiz n = fix (==) (apro n) (n/2)
Y funciona:
*
Main> raiz 2
1.414213562373095
Lo interesante es que ahora podemos cambiar f acilmente la funci on de comparaci on y la de generaci on de estimaciones.
Por ejemplo, vamos a comparar de manera que el valor absoluto sea menor que epsilon:
compabs e a b = abs (a-b) < e
Comprobamos que funciona bien:
*
Main> fix (compabs 0.1) (apro 4) 1
2.05
*
Main> take 10 (iterate (apro 4) 1)
[1.0,2.5,2.05,2.000609756097561,2.0000000929222947,2.000000000000002,2.0,2.0,2.0,2.0]
Tambi en podemos comparar por tama no relativo (aunque en este ejemplo no se nota mucho la diferencia entre ambos por
el tama no proximo a la unidad de las aproximaciones):
comprel e a b = abs (a-b) / a < e
18
Funcionar a con races c ubicas?
*
Main> fix (comprel 1E-6) (\a->0.5
*
(a+27/a2)) 1
3.000001383950946
Parece que s. Entonces vamos a intentar una raz n-sima:
root k n = fix (comprel 1E-6) (apro k n) 1
where apro k n a = 0.5
*
(a+n/a(k-1))
Desafortunadamente, funciona solo a medias:
*
Main> root 3 27
3.000001383950946
*
Main> root 3 10003
999.9984735702694
*
Main> root 4 24
Interrupted.
Con races grandes se pone a oscilar. . .
La evaluaci on no estricta nos permite tambi en incluir en una estructura de datos elementos derivados de los esenciales,
aunque se utilicen en pocas ocasiones. Solo se calcular a en cada caso lo que sea realmente necesario. Un caso tpico es
la descripci on estadstica de una poblaci on. Nos interesa la media, la matriz de covarianza, los vectores y valores propios,
etc. Una funci on, digamos stat, puede denir todo eso y luego extraemos lo que haga falta, sin repetir c alculos. Tampoco
tenemos que preocuparnos por generar todas las soluciones de un problema aunque a veces solo se necesite una.
Parece ser que el equational reasoning es mucho m as potente en lenguajes con laziness.
4 Clases de Tipos
Una de las diferencias m as grandes de Haskell con respecto a otros lenguajes es el concepto de clase de tipos. No se trata
de las clases de los lenguajes orientados a objetos. Aunque haya alguna relaci on (analizada en OOP vs type classes) se
trata de un concepto distinto; es mejor no intentar traducir los conceptos de un enfoque al otro, sino considerarlos como
complementarios.
En cierto sentido la programaci on consiste en denir funciones que convierten valores de unos tipos en otros. Sin embargo,
est a claro que no resulta nada c omodo usar diferentes smbolos para la suma de Int y la suma de Double, o diferentes
smbolos para ordenar listas de Float y listas de Bool, etc. Necesitamos reutilizar o sobrecargar las funciones para
que trabajen con diferentes tipos de manera natural y consistente. Por ejemplo, en algunos lenguajes como C++ eso se
hace mediante herencia o mediante la denici on de una funci on con el mismo nombre pero diferentes argumentos. Es un
mecanismo muy exible y pr actico en muchos casos.
En Haskell, la losofa es ligeramente distinta. Consideremos todos los tipos de datos del lenguaje, ya sean simples,
estructurados, predenidos o denidos por el usuario. Tenemos que imaginar que dentro de este universo de tipos hay
conjuntos (clases), en los que podemos meter los tipos que deseemos, sin exigir una estructura jer arquica. Por ejemplo,
la clase Eq contiene los tipos sobre cuyos valores podemos preguntar si son iguales, tienen denido un operador (==).
Muchos tipos est an en la clase Show, que posee la funci on show, capaz de convertir los valores en Strings, serializ andolos.
Tambi en est a la clase Read, que posee la funci on read capaz de analizar sint acticamente Strings para obtener un valor de
ese tipo. Una clase muy importante es Num, la de los objetos que se pueden sumar, restar y multiplicar. Hay muchas clases
de tipos en la biblioteca est andar, podemos verlas en este esquema:
19
En general, las funciones son del tipo m as general compatible con las operaciones utilizadas, lo que se reeja en su
signatura. Consideremos las signaturas de las siguientes funciones polim orcas:
concat :: [a] -> [a] -> [a]
sort :: (Ord a) => [a] -> [a]
La primera de ellas es completamente general. Dada dos listas de un cierto tipo cualquiera, sin ninguna restricci on,
concat devuelve otra lista de ese mismo tipo. Pero la segunda tiene un requisito. Su signatura se leera as: sort es
una funci on que toma una lista, cuyos elementos son de cualquier tipo que est e en la clase Ord (o sea, cuyos elementos se
puedan comparar) y devuelve una lista de ese mismo tipo. Eso signica que podemos ordenar listas de n umeros (todos los
tipos num ericos pertenece a la clase Ord, pero, por ejemplo, no podemos ordenar funciones:
*
Main Data.List> sort [5,4,7]
[4,5,7]
*
Main Data.List> let funs = [sin, (+4), \x->2
*
x+2]
*
Main Data.List> map ($7) funs
[0.6569865987187891,11.0,16.0]
*
Main Data.List> sort funs
<interactive>:1:0:
No instance for (Ord (Double -> Double))
arising from use of sort at <interactive>:1:0-8
Possible fix:
add an instance declaration for (Ord (Double -> Double))
In the expression: sort funs
In the definition of it: it = sort funs
La funci on print solo funciona con tipos de la clase Show, (mostrables):
*
Main Data.List> :t print
print :: (Show a) => a -> IO ()
La siguiente funci on require que sus argumentos est en en dos clases:
f :: (Show a, Eq a) => a -> a -> IO ()
f x y = if x == y then print x else print "hola"
Si una clase est a incluida en otra, la signatura solo exige la m as concreta:
*
Main Data.List> :t (\x -> print(x+1))
(\x -> print(x+1)) :: (Num a) => a -> IO ()
Ya que todos los tipos de la clase Num est an dentro de la clase Show.
Cada vez que creamos un tipo de datos lo acostumbrado es instalarlo en unas cuantas clases de tipos razonables, casi
siempre Show, Read, Eq (lo cual puede hacerse autom aticamente). Si el tipo admite algo como sumas y restas podemos
instalarlo en Ord, Num, Floating, etc. Todo depende de la aplicaci on.
Como ejemplo, vamos a denir un arbol binario:
20
data Binarbol a = Hoja a
| Nodo (Binarbol a) a (Binarbol a)
deriving(Show,Read,Eq)
a = Nodo (Hoja 7) 5 (Nodo (Hoja 2) 13 (Hoja 1)) -- para hacer pruebas
La instrucci on deriving hace que el compilador cree instancias razonables (basadas en los componentes) de las clases
indicadas. (Esto solo se puede hacer con las clases Eq, Ord, Enum, Bounded, Show, and Read, en las que no hay am-
biguedad, y nos permite empezar a trabajar c omodamente. Si lo deseamos luego podemos denirlas nosotros de otra
forma.) Comprobamos que las instancias funcionan:
*
Main> show a
"Nodo (Hoja 7) 5 (Nodo (Hoja 2) 13 (Hoja 1))"
*
Main> Nodo a 7 a
Nodo (Nodo (Hoja 7) 5 (Nodo (Hoja 2) 13 (Hoja 1))) 7 (Nodo (Hoja 7) 5 (Nodo (Hoja 2)
13 (Hoja 1)))
*
Main> a /= a
False
*
Main> a == Hoja 6
False
El int erprete hace print a la expresi on evaluada, y para ello utiliza show. La funci on read funciona con cualquier tipo
base en el arbol:
*
Main> read "Hoja False":: Binarbol Bool
Hoja False
*
Main> read "Hoja [4,5,6]":: Binarbol [Int]
Hoja [4,5,6]
Para poder aplicar c omodamente una funci on a todos los elementos del arbol vamos a instalar nuestro tipo en la clase
Functor:
instance Functor Binarbol where
fmap f (Hoja x) = Hoja (f x)
fmap f (Nodo izq x der) = Nodo (fmap f izq) (f x) (fmap f der)
Ahora podemos hacer lo siguiente:
*
Main> fmap (+5) a
Nodo (Hoja 12) 10 (Nodo (Hoja 7) 18 (Hoja 6))
*
Main> fmap (filter odd) $ Nodo (Hoja [1,2,3]) [4,5] (Hoja [6,7,8])
Nodo (Hoja [1,3]) [5] (Hoja [7])
La funci on fmap es la versi on general de map (para listas), que funciona en cualquier tipo functor. En realidad deberamos
usar fmap tambi en para listas, pero por tradici on se ha mantenido la versi on map en el preludio:
*
Main> :t fmap
fmap :: (Functor f) => (a -> b) -> f a -> f b
*
Main> fmap even [1..5]
[False,True,False,True,False]
El tipo Maybe tambi en es un functor:
*
Main> fmap (
*
2) Nothing
Nothing
*
Main> fmap (
*
2) (Just 5)
Just 10
Para ilustrar c omo se dene una instancia num erica, vamos a denir operaciones aritm eticas con arboles, elemento a
elemento, y si hay que operar una hoja con un nodo se utiliza el valor de la hoja en com un para todo el sub arbol que
le corresponde. Por claridad denimos antes la aplicaci on de un operador cualquiera sobre arboles con la interpretaci on
deseada:
21
binop op (Hoja x) (Hoja y) = Hoja (x op y)
binop op (Nodo i x d) (Nodo i x d) = Nodo (binop op i i) (x op x) (binop op d d)
binop op h@(Hoja x) n@(Nodo _ _ _) = binop op (Nodo h x h) n
binop op n@(Nodo _ _ _) h@(Hoja _) = binop (flip op) h n
Con binop y fmap es inmediato denir las operaciones num ericas:
instance Num a => Num (Binarbol a) where
(+) = binop (+)
(-) = binop (-)
(
*
) = binop (
*
)
fromInteger = Hoja . fromInteger
signum = fmap signum
abs = fmap abs
Comprobamos que funcionan como deseamos:
*
Main> a + (Nodo (Hoja 1) 2 (Hoja 3))
Nodo (Hoja 8) 7 (Nodo (Hoja 5) 16 (Hoja 4))
*
Main> 5
*
a-7
Nodo (Hoja 28) 18 (Nodo (Hoja 3) 58 (Hoja (-2)))
*
Main> a2
Nodo (Hoja 49) 25 (Nodo (Hoja 4) 169 (Hoja 1))
*
Main> a - a
Nodo (Hoja 0) 0 (Nodo (Hoja 0) 0 (Hoja 0))
Por supuesto, las operaciones num ericas no funcionan con arboles cuyos tipos no sean num ericos:
*
Main> 3
*
Hoja False
<interactive>:1:0:
No instance for (Num Bool)
arising from the literal 3 at <interactive>:1:0
Possible fix: add an instance declaration for (Num Bool)
In the first argument of (
*
), namely 3
In the expression: 3
*
(Hoja False)
In the definition of it: it = 3
*
(Hoja False)
Tambi en podemos denir nuestras propias clases de tipos, que tendr an operadores que podr an ser utilizados sobre todos
aquellos datos cuyos tipos se hayan hecho instancias de el. Como ejemplo, vamos a inventarnos una clase de tipos,
caracterizados porque admiten una operaci on rara:
class Rara a where
(|) :: a -> a -> a
Cuando se aplica a listas, la operaci on rara hace, por ejemplo, lo siguiente:
instance Rara [a] where
a | b = a ++ reverse b
*
Main> [1..5] | [1..3]
[1,2,3,4,5,3,2,1]
Y cuando se aplica a arboles como los anteriores, la operaci on rara hace otra cosa (completamente distinta y tambi en
absurda, pero con la misma signatura):
instance Num a => Rara (Binarbol a) where
a | b = Nodo a 0 (fmap (+5) b)
Observa que la operaci on rara sobre arboles exige que el tipo base sea num erico:
22
*
Main> Hoja 4 | Nodo (Hoja 3) 7 (Hoja 7)
Nodo (Hoja 4) 0 (Nodo (Hoja 8) 12 (Hoja 12))
Y no se admitir a con tipos base que no lo sean:
*
Main> Hoja False | Hoja True
<interactive>:1:0:
No instance for (Num Bool)
arising from use of | at <interactive>:1:0-23
Possible fix: add an instance declaration for (Num Bool)
In the expression: (Hoja False) | (Hoja True)
In the definition of it: it = (Hoja False) | (Hoja True)
Activando la extensi on -fglasgow-exts es posible denir clases con varios par ametros, es decir, relaciones entre
tipos. Por ejemplo, una colecci on puede tener como par ametros el contenedor y el tipo base. Otro ejemplo podra ser un
producto matricial m as o menos general, que admita matrices y vectores. La signatura de ese producto sera a -> b ->
c y la clase podra ser Multip a b c, con instancias Multip Mat Mat Mat, Multip Mat Vec Vec, Multip
Vec Mat Vec, e incluso Multip Vec Vec Double. Las clases multiparam etricas se usan frecuentemente con de-
pendencias funcionales para que el compilador pueda eliminar autom aticamente posibles ambig uedades en expresiones
complicadas. P. ej., en el caso de la multiplicaci on matricial se denira algo como Multip a b c | a -> b, in-
dicando que el tipo c queda determinado por los argumentos del producto. Usando esta t ecnica es posible sobrecargar
funciones teniendo en cuenta no solo el tipo de los argumentos sino tambi en el del resultado.
La denici on correcta de clases de tipos es uno de los aspectos m as complejos de Haskell. Muchas veces, para conseguir
lo que uno desea es necesario utilizar extensiones del lenguaje que pueden abrir la caja de Pandora. . .
5 Entrada/Salida y otras M onadas
La Monad es una clase de tipos distintiva de Haskell. No es f acil explicar lo que tienen en com un, lo mejor es mostrar
varios ejemplos, que nos transmitir an algo de intuici on. Posteriormente intentaremos formalizar este concepto. Mientras
tanto, se puede consultar este tutorial: All about monads.
5.1 IO Monad
Para dar una idea de c omo se maneja la entrada/salida, vamos a plantear una serie de ejercicios. La idea es que el lector
piense como resolvera el problema en su lenguaje favorito antes de ver la soluci on que se muestra aqu.
- Escribe un programa que escriba en la salida est andar el m aximo com un divisor de los n umeros que se pasan como
par ametros en la lnea de ordenes:
import System
mcd 0 0 = error "mcd 0 0 is undefined" -- or mcd 0 0 = undefined
mcd x y = gcd (abs x) (abs y)
where
gcd x 0 = x
gcd x y = gcd y (x rem y)
main = do
args <- getArgs
let nums = map read args
case nums of
[] -> error ("no has puesto argumentos")
_ -> print $ foldl mcd 0 nums
La funci on mcd es esencialmente una copia de gcd disponible en el preludio. Observa que getArgs :: IO [String]
y que read :: (Read a) => String -> a.
23
- Escribe un programa que escriba en la salida estandar el mnimo com un m ultiplo de los n umeros que llegan por la entrada
est andar.
import System
mcd 0 0 = error "mcd 0 0 is undefined" -- or mcd 0 0 = undefined
mcd x y = gcd (abs x) (abs y)
where
gcd x 0 = x
gcd x y = gcd y (x rem y)
mcm n m = (n
*
m) div mcd n m
fun :: [Integer] -> Integer
fun [] = undefined
fun xs = foldl mcm 1 xs
main = do
cadena <- getContents
let datos = map read (words cadena)
print (fun datos)
- Escribe un programa que vaya escribiendo en stdout el mnimo com un m ultiplo de los n umeros que van apareciendo en
lnea a lnea en stdin
(ejercicio para el lector)
El tipo IO a tiene como valores la descripci on de acciones que, si se ejecutan, pueden realizar alguna operaci on de
entrada/salida y despu es entregan un valor de tipo a:
putStrLn "Hola" :: IO ()
getChar :: IO Char
print :: (Show a) => a -> IO ()
readFile :: FilePath -> IO String
Podemos guardar en una lista varias acciones:
a = [putStrLn "Hola", print (2+2)] :: [IO()]
Y luego ejecutarlas, si nos parece oportuno:
Main > head a
Hola
Main > sequence a
Hola
4
Las acciones son un tipo de dato m as, que se puede manipular de forma funcional, pura. Esto nos permite denir nuestras
propias estrategias de control. Por supuesto, llegar a un momento en el que las acciones denidas se efectuar an, como
consecuencia de invocar a la funci on main, y, en ese momento, se empezar an a realizar realmente c alculos; hasta ese
momento todo han sido deniciones.
Es importante recordar que el mundo funcional y el mundo real est an comunicados por una puerta que una vez que se
traspasa ya no hay vuelta atr as: toda funci on que usa entrada salida para obtener un tipo a tendr a una signatura acabada en
IO a, (p.ej. String -> IO Int), y ese pecado no se le puede perdonar jam as
2
. Por ejemplo, si preguntamos al
compilador por el tipo de
addChar s = do
c <- getChar
return (c:s)
2
Hmm. . . , no debera hablar de esto, pero existe unsafePerformIO, no recomendado para principiantes, y necesario para implementar llamadas
a funciones externas, si conamos en que el c odigo escrito es puro.
24
que a nade un car acter tecleado por el usuario a la cadena que recibe como entrada, nos dir a:
*
Main> :t addChar
addChar :: [Char] -> IO [Char]
Efectivamente, funciona (la primera x la teclea el usuario):
*
Main> addChar "hola"
x"xhola"
Si ahora combinamos esa funci on con otras, la signatura empieza a contaminarse con el tipo IO:
*
Main> :t map
map :: (a -> b) -> [a] -> [b]
*
Main> :t map addChar
map addChar :: [[Char]] -> [IO [Char]]
Esto permite separar y aislar completamente la parte de procesamiento de datos pura de nuestra aplicaci on, donde rigen
las leyes matem aticas de la transparencia referencial, y en las que el compilador puede aplicar transformaciones y opti-
mizaciones sin ning un peligro, de la parte imperativa, en la que es necesaria una secuenciaci on estricta de acciones.
Como se comenta humorsticamente en la retrospectiva citada en la introducci on, Haskell no ha sucumbido a los cantos de
sirena de los efectos colaterales, manteniendo una actitud extremadamente puritana, como monjes en un monasterio. Esto
tiene inicialmente un coste muy elevado, y muchos lenguajes han cado en la tentaci on. Pero es una actitud que al nal
compensa: los pactos con el diablo (los efectos colaterales) terminan pasando factura, y vienen a cobrarse la deuda cuando
menos te lo esperas. Por el contrario, un lenguaje puro supone un esfuerzo mayor, por el que se paga al principio, pero que
merece la pena al nal.
Los tipos IO a est an en la clase Monad, que tienen la notaci on do y <-, para enfatizar su signicado imperativo, pero
que en realidad se pueden manipular con el operador de secuenciaci on >>=
*
Main> :t (>>=)
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
y con la funci on return, que mete su argumento en la m onada correspondiente:
Prelude> :t return
return :: (Monad m) => a -> m a
En el caso IO:
(>>=) :: IO a -> (a -> IO b) -> IO b
return :: a -> IO a
Por ejemplo:
getLine :: IO String
readFile :: FilePath -> IO String
getLine >>= readFile :: IO String
writeFile :: FilePath -> String -> IO ()
getLine >>= readFile >>= (writeFile "filename" . reverse) :: IO ()
La ultima funci on es equivalente a:
do name1 <- getLine
cadena <- readFile name1
let rev = reverse cadena
writeFile "filename" rev
return ()
El operador >>= es an alogo a la composici on de funciones, pero haciendo entrada salida cada vez (y ledo en direcci on
25
contraria). Si las acciones no tienen resultado (o no lo usamos) se pueden combinar con el operador >>.
Otro ejemplo m as interesante de secuenciaci on con >>= puede verse en el siguiente trozos de c odigo, que usa un interfaz
experimental a las funciones de procesamiento de imagen de la IPP, a las que accedemos como operaciones de entrada
salida (aunque muchas de ellas en realidad podran tratarse como funciones puras).
-- | Returns a list of interest points in the image (as unnormalized [x,y]).
-- | They are the local minimum of the determinant of the hessian (saddlepoints).
getCorners :: Int -- degree of smoothing (e.g. 1)
-> Int -- radius of the localmin filter (e.g. 7)
-> Float -- fraction of the maximum response allowed (e.g. 0.1)
-> Int -- maximum number of interest points
-> ImageFloat -- source image
-> IO [Pixel] -- result
getCorners smooth rad prop maxn im = do
let suaviza = smooth times gauss Mask5x5
h <- suaviza im >>= secondOrder >>= hessian >>= scale32f (-1.0)
(mn,mx) <- minmax h
hotPoints <- localMax rad h
>>= thresholdVal32f (mx
*
prop) 0.0 IppCmpLess
>>= getPoints32f maxn
return hotPoints
Finalmente, planteamos otro ejercicio en el que se utiliza entrada y salida:
- Dene un tipo de datos que sea un arbol recursivo, cuyas hojas son de un cierto tipo, donde cada nodo contiene otro
posible tipo de datos, y de el cuelga un n umero arbitrario de sub arboles. Dado un n umero que se pasa como argumento
crea un arbol de la estructura de divisores, gu ardalo en dsco, leelo de disco, y escribe en la salida est andar la suma de
todas las hojas.
import System
data Tree h n = Hoja h
| Nodo n [Tree h n]
deriving (Show,Read)
divis n = [x | x <- [2..n-1], n rem x == 0]
crea n = case divis n of
[] -> Hoja n
ds -> Nodo n (map crea ds)
sumah (Hoja h) = h
sumah (Nodo _ as) = sum (map sumah as)
main = do
args <- getArgs
let n = read (args!!0)
let arbol = crea n
writeFile "arbol.txt" (show arbol)
cadena <- readFile "arbol.txt"
let tree = read cadena :: Tree Int Int
print (sumah tree)
5.2 Monad Parser
Las instancias de las funciones show y read que se crean autom aticamente pueden no ser muy bonitas:
> crea 12
Nodo 12 [Hoja 2,Hoja 3,Nodo 4 [Hoja 2],Nodo 6 [Hoja 2,Hoja 3]]
26
as que:
- Escribe un analizador sint actico que lea arboles como el anterior con una gram atica m as sencillita, por ejemplo algo como
(12 :-> 2 3 (4 :-> 2) (6 :-> 2 3)), y crea un arbol de, por ejemplo, nodos Bool y hojas Float a partir de
(True :-> (2 3 (False :-> 7) 5).
La siguiente soluci on usa un monadic parser de la biblioteca est andar. Si nos jamos, se utiliza la notaci on do, <-, etc.,
pero ahora no signica entrada y salida y secuenciaci on de acciones, sino extraer cosas que van cumpliendo la gram atica,
que se dene de forma composicional:
{-# OPTIONS -fno-monomorphism-restriction #-} -- je je
import System
import Text.ParserCombinators.Parsec
data Tree h n = Hoja h
| Nodo n [Tree h n]
deriving (Show,Read,Eq)
analiza cadena = case parse parser "" cadena of
Right arbol -> arbol
Left e -> error (show e)
parser = pnodo <|> phoja
phoja = do
h <- word
return $ Hoja (read h)
pnodo = do
char (
spaces
node <- word
spaces
string ":->"
spaces
hojas <- sepEndBy1 (parser) spaces
spaces
char )
return $ Nodo (read node) hojas
word = many1 alphaNum -- muy mal
main = do
print (analiza "45" :: Tree Int Int)
print (analiza "(True :-> 2 23( False:->4 7 9 )5)" :: Tree Integer Bool)
Al ejecutarlo con las cadenas de prueba de la funci on main funciona como esperamos:
*
Main> main
Hoja 45
Nodo True [Hoja 2,Hoja 23,Nodo False [Hoja 4,Hoja 7,Hoja 9],Hoja 5]
Pero es solo un esbozo muy preliminar, para dar una idea de lo que se puede hacer. (Para hacerlo bien, gen erico de verdad,
etc., habra que denir tokens adecuadamente, etc.)
5.3 List Monad
Ahora vamos a mostrar otra situaci on en la que aparece la notaci on do, <-, etc., que tampoco es para entrada y salida.
Antes de nada, observa el siguiente ejemplo:
cosa = do
27
p <- [1 .. 5]
q <- [2,4,6]
return (p+q)
obtendramos
Main> cosa
[3,5,7,4,6,8,5,7,9,6,8,10,7,9,11]
Cuando se trata de listas, esa sintaxis trata de capturar la idea de computaciones no deterministas, que pueden obtener
varias soluciones y por tanto en los c alculos se generan todas las combinaciones (est a muy relacionado con las list com-
prehensions. En cualquier caso, el siguiente ejercicio sera:
- Resuelve el problema de las n-queen como caso particular de un algoritmo de b usqueda exhaustiva y backtracking.
Este ejercicio ilustra las operaciones de la clase de tipos m onada, en su instancia para listas. Intentaremos escribir de forma
concisa un algoritmo general de b usqueda exhaustiva (en profundidad?):
import Control.Monad(guard, mplus)
Lo siguiente es una funci on de b usqueda general. Recibe una lista de estados v alidos (podramos pensar en tableros de un
juego), una funci on okfun que nos dice si un estado es nal (o sea, que es bueno y no hay que buscar m as) y una funci on
sucfun que genera una lista de sucesores posibles a partir de un estado. El funcionamiento es muy sencillo: si el primer
elemento de la lista de casos pendientes es bueno lo a nadimos al resultado como tal y seguimos buscando en los dem as. Si
no, seguimos buscando en sus sucesores y los sucesores de los dem as.
genSearch :: (a -> Bool) -> (a -> [a]) -> [a] -> [a]
genSearch _ _ [] = []
genSearch okfun sucfun (b:bs) = do
let search = genSearch okfun sucfun -- para abreviar un poco
if okfun b
then b: search bs
else search (sucfun b mplus bs ) -- mplus de listas es (++)
Ya est a, lo podramos guardar compilado en un m odulo aparte.
Ahora vamos a utilizar esto en un problema concreto: encontrar todas las soluciones al problema de las n reinas. Denamos
una estructura de datos que representa el tablero. Podramos hacerlo as:
data Board = Board Int [Int] [Int] [Int]
pero preferimos una denci on m as autoexplicativa, en forma de registro. Adem as, instalamos este tipo de datos en la clase
de los tipos que se pueden mostrar, deniendo adecuadamente la funci on show (sobrecargada) de forma que muestre en
modo texto las piezas en el tablero:
data Board = Board { size :: Int
, cols :: [Int]
, diags :: [Int]
, idiags:: [Int]
}
instance Show(Board) where
show (Board _ cs _ _) = concat $ map ((flip pos) (length cs)) cs
where spaces = repeat
pos k n = take (k-1) spaces ++ O : take (n-k) spaces ++ "|\n"
Para poder usar la funci on de b usqueda general s olo nos falta denir cu ando una solucion es buena:
okqueen desiredSize board = desiredSize == size board
y generar los sucesores v alidos de una posici on:
28
queenFollowers size (Board ar cs ds fs) = do
let r = ar+1
c <- [1..size]
guard (not (elem c cs))
guard (not (elem (c-r) ds))
guard (not (elem (r+c) fs))
return $ Board r (c:cs) ((c-r):ds) ((r+c):fs)
queen n = genSearch (okqueen n) (queenFollowers n) [Board 0 [] [] [] ]
main = do mapM_ print $ queen 8
mapM_ print $ queen 4
*
Main> head (queen 25)
O |
O |
O |
O |
O |
O |
O |
O |
O |
O |
O |
O|
O |
O |
O |
O |
O |
O |
O |
O |
O |
O |
O |
O |
O |
5.4 State Monad
Otra situaci on en la que la abstracci on mon adica es util es en la encapsulaci on de estado. Supongamos que queremos
programar la sucesi on de Fibonacci cambiando progresivamente los dos ultimos t erminos calculados hasta el momento:
import Control.Monad.State
update :: State (Int,Int) Int
update = do
(s,sa) <- get
let n = s + sa
put (n,s)
return n
fibo k = fst $ execState (replicateM k update) (1,0)
Esta m onada tiene las funciones propias get y put para extraer y modicar el estado, que en este ejemplo no es m as
que una pareja de enteros. La funci on return genera un resultado observable en las transiciones (en este ejemplo no es
particularmente util). Observa el funcionamiento:
*
Main> runState update (2,1)
29
(3,(3,2))
*
Main> runState (replicateM 10 update) (1,0)
([1,2,3,5,8,13,21,34,55,89],(89,55))
*
Main> fibo 10
89
Expresamos un algoritmo iterativo de manera funcional, pura. Podriamos pensar en la implementaci on de un ltro de
Kalman usando esta t ecnica. . .
5.5 Maybe Monad
El tipo Maybe comentado anteriormente tambi en est a dentro de la clase de las m onadas. En este caso sus operaciones
sirven para encadenar funciones que pueden fallar:
foo x | odd x = Nothing -- only works with even numbers
| otherwise = Just (2
*
x+1)
bar x | x rem 3 /= 0 = Nothing -- only works with multiples of three
| otherwise = Just (3
*
x-2)
*
Main> foo 4
Just 9
*
Main> foo 5
Nothing
*
Main> bar 3
Just 7
*
Main> bar 4
Nothing
*
Main> Just 2 >>= foo
Just 5
*
Main> Just 2 >>= foo >>= bar
Nothing
*
Main> Just 4 >>= foo >>= bar
Just 25
Se puede hace exactamente lo mismo con la notaci on do:
f x = do
a <- foo x
b <- bar a
return b
Por ejemplo:
f :: (Integral a) => a -> Maybe a
*
Main> f 2
Nothing
*
Main> f 4
Just 25
Es importante darse cuenta de que la f denida tiene el mismo tipo que foo y bar, se podra combinar con ellos de la
misma forma, con >>= o la notaci on do:
*
Main> Just 7 >>= foo >>= f >>= bar
Nothing
30
6 Utilidades
6.1 QuickCheck
Esta biblioteca es muy pr actica para depurar. Dada una propiedad de tu algoritmo (una funci on booleana que debe ser
cierta para todas las entradas), esta biblioteca la comprueba autom aticamente con un conjunto de entradas arbitrarias de
tama no creciente. Y lo hace sin que el usuario tenga que escribir pr acticamente nada de c odigo adicional.
Por ejemplo, est a claro que aplicar dos veces reverse debe obtener el resultado inicial:
Prelude> :m + Debug.QuickCheck
Prelude Debug.QuickCheck> :t quickCheck
quickCheck :: (Testable a) => a -> IO ()
Prelude Debug.QuickCheck> quickCheck (\a -> reverse (reverse a) == a)
Loading package QuickCheck-1.0 ... linking ... done.
OK, passed 100 tests.
Prelude Debug.QuickCheck>
Veamos si es cierto que
a
b
=
1
b
a
No lo es por varias razones:
Prelude Debug.QuickCheck> quickCheck (\a b -> a/b == 1/(b/a))
Falsifiable, after 8 tests:
0.0
0.0
Prelude Debug.QuickCheck> quickCheck (\a b -> a/b == 1/(b/a))
Falsifiable, after 0 tests:
3.3333333333333335
-2.3333333333333335
Esta herramienta es capaz de descubrir un mont on de bugs originados por no considerar como listas vacas, divisiones por
cero, etc.
Vamos a ver un ejemplo m as realista de lo util que es quickCheck. Volvemos a escribir la funci on de ordenaci on y una
propiedad que debe cumplir, que cada elemento sea menor o igual que el siguiente:
import Test.QuickCheck
qsort [] = []
qsort (x:xs) = qsort (filter (<x) xs) ++ [x] ++ qsort (filter (>x) xs)
prop1 l = and $ zipWith (<=) s (tail s)
-- [a <= b | a <- s | b <- tail s]
where s = qsort l
*
Main> quickCheck prop1
OK, passed 100 tests.
Si queremos podemos ver las pruebas que hace:
*
Main> verboseCheck prop1
0:
[]
1:
[3]
2:
[]
3:
[]
4:
31
[]
5:
[3,-3,-2]
6:
[4,2,3,-5,4]
7:
[-4,3,-1,-3]
8:
[-4,2,0,-1]
etc.
Hay que tener en cuenta que si en lugar de trabajar en el int erprete (donde los tipos num ericos tienen un default) las
metemos en el c odigo tenemos que indicar el tipo base concreto:
main = do
quickCheck (prop1 :: [Double]->Bool)
verboseCheck (prop1 :: [Int]->Bool)
Parece entonces que el quicksort est a bien programado. Pero para quedarnos m as tranquilos vamos a comprobar alguna
propiedad m as. Por ejemplo, que todos los elementos de la lista est an en la lista ordenada:
xs containedIn ys = all (elem ys) xs
prop2 l = l containedIn (qsort l) && (qsort l) containedIn l
*
Main> quickCheck prop2
OK, passed 100 tests.
Tambi en es correcto. Bueno, aunque sea un poco pesado, para estar completamente seguros, vamos a comprobar que la
lista original y la ordenada tienen la misma longitud:
prop3 l = length l == length (qsort l)
Lo probamos y nos damos cuenta de que hay un bug!
*
Main> quickCheck prop3
Falsifiable, after 5 tests:
[3,3]
En la segunda llamada recursiva habamos copiado mal y puesto qsort (filter (>x) xs) en lugar de qsort
(filter (>=x) xs) y por tanto si haba elementos duplicados solo se meta uno en la lista ordenada.
La idea es escribir las propidades de tus funciones importantes, incluso antes que su implementaci on, y pasar una batera
de tests justo antes de cada commit en el repositorio (se puede automatizar).
(pendiente un ejemplo m as ilustrativo, donde creamos la instancia arbitrary de nuestros tipos de datos).
6.2 OpenGL
Est a disponible en las biblioteca est andard: HOpenGL.
6.3 GUI
Hay dos bastante buenos: Gtk2Hs y wxhaskell.
6.4 Estructuras de datos funcionales
Hay varias en la biblioteca est andar, y tambi en en Edison.
32
6.5 Proling
El compilador integra una herramienta para poder estudiar los cuellos de botella de los programas. Por ejemplo, consider-
emos el siguiente ejemplo sencillo:
f x = x quot 2
g x = 3
*
x+1
collatz :: Integer -> Integer
collatz x | even x = f x
| otherwise = g x
col n = fst $ span (>1) $ iterate collatz n
lon n = length (col n)
main = print $ maximum (map lon [1..100000])
Lo compilamos con soporte para proling y lo ejecutamos indicando al sistema de tiempo real de Haskell que deseamos el
informe de ejecuci on:
linux>ghc --make -O collatz.hs -prof -auto-all
[1 of 1] Compiling Main ( collatz.hs, collatz.o )
Linking collatz ...
linux> ./collatz +RTS -p
178
Durante la ejecuci on se crea el informe collatz.prof, que contiene lo siguiente:
linux> cat collatz.prof
Sat Dec 9 14:40 2006 Time and Allocation Profiling Report (Final)
collatz +RTS -p -RTS
total time = 3.85 secs (77 ticks @ 50 ms)
total alloc = 1,387,709,120 bytes (excludes profiling overheads)
COST CENTRE MODULE %time %alloc
col Main 54.5 87.1
collatz Main 24.7 6.2
f Main 10.4 4.1
g Main 7.8 2.1
main Main 1.3 0.4
lon Main 1.3 0.1
individual inherited
COST CENTRE MODULE no. entries %time %alloc %time %alloc
MAIN MAIN 1 0 0.0 0.0 100.0 100.0
main Main 160 1 0.0 0.0 0.0 0.0
CAF Main 154 1 0.0 0.0 100.0 100.0
main Main 161 0 1.3 0.4 100.0 100.0
lon Main 162 100000 1.3 0.1 98.7 99.6
col Main 163 100000 54.5 87.1 97.4 99.5
collatz Main 164 10753840 24.7 6.2 42.9 12.4
g Main 166 3564892 7.8 2.1 7.8 2.1
f Main 165 7188948 10.4 4.1 10.4 4.1
CAF GHC.Handle 107 3 0.0 0.0 0.0 0.0
Observamos la estructura de llamadas y el tiempo consumido por las distintas funciones, tanto por s mismas (individual),
como incluyendo el tiempo de las funciones auxiliares invocadas (inherited).
Hay muchas opciones de proling, que pueden consultarse en el manual del compilador.
6.6 Cabal
Instalaci on y distribuci on de bibliotecas: muy f acil.
6.7 Haddock
Es un sistema que fabrica documentaci on html a partir de los comentarios del c odigo, an alogo a Doxygen de C++.
33
6.8 Darcs
Es un sistema de control de revisiones escrito en Haskell, basado en una teora matem atica de los parches. Est a bastante
bien porque puedes hacer commit ofine y mandar parches por email al due no del respositorio sin necesidad de tener
privilegios en su m aquina.
6.9 Concurrencia
Haskell nos permite lanzar concurrentemente varias acciones (funciones de tipo IO a) de forma muy sencilla. En el
siguiente programita un proceso escribe una larga cadena de aes en el terminal, a la vez que otro escribe una larga cadena
de bs.
main = do
let n = 100000
forkIO $ do
putStrLn (replicate n a)
putStrLn $ (replicate n b)
Las aes y bs aparecen entremezcladas:
*
Main> main
aaabbbaaaaaabbbbaaa
... etc...
aaaaaa
La biblioteca de concurrencia proporciona varias abstracciones de comunicaci on. Por ejemplo, las MVar son variables
con sem aforo, en las que solo puedes escribir si est an vacas o leer si est an llenas (en caso contrario el proceso se queda
esperando). Tambi en hay canales de comunicaci on, etc.
(ejemplo m as completo)
Recientemente se est a trabajando con una nueva abstracci on m as potente para la concurrencia: Software Transactional
Memory (STM).
En Tackling the akward squad se explican los aspectos m as relacionados con el mundo real: Entrada/Salida, concurrencia,
llamadas a funciones de otros lenguajes, etc.
Las versiones de ghc recientes tienen soporte SMP y por tanto pueden conseguir verdadero paralelismo: Haskell Wiki:
concurrency.
6.10 Tipos existenciales
Supongamos que queremos hacer una lista heterog enea, de objetos de varios tipos. Es importante estar est aticamente
seguro de que en tiempo de ejecuci on no se mete un tipo en la lista que luego, al sacarlo, se le aplique una funci on que no
es admisible.
6.11 Programaci on gen erica
Ejercicio: dada cualquier estructura de datos denida por el usuario, escribe una funci on que busque en ella todos los
componentes de un cierto tipo y les aplique una funci on. Por ejemplo, que incremente todos los enteros que hay en una
estructura, posiblemente recursiva.
Esto es posible hacerlo en Haskell de forma muy sencilla:
import Data.Generics
f :: Int -> Int
34
f = (+1)
genincr = everywhere (mkT f)
La funci on genincr admite todo tipo de datos, e incrementa los enteros que encuentra:
*
Main> :t genincr
genincr :: forall a. (Data a) => a -> a
*
Main> genincr "hola"
"hola"
*
Main> genincr [1,2,3::Double]
[1.0,2.0,3.0]
*
Main> genincr [1,2,3::Int]
[2,3,4]
*
Main> genincr ("hola",length "hola")
("hola",5)
Funciona con cualquier tipo de datos. Debe estar en la clase Typeable y Data, pero las instancias las crea autom aticamente
el compilador:
data KK = KK {a :: Int, b :: Double} deriving (Typeable,Data,Show)
x = KK 2 3
y = [KK 1 2, KK 3 4]
*
Main> genincr x
KK {a = 3, b = 3.0}
*
Main> genincr y
[KK {a = 2, b = 2.0},KK {a = 4, b = 4.0}]
*
Main> genincr (y,(x,y))
([KK {a = 2, b = 2.0},KK {a = 4, b = 4.0}],(KK {a = 3, b = 3.0},
[KK {a = 2, b = 2.0},KK {a = 4, b = 4.0}]))
M as informaci on en Haskell Wiki: Generic Programming.
7 Ejemplo: Pattern Recognition
Una aplicaci on interesante en la que la programaci on funcional ha resultado muy c omoda es en la denici on de los algo-
ritmos sencillos de reconocimiento de patrones. El c odigo completo est a dentro de la distribuci on de GSLHaskell, por si
se quiere mirar la implementaci on detallada. Aqu solo comentaremos las ideas principales.
Nuestro objetivo es dise nar aprendedores, que analizan un conjunto de vectores etiquetados y devuelven una funci on de
clasicaci on.
type Example = (Attributes,Label)
type Classifier = Attributes -> Label
type Sample = [Example]
type Learner = Sample -> Classifier
Nos gustara evaluar la calidad de cada m aquina entren andola con unos ejemplos y obteniendo su tasa de error y su matriz
de confusi on.
errorRate :: Sample -> Classifier -> Double
confusion :: Sample -> Classifier -> Matrix Double
Ahora bien, el Classifier s olo admite como argumento el objeto a clasicar? Normalmente ser a una funci on que tiene
par ametros, optimizados en el aprendizaje. As que las funciones de evaluaci on anteriores deberan usar el clasicador con
35
la estructura generada por su algoritmo de ajuste. Y lo mismo pasa con el Learner, que deber a recibir par ametros de
trabajo (p.ej. tipo de kernel y sus par ametros en una svm, capas y nodos en una red neuronal, etc.) Este es, al menos,
el enfoque al que estamos acostumbrados. El dise no de esto en C++ nos obliga a pensar muy bien lo que queremos,
y posiblemente tendremos que adivinar el futuro. Vamos a ir paso a paso viendo lo que conseguimos con aplicaci on
parcial y closures, lo que nos permite hacer combinadores de funciones que se construyen unas a partir de otras como si
fueran datos. El objetivo es tener una especie de mecano de m aquinas de clasicaci on cuyas piezas se puedan combinar
libremente y sobre la marcha.
La primera observaci on es que un clasicador nos va a entregar dos cosas, el clasicador en s, que dado un vector de
atributos de entrada nos devuelve la clase predicha, y un estimador, que devuelve una lista de la verosimilitud, distancia, o
fuerza de cada clase. El clasicador puede construirse a partir del estimador, tomando la clase m as problable. Esto signica
que solo tenemos que preocuparnos de construir estimadores. Cuando lo tengamos, podemos construir un clasicador:
createClassifier :: InfoLabels -> Estimator -> Classifier
createClassifier ilbs f = getLabel ilbs . posMax . f
No vamos a entrar en los detalles del tipo InfoLabels, simplemente contiene informaci on sobre las etiquetas encontradas
en la muestra, y funciones para convertir ecientemente c odigos en etiquetas y viceversa.
Un estimador de clases muy sencillo est a basado en una funci on de distancia. Por ejemplo la distancia a la media:
type Distance = [Attributes] -> Attributes -> Double
ordinary :: Distance
ordinary vs = f where
m = -- la media de los datos vs
f x = norm (x-m)
Hemos creado un tipo especial anticipando que nos gustara trabajar con otras posibles funciones de distancia.
Con ella podemos fabricar un clasicador general:
-- | A generic distance-based learning machine.
distance :: Distance -> Learner
distance d exs = (c,f) where
(gs,lbs) = group exs
distfuns = map d gs
f x = map (negate.($x)) distfuns
c = createClassifier lbs f
Es muy importante resaltar que esta funci on calcula las funciones de distancia a todos los conjuntos de vectores de cada
clase y se las guarda internamente (en su closure) sin tener que preocuparnos de ellas. Y por supuesto, cada m aquina de
clasicaci on creada con (c,f) = distance dist prob, para diferentes problemas y funciones de distancia, tendr a
internamente sus datos internos independientes.
Veamos c omo funciona:
Main> study problem (distance ordinary)
La funci on study (que no merece la pena detallar aqu) coge el problema (una lista de vectores con sus clases), lo divide
en dos trozos (master y test), entrena el clasicador, muestra la tasa de error y la matriz de confusi on sobre la muestra de
test y si el problema es de 2 atributos dibuja con gnuplot un bonito mapa de clasicaci on.
(mostrar el resultado de ejecutar esto)
Ahora podemos inventar nuevas funciones de distancia:
-- | Mahalanobiss distance to a population.
mahalanobis :: Distance
mahalanobis vs = f where
m = -- mean of vs
ic = inverse of covariance matrix of vs
f x = (x-m) <> ic <> (x-m)
36
-- | distance to the nearest neighbour
nn :: Distance
nn vs v = minimum (map (dist v) vs)
where dist x y = norm (x-y)
Y podemos probarlas exactamente igual:
Main> study problem (distance mahalanobis)
(mostrar varios)
Algunas funciones de distancia pueden tener par ametros, eso no nos preocupa:
-- | distance to the pca subspace of each class
subspace :: PCARequest -> Distance
subspace rq vs = f where
Codec {encodeVector = e, decodeVector = d} = pca rq (stat (fromRows vs))
f v = norm (v - (d.e) v)
Se usara as (en estos problemas 2D de juguete no tiene mucho sentido, pero con los caracteres manuscrito de la base
mnist funciona bastante bien):
*
Main> study problem $ distance (subspace (NewDimension 2))
*
Main> study problem $ distance (subspace (ReconstructionQuality 0.8))
La funci on study o distance pueden estar precompiladas desde hace a nos y admiten perfectamente nuevas (gener-
adores de) funciones de distancia que se nos ocurran. La aplicaci on parcial de sus posibles par ametros de funcionamiento
las deja en forma del tipo Distance requerido.
Por supuesto, hay m etodos m as avanzados para fabricar clasicadores, por ejemplo una red neuronal! Es muy sencilla de
programar. La red se representa simplemente como una lista de matrices de pesos.
-- given a network and an input we obtain the activations of all nodes
forward :: NeuralNet -> Vector Double -> [Vector Double]
forward n v = scanl f (join [v,1]) (weights n)
where f v m = tanh (m <> v)
-- given a network, activations and desired output it computes the gradient
deltas :: NeuralNet -> [Vector Double] -> Vector Double -> [Matrix Double]
deltas n xs o = zipWith outer (tail ds) (init xs) where
dl = (last xs - o)
*
gp (last xs)
ds = scanr f dl (zip xs (weights n))
f (x,m) d = gp x
*
(trans m <> d)
gp = gmap $ \x -> (1+x)
*
(1-x)
updateNetwork alpha n (v,o) = n {weights = zipWith (+) (weights n) corr}
where xs = forward n v
ds = deltas n xs o
corr = map (scale (-alpha)) ds
epoch alpha n prob = foldl (updateNetwork alpha) n prob
backprop alpha n prob = scanl (epoch alpha) n (repeat prob)
Esto es lo esencial. Simplemente necesitamos alguna funci on auxiliar para pasar las etiquetas a c odigos posicionales, para
inicializar la red, etc. Lo importante es que tenemos un estimador y (a partir de el) un clasicador. Y parece que funciona:
*
Main> study problem (neural 0.1 0.05 100 [10])
*
Main> study problem (neural 0.05 0.05 100 [20,10,5])
(mostrar)
37
Hasta ahora, hemos fabricando clasicadores multiclase completos (a partir del estimador). Pero lo cierto es que muchas
veces se desarrollan t ecnicas clasicaci on binaria que luego se extienden de forma m as o menos sencilla al caso general.
Usamos la notaci on siguiente:
-- | A function that tries to discriminate between two classes of objects
type Feature = Attributes -> Double -- +/-
type TwoGroups = ([Attributes],[Attributes]) -- +/-
-- | A learning machine for binary classification problems.
type Dicotomizer = TwoGroups -> Feature
Es muy sencillo crear el combinador multiclass. Lo que hace es crear un conjunto de problemas en los que se trata de
distinguir una clase de todas las dem as, se entrenan con el dicotomizador deseado, y las features obtenidas se juntan en un
estimador (y con el se obtiene el clasicador).
multiclass :: Dicotomizer -> Learner
multiclass bin exs = (createClassifier lbs f, f) where
(gs,lbs) = group exs
f = multiclass bin gs
multiclass bin [g1,g2] = (\x -> [x,-x]) . bin (g1,g2)
multiclass bin l = f where
fs = map bin (auxgroup l)
f v = map ($v) fs
auxgroup x = zip x (map (concat . flip delete x) x)
Para probarlo, necesitamos alg un dicotomizador. Por ejemplo, una soluci on de mnimos cuadrados ingenua basada en la
pseudoinversa:
-- | mse linear discriminant using the pseudoinverse
mse :: Dicotomizer
mse (g1,g2) = f where
m = (fromRows g1 <-> fromRows g2) <|> constant 1 (size b)
b = join [constant 1 (length g1), constant (-1) (length g2)]
w = pinv m <> b
f v = tanh (join [v,1] <> w)
Funciona como debe (no muy bien en problemas no linealmente separables):
*
Main> study problem (multiclass mse)
Para mejorarlo, podramos intentar el kernel trick:
type Kernel = (Vector Double -> Vector Double -> Double)
delta f l1 l2 = matrix $ partit (length l1) $ [f x y | x <- l1, y <- l2]
kernelMSE :: Kernel -> Dicotomizer
kernelMSE kernel (g1,g2) = fun where
fun z = expan z dot a
expan z = vector $ map (kernel z) objs
a = pinv (delta kernel objs objs) <> labels
objs = g1 ++ g2
labels = join [constant 1 (length g1), constant (-1) (length g2)]
-- | polynomial Kernel of order n
polyK :: Int -> Kernel
polyK n x y = (x dot y + 1)n
-- | gaussian Kernel of with width sigma
gaussK :: Double -> Kernel
gaussK s x y = exp (-(norm (x-y) / s)2)
38
Que funciona mucho mejor, encontrando una frontera no lineal (si usamos el kernel y su par ametro adecuado).
*
Main> study problem (multiclass (kernelMSE (polyK 2)))
*
Main> study problem (multiclass (kernelMSE (polyK 5)))
*
Main> study problem (multiclass (kernelMSE (gaussK 0.2)))
Aunque por supuesto lo ideal sera enganchar con una buena implementaci on de las m aquinas de vectores de soporte
propiamente dichas.
Esta infraestructura (que viene dada de forma natural en el lenguaje, ya que no hemos hecho nada raro para conseguirla)
nos permite escribir metaclasicadores de forma inmediata. Se nos ocurren dos, en principio. Uno es los arboles de
clasicaci on y el otro el adaboost. Ambos tratar an de combinar dicotomizadores (tpicamente d ebiles), para conseguir un
dicotomizador m as fuerte (que luego se puede convertir en clasicador multiclase). Sin embargo, adaboost trabaja con
dicotomizadores que admiten un peso en cada ejemplo. A estos los llamamos WeightedDicotomizer.
type Weights = Vector Double
-- | An improved Dicotomizer which admits a distribution on the given examples.
type WeightedDicotomizer = TwoGroups -> Weights -> Feature
Uno tpico es stumps, que encuentra la mejor frontera perpendicular a un eje.
-- | weak learner which finds a good threshold on a single attribute
stumps :: WeightedDicotomizer
Sera bueno que todos los dicotomizadores fueran ponderados. Como no lo son, creamos dos combinadores que nos
permiten pasar de un tipo al otro mediante remuestreo de ruleta o pesos uniformes:
-- | Converts a WeightedDicotomizer into an ordinary Dicotomizer (using an uniform distribution).
unweight :: WeightedDicotomizer -> Dicotomizer
unweight dic (g1,g2) = dic (g1,g2) w where
m = length g1 + length g2
w = constant (1/fromIntegral m) m
-- | Converts a Dicotomizer into a WeightedDicotomizer (by resampling).
weight :: Int -- seed of the random sequence
-> Dicotomizer -> WeightedDicotomizer
weight seed dic (g1,g2) w = dic (g1,g2) where
s = ungroup [g1,g2]
ac = scanl1 (+) (toList w)
rs = take (length ac) $ randomRs (0, 1::Double) (mkStdGen seed)
rul = zip ac s
elm pos = snd $ head $ fst $ partition ((>pos).fst) rul
[g1,g2] = fst (group (breakTies seed 0.0001 $ map elm rs))
En cualquier caso, denimos el algoritmo de construcci on de un arbol de decisi on:
-- | Creates a decision tree
treeOf :: (TwoGroups -> Bool) -> Dicotomizer -> Dicotomizer
treeOf stopQ method gs@(g1,g2) = if stopQ gs || not improved then leaf else node where
n1 = length g1
n2 = length g2
leaf = if n1>n2 then const 1 else const (-1)
node v = if d v > 0 then d1 v else d2 v
d = method gs
(g11,g12) = partition ((>0).d) g1
(g21,g22) = partition ((>0).d) g2
d1 = treeOf stopQ method (g11,g21)
d2 = treeOf stopQ method (g12,g22)
improved = (length g11, length g21) /= (n1,n2) &&
(length g12, length g22) /= (n1,n2)
39
-- | stopping criterium for treeOf. A new decision node is created if the minoritary class has more than n samples
branch :: Int -> (TwoGroups -> Bool)
branch n (g1,g2) = min (length g1) (length g2) <= n
Que se usara as:
study problem (multiclass $ treeOf (branch 0) mse)
let problem = (breakTies 0 0.1 $ rings 1000)
*
Main> study problem (multiclass $ treeOf (branch 0) (unweight stumps))
*
Main> study problem (multiclass $ treeOf (branch 0) (kernelMSE (polyK 2)))
*
Main> study problem (multiclass $ treeOf (branch 0) (perceptron 0.1 0.1 10 [2]))
(perceptron es la versi on Dicotomizador de la m aquina neuronal comentada anteriormente.)
La implementaci on de adaboost no es complicada pero por brevedad solo mostramos la signatura. El primer argumento es
el n umero de rondas.
adaboost:: Int -> WeightedDicotomizer -> Dicotomizer
Y es muy f acil de usar:
let problem = (breakTies 0 0.1 $ rings 1000)
*
Main> study problem (multiclass $ adaboost 100 stumps)
*
Main> seed <- randomIO :: IO Int
1210918827
*
Main> study problem (multiclass $ adaboost 100 (weight seed mse))
Y para terminar, llegamos a la orga composicional:
let machine = multiclass $
adaboost 10 $
weight seed $ treeOf (branch 5) $
perceptron 0.1 0.1 10 [2]
*
Main> study problem machine
Es decir, hacer un clasicador multiclase a partir de de clasicadores binarios de tipo adaboost que combinan como
clasicador d ebil arboles de decisi on cuyos nodos son redes neuronales de una capa oculta con 2 elementos. Como
adaboost required un dicotomizador ponderado se lo fabricamos sobre la marcha con la funci on weight. Podemos
imaginar cosas peores. . .
Todo esto se puede estudiar en el int erprete, aunque el c odigo principal est e compilado.
Falta ver ejemplos con mnist, usando los combinadores de preprocesamiento (witPCA, etc.)
withPCA rq = withPreprocess (mef rq)
withMDF = withPreprocess mdf
study mnist (withPCA (NewDimension 20) $ distance ordinary)
study mnist (withPCA (ReconstructionQuality 0.9) $ withMDF $ distance mahalanobis)
Finalmente, para ilustrar la reusabilidad de c odigo vamos a escribir un clasicador por distancia a la localizaci on robusta
obtenida por el algoritmo de PedroE, explicado en el captulo 3. Solo tenemos que hacer algo como:
pedrodist prop l = f where
dist x y = norm (x-y)
f = dist m
m = fst (ds!!k)
ds = robustLocation dist l
k = round (prop
*
fromIntegral(length l)) -- no es lo ideal
y entonces podemos trabajar con m aquinas como por ejemplo:
study problem (distance (pedrodist 0.8))
(falta mostrar resultado)
40
8 Ejemplo: M as correcursi on
Este artculo de J. Karczamarczuk es fant astico: The Most Unreliable Technique in the World to compute .
Se puede hacer algo parecido pero solo con sumas parciales de una serie muy sencillita, no con la suma completa como en
el artculo anterior, que aprovecha una expansi on en la que sabemos que los dgitos ya no cambiar an.
Una fracci on se representa mediante una base y la lista de sus innitos dgitos:
data Frac = Frac Int [Int]
Tambi en podemos denirla al estilo de un record, que es m as c omodo para poder sacar de ella ambos componentes y para
a nadir campos en el futuro sin romper el c odigo ya disponible.
data Frac = Frac { base:: Int,
digs:: [Int] }
La funci on frac n d calcula la expansi on de n/d.
frac base 0 _ = Frac base (repeat 0)
frac base n d = Frac base (digits base n d)
digits base n d = a : digits base (base
*
b) d
where (a,b) = quotRem n d
-- metemos al tipo Frac en la clase Show haciendo que
-- muestre p.ej. los primeros 20 decimales
instance Show(Frac) where
show (Frac b (e:d)) = show e ++ "," ++ (concatMap show (take 20 d)) ++ "..."
Parece que se generan bien los dgitos:
*
Main> frac 10 1 2
0,50000000000000000000...
*
Main> frac 10 1 3
0,33333333333333333333...
*
Main> frac 10 6 7
0,85714285714285714285...
*
Main> frac 2 1 3
0,01010101010101010101...
Ahora vamos a implementar las operaciones aritm eticas.
-- suma
Frac b u <+> Frac b v
| b == b = Frac b $ cpr b (zipWith (+) u v)
| otherwise = error "bases diferentes"
-- acarreos mirando al futuro
cpr b (u0:u1:us)
| u1 < b = u0 : cpr b (u1:us) -- seguro que no hay acarreo
| u1 > b = (u0+1) : cpr b (u1-b:us) -- seguro que hay
| u1 == b = let v1:vs = cpr b (u1:us) -- lo hay si lo hay en el siguiente...
in if v1 < b
then u0:v1:vs -- no lo ha habido
else u0+1:v1-b:vs -- lo ha habido
where b = b-1
-- para negar un decimal hacemos complemento a la base,
-- quedando positivos, y la parte entera se hace negativa.
neg (Frac b (u:us)) = Frac b $ (negate u -1) : map ((b-1) -) us
41
a <-> b = a <+> neg b
-- aunque falla con fracciones acabadas en infinitos ceros,
-- ya que al sumar genera acarreo infinito. Se puede arreglar
-- pero no merece la pena arreglarlo para este ejercicio.
Parece que suma y resta bien:
*
Main> frac 10 3 7 <-> frac 10 2 7
0,14285714285714285714...
*
Main> frac 10 1 7
0,14285714285714285714...
(El tipo Frac se podra instalar en la clase Num para poder usar +,
*
, etc.)
Ya podemos obtener sumas parciales de la f ormula de Leibniz:

n=0
(1)
n
2n + 1
= 1
1
3
+
1
5

1
7
+
1
9

1
11
+ . . . =

4
Para alternar autom aticamente los t erminos positivos y negativos podemos expresarlo tambi en como:
=

n=0
4
4n + 1

4
4n + 3
Parece que funciona:
*
Main> let s = scanl1 (<+>) [frac 10 4 (4
*
k+1) <-> frac 10 4 (4
*
k+3) | k<-[0..]]
*
Main> s!!400
3,14034577128141221542...
Pero converge de forma extraordinariamente lenta y, lo que es peor, no sabemos cu ando los dgitos son denitivamente
correctos:
*
Main> s!!1000
3,14109315312144916643...
Por tanto lo mejor es utilizar la t ecnica propuesta en el divertido artculo de Karczamarczuk.
9 Cooperaci on con otros lenguajes
Se usa el fant astico Foreign Function Interface (FFI).
9.1 Haskell Main
9.1.1 C desde Haskell
Es muy sencillo usar funciones de C en Haskell, sobre todo si sus argumentos son solo tipos b asicos o arrays de tipos
b asicos. (Si son structs es un poco m as difcil, pero se puede hacer y hay herramientas que pueden automatizar la tarea.
Como ultimo recurso se pueden escribir funciones de acceso a los campos. . . )
Veamos un ejemplo muy simple. Supongamos que tenemos una funci on escrita en C:
42
#include <stdlib.h>
double foo(int n, double r) {
double s = 1;
int k;
for(k=1; k<=n; k++) {
s
*
=r;
}
return s;
}
Y su correspondiente chero de cabecera:
double foo(int n, double r);
La compilaci on de fun.c produce fun.o (tambi en podramos crear una biblioteca est atica o din amica).
gcc -c fun.c
Para usar esa funci on desde Haskell solo tenemos que usar la directiva de importaci on foreign:
{-# OPTIONS -fffi #-}
import Foreign
foreign import ccall "fun.h foo" foo :: Int -> Double -> Double
main = do
putStr "C call foo(7.5,3) = "
print (foo 3 7.5)
Y en la compilaci on a nadir el c odigo objeto (o la opci on de enlace con una biblioteca):
ghc --make Main.hs fun.o
El programa funciona como deseamos:
$ ./Main
C call foo(7.5,3) = 421.875
En realidad podramos hacerlo directamente, ya que ghc trabaja con gcc:
ghc --make Main.hs fun.c
Pero es m as frecuente la forma anterior, para acceder a funciones de bibliotecas (est aticas o din amicas) ya compiladas,
disponibles en el sistema.
Curiosamente, desde el int erprete podemos probar c omodamente la funcion C:
$ ghci Main fun.o
___ ___ _
/ _ \ /\ /\/ __(_)
/ /_\// /_/ / / | | GHC Interactive, version 6.6, for Haskell 98.
/ /_\\/ __ / /___| | http://www.haskell.org/ghc/
\____/\/ /_/\____/|_| Type :? for help.
Loading package base ... linking ... done.
Loading object (static) fun.o ... done
final link ... done
[1 of 1] Compiling Main ( Main.hs, interpreted )
Ok, modules loaded: Main.
*
Main> foo 2 2
4.0
43
*
Main> foo 5 0.99
0.9509900498999999
*
Main> foo 16 2
65536.0
*
Main> foo 3 5
125.0
*
Main> main
C call foo(7.5,3) = 421.875
9.1.2 C++ desde Haskell
Tambi en podemos usar en Haskell funciones de C++. Para ello solo debemos denirlas como extern "C" y enlazar
con libstdc++. Por ejemplo, la siguiente funci on de C++ usa una deque de la STL:
#include <cstdlib>
#include <deque>
extern "C" int foo(int n);
int foo(int n) {
std::deque<int> l;
for (int i = 1; i<=n; i++) {
l.push_back(i);
}
int s = 0;
for (int i = 0; i<l.size(); i++) {
s+=l[i];
}
return s;
}
Este es el chero de cabecera:
int foo(int n);
Compilamos la funci on C++:
$ g++ fun.cpp -c
y ya tenemos fun.o.
Ahora escribimos el programa Haskell que usar a la funci on C++:
{-# OPTIONS -fffi #-}
import Foreign
foreign import ccall "fun.h foo" foo :: Int -> Int
main = do
putStr "many calls to C++ foo = "
print (map foo [1..1000])
La compilamos a nadiendo la biblioteca de C++:
$ ghc Main.hs fun.o -L/usr/lib/gcc/i486-linux-gnu/4.0.3 -lstdc++
Y el programa parece funcionar:
$ ./a.out
44
many calls to C++ foo = [1,3,6,10,15,21,28,36,45,55,66,78,91,
...
,495510,496506,497503,498501,499500,500500]
Supongo que funcionar a bien en casos m as complejos, aunque no lo he probado. En alg un sitio dice que la funci on main
la debe compilar g++, para inicializar cosas static, lo cual tiene sentido. Veremos lo que pasa en las aplicaciones serias que
llamen a C++.
Por otra parte, no he sido capaz de dar con la forma de usar la funci on anterior de forma interpretada, hay un smbolo que
no logra encontrar en el enlace.
9.2 C o C++ Main
En este caso el procedimiento es similar para C y para C++, los ejemplos siguientes se reeren a C.
9.2.1 Ejemplo mnimo
El siguiente ejemplo muestra c omo podemos usar funciones escritas en Haskell desde nuestros programas en C.
Este es el c odigo Haskell de una cierta funci on:
{-# OPTIONS -fffi #-}
module Fun where
foreign export ccall "hfun" f :: Int -> Int
f n | even n = n quot 2
| odd n = 3
*
n+1
Lo compilamos con:
ghc --make -Wall -O -c fun.hs
Obtenemos un par de avisos sin importancia (en este caso):
fun.hs:7:0: Warning: Definition but no type signature for f
fun.hs:7:0:
Warning: Pattern match(es) are non-exhaustive
In the definition of f: Patterns not matched: _
La opci on Wall nos dice de que la funci on denida no tiene signatura y que no est a denida exhaustivamente. Deberamos
sustituir even n por otherwise. Para hacer la prueba esto no importa.
En cualquier caso, ha generado un c odigo C auxiliar (fun stub.h y fun stub.c) y obtiene dos archivos objeto:
fun.o y fun stub.o. Ahora escribimos el programa en C donde la vamos a utilizar:
#include <stdio.h>
#include <stdlib.h>
#include "fun_stub.h"
int main(int argc, char
*
argv[])
{
hs_init(&argc, &argv);
printf("hfun(5) = %d\n", hfun(5));
hs_exit();
return 0;
}
45
Y lo compilamos con ghc (que se ocupa de llamar a gcc y a enlazar con todo lo necesario):
ghc -Wall -O pru.c fun.o fun_stub.o
El programa funciona como est a previsto:
$ ./a.out
hfun(5) = 16
9.2.2 Crear el ejecutable nal con gcc
En casos m as realistas nos interesar a crear el ejecutable con gcc u otro compilador. No hay ning un problema, aunque la
lnea de ordenes es un poco m as complicada. Mejor hacemos un Makefile:
HDIR=/home/brutus/apps/ghc-6.6/lib/i386-unknown-linux
HLIBS=-L$(HDIR) -lHShaskell98 -lHSbase -lHSbase_cbits -lHSrts -lm -lgmp -ldl -lrt \
-u base_GHCziBase_Izh_static_info -u base_GHCziBase_Czh_static_info \
-u base_GHCziFloat_Fzh_static_info -u base_GHCziFloat_Dzh_static_info -u base_GHCziPtr_Ptr_static_info -u base_GHCziWord_Wzh_static_info -u base_GHCziInt_I8zh_static_info -u base_GHCziInt_I16zh_static_info -u base_GHCziInt_I32zh_static_info -u base_GHCziInt_I64zh_static_info -u base_GHCziWord_W8zh_static_info -u base_GHCziWord_W16zh_static_info -u base_GHCziWord_W32zh_static_info -u base_GHCziWord_W64zh_static_info -u base_GHCziStable_StablePtr_static_info -u base_GHCziBase_Izh_con_info -u base_GHCziBase_Czh_con_info -u base_GHCziFloat_Fzh_con_info -u base_GHCziFloat_Dzh_con_info -u base_GHCziPtr_Ptr_con_info -u base_GHCziPtr_FunPtr_con_info -u base_GHCziStable_StablePtr_con_info -u base_GHCziBase_False_closure -u base_GHCziBase_True_closure -u base_GHCziPack_unpackCString_closure -u base_GHCziIOBase_stackOverflow_closure -u base_GHCziIOBase_heapOverflow_closure -u base_GHCziIOBase_NonTermination_closure -u base_GHCziIOBase_BlockedOnDeadMVar_closure -u base_GHCziIOBase_BlockedIndefinitely_closure -u base_GHCziIOBase_Deadlock_closure -u base_GHCziIOBase_NestedAtomically_closure -u base_GHCziWeak_runFinalizzerBatch_closure -u base_GHCziConc_ensureIOManagerIsRunning_closure
HASKELL= -I$(HDIR)/include $(HLIBS)
fun.o: fun.hs
ghc -O --make -Wall -c fun.hs
pru: pru.c fun.o
gcc -O -Wall pru.c fun.o fun_stub.o $(HASKELL)
clean:
rm -f
*
.o
*
.hi
*
.out
*
_stub.c
*
_stub.h
Las bibliotecas que hay que enlazar se pueden averiguar mirando la salida de la compilaci on con ghc anterior a nadiendo
la opci on -v.
9.2.3 Transferencia de Arrays
Adem as de las funciones Haskell que operan con tipos b asicos, puede ser muy util usar desde C funciones que trabajan
con listas. Vamos a preparar un m odulo que puede ser util en el futuro:
46
{-# OPTIONS -fffi #-}
module ArrayFun(mkC, mkC2, TLL) where
import Foreign
type TLL a b = Ptr a -> Int -> Ptr b -> Int -> Ptr Int -> IO Int
mkC :: Storable a => ([a]->[a]) -> (Ptr a -> Int -> IO Int)
mkC f p n = do
l <- peekArray n p
pokeArray p (f l)
return 0
mkC2 :: (Storable a, Storable b) => ([a]->[b]) -> TLL a b
mkC2 f pIn nIn pOut maxOut pNOut = do
l <- peekArray nIn pIn
let r = f l
let n = length r
if n <= maxOut
then do
pokeArray pOut r
poke pNOut n
return 0
else do
return 1
La funci on mkC convierte una funci on que crea una lista a partir de otra del mismo tipo en una funci on de C que recibe un
array (puntero y tama no) y lo modica con esa funci on. Por su parte mkC2 es parecida a la anterior, pero pone el destino
en un array distinto (indicando su tama no real, que puede no ser el m aximo del buffer), en este caso a partir de una funci on
[a]->[b] (cuyos tipos base pueden ser distintos).
Este m odulo nos sirve como ayuda para exportar las funciones primo (que memoriza los primos que ha ido calculando)
y reverse (que convierte en double e invierte el orden de un array), que podemos denir as:
{-# OPTIONS -fffi #-}
module KK where
import ArrayFun
foreign export ccall "prime" prime :: Int -> Int
foreign export ccall "reverse" c_reverse :: TLL Int Double
c_reverse = mkC2 $ map fromIntegral . reverse
prime = (primes!!) where
primes = sieve [2..]
sieve (p:ns) = p : sieve [x | x <- ns , x mod p /= 0]
Ahora las usamos en un programa en C que podra ser el siguiente. Observa que se imprimen los 10000 primeros numeros
primos, en orden directo e inverso, y luego se hace reverse a un array.
47
#include <stdio.h>
#include <stdlib.h>
#include "funs_stub.h"
int main(int argc, char
*
argv[])
{
hs_init(&argc, &argv);
int i;
for(i=0; i<10000; i++) {
printf("%d ", prime(i));
}
for(i=10000; i>=0; i--) {
printf("%d ", prime(i));
} // estos aestn precalculados...
printf("\n");
int a[10] = {1,2,3,4,5,6,7,8,9,10};
double b[20];
int n;
int err = reverse(a,10,b,20,&n);
if(err) {
printf("Error en reverse\n");
exit(1);
}
for(i=0; i<n; i++) {
printf("%f\n",b[i]);
}
hs_exit();
return 0;
}
La parte relevante del Makele que nos permite crear el programa sera algo as como:
funs.o: funs.hs
ghc -O --make -Wall -c funs.hs
prus: prus.c funs.o
gcc -O -Wall prus.c ArrayFun.o funs.o funs_stub.o $(HASKELL)
Si ejecutamos el programa obtendremos algo como:
$ ./a.out
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109
113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233
...
443 439 433 431 421 419 409 401 397 389 383 379 373 367 359 353 349 347 337 331 317 313
311 307 293 283 281 277 271 269 263 257 251 241 239 233 229 227 223 211 199 197 193 191
181 179 173 167 163 157 151 149 139 137 131 127 113 109 107 103 101 97 89 83 79 73 71 67
61 59 53 47 43 41 37 31 29 23 19 17 13 11 7 5 3 2
10.000000
9.000000
8.000000
7.000000
6.000000
5.000000
4.000000
3.000000
2.000000
1.000000
La lista de primos inversa tarda mucho menos tiempo. Es un buen ejemplo de memoization.
48

Anda mungkin juga menyukai