Back-End

6 fev, 2015

Polimorfismo em Haskell vs C++

Publicidade

O polimorfismo paramétrico acontece quando você escreve uma função que funciona com muitos tipos de dados. Em C++, isso é muito confuso, mas em Haskell é muito fácil. Vamos dar uma olhada em um exemplo.

Vamos dizer que queremos uma função que calcula o volume de uma caixa. Em C++, usaríamos templates, de modo que nossa função funcionasse com qualquer tipo numérico:

template<typename T>
T boxVolume(T length, T width, T height)
{
    return length * width * height;
}

Templates têm uma sintaxe estranha, mas isso não é um grande aborrecimento, já que o C++ tem problemas muito maiores. E, se ao escrever o seu programa, você passar acidentalmente algumas strings para essa função?

int main()
{
    cout << boxVolume("oops","no","strings") << endl;
}

Recebemos este erro quando compilamos com g++ :

test.cpp: In instantiation of _T boxVolume(T, T, T) [with T = const    char*]_:
test.cpp:22:47:   required from here
test.cpp:8:19: error: invalid operands of types _const char*_ and _const char*_ to binary
_operator*_
    return length * width * height;

Essa mensagem de erro é um pouco difícil de entender porque usa templates. Se tivéssemos escrito nossa função para usar doubles em vez de templates, teríamos algo como:

double boxVolume(double length, double width, double height)
{
    return length * width * height;
}

Gostaríamos de receber esta mensagem de erro mais simples:

test.cpp: In function _int main()_:
test.cpp:22:47: error: cannot convert _const char*_ to _double_ for argument _1_ to _double
boxVolume(double, double, double)_
    cout << boxVolume("oops","nope","bad!") << endl;

Vemos que esse erro é mais simples e mais fácil de entender, uma vez que nos diz claramente que não podemos passar strings literais para nossa função. Além disso, não há nenhum comentário supérfluo sobre a nossa “instanciação” de boxVolume.

Agora vamos tentar escrever um boxVolume polimórfico em Haskell:

boxVolume :: a -> a -> a -> a
boxVolume length width height = length * width * height

Quando tentamos compilar, temos o erro:

test.hs:2:50:
    No instance for (Num a) arising from a use of `*'
    Possible fix:
      add (Num a) to the context of
        the type signature for boxVolume :: a -> a -> a -> a
    In the expression: length * width * height
    In an equation for `boxVolume':
        boxVolume length width height = length * width * height

Uma mensagem de erro! O que deu errado? Ele diz que tentou usar o operador * sem declarar os parâmetros como uma instância do tipo de classe Num.

Mas o que é um tipo de classe? Isso nos leva a polimorfismo ad hoc, também conhecido como sobrecarga de função. Polimorfismo ad hoc é quando uma função pode ser aplicada a diferentes tipos de argumentos, cada um com uma implementação diferente. Por exemplo, as classes STL stack e queue têm, cada uma, suas próprias funções push e pop, que, embora tenham os mesmos nomes, fazem coisas diferentes:

stack<int> s;
queue<int> q;

s.push(1); q.push(1);
s.push(2); q.push(2);
s.push(3); q.push(3);

s.pop(); q.pop();

Depois que o código acima for executado, a pilha s vai ficar com os números 1,2, enquanto a fila q vai ficar com os números 2,3. A função pop se comporta de forma diferente em pilhas e filas: chamar pop em uma pilha remove o item adicionado por último, enquanto que chamar pop em uma fila remove o item adicionado em primeiro lugar.

Haskell não suporta sobrecarga de funções, exceto por meio de tipos. Por exemplo, se fôssemos declarar especificamente nossas próprias classes Stack e Queue com funções push e funções pop:

data Stack = Stack  [Int] deriving Show
data Queue = Queue [Int] deriving Show

push :: Stack -> Int -> Stack
push (Stack xs) x = Stack (x:xs)

pop :: Stack -> Stack
pop (Stack []) = Stack []
pop (Stack xs) = Stack (tail xs)

push :: Queue -> Int -> Queue
push (Queue xs) x = Queue (x:xs)

