Cheatsheet Haskell
Linguagem funcional pura com tipos fortes e lazy evaluation
Haskell
Básico e Tipos
Tipos básicos
nome :: String nome = "Haskell" idade :: Int idade = 30 pi :: Double pi = 3.14159 ativo :: Bool ativo = True letra :: Char letra = 'A'
O Haskell tem tipagem estática e forte. Os tipos principais são Int, Integer, Float, Double, Bool, Char e String.
Pattern matching
-- Em funções (por valor): fatorial :: Int -> Int fatorial 0 = 1 fatorial n = n * fatorial (n - 1) -- Em listas: cabeca :: [a] -> a cabeca (x:_) = x -- Em tuplas: primeiro :: (a, b) -> a primeiro (x, _) = x
O pattern matching desconstrói valores diretamente nos argumentos. O _ ignora partes não usadas e (x:xs) separa a cabeça do resto da lista.
Operadores
-- Aritméticos: 2 + 3 -- 5 10 - 4 -- 6 3 * 7 -- 21 2 ^ 10 -- 1024 (potência) 10 `div` 3 -- 3 (divisão inteira) 10 `mod` 3 -- 1 (resto) -- Comparação e lógicos: 5 == 5 -- True 5 /= 3 -- True (diferente) True && False -- False True || False -- True not True -- False
Os operadores comuns incluem + - * ^ e a divisão inteira div/mod. A comparação usa == e /=; a lógica usa &&, || e not.
Inferência de tipos
-- Com anotação explícita: dobro :: Int -> Int dobro x = x * 2 -- Sem anotação (inferido): triplo x = x * 3 -- No GHCi, vê o tipo inferido: -- :t triplo -- triplo :: Num a => a -> a
O compilador infere os tipos automaticamente, mas a anotação explícita (::) torna o código mais claro e deteta erros cedo.
Where
circulo :: Double -> Double
circulo r = area
where area = pi * r * r
-- Vários bindings locais:
stats xs = (media, desvio)
where
media = sum xs / len
len = fromIntegral (length xs)
desvio = sqrt (soma / len)
soma = sum (map (\x -> (x - media)^2) xs)O where define bindings locais depois da expressão principal. As definições são visíveis em todos os guards da mesma função.
Conversão de tipos
-- Int/Integer para fractional:
media = fromIntegral (sum xs)
/ fromIntegral (length xs)
-- String para número:
n = read "42" :: Int
d = read "3.14" :: Double
-- Número para String:
s = show 42 -- "42"
t = show 3.14 -- "3.14"
-- Char e Int:
ord 'A' -- 65 (Data.Char)
chr 65 -- 'A'O Haskell não converte tipos implicitamente. Usa fromIntegral entre numéricos, read para parse de String e show para o inverso.
Definir funções
somar :: Int -> Int -> Int somar a b = a + b -- Aplicação por espaço (sem parênteses): resultado = somar 3 5 -- 8 -- Função infixa com crase: 8 `div` 3 -- 2 3 `mod` 2 -- 1
As funções recebem argumentos separados por espaços. A seta -> separa os tipos; a última é o tipo de retorno. Usa crase para chamar uma função de forma infixa.
Let / in
-- let ... in é uma expressão:
volume :: Double -> Double
volume r =
let area = pi * r * r
in area * 10
-- let dentro de list comprehension:
pares = [ y | x <- [1..10]
, let y = x * 2
, y > 5 ]
-- No GHCi (sem in):
-- let dobro n = n * 2O let ... in é uma expressão que cria bindings locais. Diferente do where, pode ser usado em qualquer lugar, inclusive dentro de comprehensions.
Guards
classificar :: Int -> String classificar n | n < 0 = "negativo" | n == 0 = "zero" | otherwise = "positivo" -- Com where partilhado: imc peso altura | imc' < 18.5 = "abaixo" | imc' < 25 = "normal" | otherwise = "acima" where imc' = peso / altura ^ 2
Os guards (|) testam condições em sequência, como um if/else legível. O otherwise é o caso final e equivale a True.
If como expressão
-- if sempre precisa de else: absoluto :: Int -> Int absoluto n = if n < 0 then -n else n -- Equivalente com guards: absoluto' n | n < 0 = -n | otherwise = n -- case (pattern matching explícito): descrever :: Bool -> String descrever b = case b of True -> "sim" False -> "nao"
Em Haskell o if é uma expressão e exige sempre o ramo else. O case ... of faz pattern matching explícito sobre um valor.
Listas
Criar listas
xs = [1, 2, 3, 4, 5] -- Range: nums = [1..10] -- 1 a 10 pares = [2,4..20] -- passo 2 letras = ['a'..'z'] -- alfabeto -- Construtor (:) : lista = 1 : 2 : 3 : [] -- [1,2,3] -- Lista vazia: vazia = []
Uma lista é homogénea (todos os elementos do mesmo tipo). O operador : (cons) acrescenta um elemento à frente; [] é a lista vazia.
foldl e foldr
-- foldl: acumula pela esquerda foldl (+) 0 [1,2,3] -- 6 -- ((0+1)+2)+3 -- foldr: acumula pela direita foldr (+) 0 [1,2,3] -- 6 -- 1+(2+(3+0)) -- Versões estritas (recomendadas): import Data.List (foldl') foldl' (+) 0 [1..1000000] -- Reimplementar sum: soma = foldl (+) 0
Um fold reduz uma lista a um valor, combinando cada elemento com um acumulador. O foldl' (estrito) evita acumular thunks e é preferível para cálculos grandes.
Funções de agregação
sum [1,2,3] -- 6 product [1,2,3,4] -- 24 maximum [3,1,4] -- 4 minimum [3,1,4] -- 1 -- and / or em listas de Bool: and [True, True] -- True or [False, True] -- True -- all / any com predicado: all even [2,4,6] -- True any odd [2,4,5] -- True
Estas funções resumem listas: sum, product, maximum e minimum. O all e o any testam um predicado sobre todos os elementos.
Operações 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 funções decompõem listas. head/tail dão o primeiro elemento e o resto; take/drop retiram N elementos; !! acede por índice.
zip e zipWith
-- zip junta duas listas em pares: zip [1,2,3] ["a","b","c"] -- [(1,"a"),(2,"b"),(3,"c")] -- zipWith aplica uma função aos pares: zipWith (+) [1,2,3] [10,20,30] -- [11,22,33] -- Parar na lista mais curta: zip [1,2,3,4] ["x","y"] -- [(1,"x"),(2,"y")]
O zip combina duas listas numa lista de pares. O zipWith faz o mesmo aplicando uma função a cada par. Ambos param na lista mais curta.
scan, replicate e cycle
-- scan: como fold, mas guarda os passos: scanl (+) 0 [1,2,3] -- [0,1,3,6] -- replicate: repete um valor: replicate 4 7 -- [7,7,7,7] -- cycle: repete a lista (infinita): take 6 (cycle [1,2]) -- [1,2,1,2,1,2] -- repeat: repete um valor (infinita): take 3 (repeat 9) -- [9,9,9]
O scanl é como um fold que devolve todos os valores intermédios. O replicate cria listas finitas; cycle e repeat criam listas infinitas.
List comprehensions
-- Dobro de cada elemento: [x*2 | x <- [1..5]] -- [2,4,6,8,10] -- Com filtro (guard): [x | x <- [1..20], even x] -- pares -- Vários geradores: [(x,y) | x <- [1..3], y <- [1..3], x /= y] -- Strings são listas de Char: [toUpper c | c <- "ola"] -- "OLA"
Uma list comprehension tem a forma [saida | gerador, filtro]. O gerador (<-) percorre a lista e os filtros (guards) selecionam os elementos.
Ranges e 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] -- Com passo: [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]
Os ranges usam ... Graças à lazy evaluation, podes definir listas infinitas como [1..] e consumir só a parte necessária com take.
map e filter
-- map aplica uma função a cada elemento: map (*2) [1,2,3] -- [2,4,6] map show [1,2,3] -- ["1","2","3"] -- filter mantém os que passam no teste: filter even [1..10] -- [2,4,6,8,10] filter (>3) [1,2,3,4,5] -- [4,5] -- Equivalente com comprehension: [x*2 | x <- [1,2,3]] -- [2,4,6]
O map transforma cada elemento e o filter seleciona os que satisfazem o predicado. Ambos devolvem uma nova lista (são imutáveis).
Strings como listas
-- String é [Char]: s = "hello" length s -- 5 reverse s -- "olleh" map toUpper s -- "HELLO" -- Concatenar: "ola" ++ " " ++ "mundo" -- Construir com concat: concat [["a","b"], ["c"]] -- "abc" -- Repetir: replicate 3 "ab" -- ["ab","ab","ab"]
Uma String é apenas uma lista de Char ([Char]), por isso todas as funções de listas funcionam com texto. O ++ concatena duas listas.
Funções
Composição (.)
-- (.) compõe duas funções: f = (*2) . (+1) f 3 -- 8 (dobro de (3+1)) -- Equivalente sem composição: g x = (*2) ((+1) x) -- Várias funções: h = sum . map (*2) . filter even h [1..10] -- 60
O operador . compõe funções da direita para a esquerda: (f . g) x = f (g x). É ideal para construir pipelines de transformações.
Lambdas
-- Função anónima com \: \x -> x * 2 \x y -> x + y -- Uso com map/filter: map (\x -> x^2) [1,2,3] -- [1,4,9] filter (\x -> x > 3) [1..5] -- [4,5] -- Pattern matching em lambda: \(x, y) -> x + y \(x:xs) -> x
Um lambda (\args -> corpo) é uma função anónima. É útil quando a função é usada uma só vez, por exemplo como argumento de map ou filter.
flip, id, const, on
import Data.Function (on) -- flip inverte os argumentos: flip (-) 5 3 -- -2 (3 - 5) -- id devolve o argumento: id 42 -- 42 -- const ignora o 2.º argumento: const 1 99 -- 1 -- on aplica função após transformar: sortBy (compare `on` length) palavras
O flip troca a ordem dos argumentos, o id é a função identidade e o const devolve sempre o primeiro valor. O on aplica uma função depois de transformar os argumentos.
Aplicação ($)
-- ($) aplica com precedência mínima: sum $ map (*2) [1..10] -- igual a: sum (map (*2) [1..10]) -- Evita parênteses aninhados: show $ length $ filter even [1..20] -- Composição vs aplicação: -- (.) combina funções -- ($) aplica função a argumento
O operador $ aplica uma função ao argumento à direita, evitando parênteses. Tem a precedência mais baixa, por isso tudo à direita é avaliado primeiro.
Recursão
-- 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 maiores
where
menores = filter (<= x) xs
maiores = filter (> x) xsA recursão é a forma natural de repetição no Haskell (não há ciclos for). Define-se um caso base e um caso recursivo, muitas vezes com pattern matching.
Currying
-- Toda função de N args é uma cadeia: somar :: Int -> Int -> Int somar a b = a + b -- Aplicação parcial: somar5 :: Int -> Int somar5 = somar 5 somar5 3 -- 8 -- Tipo real: -- somar :: Int -> (Int -> Int)
No Haskell, toda a função de vários argumentos é uma cadeia de funções de um argumento (currying). Aplicar só alguns argumentos cria uma nova função parcial.
Point-free style
-- Com argumento explícito: somaLista xs = sum (map (*2) xs) -- Point-free (sem xs): somaLista = sum . map (*2) -- Outro exemplo: par x = even x par = even -- Nem sempre é mais legível: -- f = (. (+1)) . (.) . (*)
O estilo point-free omite os argumentos, escrevendo a função como composição de outras. Fica elegante para pipelines simples, mas pode dificultar a leitura se abusado.
Secções
-- Secção: aplicação parcial de operador: (+1) -- \x -> x + 1 (*2) -- \x -> x * 2 (10-) -- \x -> 10 - x -- Operador infixo como função: (`div` 2) -- \x -> x `div` 2 -- Uso prático: map (+1) [1,2,3] -- [2,3,4] filter (>3) [1..6] -- [4,5,6]
Uma secção é um operador com um dos lados preenchido, criando uma função. (+1) é \x -> x + 1; a ordem importa em operadores não comutativos como -.
Funções de ordem superior
-- Função que recebe função: aplicar :: (Int -> Int) -> Int -> Int aplicar f x = f x aplicar (*2) 5 -- 10 -- Função que devolve função: multiplicador :: Int -> (Int -> Int) multiplicador n = \x -> x * n vezes3 = multiplicador 3 vezes3 7 -- 21
Funções de ordem superior recebem ou devolvem funções. São a base de map, filter e fold, permitindo abstrair comportamentos.
Tipos de Dados
Algebraic Data Types
-- Tipo com vários construtores: data Forma = Circulo Double | Retangulo Double Double area :: Forma -> Double area (Circulo r) = pi * r * r area (Retangulo l a) = l * a -- Uso: area (Circulo 2.0) -- 12.56 area (Retangulo 3.0 4.0) -- 12.0
Um data type define um tipo com um ou mais construtores separados por |. Cada construtor pode transportar valores, que são extraídos por pattern matching.
newtype
-- newtype: wrapper de um único valor: newtype Euros = Euros Double newtype Dolar = Dolar Double somarEuros :: Euros -> Euros -> Euros somarEuros (Euros a) (Euros b) = Euros (a + b) -- Não confundir Euros com Dolar! -- (erro de tipo em tempo de compilação)
O newtype cria um tipo distinto à volta de um único valor, sem custo em tempo de execução. É ideal para evitar confundir valores com o mesmo tipo base.
Tipos recursivos
-- Árvore binária: data Arvore a = Folha | No a (Arvore a) (Arvore a) -- Soma de todos os valores: somaArvore :: Num a => Arvore a -> a somaArvore Folha = 0 somaArvore (No x e d) = x + somaArvore e + somaArvore d -- Uso: arv = No 1 (No 2 Folha Folha) Folha somaArvore arv -- 3
Um tipo pode referir-se a si mesmo, criando estruturas recursivas como árvores. O pattern matching percorre cada ramo até ao caso base (Folha).
Record syntax
data Pessoa = Pessoa
{ nome :: String
, idade :: Int
, email :: String
}
-- Criação:
p = Pessoa "Ana" 30 "ana@mail.com"
-- Acesso automático (funções):
nome p -- "Ana"
idade p -- 30
-- Atualização (cópia):
p2 = p { idade = 31 }A record syntax (chavetas) cria automaticamente funções de acesso aos campos. Os registos são imutáveis: p { idade = 31 } devolve uma cópia atualizada.
Type synonyms
-- Sinónimo (alias, sem tipo novo): type Nome = String type Idade = Int type Pessoa = (Nome, Idade) saudar :: Pessoa -> String saudar (nome, idade) = nome ++ " tem " ++ show idade -- Em assinaturas de Either: type Resultado = Either String Int
Um type synonym (type) dá um nome alternativo a um tipo existente, melhorando a legibilidade. Não cria um tipo novo — é só um alias.
Maybe
-- Maybe representa valor opcional: -- data Maybe a = Nothing | Just a encontrar :: [Int] -> Int -> Maybe Int encontrar [] _ = Nothing encontrar (x:xs) n | x == n = Just x | otherwise = encontrar xs n -- Uso seguro: case encontrar [1,2,3] 2 of Just x -> "achado" Nothing -> "faltando"
O Maybe modela um valor que pode não existir: Just x ou Nothing. Evita erros de null, forçando o tratamento da ausência.
Case expressions
-- case faz pattern matching explícito: dia :: Int -> String dia n = case n of 1 -> "segunda" 2 -> "terca" _ -> "outro" -- Em Maybe: mostrar :: Maybe Int -> String mostrar m = case m of Just x -> "valor: " ++ show x Nothing -> "vazio"
A expressão case ... of faz pattern matching sobre um valor em qualquer ponto do código. O _ é o caso padrão que aceita qualquer coisa.
Either
-- Either: dois tipos possíveis: -- data Either a b = Left a | Right b dividir :: Double -> Double -> Either String Double dividir _ 0 = Left "Divisao por zero" dividir a b = Right (a / b) -- Convenção: Left = erro, Right = sucesso case dividir 10 0 of Left msg -> putStrLn msg Right val -> print val
O Either representa um valor que pode ser de dois tipos. Por convenção, Left transporta o erro e Right o resultado correto.
deriving
-- Deriva instâncias automaticamente: data Cor = Vermelho | Verde | Azul deriving (Show, Eq, Enum, Bounded) -- Show: permite imprimir show Vermelho -- "Vermelho" -- Eq: permite comparar Vermelho == Verde -- False -- Enum e Bounded: [Vermelho .. Azul] -- as 3 cores minBound :: Cor -- Vermelho
O deriving gera automaticamente instâncias de typeclasses comuns. Show permite imprimir, Eq comparar, Enum enumerar e Bounded dá os limites.
Monads e IO
IO Monad
main :: IO ()
main = do
putStrLn "Qual o teu nome?"
nome <- getLine
putStrLn ("Ola, " ++ nome ++ "!")
-- IO isola efeitos colaterais:
-- putStrLn :: String -> IO ()
-- getLine :: IO StringO tipo IO isola efeitos colaterais (ecrã, ficheiros, rede). O <- extrai o valor de uma ação IO dentro de um bloco do.
Bind (>>=)
-- (>>=) passa o resultado à próxima função: -- (>>=) :: m a -> (a -> m b) -> m b Just 3 >>= \x -> Just (x + 1) -- Just 4 -- Equivale ao do: exemplo = do x <- Just 3 Just (x + 1) -- (>>) ignora o resultado anterior: putStrLn "a" >> putStrLn "b"
O operador >>= (bind) pega no valor dentro da monad e passa-o a uma função que devolve nova monad. É o mecanismo que o do usa por baixo.
when, unless e forever
import Control.Monad (when, unless, forever) -- when: executa se True main = do n <- readLn :: IO Int when (n > 0) $ putStrLn "positivo" -- unless: executa se False unless (even n) $ putStrLn "impar" -- forever: repete para sempre forever $ putStrLn "loop"
Estas funções de Control.Monad controlam o fluxo. O when executa a ação se a condição for verdadeira, o unless se for falsa e o forever repete indefinidamente.
do notation
-- do encadeia ações em sequência: main :: IO () main = do putStr "Nome: " n <- getLine putStr "Idade: " i <- getLine print (n, read i :: Int) -- let dentro de do (sem in): let saudacao = "Ola " ++ n putStrLn saudacao
A notação do escreve código monádico de forma sequencial e legível. Cada linha é uma ação; o <- liga o resultado a um nome e o let cria bindings puros.
return e pure
-- return coloca valor na monad: return 5 :: Maybe Int -- Just 5 return 5 :: [Int] -- [5] return 5 :: Either e Int -- Right 5 -- pure (Applicative) faz o mesmo: pure 5 :: Maybe Int -- Just 5 -- return NÃO é o return do C! -- Não sai da função, só embrulha.
O return (ou pure) embrulha um valor puro num contexto monádico. Ao contrário de outras linguagens, não termina a função — apenas cria o valor monádico.
Maybe como Monad
-- Encadear com >>= (bind): resultado = Just 5 >>= \x -> Just (x * 2) >>= \y -> Just (y + 1) -- Just 11 -- Com do notation: calcular :: Maybe Int calcular = do x <- Just 5 y <- Just 10 return (x + y) -- Just 15 -- Se algum for Nothing, tudo é Nothing
O Maybe é uma monad: encadeia operações que podem falhar. Se qualquer passo devolver Nothing, o resto é ignorado e o resultado final é Nothing.
sequence e 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] -- Versões que ignoram resultados: sequence_ [putStrLn "a", putStrLn "b"] mapM_ putStrLn ["ola", "mundo"]
O sequence transforma uma lista de ações monádicas numa ação que devolve uma lista. O mapM combina map com sequence; o sufixo _ descarta os resultados.
Either como Monad
type Resultado = Either String Int
parseIdade :: String -> Resultado
parseIdade s = case reads s of
[(n, "")] -> Right n
_ -> Left "Idade invalida"
processar :: String -> Resultado
processar s = do
idade <- parseIdade s
if idade >= 0
then Right (idade * 2)
else Left "Idade negativa"O Either como monad propaga erros: um Left interrompe a cadeia e é devolvido de imediato. Só os Right continuam o processamento.
List Monad
-- Listas são monads (não-determinismo): [1,2,3] >>= \x -> [x, x*10] -- [1,10,2,20,3,30] -- do com listas = comprehension: pares = do x <- [1..5] y <- [1..5] return (x, y) -- Equivalente: [(x,y) | x <- [1..5], y <- [1..5]]
A lista é uma monad que modela computações não-determinísticas. O do sobre listas equivale a uma list comprehension, combinando todos os valores possíveis.
Avançado
Lazy evaluation
-- Expressões só são avaliadas quando preciso: caros = [ expensive x | x <- [1..1000000] ] -- Só calcula os 3 primeiros: take 3 caros -- short-circuit natural: -- (&&) não avalia o 2.º se o 1.º for False False && expensiveCheck -- False
A lazy evaluation adia o cálculo de uma expressão até o seu valor ser realmente necessário. Isto permite estruturas infinitas e evita trabalho desnecessário.
seq e strictness
-- seq força a avaliação:
-- seq :: a -> b -> b
soma x y = x `seq` x + y
-- BangPatterns (extensão):
{-# LANGUAGE BangPatterns #-}
soma' !x !y = x + y
-- foldl' é estrito (recomendado):
import Data.List (foldl')
foldl' (+) 0 [1..1000000]O seq força a avaliação de um valor antes de continuar, evitando acumular thunks. A extensão BangPatterns (!) marca argumentos como estritos.
Listas infinitas
-- Fibonacci com zipWith:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
take 10 fibs
-- [0,1,1,2,3,5,8,13,21,34]
-- Crivo de Eratóstenes (primos):
primos = crivo [2..]
where
crivo (p:xs) =
p : crivo [x | x <- xs, x `mod` p /= 0]
take 10 primos
-- [2,3,5,7,11,13,17,19,23,29]Com lazy evaluation, listas infinitas como fibs ou primos são definidas por autorreferência. O take força apenas os elementos pedidos.
Módulos e imports
module MeuModulo
( funcaoPublica
, TipoPublico(..)
) where
import Data.List (sort, nub)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe, isJust)
-- qualified exige prefixo:
Map.fromList [("a", 1)]Um module controla o que é exportado. O import seletivo traz só nomes específicos; o qualified obriga a usar um prefixo (ex.: Map.) para evitar conflitos.
Memoization
-- Fibonacci memoizado (lista infinita):
fib :: Int -> Integer
fib = (fibs !!)
where
fibs = 0 : 1 :
zipWith (+) fibs (tail fibs)
fib 100 -- instantâneo
-- 354224848179261915075A memoization guarda resultados já calculados. Definir fibs como lista infinita partilhada faz cada valor ser calculado uma única vez, tornando fib 100 imediato.
Foldable e Traversable
import qualified Data.Map as Map
import Data.Foldable (toList)
-- Foldable: reduz estruturas a um valor
m = Map.fromList [("a",1),("b",2)]
sum m -- 3 (soma os valores)
toList m -- [1,2]
-- Traversable: map com efeitos
traverse print [1,2,3] :: IO [()]
-- foldMap combina com um Monoid:
foldMap show [1,2,3] -- "123"Foldable generaliza o fold para qualquer estrutura (listas, mapas, árvores). Traversable permite mapear com efeitos, como IO ou Maybe, preservando a forma.
Data.Map e Data.Set
import qualified Data.Map as Map
import qualified Data.Set as Set
-- Map (dicionário):
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])O Data.Map é um dicionário chave-valor e o Data.Set um conjunto sem duplicados. Importa-os com qualified para evitar conflitos de nomes com o Prelude.
Exceções
import Control.Exception (try, catch, SomeException)
-- try devolve Either:
main = do
r <- try (evaluate (1 `div` 0))
:: IO (Either SomeException Int)
case r of
Left ex -> putStrLn ("Erro: " ++ show ex)
Right v -> print v
-- catch com handler:
catch (print (read "abc" :: Int))
(\(e :: SomeException) -> putStrLn "falhou")Exceções em Control.Exception lidam com erros em IO. O try converte a exceção num Either; o catch executa um handler quando ocorre um erro.
Typeclasses
Definir typeclass
-- Uma typeclass declara operações: class Descritivel a where descrever :: a -> String -- Com método por omissão: class Saudacao a where saudar :: a -> String saudar _ = "Ola!" -- default
Uma typeclass declara um conjunto de funções que vários tipos podem implementar. É semelhante a uma interface, mas baseada em tipos — define comportamento, não dados.
Read e Enum
-- Read: parse de String (inverso de Show) read "42" :: Int -- 42 read "3.14" :: Double -- 3.14 read "True" :: Bool -- True -- Enum: tipos enumeráveis ['a'..'e'] -- "abcde" fromEnum 'A' -- 65 toEnum 65 :: Char -- 'A' -- succ e pred: succ 5 -- 6 pred 5 -- 4
O Read faz parse de uma String para um valor (requer anotação de tipo). O Enum permite sequências com .. e funções succ/pred.
Instanciar typeclass
data Pessoa = Pessoa String Int
instance Descritivel Pessoa where
descrever (Pessoa n i) =
n ++ " (" ++ show i ++ ")"
-- Vários tipos, mesma interface:
data Produto = Produto String Double
instance Descritivel Produto where
descrever (Produto n p) = n ++ ": " ++ show pUma instance implementa a typeclass para um tipo concreto. Cada tipo dá a sua própria definição dos métodos declarados na classe.
Functor
-- Functor: mapeia dentro de um contexto class Functor f where fmap :: (a -> b) -> f a -> f b -- Em listas (fmap = map): fmap (*2) [1,2,3] -- [2,4,6] -- Em Maybe: fmap (*2) (Just 5) -- Just 10 fmap (*2) Nothing -- Nothing -- Operador <$> é sinónimo de fmap: (*2) <$> Just 5 -- Just 10
Um Functor é um contentor sobre o qual se pode mapear uma função com fmap. Aplica a função ao valor dentro do contexto (lista, Maybe, etc.) sem o alterar.
Constraints
-- Constraint simples: maior :: Ord a => a -> a -> a maior x y = if x > y then x else y -- Várias constraints: mostrarPar :: (Show a, Show b) => (a, b) -> String mostrarPar (x, y) = show x ++ ", " ++ show y -- Contexto de classe: class Eq a => Ordenavel a where comparar :: a -> a -> Ordering
Uma constraint (Ord a =>) restringe os tipos aceites por uma função. Exige que o tipo pertença a certa typeclass para usar as suas operações.
Applicative
-- Applicative: funções dentro do contexto -- pure coloca um valor no contexto pure 5 :: Maybe Int -- Just 5 -- <*> aplica função embrulhada: Just (+) <*> Just 3 <*> Just 4 -- Just 7 -- Combinar com <$> : (+) <$> Just 3 <*> Just 4 -- Just 7 -- Em listas: (*) <$> [1,2] <*> [10,20] -- [10,20,20,40]
Um Applicative estende o Functor: permite aplicar funções que também estão dentro de um contexto, usando pure e o operador <*>.
Show, Eq e Ord
-- Show: converter para String show 42 -- "42" show True -- "True" -- Eq: igualdade (==, /=) 5 == 5 -- True -- Ord: ordenação (<, >, compare) compare 3 5 -- LT compare 5 5 -- EQ compare 7 5 -- GT -- sort precisa de Ord: import Data.List (sort) sort [3,1,2] -- [1,2,3]
Show converte valores em String, Eq permite == e /=, e Ord acrescenta ordenação (<, >, compare).
Typeclass Monad
-- Monad: encadeia operações com contexto class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b return :: a -> m a -- Exemplo com Maybe: Just 5 >>= \x -> Just (x * 2) -- Just 10 -- return é o mesmo que pure: return 5 :: Maybe Int -- Just 5
Uma Monad permite encadear operações que devolvem valores com contexto, através do bind (>>=). O return coloca um valor puro no contexto monádico.
Ferramentas e Boas Práticas
GHC e compilação
# Compilar um executável: ghc -o programa Main.hs # Compilar com otimização: ghc -O2 -o programa Main.hs # Gerar só objectos: ghc --make Main.hs # Verificar tipos sem correr: ghc -fno-code Main.hs
O GHC é o compilador principal do Haskell. O --make compila o módulo e as suas dependências; a flag -O2 ativa otimizações de performance.
Haddock (documentação)
-- | Descrição da função (Haddock): dobro :: Int -> Int dobro x = x * 2 -- ^ argumento -- * Secção -- >>> dobro 3 (exemplo doctest) -- 6 # Gerar documentação: cabal haddock stack haddock
O Haddock gera documentação HTML a partir de comentários especiais. O -- | documenta a definição seguinte e o >>> cria exemplos testáveis com doctest.
GHCi (REPL)
# Iniciar o REPL: ghci > :l ficheiro.hs -- carregar módulo > :r -- recarregar > :t (+) -- ver tipo > :i Int -- info do tipo > :set +t -- mostrar tipos > :browse Data.List -- listar funções
O GHCi é o REPL interativo. Os comandos :t mostram o tipo, :i dão informação detalhada e :l/:r carregam e recarregam módulos.
Estrutura de projeto
meu-projeto/
app/
Main.hs -- executável
src/
MeuModulo.hs -- biblioteca
test/
Spec.hs -- testes
meu-projeto.cabal
stack.yaml
-- Módulo = ficheiro:
-- src/Data/Util.hs -> module Data.UtilUm projeto típico separa app/ (executável), src/ (biblioteca) e test/. O caminho do ficheiro define o nome do módulo: src/Data/Util.hs é module Data.Util.
Cabal
# Criar projeto: cabal init # Ficheiro .cabal define o projeto: # name, version, build-depends... # Comandos principais: cabal build -- compilar cabal run -- executar cabal test -- testes cabal repl -- GHCi do projeto cabal update -- atualizar índice
O Cabal gere projetos e dependências. O ficheiro .cabal descreve o pacote e as suas dependências em build-depends; o cabal build compila tudo.
Boas práticas e debug
-- Escreve assinaturas de tipo sempre:
dobro :: Int -> Int
-- Usa undefined como placeholder:
funcaoComplexa :: a -> b
funcaoComplexa = undefined
-- Debug com trace (Debug.Trace):
import Debug.Trace (trace)
f x = trace ("x = " ++ show x) (x * 2)
-- Testes rápidos no GHCi:
-- :t expressaoAssina sempre os tipos, usa undefined como esqueleto e recorre ao trace de Debug.Trace para imprimir valores durante o debug sem alterar o tipo da função.
Stack
# Criar projeto a partir de template: stack new meu-projeto # Comandos principais: stack build -- compilar stack run -- executar stack test -- testes stack ghci -- REPL stack exec -- programa -- correr binário
O Stack é uma alternativa ao Cabal com versões de GHC reproduzíveis, definidas no stack.yaml. Garante que todos os developers usam o mesmo toolchain.