DEV Community

Cover image for Protocolos com Elixir
Willian Frantz
Willian Frantz

Posted on • Edited on

Protocolos com Elixir

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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?

dancing cat gif

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
lcezermf profile image
Luiz Cezer

Show de bola o bola, bem explicado, parabéns!