pop :: Queue -> Queue
pop (Queue []) = Queue []
pop (Queue xs) = Queue (init xs)

Isso resulta em um erro de compilação:

stack.hs:11:1:
    Duplicate type signatures for `push'
    at stack.hs:4:1-4
       stack.hs:11:1-4

stack.hs:12:1:
    Multiple declarations of `push'
    Declared at: stack.hs:5:1
                 stack.hs:12:1

stack.hs:14:1:
    Duplicate type signatures for `pop'
    at stack.hs:7:1-3
       stack.hs:14:1-3

stack.hs:15:1:
    Multiple declarations of `pop'
    Declared at: stack.hs:8:1
                 stack.hs:15:1

Mudar os nomes de nossas funções push e pop para, digamos, stackPush, stackPop, queuePush e queuePop deixaria melhores a compilação e a organização do programa.

Uma maneira mais genérica, no entanto, é a de criar uma classe de tipo. Vamos fazer um classe de tipo chamada Sequence que implementa nossas funções push e pop.

class Sequence s where
    push :: s -> Int -> s
    pop :: s -> s

Essa declaração de classe de tipo diz que qualquer tipo de dados ou instâncias dessa Sequence pode usar as operações push e pop, ou, em outras palavras, pode adicionar e remover um Int. Ao criar instâncias Stack e Queue na Sequence, os dois tipos de dados podem ter suas próprias implementações das funções push e pop!

instance Sequence Stack where
    push (Stack xs) x = Stack (x:xs)
    pop (Stack []) = Stack []
    pop (Stack xs) = Stack (tail xs)

instance Sequence Queue where
    push (Queue xs) x = Queue (x:xs)
    pop (Queue []) = Queue []
    pop (Queue xs) = Queue (init xs)

Substituir as nossas definições de função com essas instâncias de classe de tipo Sequence permite que nossa compilação do programa seja realizada.

Classes de tipo também são uma parte importante do uso de templates em definições de funções. Em nossa função boxVolume, temos um erro porque tentamos usar o operador * sem declarar a variável a como uma instância de classe de tipo Num. A classe de tipo Num é usada basicamente para qualquer coisa que represente um número, como Int, Float, Double, e ela permite que sejam usadas operações matemáticas comuns de + , -, e *.

Vamos mudar a nossa função para declarar que a é um Num:

boxVolume :: (Num a) => a -> a -> a -> a
boxVolume length width height = length * width * height

Isso é chamado de adicionar uma restrição de classe. Sempre que quiser declarar uma função de template que se baseia em outras funções, temos que adicionar uma restrição de classe que repassa tanto para o usuário quanto para o compilador os tipos de dados que podem ser enviados na função.

Se tivéssemos que chamar boxVolume através de strings, teríamos uma simples mensagem de erro:

ghci> boxVolume "a" "b" "c"

<interactive>:14:1:
    No instance for (Num [Char]) arising from a use of `boxVolume'
    Possible fix: add an instance declaration for (Num [Char])
    In the expression: boxVolume "a" "b" "c"
    In an equation for `it': it = boxVolume "a" "b" "c"

O compilador nos informa que não é possível avaliar essa função porque strings não são números! Se realmente quiser utilizar strings, poderíamos tornar String uma instância de classe de tipo Num, e então essa função iria funcionar (claro que os motivos pelos quais você iria querer fazer isso está além da minha compreensão). Esse é o poder do polimorfismo paramétrico combinado com tipos de classe.

Então, é isso. Em C++, embora possamos facilmente implementar polimorfismo ad hoc através de sobrecarga de funções, o polimorfismo paramétrico é um pouco mais complicado. Isso é facilitado em Haskell, especialmente com a utilização de tipos de classes, que garantem que os dados passados para funções funcionem, e orientam o usuário sobre o que eles podem passar (ou não) para uma função. Use tipos de classes em seu próprio benefício quando escrever seu próximo programa Haskell!

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em https://izbicki.me/blog/polymorphism-in-haskell-vs-c%2B%2B.html