Tipos e Typeclasses
Acredite no tipo (Believe the type)

Anteriormente, mencionamos que o Haskell possui um sistema de tipos estático. O tipo de cada expressão é conhecido em tempo de compilação, o que leva a um código mais seguro. Se você escrever um programa onde tenta dividir um tipo booleano por algum número, ele nem compilará. Isso é bom porque é melhor detectar esses erros em tempo de compilação do que ter seu programa travando. Tudo em Haskell tem um tipo, então o compilador pode raciocinar bastante sobre seu programa antes de compilá-lo.
Ao contrário de Java ou Pascal, Haskell possui inferência de tipo. Se escrevermos um número, não precisamos dizer a Haskell que é um número. Ele pode inferir isso por conta própria, portanto, não precisamos escrever explicitamente os tipos de nossas funções e expressões para fazer as coisas. Cobrimos alguns dos fundamentos do Haskell com apenas uma visão muito superficial dos tipos. No entanto, entender o sistema de tipos é uma parte muito importante do aprendizado de Haskell.
Um tipo é uma espécie de rótulo que toda expressão possui. Ele nos
diz em qual categoria de coisas essa expressão se encaixa. A expressão
True é um booleano, "hello" é uma string,
etc.
Agora usaremos o GHCI para examinar os tipos de algumas expressões.
Faremos isso usando o comando :t que, seguido por qualquer
expressão válida, nos diz seu tipo. Vamos dar uma volta.
ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t "HELLO!"
"HELLO!" :: [Char]
ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)
ghci> :t 4 == 5
4 == 5 :: Bool
Aqui vemos que fazer
:t em uma expressão imprime a expressão seguida por
:: e seu tipo. :: é lido como “tem o tipo de”.
Tipos explícitos são sempre denotados com a primeira letra em maiúscula.
'a', ao que parece, tem um tipo de Char. Não é
difícil concluir que significa caractere. True é
do tipo Bool. Isso faz sentido. Mas o que é isso? Examinar
o tipo de "HELLO!" produz um [Char]. Os
colchetes denotam uma lista. Então lemos isso como sendo uma lista
de caracteres. Ao contrário das listas, cada tamanho de tupla tem
seu próprio tipo. Portanto, a expressão (True, 'a') tem um
tipo de (Bool, Char), enquanto uma expressão como
('a', 'b', 'c') teria o tipo de
(Char, Char, Char). 4 == 5 sempre retornará
False, então seu tipo é Bool.
Funções também têm tipos. Ao escrever nossas próprias funções, podemos optar por dar-lhes uma declaração de tipo explícita. Isso geralmente é considerado uma boa prática, exceto ao escrever funções muito curtas. A partir daqui, daremos a todas as funções que fizermos declarações de tipo. Lembra da compreensão de lista que fizemos anteriormente que filtra uma string para que apenas as maiúsculas permaneçam? Veja como fica com uma declaração de tipo.
removeNonUppercase :: [Char] -> [Char]
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]
removeNonUppercase tem um tipo de
[Char] -> [Char], o que significa que mapeia de uma
string para uma string. Isso ocorre porque recebe uma string como
parâmetro e retorna outra como resultado. O tipo [Char] é
sinônimo de String, então é mais claro se escrevermos
removeNonUppercase :: String -> String. Não precisávamos
dar a essa função uma declaração de tipo porque o compilador pode
inferir por si mesmo que é uma função de uma string para uma string, mas
fizemos assim mesmo. Mas como escrevemos o tipo de uma função que recebe
vários parâmetros? Aqui está uma função simples que pega três inteiros e
os soma:
addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z
Os parâmetros são separados por -> e não há distinção
especial entre os parâmetros e o tipo de retorno. O tipo de retorno é o
último item na declaração e os parâmetros são os três primeiros. Mais
tarde, veremos por que todos eles são apenas separados por
-> em vez de ter alguma distinção mais explícita entre
os tipos de retorno e os parâmetros, como
Int, Int, Int -> Int ou algo assim.
Se você quiser dar à sua função uma declaração de tipo, mas não tiver
certeza de qual deve ser, sempre poderá apenas escrever a função sem ela
e depois verificá-la com :t. Funções são expressões também,
então :t funciona nelas sem problemas.
Aqui está uma visão geral de alguns tipos comuns.
Int significa inteiro. É usado para
números inteiros. 7 pode ser um Int, mas
7.2 não pode.
Acredite no tipo
Int é limitado, o que significa que tem um valor mínimo
e um máximo. Geralmente em máquinas de 32 bits o máximo Int
possível é 2147483647 e o mínimo é -2147483648.
Integer significa, er… também
inteiro. A principal diferença é que não é limitado, então pode ser
usado para representar números realmente muito grandes. Quero dizer,
muito grandes mesmo. Int, no entanto, é mais eficiente.
factorial :: Integer -> Integer
factorial n = product [1..n]
ghci> factorial 50
30414093201713378043612608166064768844377641568960512000000000000
Float é um ponto flutuante real com
precisão simples.
circumference :: Float -> Float
circumference r = 2 * pi * r
ghci> circumference 4.0
25.132742
Double é um ponto flutuante real com
o dobro da precisão!
circumference' :: Double -> Double
circumference' r = 2 * pi * r
ghci> circumference' 4.0
25.132741228718345
Bool é um tipo booleano. Pode ter
apenas dois valores: True e False.
Char representa um caractere. É
denotado por aspas simples. Uma lista de caracteres é uma string.
Tuplas são tipos, mas dependem de seu comprimento, bem como dos tipos
de seus componentes, portanto, teoricamente, há um número infinito de
tipos de tuplas, o que é demais para cobrir neste tutorial. Observe que
a tupla vazia () também é um tipo que
pode ter apenas um único valor: ()
Variáveis de tipo (Type variables)
Qual você acha que é o tipo da função head? Como
head pega uma lista de qualquer tipo e retorna o primeiro
elemento, o que poderia ser? Vamos verificar!
ghci> :t head
head :: [a] -> a
Hmmm! O que é esse a?
É um tipo? Lembre-se de que declaramos anteriormente que os tipos são
escritos em letras maiúsculas, portanto, não pode ser exatamente um
tipo. Como não está em maiúsculas, na verdade é uma variável de
tipo. Isso significa que a pode ser de qualquer
tipo. Isso é muito parecido com genéricos em outras linguagens, só que
em Haskell é muito mais poderoso porque nos permite escrever facilmente
funções muito gerais se elas não usarem nenhum comportamento específico
dos tipos nelas. Funções que possuem variáveis de tipo são chamadas de
funções polimórficas. A declaração de tipo de
head afirma que ela recebe uma lista de qualquer tipo e
retorna um elemento desse tipo.
Embora as variáveis de tipo possam ter nomes com mais de um caractere, geralmente damos a elas nomes como a, b, c, d …
Lembra de fst? Retorna o primeiro componente de um par.
Vamos examinar seu tipo.
ghci> :t fst
fst :: (a, b) -> a
Vemos que fst recebe uma tupla que contém dois tipos e
retorna um elemento que é do mesmo tipo que o primeiro componente do
par. É por isso que podemos usar fst em um par que contém
quaisquer dois tipos. Observe que apenas porque a e
b são variáveis de tipo diferentes, eles não precisam ser
tipos diferentes. Apenas afirma que o tipo do primeiro componente e o
tipo do valor de retorno são os mesmos.
Typeclasses 101

