Code Compare

The same task, eight ways

Pick a topic to see it written in Erlang, Elixir, Gleam, LFE, and Luerl, side by side, with a note on what is idiomatic in each.

01

Hello, World

The classic first program: print the line Hello, BEAM!. Notice how the BEAM languages diverge already on day one - Erlang and LFE reach for io:format, Elixir has a friendly IO.puts, Gleam routes everything through its typed gleam/io module, and Luerl runs plain Lua's print. Watch the surface syntax (S-expressions vs. pipes vs. ML-style) while the underlying runtime stays the same.

02

Variables & Types

Bind an integer, a float, a string, and a boolean, then print one line that uses them all. Notice how each language denotes a binding - = in Erlang/Elixir/LFE-ish forms, let in Gleam, local in Lua - and which of them actually enforce immutability. On the BEAM, Erlang, Elixir, Gleam, and LFE all treat values as immutable (a name binds once, or rebinding just makes a new value); Lua via Luerl is the odd one out, where local variables are genuinely mutable. Watch too how each language spells its types: Erlang/LFE use lowercase atoms like true, Elixir has the same atoms with true/false, Gleam has a real Bool type, and Lua has actual true/false booleans plus a single number type.

03

Functions

Functions are the heart of every BEAM language, and most of them lean on pattern matching across multiple clauses rather than if/else ladders. Notice how Erlang, Elixir, and LFE dispatch on guards (when) while statically typed Gleam exhaustively matches with case, and Lua (Luerl) falls back to ordinary conditionals since it has no clauses. Each example also shows a higher-order use - passing the function to a list operation like map - which is the everyday way you reuse logic on the BEAM.

04

Pattern Matching

Pattern matching is the beating heart of BEAM programming: instead of testing and indexing, you describe the shape of data and let the runtime bind the pieces. Watch the same task - destructuring an {ok, Value}/{error, Reason} tuple and the list shapes [], [x], and [head|tail] - expressed as multi-clause functions, case expressions, and assignments. Notice how Erlang, Elixir, and LFE share the same dynamic match semantics, Gleam adds compiler-enforced exhaustiveness, and Luerl (Lua) has to fake it all by hand.

05

Recursion

The BEAM has no mutable loops, so recursion is the control structure for iteration. The idiom to learn is tail recursion with an accumulator: each call passes the running total forward so the compiler can reuse the stack frame, turning recursion into a constant-space loop. Watch how every language splits the work into a base case (empty list) and a recursive case (head plus the recursion on the tail), whether via multiple function clauses, case, or match.

06

Collections (map/filter/fold)

One tiny pipeline in all five BEAM languages: from [1,2,3,4,5,6], keep the evens, double them, and sum the result (24). Notice how the same trio of operations is spelled differently - Erlang and LFE lean on the lists module, Elixir threads data through the |> pipe with Enum, Gleam composes typed gleam/list functions with a labelled from: accumulator, and Luerl falls back to plain Lua loops since standard Lua ships no higher-order list library.

07

Closures

