← Code Compare

Concurrency

This is the BEAM topic. The virtual machine's superpower is cheap, isolated processes that share nothing and talk only by sending immutable messages - millions of them, each with its own heap and mailbox. The task is the canonical one: spawn 5 workers that each square a number 1..5, have each send its result back, and let the parent receive and sum them to 55. Watch Erlang, Elixir, and LFE use the raw spawn/!/receive trio, Gleam wrap the very same primitives in typed actors so messages are checked at compile time, and Luerl stand apart - Lua has no BEAM processes of its own, so true concurrency has to come from the Erlang host.

Show: ErlangElixirGleamLFELuerl
Erlang
-module(squares).
-export([run/0]).

run() ->
    Self = self(),
    %% spawn one process per number; each sends {N, N*N} back to us.
    lists:foreach(
        fun(N) ->
            spawn(fun() -> Self ! {N, N * N} end)
        end,
        lists:seq(1, 5)),
    collect(5, 0).

%% receive exactly Count messages, summing the squares.
collect(0, Sum) -> Sum;
collect(Count, Sum) ->
    receive
        {_N, Square} -> collect(Count - 1, Sum + Square)
    end.

%% squares:run() =:= 55

Erlang's three primitives do everything: spawn/1 starts a process, Pid ! Msg sends asynchronously, and receive blocks the parent until a matching message arrives - here we loop receive five times to drain the mailbox and accumulate the sum.

Elixir
defmodule Squares do
  def run do
    parent = self()

    # spawn one process per number; each sends {n, n*n} back.
    1..5
    |> Enum.each(fn n ->
      spawn(fn -> send(parent, {n, n * n}) end)
    end)

    # collect 5 replies and sum the squares.
    Enum.reduce(1..5, 0, fn _i, sum ->
      receive do
        {_n, square} -> sum + square
      end
    end)
  end
end

# Squares.run() == 55
# Real code often reaches for Task.async_stream/2 instead of raw spawn/send.

Elixir wraps the same spawn/send/receive trio in friendlier names and pipes; Enum.reduce/3 over 1..5 drives the parent's receive loop, though everyday code usually prefers higher-level Tasks.

Gleam
import gleam/erlang/process.{type Subject}
import gleam/int
import gleam/io
import gleam/list

pub fn run() -> Int {
  // A Subject is a typed channel: it only ever carries Int here.
  let inbox: Subject(Int) = process.new_subject()

  // spawn one process per number; each sends back its square.
  list.each([1, 2, 3, 4, 5], fn(n) {
    process.spawn(fn() { process.send(inbox, n * n) })
  })

  // receive 5 typed messages and fold them into a sum.
  list.fold([1, 2, 3, 4, 5], 0, fn(sum, _i) {
    case process.receive(inbox, within: 1000) {
      Ok(square) -> sum + square
      Error(_) -> sum
    }
  })
}

pub fn main() {
  io.println(int.to_string(run()))
  // prints "55"
}

Gleam keeps the BEAM model but makes it sound: a Subject(Int) is a statically typed mailbox, so send and receive can only carry Int, and receive returns a Result (with a timeout) instead of blocking forever - no untyped messages, no surprises.

LFE
(defmodule squares
  (export (run 0)))

(defun run ()
  (let ((self (self)))
    ;; spawn one process per number; each sends (tuple n (* n n)) back.
    (lists:foreach
      (lambda (n)
        (spawn (lambda () (! self (tuple n (* n n))))))
      (lists:seq 1 5))
    (collect 5 0)))

;; receive Count messages, summing the squares.
(defun collect
  ((0 sum) sum)
  ((count sum)
   (receive
     ((tuple _n square) (collect (- count 1) (+ sum square))))))

;; (squares:run) => 55

LFE expresses the identical primitives as S-expressions: spawn takes a lambda, (! pid msg) is the send operator, and receive pattern-matches the incoming tuple - the two-clause collect recurses until the mailbox is drained.

Luerl
-- Luerl runs Lua *on* the BEAM, but plain Lua has no processes,
-- no spawn, no mailbox, and no message passing of its own.
-- So "concurrency" here is just a sequential map over the numbers:
local function squares()
  local sum = 0
  for n = 1, 5 do
    sum = sum + n * n   -- the work a BEAM worker would do, inline
  end
  return sum
end

print(squares())  --> 55

-- To get *real* concurrency you must cross back into the host:
-- the embedding Erlang/Elixir app spawns the processes and runs
-- a small Lua chunk inside each one via luerl:do/2, then collects
-- the results on the BEAM side. The parallelism lives in Erlang,
-- not in the Lua code.

Lua has no native BEAM processes, so the language itself can only compute the sum sequentially; genuine concurrency comes from the host - the Erlang/Elixir program that embeds Luerl spawns the processes and runs each Lua chunk inside one, doing the spawning and collecting on the BEAM side.