Declaração de Tipos Algébricos e Classes de Tipos (ADTs)¶
No desenvolvimento de sistemas complexos, a capacidade de modelar o domínio do problema por meio de tipos precisos é uma das maiores vantagens do Haskell. Neste capítulo, estudaremos como estender o sistema de tipos definindo Sinônimos de Tipos, Tipos Algébricos de Dados (ADTs), Sintaxe de Registro (Records) e como criar ou estender Classes de Tipos (Typeclasses).
1. Sinônimos de Tipos: Criando Apelidos (type)¶
A forma mais simples de definir um novo tipo em Haskell é por meio de um Sinônimo de Tipo utilizando a palavra-chave type. Um sinônimo funciona apenas como um apelido legível para um tipo existente, facilitando a documentação e legibilidade do código:
Os sinônimos de tipo podem aceitar parâmetros de tipo, funcionando de forma genérica:
Warning
Como os sinônimos de tipo são apenas "apelidos", o compilador não impede que você passe um par comum (Double, Double) para uma função que espera uma Coordenada. Para obter isolamento completo e segurança de tipos em tempo de compilação, devemos utilizar newtype ou data.
Além disso, sinônimos de tipo não podem ser recursivos (por exemplo, tentar declarar
type Arvore = (Int, [Arvore])resultará em erro).
2. Tipos Novos com Isolamento: newtype¶
Quando queremos um tipo distinto (não apenas um apelido) que envolve exatamente um construtor com um único campo, usamos newtype:
Diferentemente de type, o compilador trata Dolar e Float como tipos incompatíveis — impossível passar um Float cru onde se espera Dolar. Diferentemente de data, o newtype não tem custo em tempo de execução: o "envelope" existe apenas em tempo de compilação e é removido pelo compilador. É a ferramenta ideal para dar segurança de tipos a valores primitivos (IDs, moedas, unidades de medida).
3. Tipos Algébricos de Dados (ADTs) (data)¶
Para criar tipos de dados inteiramente novos com seus próprios construtores de valor, utilizamos a palavra-chave data. Um Tipo Algébrico de Dados é composto por uma combinação de dois padrões matemáticos: Tipos Soma (alternativas) e Tipos Produto (combinações).
┌────────────────────────┐
│ Tipos Algébricos │
│ de Dados │
└──────────┬─────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
[Tipo Soma] [Tipo Produto]
(Alternativas) (Combinações)
Vermelho | Verde | Azul Circulo Float | Retangulo Float Float
1. Tipos Soma (Enumerações)¶
Representam valores que podem assumir uma entre várias alternativas distintas:
Aqui, Vermelho, Verde, Sim e Nao são chamados de Construtores de Valor. Eles funcionam como constantes que representam os dados concretos do tipo.
2. Tipos Produto¶
Representam valores compostos por múltiplos campos de dados combinados:
O tipo Forma possui três construtores de valor. O construtor Circulo carrega um Float (o raio). O construtor Retangulo carrega dois valores Float (largura e altura). Podemos usar casamento de padrões para inspecionar e extrair esses valores:
area :: Forma -> Float
area (Circulo r) = pi * r ^ 2
area (Retangulo l a) = l * a
area (Quadrado l) = l ^ 2
Analogia com Outras Linguagens¶
Os ADTs unificam, em um único mecanismo, várias construções que outras linguagens oferecem separadamente:
structdo C / classe de dados: um ADT com um único construtor e vários campos agrupa valores relacionados — a diferença é que os campos em Haskell são posicionais e anônimos (a menos que usemos a sintaxe de registro, a seguir).enumdo C/Java: um tipo soma de construtores sem argumentos (data Cor = Vermelho | Verde | Azul) representa valores simbólicos — mas, ao contrário do C, os construtores não são inteiros disfarçados, e o compilador impede misturá-los com números.uniondo C: um tipo soma com argumentos é uma "união disjunta" segura: o valor lembra qual construtor o criou, e o pattern matching nos obriga a tratar cada caso — em C, é o programador quem precisa rastrear manualmente qual alternativa a união contém, uma fonte clássica de bugs.
ADTs vs. Tuplas¶
Dois pares (String, String) são sempre do mesmo tipo, mesmo que um represente um animal e outro um móvel — o sistema de tipos não pode nos proteger da mistura. Já dois ADTs estruturalmente idênticos com nomes diferentes são tipos distintos:
Regra prática: se você usa um valor composto amplamente no código, declare um data — ganha segurança de tipos e legibilidade; para usos pequenos e localizados, uma tupla resolve.
4. Sintaxe de Registro (Record Syntax)¶
Quando definimos tipos produto com muitos campos, acessar os valores por posição em casamento de padrões torna-se confuso e propenso a erros. Para resolver isso, Haskell fornece a Sintaxe de Registro (Record Syntax), que permite nomear os campos:
Vantagens da Sintaxe de Registro:¶
- Funções Seletoras Automáticas: O compilador cria automaticamente funções com o nome de cada campo para extrair o valor de um registro. Ex:
nome meuUsuarioretorna aStringdo nome. - Atualização Prática de Registros: Podemos atualizar campos facilmente por meio de uma sintaxe de cópia imutável:
5. Tipos Parametrizados e o Maybe¶
Assim como as listas, nossos próprios tipos podem ser polimórficos: basta introduzir variáveis de tipo na declaração. O exemplo mais importante do Prelude é o tipo Maybe, que representa um valor que pode estar presente ou ausente:
A variável a indica que Maybe recebe outro tipo como parâmetro: Maybe Int, Maybe String, Maybe [Bool] são tipos distintos construídos a partir dele.
Tratando Erros: error vs. Maybe¶
Haskell oferece a função error :: String -> a, que aborta a execução imediatamente com uma mensagem. O problema: quem chama a função não consegue distinguir um erro recuperável de uma falha fatal. Compare:
-- Versão que "explode":
meuSegundo :: [a] -> a
meuSegundo xs = if null (tail xs)
then error "lista muito curta"
else head (tail xs)
-- Versão total e segura com Maybe:
segundoSeguro :: [a] -> Maybe a
segundoSeguro (_:x:_) = Just x
segundoSeguro _ = Nothing
Na versão com Maybe, devolvemos Nothing quando a lista é curta demais — e o chamador decide o que fazer, em vez de o programa cair. Note também como o pattern matching (_:x:_) torna a versão segura mais concisa: o padrão casa apenas com listas de pelo menos dois elementos, ligando x ao segundo.
6. Tipos Recursivos: Modelando Estruturas de Dados¶
Os tipos declarados com data podem ser recursivos, ou seja, conter referências a si mesmos em seus campos. Isso nos permite modelar estruturas de dados complexas, como árvores binárias:
Uma árvore binária ou é um nó com um valor e dois filhos (que também são árvores binárias), ou é uma folha vazia. Em Java, a classe equivalente usaria null para indicar a ausência de um filho; em Haskell não existe null — usamos explicitamente o construtor Folha, e o compilador nos obriga a tratá-lo no pattern matching. Toda uma classe de erros (NullPointerException) simplesmente não existe.
Podemos processar essa árvore utilizando recursão sobre sua estrutura:
7. Implementando Classes de Tipos Personalizadas¶
Vimos na Unidade 1 que classes de tipos (como Eq e Ord) governam comportamentos comuns. Haskell nos permite declarar novas classes de tipos por meio da palavra-chave class:
Aqui, declaramos uma classe Serializavel que exige que qualquer tipo participante implemente a função serializar.
Declarando Instâncias (instance)¶
Para fazer um tipo de dados fazer parte de uma classe de tipos, escrevemos uma declaração de instance:
instance Serializavel Cor where
serializar Vermelho = "#FF0000"
serializar Verde = "#00FF00"
serializar Azul = "#0000FF"
serializar Amarelo = "#FFFF00"
Derivação Automática (deriving)¶
Para classes de tipos muito comuns da biblioteca padrão (Show, Read, Eq, Ord), o compilador do Haskell é inteligente o bastante para gerar as implementações de instância automaticamente. Basta adicionar a cláusula deriving no final da declaração do tipo:
No próximo capítulo, veremos como utilizar o encapsulamento de tipos algébricos para lidar com a manipulação de Entrada/Saída de forma segura através da Monad de IO.
Nota de atribuição: partes deste capítulo adaptam material de Real World Haskell, de Bryan O'Sullivan, Don Stewart e John Goerzen (book.realworldhaskell.org), sob a licença Creative Commons Attribution-Noncommercial 3.0.