Os protocolos são uma forma de alcançar polimorfismo utilizando Elixir. Podemos determinar ações para uma função genérica baseado no tipo de dado inserido.
Digamos que você precisa implementar uma função que será responsável por recuperar o primeiro elemento de uma lista.
Teríamos algo mais ou menos assim:
defmodule MyList do
def first_item([item | _list]), do: item
end
Usando este exemplo no IEx, conseguimos visualizar o seu funcionamento:
iex> my_list = [1, 2, 3, 4, 5, 6, 7]
[1, 2, 3, 4, 5, 6, 7]
iex> MyList.first_item(my_list)
1
Uma vez que criamos este módulo MyList
e definimos a função para receber uma lista como parâmetro através de pattern matching, a sua implementação fica limitada a somente listas não-vazias.
O uso de outro tipo de dado como argumento geraria o seguinte erro:
iex> MyList.first_item(%{t: 2, b: 3})
** (FunctionClauseError) no function clause matching in MyList.first_item/1
Podemos resolver esse problema de duas maneiras, onde uma delas seria implementar uma nova função com um guard que cumpra os requisitos para Maps:
defmodule MyList do
def first_item([item | _list]), do: item
def first_item(map) when is_map(map) do
map
|> Enum.to_list()
|> List.first()
end
end
E dessa forma seria possível utilizar maps e listas como argumentos, ex:
iex> list = [1,2,3,4,5,6,7,8,9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}
iex> MyList.first_item(list)
1
iex> MyList.first_item(map)
{:a, 1}
Enquanto isso, a segunda opção para resolver este problema seria implementar um protocolo para essa função e garantir que esse protocolo funcione para as seguintes implementações: [Lista, Map]
Mas como fazemos isso?
Protocolos e Implementações
Fazendo uma breve analogia a linguagens OO (Orientada a Objetos), podemos ver o Protocolo em si como uma Interface, onde iremos definir a assinatura de um método, e as implementações como o famoso @override que usamos para dizer que queremos sobrescrever a implementação de determinada funcionalidade da interface que estamos implementando.
A definição de protocolos é feita através da estrutura defprotocol
, da seguinte forma:
defprotocol MyList do
def first_item(value)
end
Não precisamos dizer como a função first_item/1
irá funcionar, basta indicar que estamos criando esse protocolo e que ele precisa possuir as devidas implementações para funcionar de acordo com o esperado.
Agora vamos fazer as implementações usando defimpl
para o nosso caso de aceitar Listas e Maps como argumentos:
defimpl MyList, for: List do
def first_item([item | _list]), do: item
end
defimpl MyList, for: Map do
def first_item(map) do
map
|> Enum.to_list()
|> List.first()
end
end
Conclusão
Com Protocolos nós podemos definir diferentes tipos de comportamentos para uma determinada função, sempre se baseando no tipo de dado que queremos processar.
Um bom exemplo de cenário real para isso é o próprio Jason que usamos para transformar as respostas da nossa aplicação web.
O mesmo possui um protocolo chamado Jason.Encoder, contendo a assinatura do método encode. A partir desta interface podemos implementar diferentes tipos de encoding para json dependendo do tipo de dados que queremos retornar na nossa API.
Top comments (1)
Show de bola o bola, bem explicado, parabéns!