Introdução
Quando comecei a aprender Elixir, me perguntava como gerenciar estado. Diferentemente de linguagens imperativas com variáveis globais mutáveis, o modelo de dados imutáveis do Elixir e seu design orientado à concorrência (por meio da máquina virtual BEAM) exigem uma abordagem diferente. Neste artigo, explorarei como o estado é tratado em Elixir.
Contexto: BEAM VM e Concorrência
O Elixir roda na máquina virtual BEAM, projetada para alta concorrência e tolerância a falhas. Inspirada no Modelo de Ator, a BEAM trata processos como entidades leves que se comunicam por meio de passagem de mensagens. Como os dados são imutáveis, as alterações de estado são feitas criando novos valores em vez de modificar os existentes. Isso garante segurança em threads e simplifica a programação concorrente.
Loops Recursivos
A maneira mais simples de manter estado é implementando um loop recursivo. Aqui está um exemplo:
defmodule StatefulMap do
def start do
spawn(fn -> loop(%{}) end)
end
def loop(current) do
new =
receive do
message -> process(current, message)
end
loop(new)
end
def put(pid, key, value) do
send(pid, {:put, key, value})
end
def get(pid, key) do
send(pid, {:get, key, self})
receive do
{:response, value} -> value
end
end
defp process(current, {:put, key, value}) do
Map.put(current, key, value)
end
defp process(current, {:get, key, caller}) do
send(caller, {:response, Map.get(current, key)})
current
end
end
Uso:
pid = StatefulMap.start() # PID<0.63.0>
StatefulMap.put(pid, :hello, :world)
StatefulMap.get(pid, :hello) # :world
Agentes (Agents)
Outra opção é o módulo Agent
; ele permite compartilhar estado entre diferentes processos ou no mesmo processo ao longo do tempo.
Implementação de exemplo:
defmodule Contador do
use Agent
def start_link(valor_inicial) do
Agent.start_link(fn -> valor_inicial end, name: __MODULE__)
end
def valor do
Agent.get(__MODULE__, & &1)
end
def incrementar do
Agent.update(__MODULE__, &(&1 + 1))
end
end
Uso:
Contador.start_link(0)
#=> {:ok, #PID<0.123.0>}
Contador.valor()
#=> 0
Contador.incrementar()
#=> :ok
Contador.incrementar()
#=> :ok
Contador.valor()
#=> 2
Para iniciar, recomenda-se usar um supervisor:
children = [
{Contador, 0}
]
Supervisor.start_link(children, strategy: :one_for_all)
GenServer
A opção mais clássica é o comportamento GenServer
(similar a interfaces em .NET/Java), que permite gerenciar estado com requisições síncronas e assíncronas.
Callbacks principais:
-
init/1
-> quando o ator é iniciado. -
handle_call/2
-> requisição síncrona (ex.: espera resposta). -
handle_cast/3
-> requisição assíncrona (ex.: envio sem resposta).
Exemplo de GenServer
:
defmodule Pilha do
use GenServer
# Callbacks
@impl true
def init(elementos) do
estado_inicial = String.split(elementos, ",", trim: true)
{:ok, estado_inicial}
end
@impl true
def handle_call(:pop, _from, estado) do
[para_cliente | novo_estado] = estado
{:reply, para_cliente, novo_estado}
end
@impl true
def handle_cast({:push, elemento}, estado) do
novo_estado = [elemento | estado]
{:noreply, novo_estado}
end
end
Uso:
# Iniciar o servidor
{:ok, pid} = GenServer.start_link(Pilha, "hello,world")
# Este é o cliente
GenServer.call(pid, :pop)
#=> "hello"
GenServer.cast(pid, {:push, "elixir"})
#=> :ok
GenServer.call(pid, :pop)
#=> "elixir"
Conclusão
O gerenciamento de estado no Elixir depende de processos e imutabilidade. Loops recursivos oferecem controle fundamental, o Agent
simplifica o estado compartilhado, e o GenServer
fornece concorrência robusta com integração a supervisores. Cada ferramenta atende a casos de uso distintos, desde contadores simples até lógicas de estado complexas.
Referências
Trabalhando com Estado e Processos em Elixir
GenServer
Agent