Elixir para desenvolvedores JavaScript: o que te surpreende na primeira semana

1. Paradigma funcional: a primeira grande sacudida

A primeira semana com Elixir é um choque térmico para qualquer desenvolvedor JavaScript. O paradigma funcional não é apenas uma recomendação — é a lei. A imutabilidade de dados é a regra número um.

# JavaScript — você pode fazer isso sem culpa
let x = 10
x = x + 1  // x agora é 11

# Elixir — isso simplesmente não compila
x = 10
x = x + 1  # Erro! (match error)

Em Elixir, = não é atribuição — é pattern matching. Você não "reatribui" uma variável; você cria um novo vínculo. Isso força você a pensar em transformações de dados, não em mutações.

A ausência de loops tradicionais é outro choque. Adeus for, while e forEach. Em Elixir, você usa Enum.map e Enum.reduce:

# JavaScript
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map(n => n * 2)

# Elixir
numbers = [1, 2, 3, 4, 5]
doubled = Enum.map(numbers, fn n -> n * 2 end)

Pattern matching substitui if/else e switch de forma muito mais elegante:

# JavaScript
function describe(value) {
  if (value === null) return "nulo"
  if (typeof value === "number") return "número"
  return "outro"
}

# Elixir
def describe(nil), do: "nulo"
def describe(value) when is_number(value), do: "número"
def describe(_), do: "outro"

2. Sintaxe que engana: parece Ruby, mas não é

A sintaxe de Elixir lembra Ruby, mas as semelhanças são superficiais. Funções são definidas com def e defp (públicas e privadas):

defmodule MathUtils do
  def soma(a, b), do: a + b       # pública
  defp helper(a), do: a * 2       # privada
end

O operador pipe |> é uma das primeiras coisas que encanta desenvolvedores JavaScript. Ele transforma o encadeamento de funções em algo legível:

# JavaScript — encadeamento aninhado
const result = JSON.stringify(processData(filterData(getData())))

# Elixir — pipe operator
result = get_data()
         |> filter_data()
         |> process_data()
         |> Jason.encode!()

E não há return explícito. O último valor da função é automaticamente o retorno:

# JavaScript
function sum(a, b) {
  return a + b
}

# Elixir
def sum(a, b) do
  a + b  # último valor, é o retorno
end

3. Concorrência leve: o que o Event Loop do JS nunca te contou

JavaScript tem um event loop single-threaded. Elixir tem processos leves — milhares deles rodando simultaneamente sem travamentos.

# JavaScript — Promise.all para concorrência limitada
const results = await Promise.all(tasks.map(task => task()))

# Elixir — spawn para criar processos independentes
task1 = spawn(fn -> heavy_computation1() end)
task2 = spawn(fn -> heavy_computation2() end)
result1 = receive do {:result, data} -> data end
result2 = receive do {:result, data} -> data end

Cada processo Elixir é isolado, com sua própria pilha e heap. Se um processo quebra, os outros continuam. O modelo de tolerância a falhas é "deixe quebrar" — você não tenta prevenir erros, você os trata com supervisores que reiniciam processos automaticamente.

4. Recursão como estrutura de controle natural

Sem loops, a recursão se torna sua principal ferramenta para iteração. Elixir otimiza chamadas de cauda (tail call optimization), então recursão não causa estouro de pilha:

# JavaScript — loop for
function factorial(n) {
  let result = 1
  for (let i = 2; i <= n; i++) result *= i
  return result
}

# Elixir — recursão com acumulador (tail-call optimized)
def factorial(n), do: factorial(n, 1)
defp factorial(1, acc), do: acc
defp factorial(n, acc), do: factorial(n - 1, acc * n)

Percorrer listas sem for ou while:

# JavaScript
function sumList(numbers) {
  let total = 0
  for (let n of numbers) total += n
  return total
}

# Elixir
def sum_list([]), do: 0
def sum_list([head | tail]), do: head + sum_list(tail)

5. O ecossistema OTP: o que o JavaScript não tem (e sente falta)

OTP (Open Telecom Platform) é o superpoder do Elixir. GenServer é o equivalente a um "serviço" stateful e concorrente:

defmodule Counter do
  use GenServer

  # API pública
  def start_link(initial) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end

  def increment do
    GenServer.call(__MODULE__, :increment)
  end

  # Callbacks
  def handle_call(:increment, _from, state) do
    {:reply, state + 1, state + 1}
  end
end

Supervisores formam árvores de supervisão que reiniciam processos automaticamente quando falham. Tasks e Agents oferecem paralelismo simples:

# Executar tarefas em paralelo
task1 = Task.async(fn -> expensive_operation1() end)
task2 = Task.async(fn -> expensive_operation2() end)
result1 = Task.await(task1)
result2 = Task.await(task2)

6. Tratamento de erros: try/rescue vs. try/catch

A filosofia "let it crash" é radicalmente diferente do tratamento defensivo do JavaScript. Em Elixir, você frequentemente usa pattern matching em tuplas:

# JavaScript — try/catch defensivo
try {
  const result = riskyOperation()
  handleSuccess(result)
} catch (error) {
  handleError(error)
}

# Elixir — pattern matching em tuplas {:ok, result} / {:error, reason}
case risky_operation() do
  {:ok, result} -> handle_success(result)
  {:error, reason} -> handle_error(reason)
end

O with encadeia operações que podem falhar:

with {:ok, user} <- find_user(id),
     {:ok, validated} <- validate_user(user),
     {:ok, saved} <- save_user(validated) do
  {:ok, saved}
else
  {:error, reason} -> {:error, reason}
end

7. Ferramentas e fluxo de desenvolvimento

mix substitui npm/yarn com uma abordagem mais integrada:

# Criar novo projeto
mix new meu_projeto

# Compilar
mix compile

# Rodar testes
mix test

# Executar
mix run lib/meu_projeto.ex

IEx (Interactive Elixir) é muito mais que um REPL:

iex> "hello" |> String.upcase() |> String.reverse()
"OLLEH"

# Recompilar módulos sem sair do REPL
iex> recompile()

ExUnit é o framework de testes nativo, familiar para quem usou Jest ou Mocha:

defmodule MeuModuloTest do
  use ExUnit.Case

  test "soma deve funcionar" do
    assert MeuModulo.soma(2, 3) == 5
  end

  test "deve falhar com valores negativos" do
    assert_raise ArgumentError, fn ->
      MeuModulo.soma(-1, 5)
    end
  end
end

A primeira semana com Elixir é um exercício de desconstrução mental. Você abandona mutação, loops, herança de classes e tratamento defensivo de erros. Em troca, ganha concorrência real, tolerância a falhas nativa e um modelo de dados imutável que simplifica drasticamente o raciocínio sobre o código. Não é fácil, mas é transformador.

Referências