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