Se você já estudou algo sobre Javascript ou Nodejs provavelmente já sabe que o Event Loop do Node, o componente responsável por receber e processar eventos e chamadas assíncronas, é single thread, ou seja, temos apenas um fluxo contínuo executando dentro do processo principal. E caso já tenha alguma experiência com essa linguagem já deve estar acostumado com Promises, código assíncrono e funções como setTimeout()
. O objetivo desse artigo é aproveitar esse conhecimento anterior que você leitor já possui para fazer analogias com uma das linguagens mais fantásticas e poderosas que temos hoje no mundo da computação: o Elixir!
Olhando para o passado
Javascript nasceu dentro do antigo navegador Netscape em meados da década de 90. Foi criado por Brendan Eich em 10 dias como um protótipo para tornar páginas web escritas em HTML mais dinâmicas e fáceis de customizar por outros programadores. O navegador se tornou o mais popular de sua época e a linguagem em si também foi ganhando grande adesão com o passar dos anos.
Mas a Web não era a única tecnologia emergente naquela época e no final da década de 80, a Ericsson criou outra linguagem: o Erlang ( a palavra é a abreviação de “Ericsson Language”). O contexto aqui porém é totalmente diferente, pois o objetivo da Ericsson era ter uma linguagem capaz de atender a demanda pelo desenvolvimento de sofware na área de telecomunicação e telefonia. A primeira versão foi escrita em Prolog, porém foi reescrita em C alguns anos depois para se tornar mais rápida. A Erlang VM é concorrente, distribuída, tolerante a falhas e capaz de rodar em várias máquinas de forma coordenada por longos períodos de tempo sem sair do ar. Todas essas funcionalidades vieram do contexto de telefonia onde você tem milhares de usuários ao mesmo tempo fazendo chamadas que não podem ser interrompidas ou canceladas. Enquanto o Javascript crescia na Web e cada vez mais gente navegava pela Internet, o Erlang crescia em sistemas de telefonia e mais usuários compravam telefones e se comunicavam por eles.
Porém, Erlang, ao contrário de Javascript, sempre teve uma sintaxe considerada complicada além de adotar um paradigma funcional, que não era tão popular como hoje no mundo de desenvolvimento de software. Apenas para exemplo, essa é uma forma de escrever um “Hello World” nela:
-module(hello_world).
-compile(export_all).
hello() ->
io:format("hello world~n").
Origem brasileira
No começo de 2012, o programador brasileiro José Valim lançou a primeira versão do Elixir, uma nova linguagem com o objetivo de ser altamente extensível e produtiva enquanto mantém compatibilidade com a Erlang VM. Valim se inspirou em Ruby e Clojure para criar o Elixir, sendo assim ele tem uma sintaxe muito mais simples e agradável enquanto ainda traz todo o poder de fogo do Erlang. Agora, escrever um “Hello World” poderia ser feito assim:
IO.puts("Hello World")
Uma das principais vantagens é que agora programadores poderiam utilizar de todo o ecossistema do Erlang através de uma nova linguagem, fácil, mais simples de usar, mas muito poderosa e compatível com a VM. O Elixir rapidamente obteve alta popularidade e já em 2014 surgiu o Phoenix, o principal framework web usado em peso pela comunidade e inspirado no Rails do Ruby. Apesar de ter surgido aqui no Brasil, a adoção do Elixir veio primeiro por empresas de fora do país. Desde aquela época diversas companhias passaram a utilizar Elixir e Phoenix em seus projetos.
Imutabilidade
Se você veio do Javascript (ou qualquer outra linguagem popular como Python, PHP, Java) trechos como esse são triviais para você:
let name = "João";
if (nameIsShort(name)) {
name = "João da Silva";
}
return name;
Você consegue reatribuir o valor que estava em uma variável por um novo valor e a maioria das linguagens funciona dessa forma. Porém Elixir é uma linguagem funcional que segue o princípio da imutabilidade. A ideia é até bem simples: um valor uma vez atribuído não pode ser mais alterado, ele se torna imutável, seu dado não se altera. Para obter um valor diferente você deve retornar um valor novo a partir do antigo. Dessa forma, se você quer adicionar o número 5 numa lista de ímpares, o correto é retornar um vetor novo com todos os elementos antigos incluindo o 5. E por “correto” não quero dizer apenas uma boa prática, mas o próprio compilador da linguagem te obriga a fazer dessa forma. Vamos ver um exemplo em Javascript. Abra o console do navegador e cole esse código:
let configs = { dbName: "mysql", port: 5432 };
console.log(configs.port); // 5432
configs.port = 6000;
console.log(configs.port); //6000
Perceba que eu consigo alterar o dado do objeto configs
. Essa abordagem traz diversos problemas e provavelmente você já se deparou com bugs causados por funções ou classes alterando um valor sem que você saiba que ele foi explicitamente alterado. Agora vamos testar isso em Elixir. Se você já instalou a lang em sua máquina abra seu terminal e digite iex
para abrir seu terminal interativo. Agora cole esse código:
configs = %{dbName: "mysql", port: 5432}
IO.puts(configs.port)
configs.port = 6000
Veja que é a mesma operação que acabamos de fazer em Javascript mas usando a sintaxe e estrutura do Elixir. A variável configs
é um map, uma estrutura de chave e valor que o Elixir fornece para nós. O IO.puts
como já deve imaginar escreve dados na saída padrão do programa.
Mas o problema começa na terceira linha, quando tentamos reatribuir o valor de port
para 6000. Se apertar Enter e executar o código, você verá um erro parecido com isso: ** (CompileError) iex:23: cannot invoke remote function configs.port/0 inside a match
Nessa linguagem os dados são imutáveis e você não pode mais alterar eles diretamente. O próprio compilador te impede de fazer isso e te leva a escrever código no paradigma funcional. Outro detalhes nessa mensagem de erro é a palavra “match”. Para o compilador, o operador =
não é de atribuição, mas de match ou igualdade, bem parecido com o =
da matemática. Ele vai tentar igualar o valor esquerdo com o do lado direito da expressão. Pattern matching e imutabilidade são dois conceitos fundamentais que você precisa estudar para entender Elixir.
Mas e se eu precisar mudar o valor de configs.port
? Uma solução possível seria essa:
new_configs = %{configs | port: 6000}
Vamos entender o que está acontecendo aí em cima. Primeiro, em vez de tentar substituír o valor de configs
eu crio uma nova váriavel chamada new_configs
(estou utilizando snake case por ser o padrão da comunidade). Ela recebe um map também, mas seu valor é o que tem dentro de configs
com port
recebendo 6000. Perceba, eu não estou alterando o meu map, mas criando um novo usando os dados do antigo. E essa barra vertical que você vê é o operador pipe que serve para transformar dados. E também é amplamente utilizado em Elixir. Outra forma de fazer isso seria assim:
new_configs = Map.put(configs, :port, 6000)
Aqui utilizamos a função put/3
do módulo nativo Map
que nos retorna um novo map com a propriedade atualizada. E você pode confiar que essa operação é imutável pois toda a linguagem segue o mesmo princípio. Fantástico, não? Outra coisa que deve ter percebido é a sintaxe function/2
. Usamos essa sintaxe para descrever funções, expressões e operadores onde o lado esquerdo antes da barra é o símbolo ou nome da função e o lado direito é o número de argumentos.
Processos
Nodejs utiliza um modelo assíncrono e não bloqueante single threaded para alcançar concorrência e lidar com múltiplos clientes conectados ao mesmo tempo na aplicação. O Event Loop interno dele delega uma tarefa bloqueante (por exemplo, executar uma query em um banco de dados) para outro fluxo de execução separado do principal enquanto continua executando tarefas na main thread e quando essa tarefa termina, ele executa a callback associada (a comunicação assíncrona ocorre via callbacks e eventos) e retorna a resposta para o cliente. Esse modelo permite que ele responda várias requests sem bloquear todo o servidor mas o Event Loop ainda está rodando em uma só thread e é responsável por todo o processo de executar e delegar tarefas de forma assíncrona. Isso faz com que o Node acabe tendo problemas de lidar com níveis muito altos de concorrência, mas seja uma boa opção para processar tarefas que tenham uso intensivo da CPU.
Por outro a Erlang VM (BEAM) é uma máquina virtual criada justamente para lidar com centenas de milhares de processos rodando simultaneamente. Aqui cabe uma distinção. Processos dentro da Erlang VM não são os mesmos que o sistema operacional cria. Tem como principal característica serem muito leves, totalmente isolados um do outro, se comunicam apenas por mensagens, não compartilham recursos e rodam dentro da VM. Todo código Elixir roda em processos e é justamente esse modelo que permite construir sistemas altamente concorrentes, escaláveis, distribuídos e tolerante a falhas. Vários módulos internos do Elixir usam processos para executar tarefas mais rapidamente e em paralelo. O módulo File
por exemplo cria um processo separado do principal que é responsável por ler um arquivo, tornando a execução bem mais rápida. Criar um código assíncrono em Elixir pode ser feito dessa forma:
IO.puts("hello")
Task.async(fn -> IO.puts("concurrency code") end)
IO.puts("bye")
O módulo Task
é construído em cima de funções nativas da linguagem para facilitar a criação e manipulação de tarefas assíncronas e código concorrente. A função async/1
cria um novo processo que envia uma mensagem para o processo pai assim que termina de executar seu trabalho. Por baixo dos panos, a VM se utiliza de todos os cores da CPU durante sua execução, extraindo o máximo possível de performance e muitas vezes mantendo um baixo consumo de memória.
Essa arquitetura é baseada no modo como o Erlang gerencia processos, que por sua vez é baseada no “actors model”. O modelo de atores é um modelo matemático e computacional proposto em 1973 por Carl Hewitt. Funciona de uma forma bem simples: cada ator é um bloco independente de execução. Ele é único, mantém seu próprio estado de informação e não pode controlar ou alterar o estado de outros atores de forma direta. A única maneira de fazer isso é enviando ou recebendo mensagens. Nesse modelo você pode ter diversos atores atualizando suas informações, criando outros atores e se comunicando entre si por mensagens. Se um ator deixa de existir, não afetará o resto a menos que tenha sido programado explicitamente para isso. Em Elixir, os atores são os processos, as mensagens são os dados passados usando a função send/2
e recebidas através de receive/1
. O modelo de atores é uma das melhores alternativas para evitar problemas de lock em sistemas concorrentes.
A cada dia que passa os projetos que trabalhamos requerem cada vez mais poder de processamento, onde milhares de usuários usam nossos apps ao mesmo tempo e esperam um tempo rápido de resposta. Todos estão conectados, trocando mensagens, enviando fotos, escrevendo textos e assistindo vídeos. Elixir foi feito para um mundo concorrente, é uma das linguagens mais incríveis que temos hoje e você não se arrependerá de estudá-la.