DevTools

Cheatsheet Haskell

Linguagem funcional pura com tipos fortes e lazy evaluation

Voltar às linguagens
Haskell
70 cards encontrados
Categorias:
Versões:

Básico e Tipos


10 cards
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 * 2

O 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


10 cards
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


9 cards
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) xs

A 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


9 cards
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


9 cards
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 String

O 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


8 cards
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
-- 354224848179261915075

A 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


8 cards
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 p

Uma 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


7 cards
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.Util

Um 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 expressao

Assina 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.