Moisés Gabriel Cachay Tello
Creator, destructor.
Haskell
Una introducción
@xpktro - Functional Programming Perú
Versión Original:
Por qué deberías aprender programación funcional ya mismo. - Andrés Marzal
Trasfondo: Diseño por limitaciones
Mario Lavista
La vida (a) leve
Para Ulalume
Y
se
oía
Berg
Dufai
Mozart
Debussy
Schumann
Beethoven
Stravinski
Lutoslavski:
orquestarías
estudiándolos
brillantísimas
extraordinarias
transformaciones
entremezclándoles
pluridimensionales
pseudododecafónicas
sobreornamentaciones
hipermeldessonhnianas.
OuLiPo ('60)
Literatura basada en limitaciones brutales:
Propuesta: OuProgPo
Presentando la Programación Funcional
Sales Pitch
Cada vez más características de la programación funcional se incluyen en los lenguajes de programación comunes.
Fundamentos Matemáticos
La programación funcional está fundamentada en dos principios matemáticos:
Cálculo Lambda
Category Theory
$$ \lambda x.x^{2} $$
$$ (\lambda x.x^{2}) \; 2 = 4 $$
Cálculo Lambda
Un sistema formal de representación y aplicación de funciones
$$ \lambda x.x^{2} $$
$$ (\lambda x.x^{2}) \; 2 = 4 $$
$$ \lambda (x, y).x^{2}+y^{2} $$
$$ \lambda x. \lambda y.x^{2}+y^{2} $$
$$ \lambda x.(\lambda y.x^{2}+y^{2}) $$
$$ (\lambda x.(\lambda y.x^{2}+y^{2})) \; 4 = \lambda y.16 + y^{2} $$
$$ (\lambda y.16+y^{2}) \; 3 = 25 $$
$$ (\lambda x.\lambda y.x^{2}+y^{2}) \; 4 \; 5= 25 $$
Características funcionales puras de Haskell
Haskell es el lenguaje más usable que incluye características funcionales puras:
Caso práctico: FizzBuzz
Un ejercicio de programación clásico: escribe un programa que muestre los números del 1 al 100. Cuando se alcance un múltiplo de 3 mostrar "fizz!" a cambio, para múltiplos de 5 mostrar "buzz!"; para múltiplos de ambos números mostrar "fizzbuzz!".
one!
two!
fizz!
four!
buzz!
fizz!
seven!
eight!
fizz!
buzz!
eleven!
fizz!
thirteen!
fourteen!
fizzbuzz!
sixteen!
seventeen!
fizz!
nineteen!
buzz!
fizz!
twenty two!
fizzbuzz :: Int -> String
fizzbuzz n = undefined
"undefined" es un valor derivado de todos los tipos que representa una computación no-exitosa
fizzbuzz :: Int -> String
fizzbuzz n = "one!"
main = print (fizzbuzz 1)
"main" es una función especial que se llama cada vez que tu programa se ejecuta (el punto de entrada)
Caso práctico: FizzBuzz
fizzbuzz n = if n == 1 then "one!" else "two!"
main = print (fizzbuzz 2)
Todo, incluso "if" es una expresión, y por tanto siempre retorna un valor. Podríamos hacer uno propio:
ifThenElse :: Bool -> a -> a -> a
ifThenElse cond thenVal elseVal =
case cond of
True -> thenVal
False -> elseVal
fizzbuzz n = ifThenElse (n==1) ("one"++"!") ("two"++"!")
Caso práctico: FizzBuzz
fizzbuzz :: Int -> String
fizzbuzz n | n == 1 = "one!"
fizzbuzz n | n /= 1 = "two!"
¡Hora de refactorizar! presentando los guards:
Ya que la segunda condición siempre sucederá para n != 1, podemos escribirla así:
fizzbuzz n | n == 1 = "one!"
| True = "two!"
-- O:
fizzbuzz n | n == 1 = "one!"
| otherwise = "two!"
Caso práctico: FizzBuzz
fizzbuzz :: Int -> String
fizzbuzz 1 = "one!"
fizzbuzz n = "two!"
Si los guards tienen la forma "n == something", se pueden escribir de forma más limpia:
Y si algún parámetro no es importante para el guard, se puede reemplazar por _
fizzbuzz :: Int -> String
fizzbuzz 1 = "one!"
fizzbuzz _ = "two!"
Caso práctico: FizzBuzz
lessThan20 :: Int -> String
lessThan20 n
| n > 0 && n < 20 =
let answers = words ("one two three four five six seven eight nine ten " ++
"eleven twelve thirteen fourteen fifteen sixteen " ++
"seventeen eighteen nineteen")
in answers !! (n-1)
En inglés, los números menores a 20 tienen nombres propios, hagamos una función para representarlos:
words :: String -> [String]
Caso práctico: FizzBuzz
tens :: Int -> String
tens n
| n >= 2 && n <= 9 =
answers !! (n-2)
where
answers = words "twenty thirty forty fifty sixty seventy eighty ninety"
Necesitamos también una función para nombrar las decenas:
"let in" y "where" se pueden intercambiar y se usan dependiendo de la legibilidad que necesite
Caso práctico: FizzBuzz
number :: Int -> String
number n
| 1 <= n && n < 20 = lessThan20 n
| n `mod` 10 == 0 && n < 100 = tens (n `div` 10)
| n < 100 = tens (n `div` 10) ++ " " ++ lessThan20 (n `mod` 10)
| n == 100 = "one hundred"
Combinando nuestras dos funciones en una:
Los guards se convierten en una construcción elegante y útil para agrupar un conjunto de condiciones complejas
Caso práctico: FizzBuzz
main = putStr (unlines (map number [1..30]))
Revisamos lo que tenemos hasta ahora mostrándolo en pantalla:
unlines :: [String] -> String
map :: (a -> b) -> [a] -> [b]
-- Por ejemplo:
map (\x -> x * 2) [1,2,3]
-- [2,4,6]
"unlines" junta una lista de cadenas con saltos de línea
"map" toma una función y la aplica a cada elemento de una lista
Caso práctico: FizzBuzz
fizzbuzz :: Int -> String
fizzbuzz n
| n `mod` 3 == 0 && n `mod` 5 == 0 = "fizzbuzz!"
| n `mod` 3 == 0 = "fizz!"
| n `mod` 5 == 0 = "buzz!"
| otherwise = number n ++ "!"
main = putStr (unlines (map fizzbuzz [1..100]))
Refactorización final, mostrando fizz! y buzz! y fizzbuzz!
one!
two!
fizz!
four!
buzz!
fizz!
seven!
eight!
fizz!
buzz!
eleven!
fizz!
thirteen!
fourteen!
fizzbuzz!
sixteen!
seventeen!
fizz!
nineteen!
buzz!
fizz!
twenty two!
twenty three!
fizz!
buzz!
twenty six!
fizz!
twenty eight!
twenty nine!
fizzbuzz!
thirty one!
thirty two!
fizz!
thirty four!
buzz!
fizz!
thirty seven!
thirty eight!
fizz!
buzz!
forty one!
fizz!
forty three!
forty four!
fizzbuzz!
forty six!
forty seven!
fizz!
forty nine!
buzz!
fizz!
fifty two!
fifty three!
fizz!
buzz!
fifty six!
fizz!
fifty eight!
fifty nine!
fizzbuzz!
sixty one!
sixty two!
fizz!
sixty four!
buzz!
fizz!
sixty seven!
sixty eight!
fizz!
buzz!
seventy one!
fizz!
seventy three!
seventy four!
fizzbuzz!
seventy six!
seventy seven!
fizz!
seventy nine!
buzz!
fizz!
eighty two!
eighty three!
fizz!
buzz!
eighty six!
fizz!
eighty eight!
eighty nine!
fizzbuzz!
ninety one!
ninety two!
fizz!
ninety four!
buzz!
fizz!
ninety seven!
ninety eight!
fizz!
buzz!
Caso práctico: FizzBuzz
Caso práctico: Puntuación de Bowling
Para esta demostración, puntuaremos un juego de bolos:
Frame | Bolos | Tipo | Bonus | Score | Total |
1 | 1, 4 | Open | 1+4=5 | 0+5=5 | |
2 | 4, 5 | Open | 4+5=9 | 5+9=14 | |
3 | 6, 4 | Spare | 5 | 6+4+5=15 | 14+15=29 |
4 | 5, 5 | Spare | 10 | 5+5+10=20 | 29+20=49 |
5 | 10 | Strike | 0, 1 | 10+0+1=11 | 49+11=60 |
6 | 0, 1 | Open | 0+1=1 | 60+1=61 | |
7 | 7, 3 | Spare | 6 | 7+3+6=16 | 61+16=77 |
8 | 6, 4 | Spare | 10 | 6+4+10=20 | 77+20=97 |
9 | 10 | Strike | 2, 8 | 10+2+8=20 | 97+20=117 |
10 | 2, 8 | Spare | 6 | 2+8+6=16 | 117+16=133 |
Recibiremos una lista con los bolos derribados en cada tirada:
[1, 4, 4, 5, 6, 4, 5, 5, 10, 0, 1, 7, 3, 6, 4, 10, 2, 8, 6]
Crearemos un tipo de dato llamado Frame para representar un frame y puntuar
toFrames :: [Int] -> [Frame]
score :: [Frame] -> Int
Caso práctico: Puntuación de Bowling
Un Tipo de Dato Algebraico se define con todos sus valores posibles, en este caso separados por el caracter | (un Sum Type)
data Frame = Open Int Int
| Spare Int Int
| Strike Int Int
toFrames :: [Int] -> [Frame]
toFrames pins = undefined
data Frame = Open { pins1 :: Int
, pins2 :: Int
}
| Spare { pins1 :: Int
, bonus1 :: Int
}
| Strike { bonus1 :: Int
, bonus2 :: Int
}
Caso práctico: Puntuación de Bowling
Una typeclass es un conjunto de operaciones (funciones) que cualquier tipo de dato puede implementar (instanciar/derivar)
class Eq a where
(==) :: a -> a -> Bool
x == y = not (x /= y)
(/=) :: a -> a -> Bool
x /= y = not (x == y)
instance Eq Frame where
Open x y == Open x' y' = x == x' && y == y'
Spare x y == Open x' y' = x == x' && y == y'
Strike x y == Open x' y' = x == x' && y == y'
_ == _ = False
Caso práctico: Puntuación de Bowling
El compilador de Haskell puede autogenerar código para algunas derivaciones de clase comunes
data Frame = Open Int Int
| Spare Int Int
| Strike Int Int
deriving (Eq, Show)
instance Show Frame where
show (Open x y) = "Open " ++ show x ++ " " ++ show y
show (Spare x y) = "Spare " ++ show x ++ " " ++ show y
show (Strike x y) = "Strike " ++ show x ++ " " ++ show y
Caso práctico: Puntuación de Bowling
Igualamos el argumento de nuestra función con la forma (primero : segundo : resto)
toFrames :: [Int] -> [Frame]
toFrames (x:y:ys) = Open x y : toFrames ys
toFrames [] = []
":" es el operador de construcción de listas:
(:) :: a -> [a] -> [a]
-- [1, 2, 3] == 1:2:3:[]
Caso práctico: Puntuación de Bowling
Consideremos el concepto de Spare
toFrames (x:y:z:ys)
| x+y == 10 = Spare x z : toFrames (z:ys)
| otherwise = Open x y : toFrames (z:ys)
toFrames [x, y] = [Open x y]
toFrames [] = []
Usamos nuevamente los guards y añadimos una nueva definición para listas de 2 elementos.
Caso práctico: Puntuación de Bowling
Cuando se hace un Spare en el frame final, se debe hacer una tirada adicional, por lo que debemos llevar la cuenta del frame actual.
toFrames pins = go 1 pins
where
go 10 [x, y] = [Open x y]
go 10 [x, y, z]
| x + y == 10 = [Spare x z]
go n (x:y:z:ys)
| x + y == 10 = Spare x z : go (n+1) (z:ys)
| otherwise = Open x y : go (n+1) (z:ys)
Caso práctico: Puntuación de Bowling
Finalmente, se considera el tipo Strike:
toFrames :: [Int] -> [Frame]
toFrames pins = go 1 pins
where
go 10 [x, y] = [Open x y]
go 10 [x, y, z]
| x == 10 = [Strike y z]
| x + y == 10 = [Spare x z]
go n (x:y:z:ys)
| x == 10 = Strike y z : go (n+1) (y:z:ys)
| x + y == 10 = Spare x z : go (n+1) (z:ys)
| otherwise = Open x y : go (n+1) (z:ys)
Caso práctico: Puntuación de Bowling
Nos falta algo, ¿qué sucede si nos quedamos sin números o si parte de ellos no es consistente?
toFrames :: [Int] -> Maybe [Frame]
"Maybe" es un tipo usado para representar resultados 'opcionales':
data Maybe a = Just a
| Nothing
Caso práctico: Puntuación de Bowling
Hagamos que nuestra función devuelva un valor de tipo "Maybe":
toFrames :: [Int] -> Maybe [Frame]
toFrames pins = go 1 pins
where
go 10 [x, y]
| x + y < 10 = Just [Open x y]
| otherwise = Nothing
go 10 [x, y, z]
| x == 10 = Just [Strike y z]
| x + y == 10 = Just [Spare x z]
| otherwise = Nothing
go n (x:y:z:ys)
| x == 10 = fmap (Strike y z :) (go (n+1) (y:z:ys))
| x + y == 10 = fmap (Spare x z :) (go (n+1) (z:ys))
| x + y < 10 = fmap (Open x y :) (go (n+1) (z:ys))
| otherwise = Nothing
go _ _ = Nothing
Caso práctico: Puntuación de Bowling
Finalmente, hacemos nuestra función de puntuación:
frameScore :: Frame -> Int
frameScore (Open x y) = x + y
frameScore (Spare _ y) = 10 + y
frameScore (Strike x y) = 10 + x + y
score :: [Frame] -> Int
score frames = sum (map frameScore frames)
main = print (score frames)
where
Just frames = toFrames throws
throws = [1, 4, 4, 5, 6, 4, 5, 5, 10, 0, 1, 7, 3, 6, 4, 10, 2, 8, 6]
-- > 133
Caso práctico: Puntuación de Bowling
¿Ahora qué?
Muchas cosas se dejaron atrás que pueden averiguarse:
?
By Moisés Gabriel Cachay Tello
Una introducción a Haskell, basada en "Por qué deberías aprender programación funcional ya mismo", una charla de Andrés Marzal.