Clojure na prática — parte 2

Polimorfismo, Multimethods, Protocols e Records.

Álefe Nascimento da Silva
6 min readJan 18, 2021

Olá! Seja bem vindo a segunda parte da série de artigos sobre Clojure na prática. Ao término deste artigo você terá aprendido sobre:

  • O que é polimorfismo;
  • Como aplicar polimorfismo em Clojure;
  • O que são Multimethods, Records e Protocols;
  • Quando utilizar Multimethods ou Protocols;
  • As vantagens de programar para abstrações.

Este artigo assume que você já tenha lido a parte 1 desta série.

Abstrações

Image from pinterest

É muito comum encontrarmos a necessidade de utilizar funções genéricas em nossos códigos para tratar casos semelhantes de maneira diferente. Geralmente, tentamos generalizar o problema que estamos tentando resolver, de maneira que possamos atacá-lo de uma forma mais flexível e dinâmica.

Clojure foi escrito com base em diversas abstrações que permitem que isso seja simples de ser alcançado. Por exemplo, muitas funções da biblioteca padrão do Clojure são aplicáveis à diversos tipos de estruturas de dados diferentes, mas que se comportam como "sequências" ou "coleções" de dados.

Observe alguns exemplos no trecho de código abaixo:

Perceba que, dependendo da estrutura de dados que passamos como parâmetro para as funções acima, um resultado diferente foi obtido. Ou seja, utilizando as mesmas funções, diferentes comportamentos foram observados.

Caso quiséssemos implementar a nossa própria versão da função conj, de maneira bem simplista, poderíamos fazer algo como isto:

Apesar de termos obtido um resultado semelhante ao da função original, essa implementação exige a criação de uma nova condicional para cada caso que venhamos a suportar no futuro.

Mas se o nosso objetivo for suportar diversas estruturas de dados diferentes, sem precisar criar uma função específica para cada caso, como poderíamos fugir dessa cadeia de condicionais?

Polimorfismo

Apesar do exemplo ser bem simples, ele nos dá um indício de um problema: estamos criando uma dependência explícita entre a estrutura de dados passada como parâmetro para a função e o processamento que deve ser realizado.

Em muitos casos, uma cadeia condicional simples pode ser inofensiva. Em outros, desejamos ter o máximo de flexibilidade para deixar o nosso código o menos acoplado possível (ou seja, sem muitas dependências explícitas entre os usuários de nossas funções e as funções em si).

Quanto menor o acoplamento, maior a facilidade de se estender e alterar um código, podemos realizar mais alterações com uma menor chance de quebrar outras partes.

Image from DevHumor

Se você possui alguma experiência em programação, provavelmente já ouviu falar sobre polimorfismo. De maneira bem simples, podemos definir polimorfismo em programação como uma forma de reutilizar um mesmo código de diversas maneiras diferentes. Ou seja, utilizar um mesmo trecho de código que se comporta de maneira diferente de acordo com "o que/aonde" este será aplicado.

Veremos à seguir algumas das diferentes formas de se realizar polimorfismo em Clojure e esse conceito ficará ainda mais claro.

Multimethods

Clojure suporta polimorfismo em tempo de execução por meio de um mecanismo de "multi métodos". Esse mecanismo permite que, de acordo com o tipo, valor, metadado ou relacionamento entre os argumentos recebidos por uma função, um processamento diferente seja aplicado:

No exemplo acima, utilizamos uma função dispatcher simples para decidir qual "método" será executado, mas nada nos impede de utilizar lógicas mais complexas para tomar decisões em funções dispatchers. Além disso, Multimethods suportam multi-aridade e dispatch baseado em múltiplos argumentos:

Utilizando simples construções da linguagem, conseguimos simular algumas situações de polimorfismo sem criar nenhuma condicional explícita ou algo do tipo. Se mais casos vierem a surgir no futuro, basta adicionar novos métodos sem precisar alterar algum que já tenha sido criado.

Caso queira entender mais sobre Multimethods, dê uma olhada aqui e aqui.

Protocols e Records

No último exemplo, realizamos o dispatch de acordo com o tipo do dado que passamos como parâmetro para o Multimethod type-ex. Para lidar com polimorfismo baseado em tipos, Clojure oferece uma outra alternativa mais eficiente: Protocols.

Enquanto um Multimethod compreende apenas uma operação polimórfica, um Protocol agrupa um conjunto de uma ou mais operações polimórficas, baseados no tipo do primeiro argumento em que essas operações são aplicadas. Protocols são semelhantes as interfaces que encontramos em algumas linguagens de programação, porém com algumas características particulares.

Antes de criar nossos primeiros Protocols, vamos entender o que são Records na prática através dos exemplos abaixo:

Records não só formalizam quais são os campos esperados em sua construção, como também fornecem os mesmos comportamentos de um mapa Clojure comum.

Agora que entendemos como Records funcionam, vamos ver como estes se unem aos Protocols para oferecer polimorfismo de alta performance:

Certifique-se de ter compreendido com clareza os exemplos anteriores e aproveite para praticar um pouco no seu próprio REPL local. Como um exercício prático, tente criar uma implementação do protocolo Shape para um triângulo ou para qualquer outra forma geométrica.

Agora que vimos como Multimethods e Protocols funcionam, quando devemos optar pelo uso de um ao invés do outro?

Multimethods vs Protocols

Why not both?

Para dispatch baseado em tipo, Protocols são mais performáticos que Multimethods por utilizarem algumas otimizações em tempo de execução da JVM. Apesar de ser bastante comum, este é o único caso suportado por Protocols, valendo apenas para o primeiro parâmetro passado para a função. Multimethods por sua vez oferecem mais flexibilidade para realizar dispatch baseado nos valores, tipos, hierarquia dos parâmetros passados e etc.

Sendo assim, podemos dizer que Protocols são mais indicados para dispatchs baseados em tipo e Multimethods para casos onde se deseja ter uma maior flexibilidade para realizar o dispatch.

Lembrando que nada te impede de utilizar os dois para criar abstrações poderosas em seu código :)

Conclusão

Image from pexels

Abstrações nos permitem tirar o foco dos detalhes de implementação e nos ajudam a focar em conceitos mais genéricos. Se tivéssemos que aprender todos os detalhes técnicos utilizados na construção de qualquer carro que fôssemos dirigir, provavelmente poucos de nós nos tornaríamos condutores. Mas, como toda a complexidade da implementação interna de um carro é abstraída por uma interface mais simples, fica fácil dirigir praticamente qualquer veículo que possua um volante, um acelerador e um freio.

Pensar em abstrações enquanto construímos nossos códigos nos ajuda a criar aplicações mais robustas, menos acopladas e com uma maior facilidade de extensão, e como vimos neste artigo, Clojure possui um ótimo conjunto de ferramentas que nos permite atender à estes pontos.

Próximos Passos

Este artigo foi apenas um resumo do que é possível fazer utilizando a biblioteca padrão do Clojure. Existem outras operações disponíveis para a criação de tipos, hierarquia de Multimethods, multi aridade de Protocols e etc.

Como sugestão, seguem alguns docs que podem te auxiliar a aprender ainda mais sobre os pontos abordados:

Continue acompanhando esta série de artigos para aprender mais sobre Clojure e não se esqueça de deixar seu feedback nos comentários :)

Até logo!

--

--

Álefe Nascimento da Silva

Lead Software Engineer at JobGet. Ex-Nubank. Passionate about learning new things. Brazillian living in Canada 🇧🇷 🇨🇦