Qual minha opinião sobre Elixir

Trabalhei mais de um ano com Elixir e depois desse tempo adquiri conhecimento suficiente para dizer o que gostei e o que não gostei.
Pra deixar claro, esse não é um post para falar bem ou mal da linguagem. Minha intenção é só compartilhar um pouco das alegrias e dores que tive com a linguagem nesse tempo.

A idéia do Elixir é excelente, acredito que a linguagem tem um futuro próspero pela frente.

Uma coisa que eu acho super legal é que tem muito espaço para contribuir na comunidade.
Quem já tentou sabe que não é fácil colocar um código num repositório open source.

Mas Elixir ainda sendo uma linguagem nova é fácil encontrar coisas pra codar nas principais bibliotecas. Ecto, plugins para conduit são exemplos.

Programar pensando funcional para todos os problemas que você vai resolver é algo realmente desafiador.
A orientação a objetos foi feita pensando em procedimentos e estrutura de dados e funciona muito bem para programar regras de negócio (umas das coisas que senti falta é de uma abstração de classes).

Dores

Todos os itens que vou descrever não são relacionados com a linguagem em si, mas como a mesma é mantida.
Talvez aconteçam por conta do tamanho da comunidade, que ainda não é grande o suficiente para mitigar esses problemas, ou não há empresas suficiente patrocinando, não sei.

Backport

No site oficial podemos ver que somente a versão lançada como estável recebe correção de bugs. As antigas recebem somente atualização de segurança.

23 de abril de 2020

Breaking changes em patch ou minor versions

Essa está mais para decepção do que dor.

Elixir utiliza o padrão semantic version https://hexdocs.pm/elixir/Version.html
Porém algumas decisões são tomadas de forma arbitrária sem se preocupar com a semântica de versões.
Isso significa que o comportamento pode ser alterado num minor version e as vezes em patches.

Exemplo

Execute o código abaixo no Elixir 1.6 e 1.7.

defmodule ScopeChange do
  def test_return do
    x = 10
    if true do
      x = 0
    end
    x
  end
end

Nas versões 1.6 ou menos, o resultado será 0.
Nas versões 1.7 ou mais, o resultado será 10.

Mudar o minor version com uma alteração desse tipo vai atrapalhar demais qualquer upgrade da linguagem.
Tropecei por esse problema algumas vezes em situações diferentes, tanto com a linguagem quanto com libs oficiais, como o Ecto.

É difícil manter um projeto grande com a versão do Elixir atualizada, porque as coisas quebram com facilidade quando se faz um upgrade. Além de coisas que quebram silenciosamente, como a informada acima.

Daí você pode dizer que um warning gigante aparece:

Note variables defined inside case, cond, fn, if and similar do not leak. If you want to conditionally override an existing variable "x", you will have to explicitly return the variable. For example:

if some_condition? do
  atom = :one
else
  atom = :two
end

should be written as

atom =
  if some_condition? do
    :one
  else
    :two
  end

Ok, concordo. Mas com o passar do tempo é impossível eliminar os warnings no Elixir.
As bibliotecas geralmente tem muitos warnings (que aparecem durante a compilação do seu projeto), e quando você faz upgrade da linguagem, mais uma tonelada de warnings são gerados e fica difícil enxergar uma nota tão importante como a descrita acima.

A linguagem que tive melhor a experiência com relação a versionamento, backport e cuidados com breaking changes foi NodeJS.
Veja como são classificadas as funcionalidades: https://nodejs.org/dist/latest-v12.x/docs/api/documentation.html#documentation_stability_index

Fechamento prematuro de issues

Segundo o Valim, foi adotado o padrão ABC (Always be closing).

Eu acredito que essa prática não seja saudável. Coisas importantes podem se perder, pois as coisas não parecem estar abertas à discussão. Quando alguém dá uma sugestão de como resolver, uma resposta é dada e a issue é fechada.

