DevTools

Cheatsheet Haskell

Linguagem funcional pura com tipos fortes e lazy evaluation

Volver a los lenguajes
Haskell
70 tarjetas encontradas
Categorías:
Versiones:

Básico e Tipos


10 cards
Tipos básicos
nombre :: String
nombre = "Haskell"

edad :: Int
edad = 30

pi :: Double
pi = 3.14159

activo :: Bool
activo = True

letra :: Char
letra = 'A'

Haskell tiene tipado estático y fuerte. Los tipos principales son Int, Integer, Float, Double, Bool, Char y String.

Pattern matching
-- En funciones (por valor):
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)

-- En listas:
cabeza :: [a] -> a
cabeza (x:_) = x

-- En tuplas:
primero :: (a, b) -> a
primero (x, _) = x

El pattern matching deconstruye valores directamente en los argumentos. El _ ignora partes no usadas y (x:xs) separa la cabeza del resto de la lista.

Operadores
-- Aritméticos:
2 + 3      -- 5
10 - 4     -- 6
3 * 7      -- 21
2 ^ 10     -- 1024 (potencia)
10 `div` 3 -- 3 (división entera)
10 `mod` 3 -- 1 (resto)

-- Comparación y lógicos:
5 == 5     -- True
5 /= 3     -- True (distinto)
True && False  -- False
True || False  -- True
not True       -- False

Los operadores comunes incluyen + - * ^ y la división entera div/mod. La comparación usa == y /=; la lógica usa &&, || y not.

Inferencia de tipos
-- Con anotación explícita:
doble :: Int -> Int
doble x = x * 2

-- Sin anotación (inferido):
triple x = x * 3

-- En GHCi, ve el tipo inferido:
-- :t triple
-- triple :: Num a => a -> a

El compilador infiere los tipos automáticamente, pero la anotación explícita (::) hace el código más claro y detecta errores pronto.

Where
círculo :: Double -> Double
círculo r = area
  where area = pi * r * r

-- Varios bindings locales:
stats xs = (media, desvio)
  where
    media  = sum xs / len
    len    = fromIntegral (length xs)
    desvio = sqrt (suma / len)
    suma   = sum (map (\x -> (x - media)^2) xs)

El where define bindings locales después de la expresión principal. Las definiciones son visibles en todos los guards de la misma función.

Conversión de tipos
-- Int/Integer a fraccionario:
media = fromIntegral (sum xs)
        / fromIntegral (length xs)

-- String a número:
n = read "42" :: Int
d = read "3.14" :: Double

-- Número a String:
s = show 42        -- "42"
t = show 3.14      -- "3.14"

-- Char e Int:
ord 'A'   -- 65  (Data.Char)
chr 65    -- 'A'

Haskell no convierte tipos implícitamente. Usa fromIntegral entre numéricos, read para parsear una String y show para lo inverso.

Definir funciones
sumar :: Int -> Int -> Int
sumar a b = a + b

-- Aplicación por espacio (sin paréntesis):
resultado = sumar 3 5   -- 8

-- Función infija con acento grave:
8 `div` 3   -- 2
3 `mod` 2   -- 1

Las funciones reciben argumentos separados por espacios. La flecha -> separa los tipos; el último es el tipo de retorno. Usa acentos graves para llamar una función de forma infija.

Let / in
-- let ... in es una expresión:
volumen :: Double -> Double
volumen r =
  let area = pi * r * r
  in area * 10

-- let dentro de una list comprehension:
pares = [ y | x <- [1..10]
            , let y = x * 2
            , y > 5 ]

-- En GHCi (sin in):
-- let doble n = n * 2

El let ... in es una expresión que crea bindings locales. A diferencia del where, puede usarse en cualquier lugar, incluso dentro de comprehensions.

Guards
clasificar :: Int -> String
clasificar n
  | n < 0     = "negativo"
  | n == 0    = "cero"
  | otherwise = "positivo"

-- Con where compartido:
imc peso altura
  | imc' < 18.5 = "bajo"
  | imc' < 25   = "normal"
  | otherwise   = "alto"
  where imc' = peso / altura ^ 2

Los guards (|) prueban condiciones en secuencia, como un if/else legible. El otherwise es el caso final y equivale a True.

If como expresión
-- if siempre necesita else:
absoluto :: Int -> Int
absoluto n = if n < 0 then -n else n

-- Equivalente con guards:
absoluto' n
  | n < 0     = -n
  | otherwise = n

