Features
How each language does it
A near-exhaustive matrix. Toggle languages to focus; each cell explains how the feature works there.
Columns:
ErlangElixirGleamLFELuerl
Concurrency & Distribution
| Feature | Erlang | Elixir | Gleam | LFE | Luerl |
|---|---|---|---|---|---|
| Lightweight BEAM processesCheap, isolated user-space processes spawned by the millions, not OS threads. | YesFirst-class. spawn/1,3 creates a process with its own tiny heap (a few hundred words) and stack. Millions can run on one node; the VM (not the OS) schedules them. |
YesSame BEAM processes, exposed via spawn/1, Process, and Task. A process is the unit of concurrency, isolation, and failure - the runtime model is identical to Erlang's. |
YesCompiles to Erlang, so it gets real BEAM processes. The gleam/erlang/process library wraps spawning behind a statically typed API (Subject, Pid) so spawned work stays type-checked. (The JS target has no BEAM processes.) |
YesLisp Flavoured Erlang sits directly on the BEAM; (spawn ...) and the erlang BIFs create the same lightweight processes Erlang does. No runtime difference, just Lisp syntax. |
NoLuerl is a Lua interpreter written in Erlang; Lua code has no BEAM processes of its own. A Luerl state runs inside whatever single Erlang process hosts it. To get concurrency you spawn Erlang processes (each with its own Luerl state) from the host side - Lua sees none of it. |
| Message passing & selective receiveAsync, copy-by-value messages between mailboxes, with pattern-matched selective receive. | YesCore primitive: Pid ! Msg sends asynchronously to a process mailbox; receive pattern-matches messages and can selectively pull a matching one out of order (selective receive). No shared memory - messages are copied. |
Yessend(pid, msg) and receive do ... end map straight onto BEAM send/receive, including selective receive via pattern clauses and after for timeouts. |
YesSends go through a typed Subject(msg) so the message type is checked at compile time; process.receive reads from the mailbox. Gleam deliberately exposes a typed, simplified view rather than raw untyped !/receive, so arbitrary selective receive is not idiomatic. |
YesHas the full Erlang model with Lisp forms: (! pid msg) (or (send pid msg)) and a (receive ...) macro with pattern clauses, guards, and (after ...), including selective receive. |
NoLua has no mailboxes or receive. Any message passing happens between the Erlang processes that wrap Luerl states; the host marshals data in and out of each state. Lua itself just runs synchronous function calls. |
| The actor modelConcurrency as isolated actors that share nothing and communicate only by messages. | YesErlang is the canonical actor-model language: each process is an actor with private state, no shared memory, and an asynchronous mailbox. This 'share nothing + message passing' design is the foundation of everything else here. | YesInherits the actor model wholesale from the BEAM; Agent, GenServer, and Task are conveniences layered on top of process-as-actor. |
YesActors are first-class and type-safe: the gleam/otp/actor module gives each actor a typed message type and state, with the compiler enforcing that only valid messages are sent. |
YesPure actor model, same as Erlang - processes are share-nothing actors driven by receive. LFE adds no semantic change, only Lisp syntax and macros. |
NoLua is not an actor language and Luerl does not give Lua actors. You can simulate actors by giving each one its own Erlang process plus Luerl state on the host, but within Lua there is no actor abstraction. |
| OTP behaviours (gen_server / GenServer)Reusable generic process patterns: gen_server, gen_statem, gen_event, supervisor. | YesOTP ships the behaviours - gen_server, gen_statem, gen_event, supervisor - as part of the standard library. You implement callback functions (init/1, handle_call/3, ...) and the framework runs the loop. |
YesWraps the same OTP behaviours in friendlier modules: GenServer, :gen_statem, and the use GenServer macro generate the callback scaffolding. It is the same OTP, just with Elixir ergonomics. |
Yesgleam/otp provides typed equivalents - actor (a typed gen_server-like server) and supervisors - built on OTP. The compiler enforces message and state types, which raw Erlang gen_server cannot. |
YesImplements OTP behaviours directly: (behaviour gen_server) plus defun callbacks. LFE has access to all of OTP and adds macro helpers (e.g. the lfe library's defmodule/defbehaviour style) for less boilerplate. |
NoLua code cannot define OTP behaviours - there is no callback-process framework inside Luerl. If you want an OTP server backed by Lua, you write a gen_server in Erlang/Elixir that calls into a Luerl state for its logic; the behaviour lives in the host, not in Lua. |
| Supervision treesSupervisors that monitor child processes and restart them per a defined strategy. | YesThe supervisor behaviour builds restart trees: children are started, monitored, and restarted under strategies (one_for_one, one_for_all, rest_for_one) with restart intensity limits. This is the heart of 'let it crash' fault tolerance. |
YesSupervisor and DynamicSupervisor plus child specs and Application start trees give the same OTP supervision with declarative child specs. |
Yesgleam/otp/supervisor (and the static-supervisor APIs) build supervision trees with typed child specs, so child start functions and return types are checked at compile time. |
YesUses the Erlang supervisor behaviour with Lisp callbacks; child specs and strategies are written as LFE data. Fully equivalent to Erlang's supervision. |
NoLua has nothing to supervise - no processes, no restarts. Resilience for Luerl-backed code is provided by supervising the Erlang processes that own the Luerl states; if one crashes, an OTP supervisor on the host restarts it and creates a fresh Luerl state. |
| Links & monitorsBidirectional links propagate exits; unidirectional monitors notify of a process's death. | Yeslink/1 couples two processes so an exit signal propagates (the basis of supervision); monitor/2 is one-way and delivers a {'DOWN', ...} message when the target dies. process_flag(trap_exit, true) turns exit signals into messages. |
YesProcess.link/1, Process.monitor/1, and spawn_link expose the same primitives; the {:DOWN, ref, ...} message and trap_exit work identically. |
Yesgleam/erlang/process provides link, monitor_process, and a typed selector to receive ProcessDown messages, keeping monitor handling within the type system rather than matching raw 'DOWN' tuples. |
YesCalls the Erlang BIFs directly: (link pid), (monitor 'process pid), (spawn_link ...), and traps exits via (process_flag 'trap_exit 'true). Same semantics as Erlang. |
NoThere is no Lua process to link or monitor. Links and monitors apply only to the Erlang host processes wrapping Luerl; from Lua's perspective the concept does not exist. |
| Node distribution & clusteringTransparent message passing and spawning across networked BEAM nodes. | YesDistribution is built in: named nodes connect into a mesh, and Pid ! Msg, spawn/4, and rpc work across the network transparently (location transparency). Cookies authenticate nodes; the global/pg modules add registration. |
YesSame distributed BEAM via Node.connect/1, Node.spawn/2, and :rpc. Libraries like libcluster automate node discovery, but the core distribution is native. |
PartialRunning on the Erlang target, Gleam programs participate in BEAM distribution and clustering at the runtime level. There is limited typed library surface for distribution, so cross-node sends often drop to lower-level/Erlang interop; the JS target has no distribution at all. | YesInherits full BEAM distribution; node connection, remote spawn, and rpc are available through the Erlang BIFs in Lisp syntax. No difference from Erlang. |
NoLua code has no nodes and cannot send across the cluster. A Luerl state is local to one Erlang process on one node. Distribution is the host's job: Erlang/Elixir code can ship results between nodes and feed them into local Luerl states, but Lua never sees the cluster. |
| Preemptive schedulers, per-process heaps & GCFair preemptive scheduling plus per-process heaps that are collected independently. | YesThe VM runs one scheduler per CPU core and preempts processes by reduction counting, so no process can starve the others (soft real-time). Each process owns a private heap that is garbage-collected on its own, so GC of one process never pauses the whole system. | YesRuns on the identical BEAM VM, so it gets the same per-core preemptive schedulers, reduction-based fairness, and isolated per-process heaps and GC for free. | YesOn the Erlang target, Gleam runs on the BEAM and inherits its preemptive schedulers and per-process heap GC unchanged. (On the JavaScript target it instead uses the host JS engine's single-threaded event loop and GC - no BEAM scheduling.) | YesCompiles to BEAM bytecode, so LFE processes are scheduled and garbage-collected exactly like Erlang's - preemptive, fair, and per-process. No runtime difference. | PartialA Luerl state runs inside one Erlang process, so it benefits from BEAM scheduling and per-process GC at the host level - that host process is preempted and collected like any other. But Lua execution itself is not independently preemptible: a long Lua loop runs to completion within its host process unless the embedder steps/limits it, and Lua tables/values live on (and are GC'd with) the host process heap. |
Functional Style
| Feature | Erlang | Elixir | Gleam | LFE | Luerl |
|---|---|---|---|---|---|
| Immutability by defaultOnce a value is bound it never changes; "updates" build new terms. | YesAll Erlang terms are immutable. A variable is single-assignment within a function clause: once bound, rebinding X is a match, not mutation. "Modifying" a record, map or list returns a fresh term and shares structure under the hood. |
YesBuilt on the same BEAM term model, so every data structure is immutable. Elixir allows rebinding a name (x = 1; x = 2), but this just points the name at a new immutable value; the old value is untouched. Functions return new structures rather than mutating in place. |
YesGleam has no mutable variables at all and no reassignment of let bindings (each let introduces a new binding via shadowing). Combined with its sound static type system, immutability is enforced at compile time, not just by convention. |
YesLFE is a Lisp that compiles to Core Erlang and uses the identical BEAM term model, so all data is immutable. Its let/let* bindings are lexically scoped and single-assignment; there is no setf/set! style in-place mutation of bound values. |
PartialThe language is Lua, which is mutable: tables and variables can be reassigned in place from the script's point of view. Luerl's trick is that the entire Lua state lives in one immutable Erlang data structure threaded through execution, so the BEAM host sees immutability while the Lua program sees ordinary mutable semantics. |
| First-class & higher-order functionsFunctions are values: passed as arguments, returned, and stored. | YesFuns are first-class values created with fun or captured with fun mod:f/1. Higher-order list functions (lists:map/2, lists:foldl/3, lists:filter/2) are core idiom and used everywhere. |
YesAnonymous functions use fn -> end (called with .()) and named functions are captured with the & operator (&String.upcase/1). The Enum and Stream modules are built around higher-order functions. |
YesFunctions are first-class and fully typed: a function value has a concrete function type like fn(Int) -> Int, inferred and checked. Anonymous functions use fn(x) { ... }, and higher-order helpers live in gleam/list. |
YesFuns are first-class. LFE uses lambda and match-lambda for anonymous functions and (fun f 1) to capture a named function, and it can call any Erlang higher-order function such as lists:map. |
YesLua treats functions as first-class values and supports closures, so passing, returning and storing functions all work in Luerl. Standard higher-order patterns over tables (e.g. via a map helper) behave as in stock Lua. |
| Recursion & tail-call optimizationLoops are expressed as recursion; tail calls run in constant stack space. | YesErlang has no loop constructs; iteration is recursion. The BEAM guarantees proper tail-call optimization, so a tail-recursive function (often using an accumulator) runs in constant stack space and underpins long-lived server loops. | YesInherits the BEAM's guaranteed tail-call optimization, so tail-recursive functions are stack-safe. Idiomatic Elixir often prefers Enum/Stream and Recursion-via-private-clauses with accumulators over hand-written loops. |
YesGleam has no while/for loops, so recursion is the way to iterate, and tail calls are optimized on the BEAM target by the same VM guarantee. (On the JavaScript target, deep non-tail recursion can still overflow the host stack.) |
YesAs a BEAM Lisp, LFE relies on recursion for iteration and benefits from the VM's proper tail-call optimization. Tail-recursive functions and accumulator patterns run in constant stack space. | YesLua itself mandates proper tail calls in its specification, and Luerl implements Lua's semantics, so a return f(...) tail call does not grow the stack. Note this is Lua's own TCO contract realized inside the Erlang interpreter, not BEAM-process recursion. |
| List & binary comprehensionsDeclarative generators + filters that build lists (or binaries) from sources. | YesErlang has both list comprehensions [E || ...] and binary comprehensions << <<E>> || ... >>, with multiple generators and boolean filters. As of OTP 26+ comprehensions can also generate into maps. |
YesThe for special form is a comprehension supporting multiple generators, into: (collect into lists, maps, binaries, etc.), filters, :reduce, and bitstring generators. It is the BEAM list/binary comprehension generalized. |
NoGleam deliberately has no comprehension syntax. The same work is done with explicit higher-order functions from gleam/list such as list.filter and list.map, keeping the language small and the types explicit. |
YesLFE provides list comprehensions and binary comprehensions via the lc and bc forms (with <- / <= generators and guard filters), mapping directly onto Erlang's comprehension support. |
NoStandard Lua has no comprehension syntax, and Luerl implements standard Lua, so there are none. The equivalent is an explicit for loop building a result table (for i, v in ipairs(t) do ... end). |
| The pipe operatorThread a value through a chain of calls left-to-right instead of nesting. | NoErlang has no pipe operator; you nest calls (f(g(h(X)))) or bind intermediate variables. There are macro/library experiments, but nothing is part of the language or standard library. |
YesThe |> operator passes the value on its left as the first argument of the call on its right, making data-transformation pipelines a defining Elixir idiom. It is plain macro-based syntax, not a library. |
YesGleam's |> pipes the left value into the first argument of the right-hand call (or applies it directly if the right side is a function value). It is a core part of idiomatic, type-checked Gleam code. |
NoCore LFE has no pipe operator and S-expression nesting makes one less necessary, though prefix syntax keeps calls readable. Some community macro libraries add threading macros, but they are not part of the language. | NoStandard Lua has no pipe operator, and Luerl follows standard Lua, so there is none. Chaining is done with nested calls or method-call syntax (obj:m()), the latter only on values set up as objects. |
| Pattern matching as control flowDestructure and branch on the shape of data instead of using flags/ifs. | YesPattern matching is fundamental: it drives =, function clause selection, case, and receive, and works on tuples, lists, maps and binaries, with optional guards (when). The = operator itself is a match, not assignment. |
YesPattern matching powers =, multi-clause functions, case, cond (via guards), with, and receive. The pin operator ^ matches against an existing value, and guards add extra conditions to a clause. |
Yescase is the central control-flow construct and the compiler performs exhaustiveness checking: a match that misses a possible variant of a type is a compile error. Patterns destructure custom types, tuples, lists and strings, with guards via if. |
YesPattern matching is pervasive, exposed through case, match-lambda, function clauses, let with patterns, and receive, with guards via when. It compiles to the same Core Erlang matching machinery. |
NoStandard Lua has no pattern matching as a control-flow construct (its string.match is text/regex-style matching, a different concept). Branching uses if/elseif, and multiple-assignment can destructure a fixed number of return values, but there is no shape-based dispatch. |
| No shared mutable stateConcurrent work shares nothing; isolated processes communicate by messages. | YesThe actor model is core: lightweight BEAM processes share no memory and communicate only by asynchronous message passing, with each process owning private, immutable data. This share-nothing design is what makes Erlang concurrency safe and fault-tolerant. | YesRuns on the same share-nothing BEAM: processes (often via Task, Agent, GenServer) are isolated and exchange messages, so there is no shared mutable memory and thus no locks or data races for ordinary state. |
YesOn the BEAM target Gleam uses the same isolated-process, message-passing model (via OTP libraries), and immutability is statically enforced, so there is no shared mutable state. On the JavaScript target it follows JS's single-threaded model instead. | YesAs a native BEAM language, LFE uses spawn / send (!) / receive exactly like Erlang: share-nothing processes communicating by message passing, with immutable per-process data. |
PartialLuerl is a Lua interpreter, not a BEAM language, so it has no processes of its own and a Lua chunk runs inside whatever Erlang process the host calls it from. Sharing nothing across instances comes from the host: each separate Luerl state is an independent immutable Erlang term, so two BEAM processes each holding their own state cannot mutate each other's. |
Runtime & Interop
| Feature | Erlang | Elixir | Gleam | LFE | Luerl |
|---|---|---|---|---|---|
| Compiles to BEAM bytecodeWhether the language compiles to `.beam` modules that run natively on the Erlang virtual machine. | YesThe reference case: the erlc compiler turns .erl source into .beam bytecode for the BEAM (Bogdan/Björn's Erlang Abstract Machine), the VM Erlang was built around. Internally it lowers through Core Erlang before code generation. |
YesElixir source is compiled to BEAM bytecode (the Elixir.-prefixed .beam modules), so it shares Erlang's runtime, schedulers, and OTP. The mix build tool drives compilation. |
YesGleam's Rust-written compiler emits Erlang source, which is then compiled by erlc to .beam. So Gleam reaches BEAM bytecode via Erlang as an intermediate, and the running artifacts are ordinary BEAM modules. (Gleam can alternatively target JavaScript instead of the BEAM.) |
YesLFE (Lisp Flavoured Erlang) compiles its S-expressions directly to Core Erlang, producing 100% Erlang-compatible .beam files. Because it sits on the same compiler back end, OTP/compiler advances flow straight through. |
NoLuerl does not compile Lua to BEAM bytecode. It is a Lua interpreter written in Erlang: Lua source is parsed and compiled to Luerl's own internal instruction form and then executed by Erlang code. The only .beam files involved are Luerl's own Erlang modules, not the user's Lua. |
| Calling Erlang/OTPHow directly the language can call existing Erlang modules and use OTP behaviours (gen_server, supervisor, etc.). | YesNative. Erlang is the platform: calls to lists, gen_server, supervisor, and every other module/behaviour are first-class with no bridging at all. |
YesSeamless and zero-cost. Erlang modules are plain atoms in Elixir, written as :module.function(args); OTP behaviours like GenServer and Supervisor are thin wrappers over the Erlang ones. |
YesErlang functions are called via external function bindings (@external(erlang, "module", "fun")), which let you give an existing Erlang function a Gleam type signature. The call itself is direct (no marshalling), but because Gleam is statically typed you must supply types at the boundary, and unsound types there are unchecked. OTP is wrapped by Gleam's type-safe gleam_otp library. |
YesZero-penalty, because LFE emits Core Erlang. Erlang modules are called with (module:function args) form and OTP behaviours are used directly, so LFE mixes freely with Erlang and Elixir in one release. |
PartialThe host Erlang/Elixir program calls Erlang/OTP normally, and it can expose chosen Erlang functions to the Lua sandbox. But Lua code running inside Luerl cannot reach arbitrary Erlang modules or spawn OTP processes itself - only what the embedder explicitly injects is visible, which is by design for sandboxing. |
| NIFs, ports & C FFIAccess to native code via NIFs (loaded C/Rust functions), ports (external OS processes), and the C node/driver mechanisms. | YesFirst-class. Erlang defines NIFs (erl_nif), ports and port drivers, and the C node interface (erl_interface/ei); these are the canonical mechanisms every other BEAM language reuses. |
YesUses the same BEAM facilities. NIFs and ports are available directly; the community Rustler library makes writing safe NIFs in Rust ergonomic, and Port/System.cmd wrap ports/OS processes. |
Via libraryGleam has no native FFI of its own, but on the Erlang target it reaches NIFs and ports by binding to the relevant Erlang functions through @external. In practice you call an Erlang/Rustler NIF or open a port via an external declaration; there is no Gleam-level C FFI. |
YesFull access to the BEAM's native mechanisms because LFE compiles to Core Erlang: erlang:load_nif, ports, and drivers are all callable directly, just in S-expression syntax. |
NoLuerl deliberately uses no NIFs, ports, or C dependencies - it is pure Erlang. Lua running inside Luerl has no native FFI and cannot load C libraries; that purity is what gives it strong sandboxing. (The host application around it can still use NIFs/ports normally.) |
| JavaScript compile targetAbility to compile the same source to JavaScript so it runs in the browser, Node, Deno, etc. - off the BEAM. | NoErlang has no official JavaScript backend; it targets the BEAM. (Third-party transpilers have existed but are not part of Erlang/OTP and are not mainstream.) | NoElixir compiles only to BEAM bytecode; there is no JavaScript target. Browser interactivity is instead delivered server-side via Phoenix LiveView rather than by compiling Elixir to JS. | YesA defining Gleam feature: the same Gleam code can compile to JavaScript as an alternative to Erlang, running in browsers and on Node/Deno/Bun. It emits TypeScript definitions for safe interop, and you choose the backend per build (--target javascript). |
NoLFE targets Core Erlang / the BEAM only; it has no JavaScript backend. | NoNot applicable - Luerl is an Erlang library that interprets Lua on the BEAM; it produces no JavaScript and has no compile target other than its own in-VM execution. |
| Metaprogramming & macrosCompile-time code generation: macros that transform the AST (`quote`/`unquote`, Lisp macros) versus none. | PartialErlang has the C-style preprocessor (-define, -include, ?MACRO) and parse transforms that rewrite the abstract forms at compile time, but it has no hygienic, expression-level macro system. Parse transforms are powerful but low-level and rarely used directly. |
YesFirst-class hygienic macros: quote captures code as AST and unquote injects values, and defmacro defines compile-time transformations. So much of the language (defmodule, if, unless) is itself built from macros. |
NoGleam intentionally has no macros or metaprogramming. Keeping the language tiny and predictable is an explicit design goal, so there is no quote/unquote and no compile-time code generation; code does only what it literally says. |
YesHomoiconic Lisp: code is data, and LFE offers full Common-Lisp-style (unhygienic) macros via defmacro with quasiquote/unquote (`, ,, ,@). This brings true compile-time metaprogramming to the BEAM as the native idiom. |
NoThe Lua language has no macro system, so neither does Luerl. (Lua does support runtime metaprogramming via metatables, but that is dynamic dispatch, not compile-time macros.) |
| Embedding & sandboxed scriptingRunning the language as an embedded, sandboxed scripting engine inside a host BEAM application for user-supplied code. | PartialErlang can evaluate code at runtime (erl_scan/erl_parse/erl_eval) and load modules dynamically, but it is not a sandbox: evaluated Erlang has full VM access. Safe execution of untrusted code is not a built-in goal. |
PartialElixir can evaluate strings at runtime with Code.eval_string/2, but like Erlang this is not sandboxed - evaluated code can do anything. For safely running untrusted scripts, Elixir apps commonly embed Luerl instead. |
NoGleam is an ahead-of-time compiled language with no runtime eval; it is not designed to be embedded as a scripting engine for user-supplied code. |
PartialBeing a Lisp, LFE can read and eval forms at runtime and has a capable REPL, so it supports interactive code loading - but, as with Erlang, this is not a security sandbox for untrusted input. |
YesThis is Luerl's whole purpose. It is embeddable: a host Erlang/Elixir app calls luerl:do/2 (or eval/call) to run Lua, threading the entire Lua state through as one immutable data structure. Untrusted scripts run sandboxed inside the BEAM's safety guarantees, the host controls exactly which functions Lua sees, and there are no NIFs/C to escape through. Ideal for config, game logic, and user plugins. |
| Exposing host functions to scriptsWhether the host can inject native (BEAM) functions for embedded scripts to call, and read/write the script's variables. | YesAs the host language, Erlang exposes any function by passing funs/modules around; with Luerl it can register Erlang funs into the Lua environment and read back Lua values. Outside an embedded engine the concept is just ordinary function passing. | YesAn Elixir host embedding Luerl can inject Elixir/Erlang functions into the Lua state and read or set Lua globals, so user scripts call back into the application through a controlled API surface. | NoNot applicable - Gleam is not used as an embedded scripting host with a runtime callback/eval model. | YesLike Erlang, LFE can pass funs and modules into any embedded engine (e.g. Luerl) and exchange values with it, since it has the same runtime access to the BEAM. | YesCore to the embedding model: the host registers Erlang funs into Lua tables (luerl:set_table_keys/3) so Lua scripts can call host code, and it can read/write Lua variables (luerl:get_table_keys/set_table_keys). Lua and the BEAM exchange values both directions through the explicit Luerl state. |
Reliability & Errors
| Feature | Erlang | Elixir | Gleam | LFE | Luerl |
|---|---|---|---|---|---|
| "Let it crash" philosophyLet a process die on unexpected errors and let a supervisor restart it from a clean state, instead of coding defensively for every fault. | YesThe defining idiom of the platform. Joe Armstrong's thesis argued you cannot make a single process fault-tolerant, so you isolate work in cheap processes and let them crash rather than program defensively. A crashing process emits an exit signal; its supervisor restarts it. You write the happy path and let the OTP supervision tree handle the rest. | YesInherits the philosophy wholesale via OTP. Elixir code uses pattern matches and bang (!) functions that raise on failure, trusting supervisors to recover. The community phrasing is sometimes softened to "let it crash, then restart clean", but the mechanism is identical to Erlang. |
PartialRuns on the BEAM so processes can crash and be supervised, but Gleam's design philosophy is the opposite: its sound static types and mandatory Result handling push you to make impossible states unrepresentable so the happy path is the only path. You let crashes happen for truly unexpected faults (panic, let assert), not as a routine error strategy. |
YesLFE is Lisp syntax over the same Erlang VM and OTP, so "let it crash" applies unchanged. A failed (case ...) match or (error ...) raises, the process exits, and an OTP supervisor restarts it. |
NoLuerl is a Lua 5.x interpreter written in Erlang; Lua scripts run inside an Erlang process, not as BEAM processes of their own. A Lua runtime error surfaces as an Erlang error/throw in the host. The "let it crash" pattern belongs to the Erlang process that embeds Luerl - Lua code itself has no processes to crash or supervise. |
| Supervisor restart strategiesA supervisor process watches child processes and restarts them under a chosen policy (one_for_one, one_for_all, rest_for_one, simple_one_for_one) with restart intensity limits. | YesCore OTP behaviour. supervisor callbacks return child specs and a strategy: one_for_one (restart only the dead child), one_for_all (restart all siblings), rest_for_one (restart the dead child and those started after it), plus simple_one_for_one/DynamicSupervisor for many identical workers. Restart intensity (max restarts in a window) escalates a crash up the supervision tree if a child keeps dying. |
YesFirst-class via the Supervisor and DynamicSupervisor modules with the same strategies (:one_for_one, :one_for_all, :rest_for_one). Children are declared as a list of child specs; max_restarts/max_seconds bound the restart intensity. Phoenix and most libraries ship their own supervision trees. |
Via libraryGleam has no supervisor in the language; it uses OTP through the official gleam_otp library, which provides type-safe wrappers over Erlang's supervisor (e.g. static_supervisor with OneForOne/OneForAll/RestForOne). The strategies are the BEAM ones, exposed with Gleam types and a builder API. |
YesUses the OTP supervisor behaviour directly from Lisp. The child spec map and {strategy, intensity, period} tuple are written as LFE data; all four strategies are available since it is the same OTP machinery. |
NoLua scripts running under Luerl are not BEAM processes and cannot be children of an OTP supervisor. You would supervise the Erlang process that hosts the Luerl state; if you want a Lua worker restarted, the surrounding Erlang/Elixir code must re-create the Luerl VM state on restart. Luerl provides no supervision primitives of its own. |
| Links & monitors for failure propagationLinks make two processes die together via exit signals; monitors give a one-way DOWN notification when a watched process dies, without coupling lifetimes. | Yeslink/1 creates a bidirectional bond - when one process exits abnormally the signal propagates and kills the linked process (unless it traps exits with process_flag(trap_exit, true), turning signals into {'EXIT', Pid, Reason} messages). monitor/2 is one-way and non-propagating: the watcher receives a {'DOWN', Ref, process, Pid, Reason} message. Supervisors are built on links + trap_exit. |
YesExposed as Process.link/1, Process.monitor/1, and spawn_link/spawn_monitor. Trapping exits is Process.flag(:trap_exit, true), converting linked exits into {:EXIT, pid, reason} messages; monitors deliver {:DOWN, ref, :process, pid, reason}. GenServer and Task use these under the hood. |
Via libraryAvailable through gleam_erlang/gleam_otp: process.link, process.monitor, and spawn_link. Monitor results arrive as typed messages on a Subject, fitting Gleam's selector-based receive. The underlying mechanism is BEAM links/monitors; Gleam adds type safety around the message payloads. |
YesCalls the same BIFs from Lisp: (link pid), (monitor 'process pid), (spawn_link ...), and (process_flag 'trap_exit 'true). 'DOWN and 'EXIT tuples are matched in (receive ...) just as in Erlang. |
NoLua has no concept of processes, so there is nothing to link or monitor. A Luerl state is just an Erlang term manipulated by a host process. Any linking/monitoring happens between Erlang processes around Luerl, not within Lua code. |
| Structured exception handling (try/catch/rescue)Constructs to catch raised exceptions, throws, and exits in-process, with cleanup that always runs (after/ensure). | Yestry Expr of Pattern -> ... catch Class:Reason:Stack -> ... after Cleanup end catches all three exception classes (error, throw, exit). The older catch Expr also works. Raised with error/1, throw/1, exit/1. Idiomatically used sparingly - most failures are left to crash. |
Yestry/rescue/catch/after with named exception structs; rescue matches Elixir exceptions (rescue e in RuntimeError ->), catch handles :throw/:exit, and after always runs. raise/reraise create exceptions. Like Erlang, idiomatic code prefers tagged tuples and supervisors over try/rescue. |
NoGleam has no exception-handling syntax - there is no try/catch/rescue in the language. Recoverable failures are returned as Result/Option values and handled by the type system. Unrecoverable faults use panic or let assert, which crash the process (caught only by a supervisor, not by user code). On the JS target this maps to thrown errors but you still don't catch them in Gleam. |
YesProvides Lisp forms over the Erlang machinery: (try ... (case ...) (catch ...) (after ...)) and the error/throw/exit functions. Same three exception classes and after cleanup semantics as Erlang. |
PartialLua itself has no try/catch; protected calls are done with pcall/xpcall and errors are raised with error(...). Luerl implements these Lua semantics, so a script can guard risky calls. There is no finally-style block in Lua (you emulate cleanup manually). The protection happens entirely within the embedded Lua state, not via BEAM try/catch. |
| Error-value conventions: {ok,_}/{error,_} vs Result vs pcallHow recoverable errors are represented as ordinary return values rather than exceptions: tagged tuples, a typed Result, or Lua's protected-call status flag. | YesThe convention is tagged tuples: {ok, Value} on success and {error, Reason} on failure, matched with case. There is no compiler enforcement - it is a discipline. Some functions instead return a bare value and crash on failure (the !-style happy-path variant). |
YesSame {:ok, value} / {:error, reason} tuple convention, idiomatically threaded with case, with, and the bang/non-bang pairing (File.read/1 returns a tuple; File.read!/1 raises). Not type-checked, but pervasive across the standard library. |
YesUses the statically-typed Result(value, error) from the prelude (Ok(x) / Error(e)) - and Option(value). Because Gleam has no exceptions, the type system forces you to handle the Error branch (an unhandled Result is a type/exhaustiveness issue), composed with result.try, result.map, and use. This is the soundest of the five error models. |
YesFollows the Erlang tagged-tuple convention exactly, written as Lisp tuples `#(ok ,value) / `#(error ,reason) and matched in (case ...). No static enforcement, same discipline as Erlang. |
PartialLua's idiom differs: functions return nil, errmsg on failure (the multi-return convention), and protected execution uses pcall, which returns false, err instead of propagating. Luerl supports both. There is no {ok, _} tuple or typed Result - error handling is the pcall/multi-return status-flag style of Lua 5.x. |
| Hot code loading / live upgradesReplacing a module's code in a running system without stopping it, including coordinated multi-module release upgrades (OTP relups, appups). | YesA built-in VM capability: the code server keeps two versions (current and old) of each module; a fully-qualified call (Module:fun(...)) switches a process to the new version. OTP release_handler plus .appup/.relup scripts orchestrate versioned upgrades, and gen_server's code_change/3 callback migrates process state. This is how the canonical "nine nines" telecom uptime was achieved. |
PartialThe BEAM capability is fully present and code_change/3 works, but hot upgrades are not the common deployment model in the Elixir community. They are possible via distillery/relx-style appups/relups, yet most teams deploy with blue-green/rolling restarts instead. So the runtime support is first-class while the practical tooling and idiom make it less than turnkey. |
PartialGleam compiles to Erlang and runs on the BEAM, so module reloading mechanically works. However Gleam provides no release-upgrade tooling (no appup/relup generation) and offers no code_change-style typed migration story, so coordinated live upgrades are not a supported workflow. Reloading a single module at the REPL/dev is feasible; production hot upgrades are not idiomatic. |
YesRuns on the BEAM and compiles to standard Erlang modules, so it uses the same code server, two-version loading, and OTP release_handler/appup/relup machinery. code_change is implemented as an ordinary OTP callback in LFE. |
Via libraryLua code in Luerl is not BEAM bytecode, so the VM's hot-loading does not apply to scripts. But because a Luerl state is a plain Erlang term, the host can hot-swap Lua by re-evaluating new source into a fresh or existing state (luerl:do/2, luerl:load/2) and replacing the state it holds - a library/host-level technique, not the BEAM's module reload. |
| Fault isolation per processEach unit of work runs in a lightweight isolated process with no shared mutable memory, so one failure cannot corrupt others; crashes are contained. | YesThe BEAM's foundational property: millions of cheap processes, each with a private heap and no shared mutable state, scheduled preemptively. A crash frees that process's heap and cannot leave a half-mutated structure visible to anyone else - the worst case is a lost message, recoverable by retry/supervision. This share-nothing isolation is what makes "let it crash" safe. | YesIdentical model - spawn, Task, and GenServer create isolated BEAM processes with private heaps and message-passing only. A crashing GenServer takes down nothing but itself (and its links), keeping faults contained to one failure domain. |
YesOn the Erlang target Gleam uses the same isolated BEAM processes via gleam/erlang/process and gleam_otp actors - each actor has its own heap and crashes independently. (On the JavaScript target there are no BEAM processes, so this per-process isolation does not exist; the guarantee is BEAM-only.) |
YesSame BEAM process model accessed from Lisp: (spawn ...) creates an isolated process with a private heap and no shared mutable state. Crash containment is exactly Erlang's. |
NoLuerl deliberately runs Lua inside a single Erlang process as a sandboxed, purely-functional state value - it has no Lua-level processes, threads, or coroutine-based isolation between scripts. Isolation comes only from the host: run each Luerl state in its own Erlang process to get BEAM-level fault isolation. Lua code by itself shares the one state and one failure domain. |
Tooling & Ecosystem
| Feature | Erlang | Elixir | Gleam | LFE | Luerl |
|---|---|---|---|---|---|
| Build toolThe standard way to compile a project, manage deps, and run tasks. | Yesrebar3 is the de-facto build tool: it compiles, fetches deps from Hex/Git, runs tests, and builds releases. Driven by a rebar.config file. The older erlc/Makefile and erlang.mk workflows also remain in use. |
Yesmix ships with Elixir and is the canonical build tool: mix.exs declares the project and deps, and mix compile/mix run/mix test drive everything. Custom tasks are just modules under Mix.Tasks. |
YesThe single gleam binary (written in Rust) is the build tool: gleam build compiles to Erlang or JS, gleam.toml declares the project, and gleam add manages Hex deps. Build tooling is first-class and batteries-included. |
YesLFE projects build with rebar3 via the rebar3_lfe plugin (it adds rebar3 lfe compile, repl, run, etc.). Because LFE compiles to BEAM modules, the whole Erlang build/release machinery applies. |
Via libraryLuerl has no build tool of its own - it is an Erlang library (luerl) you add to a host Erlang/Elixir project. You build with that host's tool (rebar3/mix); Lua scripts themselves are plain .lua files loaded at runtime, not compiled by a build step. |
| Hex package managerPublishing and fetching reusable libraries from a shared registry. | YesHex (hex.pm) is the shared BEAM package registry. rebar3 resolves and fetches Hex deps declared in rebar.config, and rebar3 hex publish ships Erlang libraries to it. |
YesHex is integrated into mix: deps go in mix.exs, mix deps.get fetches them, and mix hex.publish publishes. Hex was created by the Elixir community and is its primary registry. |
YesGleam publishes to and consumes the same Hex registry. gleam add wisp records the dep in gleam.toml, and gleam publish pushes a package. Gleam packages interoperate with Erlang/Elixir Hex packages. |
YesLFE uses Hex through rebar3 just like Erlang - deps are listed in rebar.config, and LFE libraries are published to hex.pm as ordinary BEAM packages consumable from any BEAM language. |
Via libraryLuerl itself is distributed as a Hex/Erlang package ({luerl, ...}), but Lua code has no Hex equivalent - Lua's own ecosystem (LuaRocks) is unrelated and not used here; you vendor .lua files into the host project. |
| Shell / REPLAn interactive prompt for evaluating code and inspecting a running node. | YesThe erl shell is a full interactive prompt and a live window into a running BEAM node - you can hot-load code, observe processes, and run an attached production node. rebar3 shell starts it with your app loaded. |
YesIEx (iex) is Elixir's interactive shell, with history, tab-completion, h/i helpers, a break/pry debugger, and remote-shell access into running nodes. iex -S mix boots it with the project loaded. |
Partialgleam shell drops you into the underlying Erlang erl shell with your project compiled and available, so you can call your Gleam functions as Erlang module:function(...). There is no native, type-checked Gleam REPL. |
Yeslfe (or rebar3 lfe repl) starts a genuine Lisp REPL reading LFE s-expressions, with (slurp ...) to load files and macro expansion at the prompt - a first-class interactive experience on the BEAM. |
Via libraryLuerl has no standalone shell; you get a Lua REPL only by writing a small Erlang loop that feeds lines to luerl:do/2. In practice you experiment from the host's erl/iex by calling the Luerl API. |
| Code formatterAn official tool that rewrites source to a canonical style. | PartialErlang has no single canonical formatter shipped with OTP. The community standard is erlfmt (a rebar3 plugin / standalone, originally from WhatsApp), widely used but third-party rather than official. |
Yesmix format is built into Elixir and enforces a single official style driven by .formatter.exs. It is ubiquitous in the ecosystem and integrates with editors on save. |
Yesgleam format is built into the gleam binary with a single, non-configurable canonical style (zero options by design), so all Gleam code looks the same. |
PartialThere is no official LFE formatter. Formatting relies on editor Lisp indentation (paredit/lisp-mode) and conventions; some lean on lfe_io pretty-printing rather than a dedicated source formatter. |
NoLuerl ships no formatter - it executes Lua source, it doesn't format it. Any styling of .lua files would come from external Lua tools (e.g. stylua), which are outside the BEAM ecosystem. |
| Documentation generationGenerating browsable HTML API docs from source and doc comments. | YesHistorically edoc generates HTML from %% @doc tags. Modern OTP (27+) adds built-in -doc attributes and ex_doc is now the recommended HTML generator for Erlang too, sharing Elixir's polished output. |
YesExDoc turns @doc/@moduledoc attributes and Markdown into a polished, searchable HTML site; mix docs runs it. Doc strings are first-class data stored in the BEAM chunks and readable from IEx via h. |
Yesgleam docs build generates HTML API docs from /// doc comments, and gleam publish automatically uploads them to HexDocs. Documentation is a built-in part of the toolchain. |
PartialLFE can produce docs via the rebar3_lfe plugin and lodox (an ExDoc-style generator for LFE), but it is less polished than ExDoc and not as universally adopted. |
NoLuerl provides no Lua-doc generator. Documenting Lua scripts would require external Lua tooling (e.g. LDoc), which is unrelated to the BEAM doc tools. |
| Test frameworkBuilt-in or standard frameworks for unit and integration tests. | YesOTP ships EUnit (lightweight unit tests) and Common Test (ct, for large integration/system suites). rebar3 eunit and rebar3 ct run them; property testing is available via PropEr/proper. |
YesExUnit ships with Elixir and is the standard test framework, run with mix test. It supports async tests, doctest (tests embedded in @doc), and rich assertion diffs out of the box. |
Yesgleeunit is the conventional test library (a thin Gleam wrapper over Erlang's EUnit, also running on the JS target); gleam test discovers and runs the *_test functions in test/. |
YesLFE has ltest, a native testing framework over EUnit/Common Test with Lisp-friendly macros (deftest, is-equal, ...). It can also call EUnit directly since LFE compiles to BEAM. |
Via libraryLuerl has no Lua test framework. You test Luerl behaviour from the host side with Erlang's EUnit/Common Test or Elixir's ExUnit, asserting on the results of luerl:do/2 calls. |
| LSP / editor supportA Language Server and editor integrations for completion and diagnostics. | YesErlang LS and the newer ELP (Erlang Language Platform, by WhatsApp/Meta) provide completion, navigation, and diagnostics; combined with long-standing Emacs erlang-mode and editor plugins. |
YesMature LSP support via ElixirLS and the newer Expert/lexical/next-ls servers, giving completion, inline diagnostics, formatting on save, and a debugger across VS Code, Emacs, Neovim, etc. |
YesGleam ships a language server built into the gleam binary (gleam lsp). Because the language is statically typed, it gives precise type-aware completion, hover, rename, and error reporting with no extra install. |
PartialNo dedicated LFE language server. Editing relies on generic Lisp editor support (paredit, structural editing, REPL integration) plus the underlying Erlang tooling for the compiled output. | NoLuerl has no editor tooling of its own. You'd edit .lua files with a generic Lua LSP (e.g. lua-language-server) and the host project's Erlang/Elixir LSP for the surrounding code. |
| Releases & deploymentPackaging the app plus the runtime into a self-contained deployable artifact. | YesOTP releases bundle your apps with a trimmed BEAM runtime into a self-contained, startable package, with optional hot code upgrades (relups). Built via rebar3 release (using relx). |
Yesmix release builds an OTP release containing your code and the ERTS, runnable with no Elixir/Erlang installed on the target. Supports runtime config and remote console/eval commands. |
PartialGleam compiles to Erlang and produces an entrypoint, but has no native release command; for production OTP releases you typically generate the Erlang and build with rebar3/relx (or run via the gleam-produced erl invocation). The JS target deploys like any Node app. |
YesLFE compiles to standard BEAM modules, so it uses the same OTP release machinery as Erlang: rebar3 release/relx packages an LFE app with the runtime, hot upgrades included. |
Via libraryLuerl has nothing to release on its own - it is embedded in a host BEAM app. The host's release (via rebar3/mix) includes the luerl dependency and any bundled .lua scripts; Luerl runs no node of its own. |
Types & Data
| Feature | Erlang | Elixir | Gleam | LFE | Luerl |
|---|---|---|---|---|---|
| Static vs dynamic typingAre types checked by the compiler before the program runs, or only as tagged values at runtime? | NoDynamically typed. Every value carries its type tag at runtime and the BEAM checks it then; there is no compile-time type checker. Typing is strong - no implicit coercions, so 1 + "x" raises a badarith error rather than guessing - but type errors surface as runtime crashes (caught by supervisors), not compiler errors. |
NoDynamically + strongly typed, like the Erlang it builds on. As of Elixir 1.17+ the compiler ships a set-theoretic type system that infers and warns about some local type errors, but it is gradual and advisory - it does not (yet) statically type-check whole programs or block compilation on a type mismatch. | YesStatically typed. The single BEAM language here that type-checks the whole program at compile time. Types are checked before any code runs, so a type error stops the build; there is no runtime type tag inspection needed for correctness. It compiles to Erlang (and to JavaScript). | NoDynamically + strongly typed. Lisp Flavoured Erlang shares Erlang's runtime and value model exactly, so it has the same tagged, dynamically-checked values and no compile-time type checker. | NoDynamically typed, because it is Lua 5.x (implemented in Erlang). Lua's eight runtime types (nil, boolean, number, string, function, table, userdata, thread) are checked at runtime. Note Lua is weakly typed in places - e.g. strings coerce to numbers in arithmetic - unlike Erlang's strong dynamic typing. |
| Type inferenceCan the compiler work out the types of expressions without explicit annotations? | NoNo type inference in the compiler - there are no static types to infer. Dialyzer (a separate tool) performs success typing inference for discrepancy analysis, but that is optional offline analysis, not compile-time inference. | PartialThe new set-theoretic type system (1.17+) infers types of function arguments and bodies to emit warnings, and this inference is steadily expanding release by release. It is still gradual/advisory rather than a full Hindley-Milner pass over the program. | YesFirst-class sound, complete type inference (a Hindley-Milner-style algorithm). Annotations on top-level functions are conventional but the compiler can infer every type, including across modules, with no Any/dynamic escape hatch leaking in silently. |
NoNo type inference - same dynamic runtime as Erlang. Dialyzer can be run over compiled LFE/BEAM modules for after-the-fact success typing, but nothing is inferred at compile time. | NoLua has no static types, so there is nothing to infer. Luerl simply executes Lua dynamically inside the Erlang VM. |
| Type specs & DialyzerOptional type annotations (-spec / @spec) plus a static analyzer that flags type discrepancies. | YesErlang invented this approach: -type and -spec attributes annotate functions, and Dialyzer uses success typing over the PLT to find provable discrepancies (dead clauses, type clashes) without rejecting correct-but-unannotated code. Specs are optional and not enforced at runtime. |
YesElixir exposes the same machinery as @type and @spec, which compile to Erlang's type/spec attributes, so Dialyzer (usually via Dialyxir) works on Elixir too. The newer built-in set-theoretic types complement, but do not replace, Dialyzer. |
NoNot applicable in this form. Gleam does not use -spec/Dialyzer because its own compiler already type-checks everything statically and soundly - Dialyzer's optional, best-effort analysis would be redundant and weaker. |
PartialLFE supports type and spec declarations (e.g. defspec/deftype forms) that emit Erlang's spec attributes, so Dialyzer can analyse the resulting BEAM files. Tooling and docs are less polished than Erlang's, hence partial. |
NoLua has no type specs and Dialyzer analyses BEAM bytecode, not Lua source. Luerl scripts are plain Lua interpreted at runtime, so there is nothing for Dialyzer to inspect. |
| Sum / union types & custom typesUser-defined tagged variants that model 'one of several shapes' of data. | PartialThere is no nominal ADT value construct; the idiom is tagged tuples like {ok, V} / {error, R} matched at runtime. You can describe the union in a -type for Dialyzer (t() :: {ok, term()} | {error, term()}), but the compiler does not enforce exhaustiveness or construction. |
PartialSame as Erlang at the value level - tagged tuples / atoms - with @type t :: {:ok, term} | {:error, term} for Dialyzer. Structs give nominal records but not closed sum types; the set-theoretic system adds union types to the analysis, still without enforced exhaustiveness. |
YesFirst-class custom types (sum/algebraic types): declare a type with multiple constructors, each carrying typed fields. The compiler tracks the variant and enforces that case covers every one. Result(a, e) and Option(a) in the stdlib are ordinary custom types. |
PartialUses Erlang's runtime model, so sum types are the same tagged-tuple convention - (tuple 'ok value) / (tuple 'error reason) - optionally described with a deftype union for Dialyzer. No compiler-enforced variants. |
NoLua has no type declarations at all. The closest pattern is a table with a tag/kind field that you switch on by hand; nothing is checked, constructed, or enforced by the language. |
| Records vs structs vs maps vs Lua tablesThe language's main keyed/composite data structures for named fields and key-value data. | YesTwo distinct tools: records (-record(...), compile-time syntactic sugar over tagged tuples with named fields, fixed at compile time) and maps (#{k => v}, dynamic key-value with O(log n)/HAMT-backed access, the modern default for structured data). |
YesStructs are the headline feature - defstruct over a tagged map keyed by :__struct__, giving named fields, default values and compile-time field checking. Plain maps (%{}) cover dynamic data; Erlang records are available via the Record module but rarely used. |
YesUses custom-type records: a constructor with named, typed fields (accessed as user.name), checked statically. For dynamic string-keyed data it offers a typed Dict in the stdlib rather than an open struct, keeping field access type-safe. |
YesHas full access to Erlang records via defrecord (generating make/match/field macros) and to maps with map/mref/mset forms. Same underlying tuple-record and map representations as Erlang. |
YesThe table is Lua's sole composite type and does everything: records/structs (string keys), arrays (1-based integer keys), and maps, with optional metatables for OO-style behaviour. Luerl represents these tables inside Erlang and maps them to Erlang terms at the interop boundary. |
| Exhaustive pattern matchingDoes the compiler verify that a match/case covers every possible shape of the value? | NoPattern matching is pervasive and powerful, but not checked for exhaustiveness by the compiler. A missing clause is only discovered at runtime as a case_clause/function_clause error. Dialyzer can sometimes flag unreachable or impossible clauses, but does not prove totality. |
PartialSame runtime semantics as Erlang - a missing branch raises CaseClauseError at runtime. The compiler emits warnings for some clearly-missing/redundant clauses (improving with the set-theoretic types), but does not guarantee exhaustiveness across user-defined data. |
YesExhaustiveness is enforced at compile time. Every case must cover all variants of its custom type (and all list shapes); omitting a branch is a compile error, not a runtime crash. There is no fall-through and no need for a defensive catch-all. |
NoInherits Erlang's matching, including the lack of exhaustiveness checking: an uncovered pattern in a case or function head is a runtime case_clause/function_clause error. |
NoLua has no pattern matching at all (its string.match is regex-like text matching, unrelated). Structural matching is emulated with if/elseif chains on a tag field, so 'exhaustiveness' is entirely the programmer's responsibility - a missed case just silently returns nil. |
| No-null guaranteeDoes the type system make the absence of a value explicit and impossible to ignore (no billion-dollar mistake)? | NoThere is no null/nil type hole per se, but absence is modelled with conventions like the undefined atom or {ok, V} | error. Nothing forces you to handle the absent case - forgetting to is a runtime crash, not a type error. |
Nonil is a first-class value that can appear almost anywhere, and many functions return nil on absence. Optionality is conventional ({:ok, v}/:error, or Access/Map.get defaults) but not enforced, so unexpected nil is a common runtime error source. |
YesNo null and no nil - guaranteed by the type system. Absence must be expressed explicitly with Option(a) (Some/None), and exhaustiveness checking forces you to handle the None case. This eliminates the 'billion-dollar mistake' entirely. |
NoSame as Erlang - uses the undefined atom and tagged tuples for absence by convention, with no type-level guarantee that the empty case is handled. |
NoThe opposite extreme: nil is everywhere in Lua. Reading a missing table key yields nil, unset variables are nil, and there is no static check forcing you to account for it - a classic source of silent bugs. |