Exemplo na migração Ecto

Houve uma alteração que quebrava a migração de alguns usuários.
Houve uma proposta de solução, houve uma resposta, mas logo a issue foi fechada.
Foi falado para o autor da issue abrir um merge request para correção e fim de papo. Me passou a impressão de que o assunto não está aberto a discussão.

https://github.com/elixir-ecto/ecto_sql/issues/170

Alegrias

Tive ótimas impressões da linguagem e os itens que vou falar são a razão por eu acreditar que Elixir é uma linguagem que tem futuro.

Pattern Matching

Essa parte é minha preferida. Para quem não conhece pattern matching, se parece com o unpack de listas ou tupla do Python, só que muito mais poderoso e para todos os tipos.

Abaixo você pode lembrar como fazer um unpack de tuplas com python. E a mesma coisa com Elixir

t = (0, 1, (2, 3, 4))

a, b, c = t
print(a) # 0
print(b) # 1
print(c) # (2, 3, 4)

a, b, (c, d, e) = t
print(a) # 0
print(b) # 1
print(c) # 2
print(d) # 3
print(e) # 4

Não reparem o highlight quebrado a seguir, não existe Elixir na lib que uso no blog. Desculpem-me.

t = {0, 1, {2, 3, 4}} # {0, 1, {2, 3, 4}}
{a, b, c} = t # {0, 1, {2, 3, 4}}
a # 0
b # 1
c # {2, 3, 4}
{a, b, {c, d, e}} = t # {0, 1, {2, 3, 4}}
a # 0
b # 1
c # 2
d # 3
e # 4

No exemplo acima não há nenhuma diferença quanto à funcionalidade. Então vamos para um exemplo um pouco mais completo, usando funções

defmodule Furniture do
  def item(:chair, %{seat_height: h, width: w, length: l}) do
    "Chair with seat height: #{h}, width: #{w}, length: #{l}"
  end

  def item(:table, %{height: h, width: w, length: l}) do
    "Table with height: #{h}, width: #{w}, length: #{l}"
  end
end

Furniture.item(:chair, %{seat_height: 45, width: 40, length: 40})
# "Chair with seat height: 45, width: 40, length: 40"

Furniture.item(:table, %{height: 90, width: 120, length: 90})
# "Table with height: 90, width: 120, length: 90"

O pattern matching funciona com valores estáticos de qualquer tipo, como o átomo que é recebido no primeiro argumento ou valores dinâmicos de qualquer tipo.
Tem como falar o dia todo de possibilidades com o pattern matching, mas não vou desviar o assunto do post. O fato é que esse recurso deixa seu código bem limpo em várias situações.

Eu desejaria que toda linguagem tivesse isso.

Concorrência

Task, Agent, send e receive são elementos cruciais para se construir uma aplicação multithread em Elixir.
A elixirSchool.com tem uma área reservada para falar de concorrência e acho que os exemplos são bons pra entender como criar processos e fazer seu programa se comunicar com eles.

O GenServer é outra coisa boa para facilitar a criação de aplicações multithread. É fácil adicionar um processo na árvore de supervisão e todos os seus processos seguirão o mesmo padrão de código. Chamá-los externamente é simples também.
Vale dar uma lida na documentação do GenServer.

Já implementei aplicações multithread com PHP (sim tem como e não, não é legal) e em python (funciona bem), mas com Elixir o trabalho é bem menor.

Dialyzer

Esse assunto é muito interessante pra mim por que tenho sentimentos contraditórios sobre o Dialyzer.
Depois do Elixir 1.7, o ElixirLS ficou bem mais poderoso, não pesquisei em detalhes mas quando usei fez muita diferença no meu cotidiano.

Enfim, definir os tipos é muito bom para fins de auto completar e você consegue definir os tipos de argumentos e retornos.

A idéia é ótima, o problema é só a sintaxe (daí o mixed feeling).