-- case (pattern matching explícito):
describir :: Bool -> String
describir b = case b of
  True  -> "si"
  False -> "no"

En Haskell el if es una expresión y exige siempre la rama else. El case ... of hace pattern matching explícito sobre un valor.

Listas


10 cards
Crear listas
xs = [1, 2, 3, 4, 5]

-- Range:
nums   = [1..10]      -- 1 a 10
pares  = [2,4..20]    -- paso 2
letras = ['a'..'z']   -- alfabeto

-- Constructor (:) :
lista = 1 : 2 : 3 : []   -- [1,2,3]

-- Lista vacía:
vacia = []

Una lista es homogénea (todos los elementos del mismo tipo). El operador : (cons) añade un elemento al frente; [] es la lista vacía.

foldl y foldr
-- foldl: acumula por la izquierda
foldl (+) 0 [1,2,3]   -- 6
-- ((0+1)+2)+3

-- foldr: acumula por la derecha
foldr (+) 0 [1,2,3]   -- 6
-- 1+(2+(3+0))

-- Versiones estrictas (recomendadas):
import Data.List (foldl')
foldl' (+) 0 [1..1000000]

-- Reimplementar sum:
suma = foldl (+) 0

Un fold reduce una lista a un valor, combinando cada elemento con un acumulador. El foldl' (estricto) evita acumular thunks y es preferible para cálculos grandes.

Funciones de agregación
sum [1,2,3]       -- 6
product [1,2,3,4] -- 24
maximum [3,1,4]   -- 4
minimum [3,1,4]   -- 1

-- and / or en listas de Bool:
and [True, True]   -- True
or [False, True]   -- True

-- all / any con predicado:
all even [2,4,6]   -- True
any odd [2,4,5]    -- True

Estas funciones resumen listas: sum, product, maximum y minimum. El all y el any prueban un predicado sobre todos los elementos.

Operaciones básicas
xs = [1, 2, 3, 4, 5]

head xs      -- 1
tail xs      -- [2,3,4,5]
last xs      -- 5
init xs      -- [1,2,3,4]
length xs    -- 5
reverse xs   -- [5,4,3,2,1]
take 3 xs    -- [1,2,3]
drop 2 xs    -- [3,4,5]
xs !! 2      -- 3 (índice)
null xs      -- False

Estas funciones descomponen listas. head/tail dan el primer elemento y el resto; take/drop retiran N elementos; !! accede por índice.

zip y zipWith
-- zip junta dos listas en pares:
zip [1,2,3] ["a","b","c"]
-- [(1,"a"),(2,"b"),(3,"c")]

-- zipWith aplica una función a los pares:
zipWith (+) [1,2,3] [10,20,30]
-- [11,22,33]

-- Se detiene en la lista más corta:
zip [1,2,3,4] ["x","y"]
-- [(1,"x"),(2,"y")]

El zip combina dos listas en una lista de pares. El zipWith hace lo mismo aplicando una función a cada par. Ambos se detienen en la lista más corta.

scan, replicate y cycle
-- scan: como fold, pero guarda los pasos:
scanl (+) 0 [1,2,3]   -- [0,1,3,6]

-- replicate: repite un valor:
replicate 4 7         -- [7,7,7,7]

-- cycle: repite la lista (infinita):
take 6 (cycle [1,2])  -- [1,2,1,2,1,2]

-- repeat: repite un valor (infinita):
take 3 (repeat 9)     -- [9,9,9]

El scanl es como un fold que devuelve todos los valores intermedios. El replicate crea listas finitas; cycle y repeat crean listas infinitas.

List comprehensions
-- Doble de cada elemento:
[x*2 | x <- [1..5]]      -- [2,4,6,8,10]

-- Con filtro (guard):
[x | x <- [1..20], even x]   -- pares

-- Varios generadores:
[(x,y) | x <- [1..3], y <- [1..3], x /= y]

-- Las strings son listas de Char:
[toUpper c | c <- "hola"]   -- "HOLA"

Una list comprehension tiene la forma [salida | generador, filtro]. El generador (<-) recorre la lista y los filtros (guards) seleccionan los elementos.

Ranges y listas infinitas
-- Range finito:
[1..10]      -- [1,2,...,10]
[10,9..1]    -- [10,9,...,1]

-- Range infinito (lazy):
todos = [1..]        -- infinita
take 5 todos         -- [1,2,3,4,5]

-- Con paso:
[1,3..20]    -- [1,3,5,7,9,11,13,15,17,19]
[0,0.5..3]   -- [0.0,0.5,1.0,...,3.0]

Los ranges usan ... Gracias a la lazy evaluation, puedes definir listas infinitas como [1..] y consumir solo la parte necesaria con take.

map y filter
-- map aplica una función a cada elemento:
map (*2) [1,2,3]        -- [2,4,6]
map show [1,2,3]        -- ["1","2","3"]

-- filter mantiene los que pasan el test:
filter even [1..10]     -- [2,4,6,8,10]
filter (>3) [1,2,3,4,5] -- [4,5]

-- Equivalente con comprehension:
[x*2 | x <- [1,2,3]]    -- [2,4,6]

El map transforma cada elemento y el filter selecciona los que satisfacen el predicado. Ambos devuelven una nueva lista (son inmutables).

Strings como listas
-- String es [Char]:
s = "hello"
length s        -- 5
reverse s       -- "olleh"
map toUpper s   -- "HELLO"

-- Concatenar:
"hola" ++ " " ++ "mundo"

-- Construir con concat:
concat [["a","b"], ["c"]]   -- "abc"

-- Repetir:
replicate 3 "ab"   -- ["ab","ab","ab"]

Una String es solo una lista de Char ([Char]), por eso todas las funciones de listas funcionan con texto. El ++ concatena dos listas.

Funções


9 cards
Composición (.)
-- (.) compone dos funciones:
f = (*2) . (+1)
f 3   -- 8  (doble de (3+1))

-- Equivalente sin composición:
g x = (*2) ((+1) x)

-- Varias funciones:
h = sum . map (*2) . filter even
h [1..10]   -- 60

El operador . compone funciones de derecha a izquierda: (f . g) x = f (g x). Es ideal para construir pipelines de transformaciones.

Lambdas
-- Función anónima con \:
\x -> x * 2
\x y -> x + y

-- Uso con map/filter:
map (\x -> x^2) [1,2,3]      -- [1,4,9]
filter (\x -> x > 3) [1..5]  -- [4,5]

-- Pattern matching en lambda:
\(x, y) -> x + y
\(x:xs) -> x

Un lambda (\args -> cuerpo) es una función anónima. Es útil cuando la función se usa una sola vez, por ejemplo como argumento de map o filter.

flip, id, const, on
import Data.Function (on)

-- flip invierte los argumentos:
flip (-) 5 3   -- -2  (3 - 5)

-- id devuelve el argumento:
id 42   -- 42

-- const ignora el 2.º argumento:
const 1 99   -- 1

-- on aplica función tras transformar:
sortBy (compare `on` length) palabras

El flip intercambia el orden de los argumentos, el id es la función identidad y el const devuelve siempre el primer valor. El on aplica una función después de transformar los argumentos.

Aplicación ($)
-- ($) aplica con precedencia mínima:
sum $ map (*2) [1..10]
-- igual a: sum (map (*2) [1..10])

-- Evita paréntesis anidados:
show $ length $ filter even [1..20]

-- Composición vs aplicación:
-- (.) combina funciones
-- ($) aplica función a argumento

El operador $ aplica una función al argumento de la derecha, evitando paréntesis. Tiene la precedencia más baja, por eso todo lo de la derecha se evalúa primero.

Recursión
-- Fibonacci:
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

-- Quicksort:
qsort :: Ord a => [a] -> [a]
qsort [] = []
qsort (x:xs) =
  qsort menores ++ [x] ++ qsort mayores
  where
    menores = filter (<= x) xs
    mayores = filter (> x) xs

La recursión es la forma natural de repetición en Haskell (no hay bucles for). Se define un caso base y un caso recursivo, muchas veces con pattern matching.

Currying
-- Toda función de N args es una cadena:
sumar :: Int -> Int -> Int
sumar a b = a + b

-- Aplicación parcial:
sumar5 :: Int -> Int
sumar5 = sumar 5

sumar5 3   -- 8

-- Tipo real:
-- sumar :: Int -> (Int -> Int)

En Haskell, toda función de varios argumentos es una cadena de funciones de un argumento (currying). Aplicar solo algunos argumentos crea una nueva función parcial.

Estilo point-free
-- Con argumento explícito:
sumaLista xs = sum (map (*2) xs)

-- Point-free (sin xs):
sumaLista = sum . map (*2)

-- Otro ejemplo:
par x = even x
par = even

-- No siempre es más legible:
-- f = (. (+1)) . (.) . (*)

El estilo point-free omite los argumentos, escribiendo la función como composición de otras. Queda elegante para pipelines simples, pero puede dificultar la lectura si se abusa.

Secciones
-- Sección: aplicación parcial de operador:
(+1)      -- \x -> x + 1
(*2)      -- \x -> x * 2
(10-)     -- \x -> 10 - x

-- Operador infijo como función:
(`div` 2)   -- \x -> x `div` 2

-- Uso práctico:
map (+1) [1,2,3]      -- [2,3,4]
filter (>3) [1..6]    -- [4,5,6]

Una sección es un operador con uno de los lados relleno, creando una función. (+1) es \x -> x + 1; el orden importa en operadores no conmutativos como -.

Funciones de orden superior
-- Función que recibe función:
aplicar :: (Int -> Int) -> Int -> Int
aplicar f x = f x

aplicar (*2) 5   -- 10

-- Función que devuelve función:
multiplicador :: Int -> (Int -> Int)
multiplicador n = \x -> x * n

veces3 = multiplicador 3
veces3 7   -- 21

Las funciones de orden superior reciben o devuelven funciones. Son la base de map, filter y fold, permitiendo abstraer comportamientos.

Tipos de Dados


9 cards
Algebraic Data Types
-- Tipo con varios constructores:
data Forma
  = Círculo Double
  | Rectangulo Double Double

area :: Forma -> Double
area (Círculo r)      = pi * r * r
area (Rectangulo l a) = l * a

-- Uso:
area (Círculo 2.0)         -- 12.56
area (Rectangulo 3.0 4.0)  -- 12.0

Un data type define un tipo con uno o más constructores separados por |. Cada constructor puede transportar valores, que se extraen por pattern matching.

newtype
-- newtype: wrapper de un único valor:
newtype Euros = Euros Double
newtype Dolar = Dolar Double

sumarEuros :: Euros -> Euros -> Euros
sumarEuros (Euros a) (Euros b) =
  Euros (a + b)

-- ¡No confundir Euros con Dolar!
-- (error de tipo en tiempo de compilación)

El newtype crea un tipo distinto alrededor de un único valor, sin coste en tiempo de ejecución. Es ideal para evitar confundir valores con el mismo tipo base.

Tipos recursivos
-- Árbol binario:
data Arbol a
  = Hoja
  | Nodo a (Arbol a) (Arbol a)

-- Suma de todos los valores:
sumaArbol :: Num a => Arbol a -> a
sumaArbol Hoja = 0
sumaArbol (Nodo x i d) =
  x + sumaArbol i + sumaArbol d

-- Uso:
arb = Nodo 1 (Nodo 2 Hoja Hoja) Hoja
sumaArbol arb   -- 3

Un tipo puede referirse a sí mismo, creando estructuras recursivas como árboles. El pattern matching recorre cada rama hasta el caso base (Hoja).

Record syntax
data Persona = Persona
  { nombre :: String
  , edad   :: Int
  , email  :: String
  }

-- Creación:
p = Persona "Ana" 30 "ana@mail.com"

-- Acceso automático (funciones):
nombre p   -- "Ana"
edad p     -- 30

-- Actualización (copia):
p2 = p { edad = 31 }

La record syntax (llaves) crea automáticamente funciones de acceso a los campos. Los registros son inmutables: p { edad = 31 } devuelve una copia actualizada.

Type synonyms
-- Sinónimo (alias, sin tipo nuevo):
type Nombre = String
type Edad = Int
type Persona = (Nombre, Edad)

saludar :: Persona -> String
saludar (nombre, edad) =
  nombre ++ " tiene " ++ show edad

-- En firmas de Either:
type Resultado = Either String Int

Un type synonym (type) da un nombre alternativo a un tipo existente, mejorando la legibilidad. No crea un tipo nuevo — es solo un alias.

Maybe
-- Maybe representa un valor opcional:
-- data Maybe a = Nothing | Just a

buscar :: [Int] -> Int -> Maybe Int
buscar [] _ = Nothing
buscar (x:xs) n
  | x == n    = Just x
  | otherwise = buscar xs n

-- Uso seguro:
case buscar [1,2,3] 2 of
  Just x  -> "encontrado"
  Nothing -> "faltante"

El Maybe modela un valor que puede no existir: Just x o Nothing. Evita errores de null, forzando el tratamiento de la ausencia.

Case expressions
-- case hace pattern matching explícito:
dia :: Int -> String
dia n = case n of
  1 -> "lunes"
  2 -> "martes"
  _ -> "otro"

-- En Maybe:
mostrar :: Maybe Int -> String
mostrar m = case m of
  Just x  -> "valor: " ++ show x
  Nothing -> "vacío"

La expresión case ... of hace pattern matching sobre un valor en cualquier punto del código. El _ es el caso por defecto que acepta cualquier cosa.

Either
-- Either: dos tipos posibles:
-- data Either a b = Left a | Right b

dividir :: Double -> Double -> Either String Double
dividir _ 0 = Left "División por cero"
dividir a b = Right (a / b)

-- Convención: Left = error, Right = éxito
case dividir 10 0 of
  Left msg  -> putStrLn msg
  Right val -> print val

El Either representa un valor que puede ser de dos tipos. Por convención, Left transporta el error y Right el resultado correcto.

deriving
-- Deriva instancias automáticamente:
data Color = Rojo | Verde | Azul
  deriving (Show, Eq, Enum, Bounded)

-- Show: permite imprimir
show Rojo   -- "Rojo"

-- Eq: permite comparar
Rojo == Verde   -- False

-- Enum y Bounded:
[Rojo .. Azul]      -- los 3 colores
minBound :: Color   -- Rojo

El deriving genera automáticamente instancias de typeclasses comunes. Show permite imprimir, Eq comparar, Enum enumerar y Bounded da los límites.

Monads e IO


9 cards
IO Monad
main :: IO ()
main = do
  putStrLn "¿Cómo te llamas?"
  nombre <- getLine
  putStrLn ("Hola, " ++ nombre ++ "!")

-- IO aísla efectos secundarios:
-- putStrLn :: String -> IO ()
-- getLine  :: IO String

El tipo IO aísla efectos secundarios (pantalla, archivos, red). El <- extrae el valor de una acción IO dentro de un bloque do.

Bind (>>=)
-- (>>=) pasa el resultado a la siguiente función:
-- (>>=) :: m a -> (a -> m b) -> m b

Just 3 >>= \x -> Just (x + 1)   -- Just 4

-- Equivale al do:
ejemplo = do
  x <- Just 3
  Just (x + 1)

-- (>>) ignora el resultado anterior:
putStrLn "a" >> putStrLn "b"

El operador >>= (bind) toma el valor dentro de la monad y lo pasa a una función que devuelve una nueva monad. Es el mecanismo que el do usa por debajo.

when, unless y forever
import Control.Monad (when, unless, forever)

-- when: ejecuta si True
main = do
  n <- readLn :: IO Int
  when (n > 0) $ putStrLn "positivo"

-- unless: ejecuta si False
  unless (even n) $ putStrLn "impar"

-- forever: repite para siempre
  forever $ putStrLn "loop"

Estas funciones de Control.Monad controlan el flujo. El when ejecuta la acción si la condición es verdadera, el unless si es falsa y el forever repite indefinidamente.

do notation
-- do encadena acciones en secuencia:
main :: IO ()
main = do
  putStr "Nombre: "
  n <- getLine
  putStr "Edad: "
  e <- getLine
  print (n, read e :: Int)

-- let dentro de do (sin in):
  let saludo = "Hola " ++ n
  putStrLn saludo

La notación do escribe código monádico de forma secuencial y legible. Cada línea es una acción; el <- liga el resultado a un nombre y el let crea bindings puros.

return y pure
-- return coloca el valor en la monad:
return 5 :: Maybe Int    -- Just 5
return 5 :: [Int]        -- [5]
return 5 :: Either e Int -- Right 5

-- pure (Applicative) hace lo mismo:
pure 5 :: Maybe Int      -- Just 5

-- ¡return NO es el return de C!
-- No sale de la función, solo envuelve.

El return (o pure) envuelve un valor puro en un contexto monádico. A diferencia de otros lenguajes, no termina la función — solo crea el valor monádico.

Maybe como Monad
-- Encadenar con >>= (bind):
resultado = Just 5 >>= \x ->
  Just (x * 2) >>= \y ->
  Just (y + 1)        -- Just 11

-- Con do notation:
calcular :: Maybe Int
calcular = do
  x <- Just 5
  y <- Just 10
  return (x + y)      -- Just 15

-- Si alguno es Nothing, todo es Nothing

El Maybe es una monad: encadena operaciones que pueden fallar. Si cualquier paso devuelve Nothing, el resto se ignora y el resultado final es Nothing.

sequence y mapM
-- sequence: lista de monads -> monad de lista
sequence [Just 1, Just 2, Just 3]
-- Just [1,2,3]
sequence [Just 1, Nothing]   -- Nothing

-- mapM: map + sequence
mapM read ["1","2","3"] :: Maybe [Int]
-- Just [1,2,3]

-- Versiones que ignoran resultados:
sequence_ [putStrLn "a", putStrLn "b"]
mapM_ putStrLn ["hola", "mundo"]

El sequence transforma una lista de acciones monádicas en una acción que devuelve una lista. El mapM combina map con sequence; el sufijo _ descarta los resultados.

Either como Monad
type Resultado = Either String Int

parseEdad :: String -> Resultado
parseEdad s = case reads s of
  [(n, "")] -> Right n
  _         -> Left "Edad inválida"

procesar :: String -> Resultado
procesar s = do
  edad <- parseEdad s
  if edad >= 0
    then Right (edad * 2)
    else Left "Edad negativa"

El Either como monad propaga errores: un Left interrumpe la cadena y se devuelve de inmediato. Solo los Right continúan el procesamiento.

List Monad
-- Las listas son monads (no determinismo):
[1,2,3] >>= \x -> [x, x*10]
-- [1,10,2,20,3,30]

-- do con listas = comprehension:
pares = do
  x <- [1..5]
  y <- [1..5]
  return (x, y)

-- Equivalente:
[(x,y) | x <- [1..5], y <- [1..5]]

La lista es una monad que modela computaciones no deterministas. El do sobre listas equivale a una list comprehension, combinando todos los valores posibles.

Avançado


8 cards
Lazy evaluation
-- Las expresiones solo se evalúan cuando hace falta:
caros = [ expensive x | x <- [1..1000000] ]

-- Solo calcula los 3 primeros:
take 3 caros

-- short-circuit natural:
-- (&&) no evalúa el 2.º si el 1.º es False
False && expensiveCheck   -- False

La lazy evaluation aplaza el cálculo de una expresión hasta que su valor sea realmente necesario. Esto permite estructuras infinitas y evita trabajo innecesario.

seq y strictness
-- seq fuerza la evaluación:
-- seq :: a -> b -> b
suma x y = x `seq` x + y

-- BangPatterns (extensión):
{-# LANGUAGE BangPatterns #-}
suma' !x !y = x + y

-- foldl' es estricto (recomendado):
import Data.List (foldl')
foldl' (+) 0 [1..1000000]

El seq fuerza la evaluación de un valor antes de continuar, evitando acumular thunks. La extensión BangPatterns (!) marca argumentos como estrictos.

Listas infinitas
-- Fibonacci con zipWith:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
take 10 fibs
-- [0,1,1,2,3,5,8,13,21,34]

-- Criba de Eratóstenes (primos):
primos = criba [2..]
  where
    criba (p:xs) =
      p : criba [x | x <- xs, x `mod` p /= 0]

take 10 primos
-- [2,3,5,7,11,13,17,19,23,29]

Con lazy evaluation, listas infinitas como fibs o primos se definen por autorreferencia. El take fuerza solo los elementos pedidos.

Módulos e imports
module MiModulo
  ( funcionPublica
  , TipoPublico(..)
  ) where

import Data.List (sort, nub)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe, isJust)

-- qualified exige prefijo:
Map.fromList [("a", 1)]

Un module controla lo que se exporta. El import selectivo trae solo nombres específicos; el qualified obliga a usar un prefijo (ej.: Map.) para evitar conflictos.

Memoization
-- Fibonacci memoizado (lista infinita):
fib :: Int -> Integer
fib = (fibs !!)
  where
    fibs = 0 : 1 :
      zipWith (+) fibs (tail fibs)

fib 100   -- instantáneo
-- 354224848179261915075

La memoization guarda resultados ya calculados. Definir fibs como lista infinita compartida hace que cada valor se calcule una única vez, haciendo fib 100 inmediato.

Foldable y Traversable
import qualified Data.Map as Map
import Data.Foldable (toList)

-- Foldable: reduce estructuras a un valor
m = Map.fromList [("a",1),("b",2)]
sum m            -- 3  (suma los valores)
toList m         -- [1,2]

-- Traversable: map con efectos
traverse print [1,2,3] :: IO [()]

-- foldMap combina con un Monoid:
foldMap show [1,2,3]   -- "123"

Foldable generaliza el fold para cualquier estructura (listas, mapas, árboles). Traversable permite mapear con efectos, como IO o Maybe, preservando la forma.

Data.Map y Data.Set
import qualified Data.Map as Map
import qualified Data.Set as Set

-- Map (diccionario):
m = Map.fromList [("a", 1), ("b", 2)]
Map.lookup "a" m    -- Just 1
Map.insert "c" 3 m

-- Set (conjunto):
s = Set.fromList [1,2,2,3]   -- {1,2,3}
Set.member 2 s    -- True
Set.union s (Set.fromList [4])

El Data.Map es un diccionario clave-valor y el Data.Set un conjunto sin duplicados. Impórtalos con qualified para evitar conflictos de nombres con el Prelude.

Excepciones
import Control.Exception (try, catch, SomeException)

-- try devuelve Either:
main = do
  r <- try (evaluate (1 `div` 0))
         :: IO (Either SomeException Int)
  case r of
    Left ex  -> putStrLn ("Error: " ++ show ex)
    Right v  -> print v

-- catch con handler:
  catch (print (read "abc" :: Int))
        (\(e :: SomeException) -> putStrLn "falló")

Las excepciones en Control.Exception manejan errores en IO. El try convierte la excepción en un Either; el catch ejecuta un handler cuando ocurre un error.

Typeclasses


8 cards
Definir typeclass
-- Una typeclass declara operaciones:
class Descriptible a where
  describir :: a -> String

-- Con método por defecto:
class Saludo a where
  saludar :: a -> String
  saludar _ = "¡Hola!"   -- default

Una typeclass declara un conjunto de funciones que varios tipos pueden implementar. Es similar a una interfaz, pero basada en tipos — define comportamiento, no datos.

Read y Enum
-- Read: parse de String (inverso de Show)
read "42" :: Int       -- 42
read "3.14" :: Double  -- 3.14
read "True" :: Bool    -- True

-- Enum: tipos enumerables
['a'..'e']      -- "abcde"
fromEnum 'A'    -- 65
toEnum 65 :: Char   -- 'A'

-- succ y pred:
succ 5   -- 6
pred 5   -- 4

El Read parsea una String a un valor (requiere anotación de tipo). El Enum permite secuencias con .. y las funciones succ/pred.

Instanciar typeclass
data Persona = Persona String Int

instance Descriptible Persona where
  describir (Persona n e) =
    n ++ " (" ++ show e ++ ")"

-- Varios tipos, misma interfaz:
data Producto = Producto String Double

instance Descriptible Producto where
  describir (Producto n p) = n ++ ": " ++ show p

Una instance implementa la typeclass para un tipo concreto. Cada tipo da su propia definición de los métodos declarados en la clase.

Functor
-- Functor: mapea dentro de un contexto
class Functor f where
  fmap :: (a -> b) -> f a -> f b

-- En listas (fmap = map):
fmap (*2) [1,2,3]   -- [2,4,6]

-- En Maybe:
fmap (*2) (Just 5)  -- Just 10
fmap (*2) Nothing   -- Nothing

-- El operador <$> es sinónimo de fmap:
(*2) <$> Just 5     -- Just 10

Un Functor es un contenedor sobre el que se puede mapear una función con fmap. Aplica la función al valor dentro del contexto (lista, Maybe, etc.) sin alterarlo.

Constraints
-- Constraint simple:
mayor :: Ord a => a -> a -> a
mayor x y = if x > y then x else y

-- Varias constraints:
mostrarPar :: (Show a, Show b) => (a, b) -> String
mostrarPar (x, y) = show x ++ ", " ++ show y

-- Contexto de clase:
class Eq a => Ordenable a where
  comparar :: a -> a -> Ordering

Una constraint (Ord a =>) restringe los tipos aceptados por una función. Exige que el tipo pertenezca a cierta typeclass para usar sus operaciones.

Applicative
-- Applicative: funciones dentro del contexto
-- pure coloca un valor en el contexto
pure 5 :: Maybe Int   -- Just 5

-- <*> aplica función envuelta:
Just (+) <*> Just 3 <*> Just 4   -- Just 7

-- Combinar con <$> :
(+) <$> Just 3 <*> Just 4        -- Just 7

-- En listas:
(*) <$> [1,2] <*> [10,20]
-- [10,20,20,40]

Un Applicative extiende el Functor: permite aplicar funciones que también están dentro de un contexto, usando pure y el operador <*>.

Show, Eq y Ord
-- Show: convertir a String
show 42        -- "42"
show True      -- "True"

-- Eq: igualdad (==, /=)
5 == 5         -- True

-- Ord: ordenación (<, >, compare)
compare 3 5    -- LT
compare 5 5    -- EQ
compare 7 5    -- GT

-- sort necesita Ord:
import Data.List (sort)
sort [3,1,2]   -- [1,2,3]

Show convierte valores en String, Eq permite == y /=, y Ord añade ordenación (<, >, compare).

Typeclass Monad
-- Monad: encadena operaciones con contexto
class Applicative m => Monad m where
  (>>=)  :: m a -> (a -> m b) -> m b
  return :: a -> m a

-- Ejemplo con Maybe:
Just 5 >>= \x -> Just (x * 2)   -- Just 10

-- return es lo mismo que pure:
return 5 :: Maybe Int   -- Just 5

Una Monad permite encadenar operaciones que devuelven valores con contexto, a través del bind (>>=). El return coloca un valor puro en el contexto monádico.

Ferramentas e Boas Práticas


7 cards
GHC y compilación
# Compilar un ejecutable:
ghc -o programa Main.hs

# Compilar con optimización:
ghc -O2 -o programa Main.hs

# Generar solo objetos:
ghc --make Main.hs

# Verificar tipos sin ejecutar:
ghc -fno-code Main.hs

El GHC es el compilador principal de Haskell. El --make compila el módulo y sus dependencias; la flag -O2 activa optimizaciones de rendimiento.

Haddock (documentación)
-- | Descripción de la función (Haddock):
doble :: Int -> Int
doble x = x * 2

-- ^ argumento
-- * Sección
-- >>> doble 3   (ejemplo doctest)
-- 6

# Generar documentación:
cabal haddock
stack haddock

El Haddock genera documentación HTML a partir de comentarios especiales. El -- | documenta la definición siguiente y el >>> crea ejemplos testeables con doctest.

GHCi (REPL)
# Iniciar el REPL:
ghci

> :l archivo.hs     -- cargar módulo
> :r                -- recargar
> :t (+)            -- ver tipo
> :i Int            -- info del tipo
> :set +t           -- mostrar tipos
> :browse Data.List -- listar funciones

El GHCi es el REPL interactivo. Los comandos :t muestran el tipo, :i da información detallada y :l/:r cargan y recargan módulos.

Estructura de proyecto
mi-proyecto/
  app/
    Main.hs        -- ejecutable
  src/
    MiModulo.hs    -- biblioteca
  test/
    Spec.hs        -- tests
  mi-proyecto.cabal
  stack.yaml

-- Módulo = archivo:
-- src/Data/Util.hs -> module Data.Util

Un proyecto típico separa app/ (ejecutable), src/ (biblioteca) y test/. La ruta del archivo define el nombre del módulo: src/Data/Util.hs es module Data.Util.

Cabal
# Crear proyecto:
cabal init

# El archivo .cabal define el proyecto:
# name, version, build-depends...

# Comandos principales:
cabal build      -- compilar
cabal run        -- ejecutar
cabal test       -- tests
cabal repl       -- GHCi del proyecto
cabal update     -- actualizar índice

El Cabal gestiona proyectos y dependencias. El archivo .cabal describe el paquete y sus dependencias en build-depends; el cabal build compila todo.

Buenas prácticas y debug
-- Escribe firmas de tipo siempre:
doble :: Int -> Int

-- Usa undefined como placeholder:
funcionCompleja :: a -> b
funcionCompleja = undefined

-- Debug con trace (Debug.Trace):
import Debug.Trace (trace)
f x = trace ("x = " ++ show x) (x * 2)

-- Tests rápidos en GHCi:
-- :t expresion

Firma siempre los tipos, usa undefined como esqueleto y recurre al trace de Debug.Trace para imprimir valores durante el debug sin cambiar el tipo de la función.

Stack
# Crear proyecto desde una plantilla:
stack new mi-proyecto

# Comandos principales:
stack build      -- compilar
stack run        -- ejecutar
stack test       -- tests
stack ghci       -- REPL
stack exec -- programa   -- ejecutar binario

El Stack es una alternativa a Cabal con versiones de GHC reproducibles, definidas en el stack.yaml. Garantiza que todos los desarrolladores usan el mismo toolchain.