Neste post vou comentar sobre a estrutura typeclass do Haskell. A minha principal referência é o capítulo 6 do Real World Haskell.
Typeclasses
Como em Haskell existem tipos parametrizados, podemos querer definir funções que aceitam parâmetros com esse tipo. Entretanto, provavelmente a implementação da função será diferente para cada tipo.
Assim, typeclass é uma estrutura para prover essa funcionalidade. A sintaxe básica é a seguinte:
class <Nome> <Tipos parametrizados> where <função 1> :: <Assinatura de tipos> <função 2> :: <Assinatura de tipos>
Apesar do nome, um typeclass é bem diferente de uma classe que estamos acostumados em linguagens orientadas à objetos.
Um caso onde typeclass é útil é na implementação do operador de igualdade. Suponha que temos dois tipos Booleano
e CorRGB
:
data CorRGB = Vermelho | Verde | Azul deriving (Show) data Booleano = Falso | Verdadeiro deriving (Show)
Podemos definir uma typeclass contendo uma função de igualdade, que recebe dois parâmetros de um mesmo tipo parametrizado a
, e retorna True
se forem iguais ou False
caso contrário.
class Compara a where igual :: a -> a -> Bool
Aí podemos definir uma implementação para cada tipo, usando uma estrutura chamada instance
. A sintaxe é semelhante ao typeclass, mas agora especificamos os tipos parametrizados e definimos a implementação da função. Para o caso do Booleano
e da CorRGB
, temos:
instance Compara Boolean where igual Verdadeiro Verdadeiro = True igual Falso Falso = True igual _ _ = False instance Compara RGBColor where igual Vermelho Vermelho = True igual Verde Verde = True igual Azul Azul = True igual _ _ = False
Também podemos prover implementações padrões para um typeclass, definindo a implementação na estrutura class. Na execução, a implementação utilizada é sempre a mais específica.
Complementando o exemplo da typeclass Compara
, podemos definir a função diferente
e definir implementações padrões para ela e também para a função igual
:
class Compara a where igual :: a -> a -> Bool igual x y = not (diferente x y) diferente :: a -> a -> Bool diferente x y = not (igual x y)
Derivação automática
Na definição de Booleano
e CorRGB
, usamos a diretiva deriving(Show)
. Essencialmente Show
é um typeclass da biblioteca padrão. A função show
desse typeclass transforma um tipo genérico em String
.
Fazendo alguns testes no terminal (ghci):
> show(Vermelho) "Vermelho" > :type show show :: Show a => a -> String
Note que o comando :type
usa uma sintaxe diferente para dizer que a função show
pertence ao typeclass Show
.
Podemos escrever nossa própria implementação para CorRGB
ao invés de usar o padrão. Para isso, removemos a declaração deriving(Show)
e adicionamos:
instance Show CorRGB where show Vermelho = "Red" show _ = "Not implemented"
Segundo [2], a funcionalidade do derive só pode ser usada com um conjunto específico de built-in typeclasses.
Typeclass com lista de tipos paramétricos
Imagine que queiramos implementar a função igual para uma lista de Booleanos. A primeira tentativa seria:
instance Compara [Boolean] where igual = undefined
No código acima, undefined
é um tipo coringa que não causa erros de compilação devido a incompatibilidade de tipos, mas que causa um erro em tempo de execução. Essa técnica de usar undefined
‘s é interessante para ir compilando versões intermediárias do seu código mesmo sem ter a implementação de todas as funções.
Enfim, veremos que tal trecho de código leva ao seguinte erro de compilação:
Illegal instance declaration for `Compara [Boolean]' (All instance types must be of the form (T a1 ... an) where a1 ... an are *distinct type variables*, and each type variable appears at most once in the instance head. Use -XFlexibleInstances if you want to disable this.) In the instance declaration for `Compara [Boolean]' Failed, modules loaded: none.
Há diversas maneiras de se contornar esse erro. Um deles é definir uma função exclusiva para listas, no caso da função igual
, podemos criar uma igualLista
,
class Compara a where ... igualLista :: [a] -> [a] -> Bool igualLista (xa:a) (xb:b) | diferente xa xb = False | otherwise = igualLista a b igualLista [ ] [ ] = True igualLista _ _ = False ...
A vantagem nesse caso é que a implementação padrão deverá servir para a maior parte dos casos, pois em geral o teste de igualdade entre duas listas consiste em verificar se todos os elementos em ambas são iguais.
No caso de você querer definir a implementação apenas para uma lista de um tipo particular (p. e. [Booleano]
), pode-se encapsular essa lista em uma estrutura chamada newtype
.
O newtype
é parecido com um tipo de dado algébrico (keyword data
), com algumas diferenças como por exemplo o newtype
exigir exatamente um data constructor e o tipo de um newtype
é resolvido em tempo de compilação, ao contrário do data
, e por isso seu uso não ocasiona overhead na execução do programa [1].
Assim, poderíamos fazer:
newtype Wrapper = Wrapper [Booleano]
Agora podemos implementar a função, com o inconveniente de encapsular os dados do novo tipo com Wrapper
:
instance Compara Wrapper where igual (Wrapper x) (Wrapper y) = igualAux x y where igualAux (xa:a) (xb:b) | diferente xa xb = False | otherwise = igualAux a b igualAux [ ] [ ] = True igualAux _ _ = False
Uma boa referência para essas e outras alternativas é o haskell wiki [3].
Conclusão
Achei esse assunto bem complicado! Pra piorar, parece que o livro Real World Haskell no qual estou me baseando, escreveu muito mal os capítulos 5 e 6, dando exemplos muito complexos (talvez seja a diretriz do “real world”, mas definitivamente isso não é didático), confusos e chatos. Há diversos comentários que compartilham dessa opinião.
Apesar de tudo, continuarei meus estudos com esse livro, mesmo que eu só use os tópicos para me guiar e procurar explicações em referências como [2] e [3].
Referências
[1] http://book.realworldhaskell.org/read/using-typeclasses.html
[2] http://en.wikibooks.org/wiki/Haskell/Class_declarations
[3] http://www.haskell.org/haskellwiki/List_instance
[…] written about typeclasses in an old post (in Portuguese). Haskell defines a typeclass called […]