Types e Typeclasses
Acredite no type
Já falamos que Haskell possui um sistema de tipos estático. O tipo de toda expressão é conhecido na hora da compilação, o que resulta num código mais seguro. Se você escrever um programa que tente dividir um tipo booleano por um número, ele nem compilará. Isto é bom porque é melhor detectarmos erros logo ao terminar de programar do que se deparar com travamentos indesejados. Tudo em Haskell tem um tipo, então o compilador pode considerar várias possibilidades antes mesmo de compilá-lo.
Ao contrário de Java ou Pascal, Haskell tem inferência de tipo. Se for digitado um número, não precisamos avisar ao Haskell que é um número. Ele consegue identificar isso automaticamente, não damos os tipos de funções e expressões explicitamente. Nós veremos apenas o básico de Haskell com uma visão apenas superficial sobre tipos. No entanto, entender o sistema de tipos é muito importante para o aprendizado de Haskell.
Tipo é algo como uma etiqueta que toda expressão têm. Nos diz em qual categoria ela se encaixa. A expressão True é booleana, "hello" [qcode] é uma String, etc.
Agora usaremos o GHCI para descobrir os tipos de algumas expressões. Faremos isso usando o comando :t que, seguido de qualquer expressão válida, retorna o seu tipo. Vamos dar uma olhada.
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
Fazendo isso, vemos que :t mostra a expressão seguida de :: e seu tipo. :: pode ser lido como "do tipo". Tipos explícitos sempre são referidos pela sua primeira letra maiúscula. 'a' , como é visto, é do tipo Char . Não é difícil de identificar que vem da palavra caracter. True é Boolean . Faz sentido. Mas... como? Vemos que o tipo de "HELLO!" é [Char] . Os colchetes denotam uma lista. Então lemos isso como uma lista de caracteres. Ao contrário de listas, cada valor da tupla tem seu tipo. Então a expressão (True, 'a') tem o tipo (Bool, Char) , e uma expressão como ('a','b','c') deverá retornar (Char, Char, Char) . 4 == 5 sempre retornará False , que é do tipo Bool .
Funções também têm tipos. Ao escrever suas próprias funções, podemos declarar explicitamente seus tipos. Isso geralmente é considerado uma boa prática exceto quando a função é muito curta. A partir daqui, daremos vários exemplos de que fazem uso desse tipo de declaração. Lembra-se da lista que criamos anteriormente e que usava filtros para retornar somente a parte maiúscula da String? Isso é muito parecido com uma declaração de tipo.
removeNonUppercase :: [Char] -> [Char] removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]
removeNonUppercase tinha o tipo [Char] -> [Char], o que significa que lidava com duas Strings, isso devido a receber uma String e retornar uma String. O tipo [Char] é sinônimo de String então ficaria mais claro se fosse escrito removeNonUppercase :: String -> String . Não precisamos fazer essa declaração de tipo para o compilador porque ele pode inferir por si só que é uma função que recebe uma String e retorna uma String. Mas como damos o tipo de uma função que tem vários parâmetros? Segue 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á nenhuma distinção entre tipos de parâmetros e retorno. O tipo do retorno é o último e os parâmetros são os três primeiros. Mais adiante veremos o porquê deles serem apenas separados por um -> ao invés de ter um destaque maior como em Int, Int, Int -> Int ou algo do gênero.
Se você deseja especificar o tipo da função, mas não tem certeza de qual o é, você pode escrevê-la normalmente e depois descobrir com o :t . Funções também são expressões, então :t funciona sem problemas.
Aqui temos um apanhado geral dos principais tipos.
Int é inteiro. É usado por números inteiros. 7 pode ser um Int mas 7.2 não. Int possui limitações de tamanho, o que significa ter um máximo e um mínimo. Geralmente computadores 32bit têm um Int máximo de 2147483647 e um mínimo de -2147483648.
Integer significa... também inteiro. A principal diferença é que não tem limitações e pode ser usado por números realmente grandes. Digo, extremamente grandes. Int , contudo, é mais eficiente.
factorial :: Integer -> Integer factorial n = product [1..n]
ghci> factorial 50 30414093201713378043612608166064768844377641568960512000000000000
Float é um número real em ponto flutuante de precisão simples.
circumference :: Float -> Float circumference r = 2 * pi * r
ghci> circumference 4.0 25.132742
Double é um número real em ponto flutuante com o dobro(!) de 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: Ou True ou False .
Char representa um caractere. É delimitado por aspas simples. Uma lista de caracteres é denominada String.
Tuplas são tipos mas possuem uma variação de acordo com a quantidade e valores que contém, então teoricamente temos tuplas com infinitos tipos, o que é bastante para esse tutorial. Note que uma tupla vazia () é também um tipo e pode assumir apenas um valor: ()
Tipo variável
Qual você acha que é o tipo da função head ? Já que head recebe uma lista e retorna o seu último elemento, qual deve ser o seu tipo? Vamos descobrir!
ghci> :t head head :: [a] -> a
Hmmm! O que é esse a ? É o tipo? Lembre-se que já vimos que o tipo é escrito com a primeira letra maiúscula, então não pode ser exatamente o tipo. E é exatamente essa diferença que nos diz ser um tipo variável. Isso significa que o a Pode ser qualquer tipo. Isso é algo como os genéricos de outras linguagens, mas em Haskell é muito mais poderoso porque nos permite facilmente escrever funções mais genéricas caso o processamento seja o mesmo para diferentes tipos. Funções que possuem tipos variáveis são denominadas funções polimórficas. A declaração de tipo em head diz que recebe uma lista de elementos de qualquer tipo e retorna um elemento dela.
Embora tipos variáveis possam ter nomes maiores de um caractere, geralmente damos a, b, c, d...
Lembra-se do fst ? Retorna o primeiro componente de um par. Vamos examinar o seu tipo.
ghci> :t fst fst :: (a, b) -> a
Vimos que fst recebe uma tupla que contém dois tipos e retorna o do primeiro elemento. É por isso que podemos usar fst em pares que contenham quaisquer tipos. Note ainda que mesmo a e b sendo de diferentes tipos variáveis, não necessariamente são de tipos diferentes. Só diz que o tipo do primeiro componente (da tupla) deve ser do mesmo que o do retorno.
Basicão de Typeclasses
Uma Typeclass (classe de tipos) é como uma interface que define um comportamento. Se um tipo é parte de uma typeclass, quer dizer que ela suporta e implementa o comportamento especificado pela classe de tipo. Muita gente vinda da orientação a objetos se confunde e acha estar diante de uma classe de OO. Bom... não. Você pode pensar que são como as interfaces de Java, mas muito melhor.
Qual deve ser o tipo da função == ?
ghci> :t (==) (==) :: (Eq a) => a -> a -> Bool
Interessante. Temos algo novo aqui, o símbolo => . Tudo antes do símbolo => é denominado class constraint (restrição de classe). Podemos ler a declaração de tipo anterior assim: a função de igualdade recebe dois argumentos de mesmo tipo e retorna um Bool . Esse tipo deve ser membro da classe Eq (que é a class constraint).
A typeclass Eq provê uma interface para o teste de igualdade. Qualquer tipo que faça sentido ser verificado por igualdade com outro tipo deve estar na typeclass Eq . Todos os tipos Haskell - exceto os de IO (tipo para lidar com entrada e saída) e funções - fazem parte da typeclass Eq .
A função elem tem o tipo (Eq a) => a -> [a] -> Bool porque usa o operador == para procurar um determinado elemento em uma dada lista.
Algumas classes de tipo básicas:
Eq é usado por tipos que suportam teste por igualdade. As funções que fazem parte dela implementam == e /=. Se existe alguma class constraint de Eq para um tipo variável em uma função, usa o operador == ou /= em algum lugar de sua definição. Todos os tipos já mencionados (com excessão de funções), são parte de Eq , então podem ser testados por 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 ordem.
ghci> :t (>) (>) :: (Ord a) => a -> a -> Bool
Todos os tipos já vistos (exceto funções) são parte de Ord . Ord engloba todas as funções de comparação comuns como > , < , >= e <= . A função compare requer dois membros de Ord de mesmo tipo e retorna sua ordenação. Ordering é uma typeclass que pode ser GT , LT ou EQ , significando maior que, menor que e igual a, respectivamente.
Para ser membro de Ord , um tipo deve ser membro do prestigioso e restrito clube do Eq .
ghci> "Abrakadabra" < "Zebra" True ghci> "Abrakadabra" `compare` "Zebra" LT ghci> 5 >= 2 True ghci> 5 `compare` 3 GT
Membros do Show podem ser representados como strings. Todos os tipos (exceto funções) são suportados pelo Show . A função que lida com a classe de tipo Show mais usada é a show . Recebe um valor de tipo presente em Show e nos mostra como string.
ghci> show 3 "3" ghci> show 5.334 "5.334" ghci> show True "True"
Read é algo o contrário da classe de tipo Show . A função read recebe uma string e retorna um tipo 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é agora tudo simples. Todos os tipos já vistos estão nessas classes de tipo. Mas o que acontece ao tentarmos 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á tentando nos dizer é que não sabe o que espera-se do retorno. Perceba que nos usos anteriores de read nós sempre fazíamos algo com o resultado. Assim, o GHCI podia inferir o tipo esperado de read . Se usassemos-o como um booleano, ele sabe que é um Bool . Mas agora ele só sabe que deve ser algum typeclass Read . Vamos dar uma olhada na declaração de tipo de read .
ghci> :t read read :: (Read a) => String -> a
Viu? Ele retorna um tipo parte de Read mas como não usamos o resultado depois, ele não sabe qual. É por isso que podemos especificar explicitamente type annotations. Type annotations servem para dizer qual tipo que você quer que uma expressão assuma. Fazemos isso adicionando :: no fim da expressão o tipo desejado. 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')
Na maioria das expressões, o compilador já pode assumir qual deve ser o tipo das expressões. Mas acontece dele não saber se deve ser Int ou Float para uma expressão como read "5" . Para ter certeza, Haskell deveria primeiro avaliar read "5" . Mas como Haskell é uma linguagem estaticamente tipada, precisa saber o tipo de todas as expressões na hora da compilação (ou no caso do GHCI, interpretação). Então dizemos ao Haskell: "Ei, essa expressão é desse tipo, caso não saiba!".
Os membros de Enum são tipos que possuem uma seqüência. A maior vantagem da typeclass Enum é poder ser usada em list ranges. Seus tipos têm sucessores e predecessores definidos, que podem ser conseguidos pelas funções succ e pred . Tipos nessa 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'
Bounded são os tipos que possuem limites - máximo e mínimo.
ghci> minBound :: Int -2147483648 ghci> maxBound :: Char '\1114111' ghci> maxBound :: Bool True ghci> minBound :: Bool False
minBound e maxBound são diferenciados por ter tipo (Bounded a) => a . São constantes polimórficas.
Todas tuplas não-vazias também estão em Bounded .
ghci> maxBound :: (Bool, Int, Char) (True,2147483647,'\1114111')
Num é uma typeclass numérica. Seus membros têm a função de agir como números. Vamos ver o tipo de um número.
ghci> :t 20 20 :: (Num t) => t
Parece que todos os números são constantes polimórficas. Elas podem tomas a forma de qualquer tipo da typeclass Num .
ghci> 20 :: Int 20 ghci> 20 :: Integer 20 ghci> 20 :: Float 20.0 ghci> 20 :: Double 20.0
Esses são os tipos da typeclass Num . Se verificar o tipo de * , descobrirá que ela aceita qualquer número.
ghci> :t (*) (*) :: (Num a) => a -> a -> a
Recebe três números do mesmo tipo. É por isso que (5 :: Int) * (6 :: Integer) resultará em erro e 5 * (6 :: Integer) funcionará e retornará um Integer , já que 5 pode tomar a forma de um Int ou de um Integer .
Para estar em Num , o tipo já deve estar em Show e Eq .
Integral também é uma typeclass numérica. Enquanto Num inclui todos os números (reais e inteiros), Integral apenas inteiros. Essa typeclass é composta por Int e Integer .
Floating inclui apenas números de ponto flutuante, então são Float e Double .
Uma função muito útil para lidar com números é fromIntegral . A declaração do seu tipo é fromIntegral :: (Num b, Integral a) => a -> b . Assim, vemos que ela recebe um número inteiro e transforma-o em algo mais genérico. Isso é útil quando você precisa que tipos inteiros e ponto flutuante trabalhem juntos. Por exemplo, a função length tem uma declaração de length :: [a] -> Int ao invés de ter algo mais geral como (Num b) => length :: [a] -> b . Acho que está assim por razões históricas, o que, na minha opinião, é besteira. Ainda assim, se tentarmos somar o tamanho de uma lista ( length ) com 3.2 teremos um erro, pois não é possível somar um Int com um número de ponto flutuante. Então para contornar, fromIntegral (length [1,2,3,4]) + 3.2 funciona perfeitamente.
Note que fromIntegral tem mais de um class constraint em sua declaração de tipo. Como pode ver, isso é válido, desde que estejam separados por vírgulas dentro de parênteses.