A closure is a function that captures variables from the scope where it was created and carries them along after that scope is gone. The shared task here is make_adder(n), which returns a new function that remembers n and adds it to its argument - call make_adder(10) and apply the result to 5 to get 15. Notice how every BEAM language treats functions as first-class values you can build at runtime and return; the only real difference is the syntax for an anonymous function (fun ... end, fn ->, fn(x) {}, lambda, or Lua's function) and how each one is later applied.

08

Strings

From name = "Ada" and year = 1815, build and print Hello, Ada! (1815), then upper-case the name to print ADA. The big watch-point is what a "string" even is: classic Erlang strings are lists of character codepoints ("Ada" is really [65,100,97]), while Elixir, Gleam, and modern Erlang prefer UTF-8 binaries. Notice too how each language interpolates or formats - Erlang/LFE io_lib/io:format with ~s/~p, Elixir #{...}, Gleam explicit <> concatenation with int.to_string, and Lua string.format - and which upper-casing function is Unicode-aware versus Latin-1 only.

09

Maps

Maps (a.k.a. dicts, hashes, associative tables) are the BEAM's go-to key/value store, and the shared task here shows their three core moves: build {"ada" => 36, "grace" => 45}, look up "ada", then return an updated copy with "ada" bumped to 37 without mutating the original. Notice that Erlang, Elixir, LFE, and Gleam are all immutable - "updating" really means producing a fresh map and leaving the old one untouched - while Luerl's Lua tables are genuinely mutable, so it has to copy by hand to mimic the BEAM. Watch too how lookup is modelled: Gleam returns a Result instead of crashing or returning nil, whereas the others lean on the {ok, _}/:error convention or a sentinel.

10

Sorting

Two sorts in all five BEAM languages: the trivial [3, 1, 2] -> [1, 2, 3], then a list of {name, age} people sorted by age descending. Watch the two flavours of API - Erlang and LFE pass a comparator fun(A, B) -> A > B to lists:sort/2, Elixir and Gleam prefer a key function (Enum.sort_by / list.sort with a by: comparator) so you say what to sort on rather than how to compare, and Luerl falls back to Lua's in-place table.sort with a <-style comparator closure.

11

Error Handling

On the BEAM, recoverable failures are values you return, not exceptions you throw. The shared task - parse a string to an integer, then compute 100 / n, where parsing can fail and n can be zero - shows two failure points threaded through a result type. Watch the convention split: Erlang, Elixir, and LFE return tagged tuples {ok, _} / {error, _} and pattern-match on them; Gleam has a real Result(a, b) type whose Ok/Error branches the compiler forces you to handle; and Lua, lacking all of this, leans on pcall and multiple return values to signal what went wrong.

12

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.

13

Types & Records

Every BEAM language needs a way to name a structured value and update it without mutation. The task is identical everywhere: model a Point with integer x and y, build (1, 2), then translate it by (3, 4) into a new point (4, 6). Watch the spectrum - Erlang's compile-time records and Elixir's map-backed structs both lean on #point{...}/%Point{} update syntax, Gleam defines a real custom type whose fields are checked by the compiler, LFE generates record macros from defrecord, and Luerl falls back to a plain Lua table with a constructor function. In each, the original point is left untouched.

14

Modules

A module is the unit of code organisation on the BEAM: a named bundle of functions, where only the ones you export are callable from outside. The shared task is a tiny Math module with double/1 and square/1, called from another scope. Notice how Erlang and LFE list exports explicitly with -export/(export ...), Elixir flips the default by marking private functions with defp, statically typed Gleam exposes a function simply by writing pub, and Lua (Luerl) - which has no module keyword - returns a table of functions instead.

15

JSON

One round-trip across all five BEAM languages: parse {"name":"ada","age":36}, bump age to 37, and serialise it back to a JSON string. Watch who ships JSON in the box versus who reaches for a library - modern Erlang/OTP 27+ has a built-in json module and Elixir leans on the ubiquitous Jason (or its own JSON since 1.18), Gleam uses gleam_json with a typed decoder, LFE just calls Erlang's json, and Luerl's plain Lua has no JSON library at all, so the idiom is to hand-roll a tiny encoder. Notice the recurring BEAM theme: object keys decode to binaries (Erlang/Elixir/LFE), Gleam makes you declare the shape up front and returns a Result, and only Lua mutates the table in place.

16

Regex & Text

The shared task - pull every key=value pair out of "alice=30, bob=25" with a capturing regex and print alice is 30; bob is 25 - exposes a real divide on the BEAM. Erlang, Elixir, Gleam, and LFE all reach for the same regex engine: OTP's re module, which wraps PCRE (Perl-compatible). The key idiom is global scan with captures: ask for all matches and grab the parenthesised groups, then format the survivors. Luerl is the outlier - it runs Lua, whose string library has no PCRE at all but its own lighter-weight Lua patterns (%a, %d, no alternation or backtracking), so watch the syntax shift from (\w+)=(\d+) to (%a+)=(%d+).

17

Behaviours & Protocols

How do you say "any type that can do X" - polymorphic dispatch over a shared contract? The task is the same everywhere: an area operation that works for both a circle and a rectangle. The BEAM languages split sharply here. Erlang and LFE use behaviours - a module declares -callback signatures and each shape is its own callback module, with dispatch done by calling Mod:area(...). Elixir reaches for a protocol, which dispatches on the runtime type of its first argument. Statically typed Gleam has no typeclasses, so the idiom is a custom type with one case per variant and a plain function that pattern-matches it. Lua (Luerl) leans on duck typing: any table that carries an area method just works, no declared interface at all.

18

Generics & Polymorphism

How does a swap that flips a pair, or a map that walks any list, stay agnostic about the types it carries? The shared task is one polymorphic swap((a, b)) -> (b, a) plus a generic map, run over both numbers and strings. The real split is where the polymorphism lives: Gleam expresses it with parametric types - #(a, b) and List(a) with real type variables the compiler checks - while Erlang, Elixir, LFE, and Lua are dynamically typed, so the same function already works on any value with no annotations at all. Notice that the BEAM four pay for this freedom with zero compile-time guarantees, whereas Gleam's swap is provably correct for every a and b before it ever runs.

19

Ranges & Iteration

One number-crunching task in all five BEAM languages: sum the integers 1..10 (the answer is 55). Watch how each language produces the sequence and then folds it - Elixir has a first-class 1..10 range plus list comprehensions, Erlang materialises the list with lists:seq/2, Gleam folds the ints with the typed built-in int.range, LFE calls the same lists:seq in S-expressions, and Luerl drops to Lua's classic three-part numeric for loop with a mutable accumulator.

20

Input & Output

The shared task: print a formatted line that mixes text with a number (here Item 7 costs $3.50, with the price padded to two decimal places), then read a line of input from the user. The headline difference is the format string: Erlang and LFE use io:format with ~-prefixed control sequences (~p, ~.2f), Lua uses C-style string.format with %-placeholders (%d, %.2f), while Elixir and Gleam avoid format strings entirely - Elixir interpolates with #{...} and Gleam concatenates explicitly with <>. Watch too how each reads input: Erlang/LFE's io:get_line, Elixir's IO.gets, Gleam binding Erlang's io:get_line via @external (returning a Charlist), and Lua's io.read.

21

Command-line Args

The shared task: read the program's command-line arguments and print each one, numbered. The big split is how the args reach your code - in Erlang and LFE an escript hands them straight to main(Args) as a list of strings; Elixir exposes them through System.argv/0; Gleam (which has no global state) loads them via the argv package's argv.load().arguments; and Lua reads the global arg table, where arg[0] is the script name and the positive indices hold the actual arguments. Notice too that the BEAM languages hand you plain Erlang strings/charlists or binaries, while Lua's arg is a 1-indexed table you walk with ipairs.

22

Testing

Every BEAM language ships (or borrows) a unit-testing story, and the shared task is the smallest one possible: assert that add(2, 3) equals 5. Notice how the assertion macro shapes each style - Erlang's EUnit recognises any function named *_test, Elixir's ExUnit wraps cases in test "..." blocks, Gleam's gleeunit auto-discovers *_test functions and uses a should pipeline, and LFE reuses EUnit through (deftest ...). Lua (Luerl) has no test framework on board, so a test is just an assert call you write yourself.