Introdução a Haskell

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)))