Introdução a Haskell
Published:
Tipos Algebraicos de Dados
Até então utilizamos os tipos de dados disponíveis na linguagem Haskell. Porém, uma linguagem de programação deve ser flexível o bastante para permitir a criação de novos tipos de dados. O Haskell possui uma das mais poderosas formas de criação de tipos denominada Tipos Algebraicos de Dados.
Os Tipos Algebraicos de Dados, ou ADT, são baseados na teoria de tipos algébricos de tal forma a permitir expressividade e flexibilidade nas definições.
Como primeiro exemplo, vamos verificar a definição do tipo Bool
:
data Bool = False | True
A palavra-chave data
indica que estamos criando um novo tipo de dados, nesse caso com o nome Bool
. Após o operador =
definimos que Bool
é False
ou (|
) True
.
O tipo Bool
é um tipo soma, pois seus possíveis valores é a soma de suas definições. Além do tipo soma, podemos definir algo com o tipo produto:
data Ponto = Ponto Double Double
Nesse caso, Ponto
é definido pela combinação de dois valores do tipo Double
, representando as coordenadas de um ponto.
Os tipos True
e False
são tipos unitários, ou seja, só assumem um único valor possível (eles mesmos), portanto a definição de Bool
permite 2 possibilidades de valor (1 + 1). No entanto, o tipo Ponto
permite a combinação de todos os valores possívels de Double
com ele mesmo, ou seja, admite Double * Double
possíveis valores.
Um outro exemplo para entender melhor o conceito de produto:
data TabVerdade = TabVerdade Bool Bool
Nesse caso, o tipo TabVerdade
pode assumir 4 valores possíveis (2 * 2): (True, True), (True, False), (False, True), (False, False)
.
Reparem que no caso dos tipos produto repetimos o nome do tipo após o operador =
. Na verdade tal nome não precisa necessariamente ser o mesmo, ele é um criador do tipo:
data Ponto = Ponto Double Double
let x = Ponto 23.4 5.2 -- x = Ponto 23.4 5.2
Da mesma forma, poderíamos fazer:
data Ponto = P Double Double
let x = P 23.4 5.2 -- x = P 23.4 5.2
Nesse momento o tipo definido acima não permite o uso da função print
para mostrar o seu valor para o usuário. Para permitir a impressão dos valores utilizamos a palavra-chave deriving
:
data Ponto = Ponto Double Double deriving (Show)
let x = Ponto 23.4 5.2
print x -- Ponto 23.4 5.2
A palavra-chave deriving
indica ao compilador que ele deve deduzir certas propriedades para o novo tipo de dado. Além de Show
o compilador pode também deduzir Read
, ou a capacidade de ler o valor a partir de uma String
, Eq, Ord
, para as relações de igualdade e ordenação, e Enum
, que permite a enumeração dos tipos soma.
Exercício 01: Crie um ADT aditivo chamado Formas
contendo ADTs que representam as formas circunferência como Circ
, retângulo como Ret
e triângulo como Tri
, através das coordenadas que as definem. Utilize a ADT Ponto
definida anteriormente.
area :: Formas -> Double
area (Circ center r) = pi*r*r
area (Ret p1 p2) = dist p1 p2
where
dist (Point x1 y1) (Point x2 y2) = abs (x1-x2) + abs (y1-y2)
area (Tri p1 p2 p3) = base * altura / 2.0
where
base = dist p1 p2
altura = 2 * sqrt (s * (s-a) * (s-b) * (s-c)) / a
s = (a+b+c)/2.0
a = base
b = dist p2 p3
c = dist p1 p3
dist (Point x1 y1) (Point x2 y2) = sqrt $ (x1-x2)**2 + (y1-y2)**2
main = do
let
circ = Circ (Point 0 0) 10.0
quad = Ret (Point 2 2) (Point 4 4)
tri = Tri (Point 0 0) (Point 0 3) (Point 4 0)
print (area circ) -- 315.16
print (area quad) -- 4
print (area tri) -- 6
Em algumas ocasiões queremos utilizar um tipo que já existe mas com outro nome em outro contexto. Por exemplo, uma possível definição de circunferência poderia ser:
data Circ = Circ Point Double
em que o ponto representa o seu centro. Podemos criar o tipo Center
sem a necessidade de criar um novo tipo, apenas apelidando o tipo ponto:
type Center = Point
data Circ = Circ Center Double
Uma variação do tipo aditivo são aqueles com a propriedade enumerativa:
data Dias = Seg | Ter | Qua | Qui | Sex | Sab | Dom
deriving (Show, Enum)
O tipo enumerativo permite o uso das funções succ
para determinar o sucessor e pred
para determinar o predecessor:
main = do
print (succ Seg) -- Ter
print (pred Ter) -- Seg
Para o caso de necessitarmos de uma enumeração circular, podemos criar as funções succ'
e pred'
como:
succ' Dom = Seg
succ' x = succ x
pred' Seg = Dom
pred' x = pred x
Exercício 02: Implemente o jogo de Pedra, Papel e Tesoura utilizando um tipo enumerativo circular.
Um outro tipo interessante no Haskell é o Maybe
definido por:
data Maybe a = Nothing | Just a
Ou seja, um valor do tipo Maybe, pode ser Nada ou Apenas o próprio tipo a
. Esse tipo pode ser utilizado para realizar operações seguras quando podem ocorrer exceções:
safeDiv :: Double -> Double -> Maybe Double
safeDiv x y
| y==0 = Nothing
| otherwise = Just (x / y)
media l = case safeDiv (sum l) (fromIntegral $ length l) of
Nothing -> 1/0 -- infinity
Just m -> m
main = do
print (media [1.0, 2.0])
print (media [])
Note também que a definição Maybe a
permite que esse tipo seja aplicado a um outro tipo qualquer, ou seja, podemos ter Maybe Double
, Maybe String
, etc.
Finalmente, vamos analisar a definição do tipo lista:
data List x = Nil | Cons x (List x)
Essa é uma definição recursiva. Ou seja, uma lista pode ser definida como Nula ou como a composição de um valor do tipo x com um valor do próprio tipo lista.
A lista [1, 2, 3]
é representada internamente como:
Cons 1 (Cons 2 (Cons 3 (Nil)))