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
Top comments (0)