En este tutorial conoceremos algunas formas de azúcar sintáctico y otros mecanismos que nos harán escribir código más corto. Aunque no nos darán más poder de cómputo, nos darán mayor poder de expresividad; algo dulce, algo que nos haga la vida más fácil.
Ya hemos visto un poco de azúcar sintáctico cuando hablamos de las listas y de como podríamos expresarlas con tipos de datos algebraicos recursivos en vez de su azúcar sintáctica: [
,]
y ,
; con eso qudó claro que el azucar sintáctico nos ofrece una alternativa menos tediosa para alguna expresión. Técnicamente este tutorial no es sobre programación funcional, sino de azúcar sintáctico y otras expresiones útiles para Haskell.
Operador de aplicación ($)
El operador de aplicaicón $
es redundante en Haskell, pues lo mismo es f p
que f $ p
, donde f
es una función y p
es un argumento. Por ejemplo:
main =
do
print (even 2)
print $ even 2
Sin embargo, aunque ambas formas producen el mismo resultado, la segunda utiliza menos paréntesis; estar cerrando paréntesis es tedioso, consume tiempo e interrumpe el flujo del pensamiento.
$
es un operador infijo asociativo a la derecha con prioridad baja.
Operador infijo significa que $
no se utiliza como función, sino como operador que toma dos argumentos, los cuales se colocan a los lados; que sea asociativo a la derecha lo diferencía de los operadores infijos asociativos a la izquierda, como el operador /
; mientras 1/2/3 = ((1/2)/3)
(asociativo a la izquierda), f1 $ f2 $ p = (f1 $ (f2 $ p))
(asociativo a la derecha). Que tenga prioridad baja significa que en caso de haber más operadores en una misma expresión, primero se asociarán los de mayor prioridad. Por ejemplo, even $ 4 / 2 + 1
se asocia de esta manera: even $ ((4 / 2) + 1)
, pues las prioridades de $
, +
y /
son tal que pr($) < pr(+) < pr(/), donde pr(op) es la prioridad de un operador op, o en otras palabras, primero se asocia la división: (4 / 2)
, después la suma: (4 / 2) + 1
y al final $
: even $ ((4 / 2) + 1)
.
(Esta es una lista de la prioridad de los operadores de Haskell)
f1 $ f2 $ p
se traduce a ($) f1 (($) f2 p)
cuando $
se utiliza como función ($)
en vez de como parámetro. Lo menciono para que quede claro que no hay nada excepcional en $
; incluso Haskell le permite al usuario definir sus propios operadores con la asociatividad y precedencia que uno escoja.
Ejemplos del uso de $
:
main =
do
print $ even $ mod 6 4
-- equals to:
($) print (($) even (($) mod 6 4))
-- that since ($) is redundant is equal to:
print (even (mod 6 4))
-- testing precedence of "$" versus "+"
print $ even $ 6 + 3
A partir de ahora, lo usaremos mucho.
Instancias derivadas (derived instances)
Esto es de lo más mágico que tiene Haskell y sólo veremos la intuición en esta sección. En el tutorial Clases de tipos daremos algunas explicaciones más detalladas.
Derivación de Eq
Supongamos que tenemos un tipo de dato para los días de la semana:
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
Si los necesitaras comparar, podrías hacer una función algo así:
compareDays d1 d2 =
case (d1, d2) of
(Monday, Monday) -> True
(Monday, _) -> False
(Tuesday, Tuesday) -> True
(Tuesday, _) -> False
...
Una manera más corta sería algo como esto:
compareDays d1 d2 =
dayToInt d1 == dayToInt d2
where dayToInt d = case d of
Monday -> 1
Tuesday -> 2
...
Pero afortunadamente podemos "derivar la instancia de Eq
" para Day
y obtener los operadores ==
y /=
gratis:
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday {-hi-}deriving Eq{-/hi-}
main = do
print $ Saturday {-hi-}=={-/hi-} Saturday
print $ Friday {-hi-}/={-/hi-} Wednesday
Derivación de Show
También se puede derivar una instancia de la "clase" Show
; esto nos permite usar la función show
sobre nuestros propios tipos; no es la única manera de usar show
sobre nuestros tipos, pero esta es la más fácil y especialmente útil para escribir tutoriales.
data Color = Black | White | Gray {-hi-}deriving Show{-/hi-}
main = putStrLn.{-hi-}show{-/hi-} $ White
Tuplas
Las tuplas son azucar sintáctico para la multiplicación de tipos. Por ejemplo, si necesitamos una estructura de datos con tres valores, en vez de escribir data ThreeValues = ThreeValues a b c
(multiplicación de los tipos a
, b
y c
) podemos no escribir nada y simplemente usar las tuplas que Haskell nos proporciona. E.g. (1,2) :: (Int, Int)
, (1,'♥',"sugar") :: (Int, Char, [Char])
, etcétera.
La solución al ejercicio 2 del tutorial Algo sobre listas y todo sobre funciones
- Write a function "average" that gets the average from a list of Doubles using "foldl". For the empty list, let the average be zero. Write a function "increaseAndSum" that helps you with the folding.
luce así:
data Tuple a b = Tuple a b
zeroes = Tuple 0 0
increaseAndSum (Tuple currentCount currentSum) x = Tuple (currentCount + 1) (currentSum + x)
average [] = 0
average ls = sum/count
where Tuple count sum = foldl increaseAndSum zeroes ls
main = print (average (take 100 [1 ..]))
Pero usando tuplas pudo haber sido más corto:
{-hi-}increaseAndSum (currentCount, currentSum) x = (currentCount + 1, currentSum + x){-/hi-}
average [] = 0
average ls = sum/count
where {-hi-}(count, sum){-/hi-} = foldl increaseAndSum {-hi-}(0,0){-/hi-} ls
main = print (average (take 100 [1 ..]))
Y ya sólo estamos entonces a un pequeño paso de que finalmente quede así (usándo una función anónima):
average [] = 0
average ls = sum/count
where (count, sum) = foldl {-hi-}(\(cc,cs) x -> (cc+1,cs+x)){-/hi-} (0,0) ls
main = print (average (take 100 [1 ..]))
Si vas a usar funciones anónimas, asegúrate de que sean fáciles de entender; en general, no abuses.
Registros
Supongamos que quieres modelar una partida de gato. Usando solo tipos algebráicos, muy probablemente modelarías el estado de esta manera:
data Mark = O | X | Empty
data Player = PO | PX
data Score = Score Int Int
data GameState = GameState [[Mark]] Player Player Score
Si hubiésemos usado registros, nos hubiera quedado de esta forma:
data Mark = O | X | Empty
data Player = PO | PX
data Score = {-hi-}Score { oVictories::Int, xVictories::Int }{-/hi-}
data GameState = {-hi-}GameState
{
boardState :: [[Mark]]
, nextToMove :: Player
, startedTheGame :: Player
, score :: Score
}{-/hi-}
Así queda un poco más clara la intención de los tipos; ahora que podemos usar "etiquetas" para los valores, debe ser fácil entender que el primer Player
(nextToMove
) representa el siguiente jugador en tirar y el segundo Player
(startedTheGame
) indica quien tiró al inicio del juego (para saber a quien le tocará iniciar en el siguiente juego).
Pero en realidad no son etiquetas, son funciones; por ejemplo, la etiqueta oVictories
en realidad es una función de tipo Score -> Int
, o sea que recibe un score y regresa el número de victorias del jugador círculo. A continuación, usaremos oVictories
como función:
data Score = Score { oVictories::Int, xVictories::Int }
someScore = Score { oVictories = 2, xVictories = 1 }
main = (print.{-hi-}oVictories{-/hi-}) someScore
Esto nos evita tener que hacer búsqueda de patrones. Si no hubiésemos usado registros, hubiésemos tenido que hacer algo como esto:
data Score = Score Int Int
someScore = Score 2 2
main = print oVictories
where oVictories = case someScore of Score oVictories _ -> oVictories
Todo eso para hacer búsqueda de patrones sobre someScore
... not cool.
Y siempre podemos construir un registro como si fuera un tipo de dato algebraico:
data Score = Score { oVictories::Int, xVictories::Int }
someScore = {-hi-}Score 2 1{-/hi-}
main = (print.oVictories) someScore
Actualización de registros
Técnicamente, ningún dato se "actualiza" en Haskell, pues eso le quitaría la pureza; cuando nos referimos a "actualizar" un dato, nos referimos a duplicar un dato con cierta variación.
Supongamos que terminó un juego de gato y ganó PO
y debemos crear un nuevo Score
que refleje esto. Sin usar registros, se podría modelar así:
import Text.Printf
data Score = Score Int Int
data Player = PO | PX deriving Eq
updateScore (Score oVictories xVictories) winner
| winner == PO = Score (oVictories + 1) xVictories
| otherwise = Score oVictories (xVictories + 1)
scoreToStr (Score oVictories xVictories) =
"O: " ++ (show oVictories) ++
", X: " ++ (show xVictories)
someScore = Score 2 2
main = do
putStrLn $ "old score: " ++ (scoreToStr someScore)
putStrLn $ "new score: " ++ (scoreToStr $ updateScore someScore PX)
Pero utilizando registros, obtenemos una sintaxis más conveniente:
data Player = PO | PX deriving Eq
data Score = Score { oVictories::Int, xVictories::Int }
updateScore {-hi-}s{-/hi-} winner
| winner == PO = {-hi-}s { oVictories = oVictories s + 1 }{-/hi-} -- "updates" oVictories, xVictories remains the same
| otherwise = {-hi-}s { xVictories = xVictories s + 1 }{-/hi-} -- "updates" xVictories, oVictories remains the same
scoreToStr s =
"O: " ++ (show.oVictories) s ++
", X: " ++ (show.xVictories) s
someScore = Score { oVictories = 2, xVictories = 1 }
main = do
putStrLn $ "old score: " ++ (scoreToStr someScore)
putStrLn $ "new score: " ++ (scoreToStr $ updateScore someScore PX)
putStrLn $ "old score remains unchanded: " ++ (scoreToStr someScore) -- important!
En este ejemplo, s { oVictories = oVictories s + 1 }
actualiza el dato oVictories
de s
y deja xVictories
sin modificar. En s { xVictories = xVictories s + 1}
, se actualiza el dato xVictories
de s
y deja oVictories
sin modificar. Es importante recalcar que en realidad no se actualiza nada, sino que se crea un segundo objeto.
Búsqueda de patrones sobre registros
Si utilizas registros para tus estructuras, debes saber que también puedes realizar búsqueda de patrones sobre estos:
data Mark = O | X | Empty
data Player = PO | PX deriving Eq
data Score = Score { oVictories::Int, xVictories::Int }
data GameState
= GameState
{ boardState :: [[Mark]]
, nextToMove :: Player
, startedTheGame :: Player
, score :: Score
}
updateScore s winner
| winner == PO = s { oVictories = oVictories s + 1 }
| winner == PX = s { xVictories = xVictories s + 1 }
scoreToStr s =
"O: " ++ (show.oVictories) s ++
", X: " ++ (show.xVictories) s
someGameState
= GameState
{ boardState = [[Empty, Empty, Empty], [Empty, Empty, Empty], [Empty, Empty, Empty]]
, nextToMove = PX
, startedTheGame = PX
, score = Score { oVictories = 1, xVictories = 0 }
}
printScore {-hi-}(GameState { score = s }){-/hi-} = putStrLn.scoreToStr $ s
main = printScore someGameState
Captura de argumentos
Cuando hacemos búsqueda de patrones sobre una estructura de datos, podemos acceder a sus valores internos, pero perdemos la capacidad de hacer referencia a la estructura completa.
Por ejemplo supongamos que queremos definir una función toAdult
que dada la información de una persona, regresa Just p
si p
es mayor de 17 años y Nothing
de lo contrario.
data Color = Red | Orange | Yellow | Green | Blue | Indigo | Violet | Pink deriving Show
data Person = P String Int Color deriving Show -- Name, age, favorite color
toAdult (P name age color)
| age > 17 = Just $ P name age color
| otherwise = Nothing
main = print $ map toAdult [P "Lay" 24 Blue, P "Jenny" 17 Pink, P "Bill" 59 Blue]
Y no está tan mal, pero con la captura de argumentos, recobramos la habilidad de hacer referencia a la estrucutra completa a la vez que podemos hacer búsqueda de patrones.
A continuación veremos el mismo ejemplo pero implementado usando captura de patrones tanto para tipos de datos algebraicos como para registros.
Captura de argumentos sobre un tipo de dato algebraico
data Color = Red | Orange | Yellow | Green | Blue | Indigo | Violet | Pink deriving Show
data Person = P String Int Color deriving Show -- Name, age, favorite color
toAdult {-hi-}p@(P _ age _){-/hi-}
| age > 17 = Just {-hi-}p{-/hi-}
| otherwise = Nothing
main = print $ map toAdult [P "Lay" 24 Blue, P "Jenny" 17 Pink, P "Bill" 59 Blue]
Captura de argumentos sobre un registro
data Color = Red | Orange | Yellow | Green | Blue | Indigo | Violet | Pink deriving Show
data Person = P { name::String, age::Int, favColor::Color } deriving Show
toAdult {-hi-}p@(P {age = a}){-/hi-}
| a > 17 = Just {-hi-}p{-/hi-}
| otherwise = Nothing
main = print $ map toAdult [P "Lay" 24 Blue, P "Jenny" 17 Pink, P "Bill" 59 Blue]
Módulos
Modularizar el código nos permite
- tener más de un espacio de nombres (namespaces)
- agrupar el código por funcionalidad
- esconder información de un módulo a otro
En esta sección veremos brevemente como crear un programa usando tres módulos, cada uno en su propio archivo y como beneficiarnos de tener más de un espacio de nombres.
Empecemos directamente con un ejemplo, con lo que has aprendido hasta ahora, deberías de poder entender el significado del código con sólo leerlo.
{-# START_FILE Color.hs #-}
module Color where -- We declare a module named "Color"
data Color = Red | Orange | Yellow | Green | Blue | Indigo | Violet | Pink deriving Show
data RGB = RGB { red::Float, green::Float, blue::Float }
rgbToStr (RGB r g b) =
"R:" ++ (show r) ++
", G:" ++ (show g) ++
", B:" ++ (show b)
colorToRGB c = RGB r g b
where (r,g,b) = case c of
Red -> (1, 0, 0)
Orange -> (1, 0.647, 0)
Yellow -> (1, 0.843, 0)
Green -> (0, 1, 0)
Blue -> (0, 0, 1)
Indigo -> (0.294, 0, 130)
Violet -> (0.933, 0.509, 0.933)
Pink -> (1, 0.752, 0.796)
-- blends two colors using this formula: http://stackoverflow.com/a/29321264
blendColors (RGB r1 g1 b1) (RGB r2 g2 b2) t =
RGB r g b
where
r = sqrt $ ((1-t)*r1)^2 + (t*r2)^2
g = sqrt $ ((1-t)*g1)^2 + (t*g2)^2
b = sqrt $ ((1-t)*b1)^2 + (t*b2)^2
{-# START_FILE Person.hs #-}
module Person where -- We declare a module named "Person"
import Color
data Person = P { name::String, age::Int, favColor::Color }
ana = P "Ana" 25 Red
bob = P "Bob" 25 Pink
blendFavColors p1 p2 t =
blendColors (colorToRGB $ favColor p1) (colorToRGB $ favColor p2) t
{-# START_FILE Main.hs #-}
module Main where -- declares a module named "Main"
import Color -- imports the Color module
import Person -- imports the Person module
main =
putStrLn $ "If we blend evenly ana's favorite color with bob's favorite color, we get "
++ (rgbToStr $ blendFavColors ana bob 0.5)
Resolviendo name clashes
En el módulo Color
está definida una función blendColors
y en el módulo Person
está definida una función blendFavColors
; la función blendFavColors
bien pudo haberse llamado también blendColors
, pero eso hubiese causado un "name clash" en el espacio de nombres del módulo Main
pues habrían dos elementos con el mismo nombre.
La ambigüedad creada por dos elementos con el mísmo nombre se puede resolver si antecedemos el uso de dichos elementos con el nombre del módulo al que pertenecen y un punto: {-hi-}Module.{-/hi-}element
.
Entonces, sí podemos llamar ambas funciones blendColors
y el ejemplo pasado quedaría así:
{-# START_FILE Color.hs #-}
module Color where -- We declare a module named "Color"
data Color = Red | Orange | Yellow | Green | Blue | Indigo | Violet | Pink deriving Show
data RGB = RGB { red::Float, green::Float, blue::Float }
rgbToStr (RGB r g b) =
"R:" ++ (show r) ++
", G:" ++ (show g) ++
", B:" ++ (show b)
colorToRGB c = RGB r g b
where (r,g,b) = case c of
Red -> (1, 0, 0)
Orange -> (1, 0.647, 0)
Yellow -> (1, 0.843, 0)
Green -> (0, 1, 0)
Blue -> (0, 0, 1)
Indigo -> (0.294, 0, 130)
Violet -> (0.933, 0.509, 0.933)
Pink -> (1, 0.752, 0.796)
-- blends to colors using this formula: http://stackoverflow.com/a/29321264
blendColors (RGB r1 g1 b1) (RGB r2 g2 b2) t =
RGB r g b
where
r = sqrt $ ((1-t)*r1)^2 + (t*r2)^2
g = sqrt $ ((1-t)*g1)^2 + (t*g2)^2
b = sqrt $ ((1-t)*b1)^2 + (t*b2)^2
{-# START_FILE Person.hs #-}
module Person where -- We declare a module named "Person"
import Color
data Person = P { name::String, age::Int, favColor::Color }
ana = P "Ana" 25 Red
bob = P "Bob" 25 Pink
{-hi-}blendColors{-/hi-} p1 p2 t =
{-hi-}Color.{-/hi-}blendColors (colorToRGB $ favColor p1) (colorToRGB $ favColor p2) t
{-# START_FILE Main.hs #-}
module Main where -- declares a module named "Main"
import Color -- imports the Color module
import Person -- imports the Person module
main =
putStrLn $ "If we blend evenly ana's favorite color with bob's favorite color, we get "
++ (rgbToStr $ {-hi-}Person.{-/hi-}blendColors ana bob 0.5)
Si escribir el nombre completo del módulo es muy tedioso, se puede declarar un alias en la importación del módulo, por ejemplo:
{-# START_FILE Main.hs #-}
module Main where
import Color
import Person {-hi-}as P{-/hi-} -- imports the Person module
main =
putStrLn $ "If we blend evenly ana's favorite color with bob's favorite color, we get "
++ (rgbToStr $ {-hi-}P.{-/hi-}blendColors ana bob 0.5)
Para una información más completa sobre la importación de módulos, visita Modules - Haskell, The Wikibook ## Interpolación de cadenas (string interpolation) ¿Recuerdas este ejemplo?
data Color = Black | White | Gray deriving Show
introduction :: String -> (Int -> (Color -> String))
introduction name age color =
"Hi, I'm " ++ name ++ ". "
++ "I'm " ++ (show age) ++ " years old "
++ "and my favorite color is " ++ (show color)
main = putStrLn $ introduction "Pluto" 3 Gray
Se puede evitar mezclar tantos String
s con ++
y patrones utilizando interpolación de cadenas. Existen varias formas, pero hay una que será familiar para muchos, Text.Printf
pues está inspirada en printf del lenguaje de programación C.
Usando interpolación de cadenas, queda más limpio:
{-hi-}import Text.Printf{-/hi-}
data Color = Black | White | Gray deriving Show
introduction :: String -> (Int -> (Color -> String))
introduction name age color =
{-hi-}printf{-/hi-} "Hi, I'm {-hi-}%s{-/hi-}. I'm {-hi-}%i{-/hi-} years old and my favorite color is {-hi-}%s{-/hi-}" name age (show color)
main = putStrLn $ introduction "Pluto" 3 Gray
En el formato declarado, indicamos que interpolaremos tres variables, la primera siendo un String
(%s
), la segunda siendo un Int
(%i
) y la tercera un String
(%s
) nuevamente.
Pero ten cuidado, pues el formato producido por printf
no es muy fuertemente tipado y puede arrojar excepciones en tiempo de ejecución si es mal utilizado:
import Text.Print
format = printf "%i" -- expects an int
main = putStrLn $ format ">:)" -- raises an exception {-hi-}in runtime{-/hi-} with an evil type error
Por esta razón, algunos sugieren que nos conformemos con ++
.
En la documentación de Text.Printf
hay una tabla con todas las conversiones soportadas.
Ejercicios
Siguiente tutorial
Y eso es todo por este tutorial. Si has seguido todos los tutoriales y hecho todos los ejercicios, te mereces un postre :) Cuando quieras, puedes continuar con el siguiente tutorial.