Abaixo eu copiei como exemplo o código aberto da uma lib chamada Liblink.

defmodule Liblink.Socket.Recvmsg.Impl do
  # Codigos...

  @spec recvmsg(:sync, state_t) ::
          {:reply, {:ok, iodata}, state_t}
          | {:reply, {:error, :timeout}, state_t}
          | {:reply, {:error, :empty}, state_t}
          | {:reply, {:ok, :badstate}, state_t}
  def recvmsg(:sync, state) do
    {fsm, data} = state.fsm

    call_fsm(fn -> fsm.recvmsg(data) end, :sync, state)
  end
    
  # Codigos...
end

Faz muito sentido podermos dizer quais todos os tipos possíveis de retorno, só dá um asco no início para ler. É bem feio mas é útil.

Release (antigo Distilery)

Para distribuir sua aplicação existe um recurso chamado release (antes de ser incorporado no core era distilery). Ele vai compilar sua aplicação e colar a VM do Erlang dentro do binário. Assim, você pode mandar a aplicação para uma máquina que não tem Elixir/Erlang.

Abaixo você pode ver como fica um Dockerfile. Na minha opinião, essa é uma forma excelente de empacotar sua aplicação.

FROM elixir:1.10-alpine as build

ENV MIX_ENV=prod

WORKDIR /app
COPY . .
RUN mix deps.get
RUN mix compile
RUN mix release

FROM alpine

WORKDIR /app/_build/prod
COPY --from=build /app/_build/prod/rel/seu_app .
ENV PATH=$PATH:/app/bin

ENTRYPOINT "seu_app"
CMD "start"

Os recursos abaixo ficarão disponíveis no seu binário.

start        Starts the system
start_iex    Starts the system with IEx attached
daemon       Starts the system as a daemon (Unix-like only)
daemon_iex   Starts the system as a daemon with IEx attached (Unix-like only)
install      Installs this system as a Windows service (Windows only)
eval "EXPR"  Executes the given expression on a new, non-booted system
rpc "EXPR"   Executes the given expression remotely on the running system
remote       Connects to the running system via a remote shell
restart      Restarts the running system via a remote command
stop         Stops the running system via a remote command
pid          Prints the operating system PID of the running system via a remote command
version      Prints the release name and version to be booted

Veredito (totalmente pessoal)

Hoje eu escolheria Elixir para resolver problemas muito pontuais de concorrência.

Digamos que eu tenha uma aplicação com alto consumo de CPU, que gerencia threads para paralelizar os processos e que para suportar a carga seja necessário dividir o processamento em mais máquinas.

Se o esforço para implementar a clusterização for inviável na linguagem que a aplicação já foi escrita, esses seriam os meus argumentos para propor o Elixir como alternativa:

  • Rede OTP - Comunicação entre os nós
  • libcluster para descoberta automática dos nós
  • PG2 para classificar os nós (tem problemas com muitos nós)
  • GenServer elimina quase todo o trabalho de se configurar as threads da aplicação.

Já para problemas do cotidiano, eu ainda acho que a linguagem e as principais libs precisam evoluir muito para se tornarem uma escolha pra mim.
De acordo com minha experiência, escrever aplicações puramente web simples ou complexas costuma ser mais rápido utilizando PHP, NodeJS, Python ou .NET Core.

Eu não consegui enxergar ganho de produtividade ao usar Elixir a não ser nos casos complexos de concorrência.
Também de acordo com minha vivência, geralmente não se precisa de algo que suporte tanta carga assim. Vejo muito over engineering, onde as pessoas pensam que o programa fará mais coisas que realmente fará ou receberá mais requisições do que realmente receberá.
Aquilo que as linguagens de mercado entregam atendem a maioria esmagadora dos problemas que as empresas enfrentam. O que pra mim dificulta a adoção da linguagem, uma vez que ela ainda está num processo de amadurecimento.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *