Cheatsheet Haskell
Linguagem funcional pura com tipos fortes e lazy evaluation
Haskell
Básico e Tipos
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 * 2El 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
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
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) xsLa 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
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
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 StringEl 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
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
-- 354224848179261915075La 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
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 pUna 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
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.UtilUn 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 expresionFirma 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.