Uma typeclass é uma espécie de interface que define algum comportamento. Se um tipo faz parte de uma typeclass, isso significa que ele suporta e implementa o comportamento que a typeclass descreve. Muitas pessoas vindas de OOP (Programação Orientada a Objetos) ficam confusas com typeclasses porque acham que são como classes em linguagens orientadas a objetos. Bem, elas não são. Você pode pensar nelas como interfaces Java, só que melhores.
Qual é a assinatura de tipo da função ==?
ghci> :t (==)
(==) :: (Eq a) => a -> a -> Bool
Nota: o operador de igualdade, == é uma
função. Assim como +, *, -,
/ e praticamente todos os operadores. Se o nome de uma
função for composto apenas por caracteres especiais, ela é considerada
uma função infixa por padrão. Se quisermos examinar seu tipo, passá-la
para outra função ou chamá-la como uma função prefixa, temos que
cercá-la entre parênteses.
Interessante. Vemos uma coisa nova aqui, o símbolo
=>. Tudo antes do símbolo => é chamado
de restrição de classe (class constraint). Podemos ler
a declaração de tipo anterior assim: a função de igualdade recebe
quaisquer dois valores que sejam do mesmo tipo e retorna um
Bool. O tipo desses dois valores deve ser um membro da
classe Eq (esta era a restrição de classe).
A typeclass Eq fornece uma interface para testar a
igualdade. Qualquer tipo em que faça sentido testar a igualdade entre
dois valores desse tipo deve ser um membro da classe Eq.
Todos os tipos padrão de Haskell, exceto IO (o tipo para lidar com
entrada e saída) e funções, fazem parte da typeclass
Eq.
A função elem tem um tipo de
(Eq a) => a -> [a] -> Bool porque usa
== sobre uma lista para verificar se algum valor que
estamos procurando está nela.
Algumas typeclasses básicas:
Eq é usado para tipos que suportam
teste de igualdade. As funções que seus membros implementam são
== e /=. Portanto, se houver uma restrição de
classe Eq para uma variável de tipo em uma função, ela usa
== ou /= em algum lugar dentro de sua
definição. Todos os tipos que mencionamos anteriormente, exceto funções,
fazem parte de Eq, então eles podem ser testados quanto à
igualdade.
ghci> 5 == 5
True
ghci> 5 /= 5
False
ghci> 'a' == 'a'
True
ghci> "Ho Ho" == "Ho Ho"
True
ghci> 3.432 == 3.432
True
Ord é para tipos que têm uma
ordenação.
ghci> :t (>)
(>) :: (Ord a) => a -> a -> Bool
Todos os tipos que cobrimos até agora, exceto funções, fazem parte de
Ord. Ord cobre todas as funções de comparação
padrão, como >, <, >= e
<=. A função compare pega dois membros
Ord do mesmo tipo e retorna uma ordenação. Ordering é um tipo que pode ser
GT, LT ou EQ, significando
maior que (greater than), menor que (lesser than) e
igual (equal), respectivamente.
Para ser um membro de Ord, um tipo deve primeiro ter
associação no prestigioso e exclusivo clube Eq.
ghci> "Abrakadabra" < "Zebra"
True
ghci> "Abrakadabra" `compare` "Zebra"
LT
ghci> 5 >= 2
True
ghci> 5 `compare` 3
GT
Membros de Show podem ser
apresentados como strings. Todos os tipos cobertos até agora, exceto
funções, fazem parte de Show. A função mais usada que lida
com a typeclass Show é show. Ela pega um valor
cujo tipo é um membro de Show e o apresenta para nós como
uma string.
ghci> show 3
"3"
ghci> show 5.334
"5.334"
ghci> show True
"True"
Read é uma espécie de typeclass
oposta a Show. A função read pega uma string e
retorna um tipo que é membro de Read.
ghci> read "True" || False
True
ghci> read "8.2" + 3.8
12.0
ghci> read "5" - 2
3
ghci> read "[1,2,3,4]" ++ [3]
[1,2,3,4,3]
Até aqui tudo bem. Novamente, todos os tipos cobertos até agora estão
nesta typeclass. Mas o que acontece se tentarmos fazer apenas
read "4"?
ghci> read "4"
<interactive>:1:0:
Ambiguous type variable `a' in the constraint:
`Read a' arising from a use of `read' at <interactive>:1:0-7
Probable fix: add a type signature that fixes these type variable(s)
O que o GHCI está nos dizendo aqui é que ele não sabe o que queremos
em troca. Observe que nos usos anteriores de read fizemos
algo com o resultado depois. Dessa forma, o GHCI poderia inferir que
tipo de resultado queríamos de nosso read. Se o usássemos
como booleano, ele sabia que tinha que retornar um
Bool.
Mas agora, ele sabe que queremos algum tipo que faça parte da classe
Read, apenas não sabe qual. Vamos dar uma olhada na
assinatura de tipo de read.
ghci> :t read
read :: (Read a) => String -> a
Viu? Ele retorna um tipo que faz parte de Read, mas se
não tentarmos usá-lo de alguma forma mais tarde, ele não tem como saber
qual tipo. É por isso que podemos usar anotações de
tipo explícitas. Anotações de tipo são uma maneira de dizer
explicitamente qual deve ser o tipo de uma expressão. Fazemos isso
adicionando :: no final da expressão e depois especificando
um tipo. Observe:
ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> (read "5" :: Float) * 4
20.0
ghci> read "[1,2,3,4]" :: [Int]
[1,2,3,4]
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')
A maioria das expressões é tal que o compilador pode inferir qual é o
seu tipo por si mesmo. Mas às vezes, o compilador não sabe se deve
retornar um valor do tipo Int ou Float para
uma expressão como read "5". Para ver qual é o tipo,
Haskell teria que realmente avaliar read "5". Mas como
Haskell é uma linguagem estaticamente tipada, ele precisa saber todos os
tipos antes que o código seja compilado (ou no caso do GHCI, avaliado).
Então temos que dizer a Haskell: “Ei, essa expressão deve ter esse tipo,
caso você não saiba!”.
Membros de Enum são tipos ordenados
sequencialmente — eles podem ser enumerados. A principal vantagem da
typeclass Enum é que podemos usar seus tipos em intervalos
de lista. Eles também têm sucessores e antecessores definidos, que você
pode obter com as funções succ e pred. Tipos
nesta classe: (), Bool, Char,
Ordering, Int, Integer,
Float e Double.
ghci> ['a'..'e']
"abcde"
ghci> [LT .. GT]
[LT,EQ,GT]
ghci> [3 .. 5]
[3,4,5]
ghci> succ 'B'
'C'
Membros de Bounded têm um limite
superior e um inferior.
ghci> minBound :: Int
-2147483648
ghci> maxBound :: Char
'\1114111'
ghci> maxBound :: Bool
True
ghci> minBound :: Bool
False
minBound e maxBound são interessantes
porque têm um tipo de (Bounded a) => a. Em certo
sentido, são constantes polimórficas.
Todas as tuplas também fazem parte de Bounded se os
componentes também estiverem nela.
ghci> maxBound :: (Bool, Int, Char)
(True,2147483647,'\1114111')
Num é uma typeclass numérica. Seus
membros têm a propriedade de poder agir como números. Vamos examinar o
tipo de um número.
ghci> :t 20
20 :: (Num t) => t
Parece que números inteiros também são constantes polimórficas. Eles
podem agir como qualquer tipo que seja um membro da typeclass
Num.
ghci> 20 :: Int
20
ghci> 20 :: Integer
20
ghci> 20 :: Float
20.0
ghci> 20 :: Double
20.0
Esses são tipos que estão na typeclass Num. Se
examinarmos o tipo de *, veremos que ele aceita todos os
números.
ghci> :t (*)
(*) :: (Num a) => a -> a -> a
Ele pega dois números do mesmo tipo e retorna um número desse tipo. É
por isso que (5 :: Int) * (6 :: Integer) resultará em um
erro de tipo, enquanto 5 * (6 :: Integer) funcionará bem e
produzirá um Integer porque 5 pode agir como
um Integer ou um Int.
Para ingressar em Num, um tipo já deve ser amigo de
Show e Eq.
Integral também é uma typeclass
numérica. Num inclui todos os números, incluindo números
reais e números inteiros, Integral inclui apenas números
inteiros (whole). Nesta typeclass estão Int e
Integer.
Floating inclui apenas números de
ponto flutuante, então Float e Double.
Uma função muito útil para lidar com números é fromIntegral. Ela tem uma declaração de
tipo de
fromIntegral :: (Num b, Integral a) => a -> b. Pela
sua assinatura de tipo, vemos que ela pega um número integral e o
transforma em um número mais geral. Isso é útil quando você quer que
tipos inteiros e de ponto flutuante funcionem bem juntos. Por exemplo, a
função length tem uma declaração de tipo de
length :: [a] -> Int em vez de ter um tipo mais geral de
(Num b) => length :: [a] -> b. Se tentarmos obter o
comprimento de uma lista e depois adicioná-lo a 3.2,
obteremos um erro porque tentamos adicionar um Int e um
número de ponto flutuante. Portanto, para contornar isso, fazemos
fromIntegral (length [1,2,3,4]) + 3.2 e tudo funciona.
Observe que fromIntegral possui várias restrições de
classe em sua assinatura de tipo. Isso é completamente válido e, como
você pode ver, as restrições de classe são separadas por vírgulas dentro
dos parênteses.