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:

  • Palíndromos
  • Ausencia de letras
  • Tamaño fijo o variable de palabras
  • Orden específico de rimas
  • Restricciones temporales

Propuesta: OuProgPo

  • No loops (for, while, do, ...)
  • Toda función debe ser declarada con un único argumento
  • Toda función debe consistir en una única expresión
  • No side-effects (no alterar nada fuera de la función)
  • Toda función debe producir el mismo resultado dado el mismo parámetro
  • Una vez que se declara un identificador, su valor no puede ser cambiado
  • Las operaciones con estructuras de datos no deben alterar la estructura original
  • El orden de ejecución de las expresiones no importa

Presentando la Programación Funcional

  • No loops (for, while, do, ...)
  • Toda función debe ser declarada con un único argumento
  • Toda función debe consistir en una única expresión
  • No side-effects (no alterar nada fuera de la función)
  • Toda función debe producir el mismo resultado dado el mismo parámetro
  • Una vez que se declara un identificador, su valor no puede ser cambiado
  • Las operaciones con estructuras de datos no deben alterar la estructura original
  • El orden de ejecución de las expresiones no importa

Sales Pitch

Cada vez más características de la programación funcional se incluyen en los lenguajes de programación comunes.

  • Funciones lambda
  • Listas por comprensión
  • Funciones de orden superior
  • Evaluación perezosa
  • Pattern matching
  • Inmutabilidad
  • Map/Filter/Reduce

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:

  • Transparencia referencial
  • Sin side effects, soporte sintáctico para las mónadas
  • Evaluación perezosa
  • Currying, las funciones son ciudadanos de primera clase
  • Sistema de tipos estáticos e inferencia de tipos
  • Optimización de llamada de cola
  • Tipos de datos algebráicos
  • Pattern matching
  • Estructuras de datos inmutables

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:

  • Mónadas / IO
  • Composición de funciones
  • Functores
  • $
  • Listas por comprensión
  • Lambdas

?

Made with Slides.com