Monoid
class Monoid a where
mempty :: a
mappend :: a -> a -> a
instance Monoid [a] where
mempty = []
mappend = (++)
-- esta instancia no es estandar
instance Monoid Bool where
mempty = False
mappend = (||)
<>
es un sinónimo de mappend
, definido en Data.Monoid
.
¿Que pasa si queremos a proposito definir mas de una instancia de algun tipo T
en la clase C
? Por ejemplo:
Monoid Int
con la operación suma y neutro 0Monoid Int
con la operación producto y neutro 1Básicamente data
con un solo constructor.
Durante la compilación, se chequea el tipo, pero durante la ejecución, Suma
y Prod
son iguales a Int
, sin “overhead” como usualmente hay cuando se crea un tipo algebráico.
Ejercicio: resolver el ejercicio del práctico sobre los monoides.
Está permitido mientras siempre hay una instancia más específica en cualquier caso.
Para permitir instancias solapadas, escribir justo después instance
:
{-# OVERLAPPING #-}
en la instancia más específica{-# OVERLAPPABLE #-}
en la instancia más generalUno de los dos es suficiente.
class C a where
f :: a -> Int
instance C a => C [a] where
...
instance {-# OVERLAPPING #-} C [Int] where
...
Ejercicio: resolver el ejercicio del práctico sobre las instancias solapadas.
a
: cálculo que devuelve un a
data Maybe a = Nothing | Just a
: cálculo que puede fallar, y en caso de éxito devuelve un a
data Either a b = Left a | Right b
: cálculo que puede fallar, en cual caso devuelve un a
(a menudo un mensaje de error o un valor que indica una excepción), sino devuelve un b
[a]
(lista): cálculo que devuelve un valor a
dentro de cero, uno o varios valores a
posibles (por razones de aleatoriedad, por ejemplo)IO a
: calculo que involucra efectos de entrada y salida (leer archivos, usar la red, imprimir en pantalla, etc.), y devuelve un valor a
IO
no tiene una implementación con constructores, es más bien un hack del compilador. ¡Pero como tipo se integra perfectamente al resto!
Haskell es un lenguaje de programación puro, es decir:
Pero… ¡a veces queremos hacer cosas así! Si la única cosa que pudieramos hacer con Haskell es escribir funciones para evaluarlas dentro de GHCi, sería interesante en teoría pero inútil en práctica.
De hecho, es posible hacer ese tipo de cosas en Haskell, pero es muy distinto a lo que se hace en otros lenguajes de programación.
IO
soluciona el problema con la purezaLa solución a esa enigma es un tipo especial llamado IO. Los valores de tipo IO a
son descripciones de cálculos con efectos, que, si los llevamos a cabo, harían (posiblemente) algunas operaciones con efectos de entrada y salida y (al final) producirían un valor de tipo a
.
Hay un nivel de indirección acá que es esencial entender. Un valor de tipo IO a
, en sí, es una cosa inerta, totalmente pura y sin efecto. Es solo la descripción de un cálculo con efecto. Una manera de pensarlo es como si fuera un programa imperativo.
Para ilustrar eso, supongamos que tenemos:
¿Qué tenemos? Una torta deliciosa, por supuesto. Así de simple.
En comparación, supongamos que tenemos:
¿Qué tenemos? ¿Una torta? No, tenemos unas instrucciones para hacer una torta, solo un papel con algunas cosas escritas encima.
No solamente no tenemos una torta, sino que estamos teniendo una receta que no tiene ningun efecto sobre nada. Tener la receta a mano no hace que nuestro horno se va a prender o que la harina se va a repartir sobre la mesada, o algo por el estilo. Para efectivamente producir una torta, la receta debe ser seguida.
Del mismo modo, un valor de tipo IO a
es solo una “receta” para producir algun valor de tipo a
(y posiblemente tener algun efecto de paso). Como cualquier otro valor, puede ser pasado como argumento, devuelto como salida de una función, guardado en una estructura de datos, o (como vamos a ver pronto) combinado con otros valores IO en recetas más complejas.
Entonces, ¿cómo los valores de tipo IO a
terminan siendo ejecutados? De una sola forma: el compilador Haskell busca un valor especial
que va a ser entregado al runtime y ejecutado. ¡Ya está! Podemos pensar en el runtime de Haskell como si fuera el chef que es el único con derecho a cocinar.
Si queremos que nuestra receta sea seguida entonces debemos hacer que sea parte de la receta grande (main
) que es dada al chef. Por supuesto, main
puede ser muy complicada, y típicamente va a ser compuesta de numerosos cálculos IO más pequeños.
Así que escribamos nuestro primer verdadero programa Haskell ejecutable. Podemos usar la función:
que, dada una String
, devuelve un cálculo IO que (cuando es ejecutado) imprime esa String en la pantalla. Entonces podemos poner lo siguiente en un archivo llamado Hello.hs
:
Luego, tipear runhaskell Hello.hs
en línea de comando resulta en nuestro mensaje siendo impreso en la pantalla. También podemos usar ghc Hello.hs
para producir un ejecutable llamado Hello
(Hello.exe
en Windows).
GHC busca un módulo llamado Main
para encontrar la acción principal. Si no ponemos cabezera en un archivo Haskell, el nombre por defecto será Main
, entonces eso funciona, por más que el archivo no se llama Main.hs
. Si queremos usar un nombre de módulo distinto, tenemos que usar una opción en línea de comando para ghc o runhaskell. Por ejemplo si tenemos Something.hs
:
Lo podemos compilar con ghc -main-is Something Something.hs
.
String
“dentro de” un IO String
Muchos usuarios nuevos de Haskell terminan preguntando algo como “Tengo un IO String
, ¿cómo lo convierto en una String
?” o “¿Cómo consigo la String
dentro del IO String
?”.
Dadas las explicaciones anteriores, debería estar claro que estas preguntas no tienen sentido: un valor de tipo IO String
es la descripción de algun cálculo, una receta, para generar una String
. No hay String
“dentro de” un IO String
, no más que hya una torta “dentro de” una receta. Para producor una String
(o una deliciosa torta), hace falta ejecutar el cálculo (o la receta). Y la única manera de hacerlo es darlo (posiblemente como parte de un valor IO más grande) al runtime de Haskell, a través de main
.
IO ()
?El tipo ()
se dice “unit” y tiene un solo valor, ()
. Es como si fuera definido con:
Aunque no es sintaxis Haskell válida. ()
es un tipo bastante tonto a primera vista: no transmite ninguna información, porque tiene solo un constructor sin argumento.
Pero es exactamente lo que necesitamos en ciertas acciones IO que no producen ningun valor al final. Haskell insiste que debe producir algo, entonces decimos que produce ()
. (Un poco como void
en C/C++ o Java.)
IO algo
putStrLn
ejecuta una acción IO pero no devuelve nada.
getLine
tiene como tipo IO String
. Eso significa que getLine
es una acción que produce una String
.
Tenemos type FilePath = String
.
Esta función lee el contenido entero de un archivo y lo guarda dentro de una String
.
Hay muchas funciones que podemos usar para hacer entrada y salida. Fijate en los módulos cuyo nombre empieza con System.
, en particular System.IO
.
Functor
Consideremos el patrón de “aplicar una función pura bajo un constructor de tipo”:
mapList :: (a -> b) -> List a -> List b
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapIO :: (a -> b) -> IO a -> IO b
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
{-# MINIMAL fmap #-}
Leyes:
Implicación: si f
representa algun efecto (por ejemplo IO
), entonces aplicar una función via fmap
no modifica ese efecto.
<$>
Es un sinónimo infijo de fmap
(de la clase Functor
)
Visualmente hace el paralelo entre:
f x
(aplicar f
al valor puro x
), yf <$> x
(aplicar f
al resultado del cálculo x
).Ejemplo:
Monad
: motivaciónPensando en IO
, ya conocemos una forma un poco limitada de secuenciar efectos IO
, es el uso de <>
de la clase Monoid
:
si tenemos una secuencia de funciones IO a
donde a
tambien pertenece a la clase Monoid
, se pueden ejecutar esas funciones en secuencia y luego combinar los resultados (con la implementación <>
para a
).
Pero mucho más que eso no podemos hacer. No podemos:
IO
que devuelven tipos distintosMonad
nos salva!La clase Monad
permite formas más poderosas de poner efectos en secuencia (que sean Maybe, Either, IO, etc.).
Empecemos con Maybe
, que representa cálculos que pueden fallar.
Queremos escribir una función que “zipea” juntos dos árboles binarios aplicándoles una función a los valores de cada nodo. Sin embargo, la función debe fallar totalmente si los árboles tiene una estructura distinta.
Maybe
necesita ayudaPrimer intento:
data Tree a = Node (Tree a) a (Tree a)
| Empty
zipTree1 :: (a -> b -> c) -> Tree a -> Tree b -> Maybe (Tree c)
zipTree1 _ (Node _ _ _) Empty = Nothing
zipTree1 _ Empty (Node _ _ _) = Nothing
zipTree1 _ Empty Empty = Just Empty
zipTree1 f (Node l1 x r1) (Node l2 y r2) =
case zipTree1 f l1 l2 of
Nothing -> Nothing
Just l -> case zipTree1 f r1 r2 of
Nothing -> Nothing
Just r -> Just $ Node l (f x y) r
Este código anda, pero es poco elegante. Hemos anidado dos case
con estructuras muy similares.
Queremos una función auxiliar como la siguiente:
bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe mx f = case mx of
Nothing -> Nothing
Just x -> f x
Usando esta función, podemos tener un código más elegante:
zipTree2 :: (a -> b -> c) -> Tree a -> Tree b -> Maybe (Tree c)
zipTree2 _ (Node _ _ _) Empty = Nothing
zipTree2 _ Empty (Node _ _ _) = Nothing
zipTree2 _ Empty Empty = Just Empty
zipTree2 f (Node l1 x r1) (Node l2 y r2) =
bindMaybe (zipTree2 f l1 l2) $ \l ->
bindMaybe (zipTree2 f r1 r2) $ \r ->
Just (Node l (f x y) r)
Monad
return
está para “envolver” un valor a
en un valor con efectos m a
de la forma más trivial posible (vemos las leyes en un ratito).
(>>=)
toma dos parámetros. El primero de tipo m a
, representa un cálculo que resulta en un valor (o varias, o ninguna) de tipo a
, y que puede tener algun “efecto”.
El segundo parámetro es una función de tipo (a -> m b)
. Es decir, una función que va a elegir cuál es el siguiente cómputo en función del resultado del primero. Es precisamente ahí que se ve la idea de la mónada de encapsular cómputos que pueden ser puestos en secuencia.
Todo lo que hace (>>=)
es poner juntas dos acciones para producir una más grande, que lleva a cabo la primera y luego la segunda, devolviendo el resultado de la segunda.
El giro importante acá es que nos toca decidir cuál acción ejecutar en segundo en función de la salida de la primera.
Monad
La clase Monad
provee una función con una implementación por defecto:
m1 >> m2
simplemente hace m1
y luego m2
, ignorando el resultado de m1
.
¡Es bastante distinto de m1 <> m2
!
Monad
Leyes:
Monad
para Maybe
:Si el primer parámetro de (>>=)
es Nothing
, entonces todo el cómputo falla; sino, si es Just x
, aplicamos el segundo parámetro a x
para decidir qué hacer después.
De paso, es común usar la letra k para el segundo parámetro de (>>=)
porque “k” significa “continuación”.
zipTree
zipTree3 :: (a -> b -> c) -> Tree a -> Tree b -> Maybe (Tree c)
zipTree3 _ (Node _ _ _) Empty = Nothing
zipTree3 _ Empty (Node _ _ _) = Nothing
zipTree3 _ Empty Empty = Just Empty
zipTree3 f (Node l1 x r1) (Node l2 y r2) =
zipTree3 f l1 l2 >>= \l ->
zipTree3 f r1 r2 >>= \r ->
return (Node l (f x y) r)
Observamos que seguimos teniendo cierto código repetido, el patrón:
Además, al anidar estas cosas, nos vamos cada vez más para la derecha.
Por eso GHC provee una notación especial: la notación do.
do
La notación do
anda con cualquier tipo que pertenece a la clase Monad
.
Consideramos el bloque do
siguiente:
GHC convierte esto en una version que usa (>>=)
:
zipTree
por última vezzipTree :: (a -> b -> c) -> Tree a -> Tree b -> Maybe (Tree c)
zipTree _ (Node _ _ _) Empty = Nothing
zipTree _ Empty (Node _ _ _) = Nothing
zipTree _ Empty Empty = Just Empty
zipTree f (Node l1 x r1) (Node l2 y r2) = do
l <- zipTree f l1 l2
r <- zipTree f r1 r2
return $ Node l (f x y) r
Un ejemplo simple:
addOneOrTwo :: Int -> [Int]
addOneOrTwo x = [x+1, x+2]
ex07 = [10,20,30] >>= addOneOrTwo
ex08 = do
num <- [10, 20, 30]
addOneOrTwo num
Podemos pensar en la mónada de las listas como codificando cálculos con distintos resultados posibles. Más arriba, num
representa los valores posibles [10, 20, 30]
y luego es agregado a 1 o 2. El resultado es una lista de 6 elementos con todos los resultados posibles.
Haskell tiene una sintaxis llamada listas en comprensión (o listas intensionales), que usan de hecho la implementación de la clase Monad
para las listas.
Una cosa linda acerca de la clase Monad
es que sólo usando return
y (>>=)
podemos construir muchos combinadores generales para programas con mónadas.
Primero, sequence
toma una lista de valores monádicos y produce un solo valor monádico que colecta los resultados. Lo que significa realmente depende de cada mónada. Por ejemplo, en el caso de Maybe
, significa que el cómputo general es exitoso solo si todos los cómputos individuales lo son; en el caso de IO significa que ejecuta todos los cómputos en secuencia.
sequence :: Monad m => [m a] -> m [a]
sequence [] = return []
sequence (ma:mas) = do
a <- ma
as <- sequence mas
return (a:as)
Usando sequence
, uno puede escribir otros combinadores, como:
replicateM :: Monad m => Int -> m a -> m [a]
replicateM n m = sequence (replicate n m)
void :: Monad m => m a -> m ()
void ma = ma >> return ()
join :: Monad m => m (m a) -> m a
join mma = do
ma <- mma
ma
when :: Monad m => Bool -> m () -> m ()
when b action =
if b
then action
else return ()
do
Ahora esto debería tener sentido:
sillyExchange :: IO ()
sillyExchange = do
putStrLn "Hello, user!"
putStrLn "What is your name?"
name <- getLine
putStrLn $ "Pleased to meet you, " ++ name ++ "!"
let
en do
jabber :: IO ()
jabber = do
wocky <- readFile "jabberwocky.txt"
let wockylines = drop 2 (lines wocky) -- quita el titulo
count <- printFirstLines wockylines
putStrLn $ "There are " ++ show count ++ " stanzas in Jabberwocky."
printFirstLines :: [String] -> IO Int
printFirstLines ls = do
let first_lines = extractFirstLines ls
putStr (unlines first_lines)
return $ length first_lines
extractFirstLines :: [String] -> [String]
extractFirstLines [] = []
extractFirstLines [_] = []
extractFirstLines ("" : first : rest)
= first : extractFirstLines rest
extractFirstLines (_ : rest) = extractFirstLines rest
Sentencias let
dentro de bloques do. La sentencia let
en un bloque do permite crear un nombre nuevo atado a un valor puro. Observen la falta de in
. Acuérdense que cuando decimos let x = y
, x
e y
tienen el mismo tipo. Cuando decimos x <- y
, y
tiene un tipo como IO a
, y entonces x
tiene como tipo a
.
return :: a -> IO a
. Si necesitamos convertir un valor puro en una acción IO, usamos return
. return
es una función común y corriente en Haskell. ¡No es lo mismo que return
en C/C++ o Java! Dentro de una acción IO, let x = y
es lo mismo que x <- return y
, pero lo anterior es mucho mejor: hace que la pureza de y
sea más obvia.
Otra forma de verlo es que que encontramos a menudo return
como última línea de un bloque do
porque cada línea debe ser del tipo IO
.
Ojo, lo siguiente:
Es equivalente a (y peor que):
Monad
Applicative
Applicative
es una clase que está “entre” Functor
y Monad
.
pure
es exactamente el return
de Monad
.<*>
generaliza fmap
pero no es tan poderoso cono >>=
.A veces la potencia de Monad
es demasiado, y Applicative
basta.
Ver https://hackage.haskell.org/package/base-4.10.1.0/docs/Control-Applicative.html
Tener a mano: