Orientação a Objetos na linguagem Elixir?!

A linguagem Elixir é projetada baseada no Paradigma Funcional e em princípios de imutabilidade. Se você vem de outra linguagem, percebe a diferença gritante, não há sequer imperativos tradicionais como for, while, class ou return. Hoje pretendo subverter isso e criar um código Elixir com instâncias, encapsulamento e métodos, que deixaria qualquer programador(a) de Java orgulhoso(a).

Não façam isso em casa… 💀

Depois mostro como implementei, mas por ora vamos digerir o código acima:

  • Há uma “classe” chamada Person.
  • Uma nova instância person gerada via construtor new.
  • Há um atributo privado para o nome, está encapsulado mas há um setter.
  • O método set_name do nome alterou dados da mesma “instância” chamada person, isto é, invocar get_name em diferentes pontos do código produziria resultados diferentes, exatamente o tipo de imprevisibilidade mental que os amantes de OOP gostam.
  • E por fim um método greet apenas para charme dando olá.

Muita coisa do que afirmei acima não é exatamente verdade, mas tenha calma, vamos construindo o raciocínio a partir dessa visão simplista.

Processos em Elixir

Antes, preciso dar um contexto. O motivo mais técnico de Elixir precisar tanto de imutabilidade é porque há muitas features nativas na linguagem para concorrência, paralelismo, tolerância a falhas, escalabilidade. A linguagem foi criada para respirar ambientes distribuídos. Imagine que uma função dessas pode rodar em um processo completamente diferente, em outro hardware, e uma vez que não podem ter memória compartilhada, é trivial passar mensagens de um lugar pra outro. As mensagens são só dados primitivos (ou, no caso do Elixir, são Erlang terms, algo mais poderoso mas explico depois). 👽

(Se esse papo foi muito complicada para você, permita-se tentar simplificar. Basta olhar por esse lado: você consegue passar a referência de uma instância de classe do Java no servidor para o JavaScript no cliente? Não, há limites físicos impedindo isso. Mas consegue fácil passar um JSON contendo strings e números via protocolos web como HTTP, os dados trafegam serializados, puros e imutáveis. Para Elixir, é como se qualquer função estivesse sempre pronta para ser executada em outro hardware quase igual uma requisição HTTP.)

Ironicamente, é justamente a imutabilidade por trás de processos que tornou possível criar essa ilusão de mutabilidade do “código Orientado a Objetos” ilustrado.

A ideia veio do livro Elixir in Action, Third Edition:

Um processo que mantém estado alterável pode ser considerado um tipo de estrutura de dados mutável. Mas você não deveria abusar de processos para evitar a Programação Funcional.

E foi assim que decidi fazer exatamente o que o livro pediu para evitar. 🥸

Elixir Agents são processos com estado interno

O segredo por trás dessa implementação foram os Agents, mas preciso explicar o que é isso antes de mostrar o código (embora nada te impeça de scrollar até o fim do texto).

Um Agent em Elixir é um processo da BEAM capaz de manter um estado interno que é transformado via funções. E esse estado pode ser substituído (não alterado). Modelar problemas com funções puras e dados imutáveis não impede a Elixir de concretamente implementar efeitos colaterais, pois afinal todo sistema da vida real é um amontoado de ações e consequências fora do sistema.

O que acontece é que modelamos as funções para produzirem coisas novas sem fazer mutações na anterior. No exemplo abaixo, há um módulo com uma função que recebe um map (objeto, dicionário, chave-valor etc.), uma cópia desse objeto é criada com um novo name e o produto disso é retornado pela função. Assim, o código cliente terá em mãos ambas as coisas e decidirá o que fazer.

Se você já programou no frontend com React, é a mesma coisa que os hooks de estado atuais, que o antigo Redux, coisas assim: estado atual combinado com ação produz estado seguinte. 🤓

E um Agent se beneficia disso para mudar coisas… sem de fato mudar nada. Todas as variáveis em Elixir são apenas “etiquetas”, você pode mudar o nome de variáveis à vontade, mas o dado em que elas apontam são preservados. Então o Agent pode receber uma ação, tirar a etiqueta de “estado atual” do anterior e colocar no produto seguinte. E isso tudo em outro core de CPU completamente diferente, em paralelo, se assim os BEAM schedulers julgarem possível.

No nosso experimento, cada “instância de classe” era só um BEAM process

Contexto dado, agora o nobre leitor está pronto para tentar digerir como implementei minha humilde Orientação a Objetos: 👊

O código completo está transcrito num Gist, clique na imagem para abrir.

Observe que:

  • A “classe” é na verdade apenas um módulo chamado Person, um namespace, só um nome bonito antes do pontinho, não é de fato uma estrutura de dados nem contrato, nem guarda atributos por instância, nada.
  • O “construtor” é uma função chamada new que inicia um processo externo contendo o estado inicial, através da biblioteca nativa Agent.
  • A “instância” resolvida pelo new é uma estrutura primitiva de chave-valor que contém funções (os “métodos”). Pois em Elixir funções são first-class citizens, isto é, são valores que podem ser guardados em variáveis igual um número ou uma string poderiam.
  • Cada “método” usa a biblioteca Agent para enviar mensagens thread-safe de leitura e escrita ao processo externo. Temos pid como o mesmo conceito que processos do sistema operacional, é o id único do processo, tipo o nome dele.

Façamos algumas reflexões mais subjetivas sobre o que foi observado.

Funcional, mas com pilares Orientados a Objetos?

Como você é atento(a) e com certeza já percebeu, o código é só sabor Orientado a Objetos. E eu não faria uma abstração desnecessária dessas em ambiente produtivo. Mas podemos tirar sérias conclusões disso.

O módulo é apenas uma abstração bonita, o que por si só ironicamente concretiza um dos pilares da Orientação a Objetos com esse nome. O código cliente é incapaz de controlar e sequer sabe que existe um Agent com pid por baixo dos panos, o foco está em “o quê” e não “como” executar. O encapsulamento reforça isso, quase tudo nesse módulo é privado a ele.

E eu só não implementei outros pilares porque não quis. 😇

Se você só programou Orientado a Objetos a vida toda, provavelmente foi ensinado que esses pilares e as classes são a única coisa que garante um código civilizado, organizado, profissional, “limpo”. Foi assim que vários professores me ensinaram por anos também. Isso não é verdade. Código ruim existe em qualquer paradigma ou linguagem, e de mesmo modo os princípios de manutenibilidade podem ser interpretados e aplicados também em qualquer paradigma ou linguagem.

Sobre o uso real dos BEAM processes

Cada função daquela está potencialmente invocando um código em outro core de CPU, com um esforço mínimo para orquestrar isso. 🧠 O Agent é só uma das inúmeras formas de simplificar o poder e simplicidade de Elixir para sistemas distribuídos, há outras muito mais robustas.

E como mencionei bem rápido, quem controla isso de verdade são os BEAM schedulers, resumidamente uma entidade na máquina virtual Erlang que decide onde e quando executar cada pedaço de código. Por padrão, a VM tem um BEAM scheduler para cada core de CPU que dividem as tarefas paralelas entre si, e internamente rodam o código de forma concorrente com escalonamento preemptivo.

Válido mencionar, que um BEAM process não é igual a um OS process. 🔧 O normal é ter dezenas, centenas, milhares de processos BEAM rodando na Erlang VM que é o runtime do Elixir. Cada um é muitíssimo mais leve que um processo convencional do sistema operacional. Isso tudo faz parte do Elixir desde o dia zero, quase todos os detalhes da linguagem foram decididos pensando nesse ambiente e em como isolar e supervisionar seguramente cada um desses múltiplos processos.

Além disso, diferente da transferência de mensagens entre processos que exige dados perfeitamente primitivos, a Erlang permitirá que suas estruturas mais avançadas sejam usadas, como structs, funções, tuplas, átomos… é bem mais poderoso que um um JSON serializado.

Considerações finais

De minha experiência como desenvolvedor, eu poderia mencionar muitos atributos de Elixir que gosto muito: simplicidade, estabilidade e previsibilidade, robustez nativa, flexibilidade… 💅🏻

Na prática, quero dizer que, por exemplo, um Agent desses pode facilmente substituir um Redis na sua infraestrutura de node único, por exemplo. 💡 É possível implementar rate limits, load balancing, cache, message broker, background jobs… tudo só com Elixir e um banco relacional, se assim você quiser e o contexto permitir. Você só precisa anexar serviços externos ao Elixir, implicando em mais custos e complexidade e manutenção à sua arquitetura, quando realmente houver necessidade para isso. É como se a linguagem já viesse com baterias inclusas para sistemas robustos, há muito mais opções para suas decisões arquiteturais.

Como nota pessoal. ✏️ Há linguagens que são flexíveis porém nada robustas e complicadas, como JavaScript/TypeScript (calma, é meu segundo ecossistema favorito!), que é forçada a sangrar retrocompatibilidade. Suportando uma miríade de comportamentos esquisitos, e para qualquer vírgula diferente de código precisa de uma biblioteca externa ou serviço na infra. E aí a cada três anos tudo muda porque arbitrariamente a comunidade decidiu que era mais legal que sim.

Meus elogios à linguagem Elixir não são teoria. Já vi excelência no uso de Elixir da forma como descrevi desde organizações com duas pessoas programando ali no backend, até centenas. De startups early stage a gigantes industriais. De CRUDs em único node até operações críticas com dependências C/C++ rodando em várias máquinas pelo mundo.

Portanto, embora este tenha sido apenas um experimento jocoso, espero que lhe tenha sido provocativo e iluminador a respeito da BEAM, do Elixir e do Paradigma Funcional.

Deixe um comentário