Introdução ao sistema de tipos do Haskell para desenvolvedores práticos

1. Por que o sistema de tipos do Haskell é diferente?

Desenvolvedores vindos de linguagens como JavaScript, Python ou Ruby frequentemente estranham o sistema de tipos do Haskell. A diferença fundamental está na combinação de tipagem estática forte com inferência de tipos.

Na prática, tipagem estática significa que todo erro de tipo é detectado em tempo de compilação, não em produção. Tipagem forte significa que não há coerções implícitas — você não pode somar um inteiro com uma string acidentalmente.

-- Isso não compila:
-- "idade: " ++ 30  -- Erro: não pode concatenar String com Int

-- Isso funciona:
"idade: " ++ show 30  -- resultado: "idade: 30"

O GHCi (Glasgow Haskell Compiler interactive) é seu melhor amigo para explorar tipos:

Prelude> :t 42
42 :: Num a => a

Prelude> :t "hello"
"hello" :: String

2. Tipos básicos e assinaturas de função

Haskell oferece tipos primitivos que você já conhece, mas com algumas particularidades:

-- Int: inteiro com tamanho fixo (depende da arquitetura)
-- Integer: inteiro de precisão arbitrária (sem overflow)
-- Float: ponto flutuante de precisão simples
-- Double: ponto flutuante de precisão dupla
-- Bool: True ou False
-- Char: um caractere Unicode

-- Assinatura explícita de função
somaQuadrados :: Int -> Int -> Int
somaQuadrados x y = x*x + y*y

-- Typeclasses básicas
-- Eq: tipos que podem ser comparados (==, /=)
-- Ord: tipos que podem ser ordenados (<, >, <=, >=)
-- Show: tipos que podem ser convertidos para String
-- Read: tipos que podem ser convertidos de String

exemploEq :: Int -> Int -> Bool
exemploEq a b = a == b  -- Int implementa Eq

3. Polimorfismo paramétrico: funções que funcionam para qualquer tipo

Diferente de herança em OOP, o polimorfismo paramétrico permite que uma função opere em qualquer tipo sem conhecer sua estrutura interna:

-- length funciona para qualquer lista
length :: [a] -> Int

-- head retorna o primeiro elemento de qualquer lista
head :: [a] -> a

-- map aplica uma função a cada elemento
map :: (a -> b) -> [a] -> [b]

-- Exemplo prático
primeiros :: [a] -> [a]
primeiros lista = take 2 lista

-- Uso:
-- primeiros [1,2,3,4]  -> [1,2]
-- primeiros ["a","b","c"] -> ["a","b"]

A variável de tipo a representa "qualquer tipo". Isso garante que a função não pode fazer suposições sobre o tipo concreto, prevenindo erros.

4. Typeclasses: o coração do polimorfismo ad-hoc

Typeclasses resolvem um problema prático: como escrever uma função que funciona para vários tipos, mas com comportamento específico para cada um?

-- Definindo uma typeclass para serialização JSON
class JSONSerializable a where
    toJSON :: a -> String
    fromJSON :: String -> Maybe a

-- Instância para Int
instance JSONSerializable Int where
    toJSON n = show n
    fromJSON s = case reads s of
        [(n, "")] -> Just n
        _ -> Nothing

-- Instância para Bool
instance JSONSerializable Bool where
    toJSON True = "true"
    toJSON False = "false"
    fromJSON "true" = Just True
    fromJSON "false" = Just False
    fromJSON _ = Nothing

-- Função polimórfica com restrição de tipo
serializarLista :: JSONSerializable a => [a] -> String
serializarLista lista = "[" ++ intercalate "," (map toJSON lista) ++ "]"

5. Tipos algébricos de dados (ADTs) na prática

ADTs combinam tipos soma (alternativas) e produto (combinações) de forma elegante:

-- Modelando um sistema de pedidos
data StatusPedido = Pendente | Processando | Enviado | Entregue
    deriving (Show, Eq)

data Item = Item {
    nome :: String,
    quantidade :: Int,
    precoUnitario :: Double
} deriving (Show)

data Pedido = Pedido {
    id :: Int,
    itens :: [Item],
    status :: StatusPedido
} deriving (Show)

-- Pattern matching para controle de fluxo
podeCancelar :: Pedido -> Bool
podeCancelar pedido = case status pedido of
    Pendente -> True
    Processando -> True
    _ -> False

-- Árvore binária de busca
data Arvore a = Vazia | No a (Arvore a) (Arvore a)
    deriving (Show)

inserir :: Ord a => a -> Arvore a -> Arvore a
inserir valor Vazia = No valor Vazia Vazia
inserir valor (No raiz esq dir)
    | valor < raiz = No raiz (inserir valor esq) dir
    | valor > raiz = No raiz esq (inserir valor dir)
    | otherwise = No raiz esq dir

6. Monads para desenvolvedores pragmáticos

Monads resolvem um problema concreto: como compor funções que produzem efeitos colaterais de forma segura?

-- Maybe: tratamento de erros sem exceções
dividir :: Double -> Double -> Maybe Double
dividir _ 0 = Nothing
dividir x y = Just (x / y)

-- Composição segura
calcularMedia :: [Double] -> Maybe Double
calcularMedia [] = Nothing
calcularMedia nums = 
    let soma = sum nums
        total = fromIntegral (length nums)
    in dividir soma total

-- Either: tratamento de erros com contexto
type Erro = String

parseInt :: String -> Either Erro Int
parseInt s = case reads s of
    [(n, "")] -> Right n
    _ -> Left $ "Não foi possível converter: " ++ s

-- IO monad: entrada/saída segura
main :: IO ()
main = do
    putStrLn "Digite seu nome:"
    nome <- getLine
    putStrLn $ "Olá, " ++ nome ++ "!"

7. Dicas para sobreviver e prosperar com tipos

Erros comuns e como interpretá-los:

-- Erro: "No instance for (Num [Char])"
-- Significa: você tentou usar um operador numérico com String
-- "abc" + "def"  -- Erro!

-- Erro: "Couldn't match expected type 'Int' with actual type 'String'"
-- Significa: você passou o tipo errado para uma função
-- length 42  -- Erro! length espera uma lista

Ferramentas essenciais:

-- No GHCi:
-- :t expressão  -> mostra o tipo
-- :i Tipo       -> mostra informações sobre um tipo/typeclass
-- :info         -> informações detalhadas

-- Hoogle: buscador de tipos Haskell (hoogle.haskell.org)
-- Busque por: (a -> b) -> [a] -> [b]  -> encontra map

O sistema de tipos acelera o desenvolvimento porque:

  1. Refatoração segura: mude um tipo e o compilador aponta todos os locais afetados
  2. Documentação viva: assinaturas de tipo são documentação executável
  3. Menos testes unitários: o compilador garante a consistência de tipos
  4. Design guiado por tipos: comece pelas assinaturas e deixe os tipos guiarem a implementação
-- Exemplo: refatorando uma função
-- Versão original
somaLista :: [Int] -> Int
somaLista = sum

-- Versão genérica (refatoração segura)
somaLista :: Num a => [a] -> a
somaLista = sum
-- O compilador garante que tudo continua funcionando

O sistema de tipos do Haskell não é apenas uma ferramenta de verificação — é uma linguagem de design que permite expressar intenções de forma precisa e segura. Dominá-lo transforma a maneira como você pensa sobre programação.

Referências