Erlang (OTP 29)
This assessment applies the coupling account — coupling as access to a shared resource, and the coupling floor [1], [2] — and the knowledge-embodiment theorem [3]; the language is assessed against its official reference [4].
1 Assessment
This page assesses Erlang in five parts: core coupling — what the language’s own constructs force; ecosystem coupling — what the surrounding platform, tooling, and community cost; engineering readiness — whether the artifact can be reproduced, verified, observed, worked in quickly, connected to other languages, and governed openly; the profile — the unscored facts a score cannot express; and the scoring detail — the mechanism marks and the arithmetic behind every score. A ✅ bullet marks a construct that helps. A ⚠️ or ❌ bullet marks a cost; the scoring model does not price every cost named here.
Core coupling 69/100 · Ecosystem coupling 76/100 · Engineering readiness 72/100
1.1 Core coupling
1.1.1 Type
- ❌ Dynamic typing: A value carries its type at run time, so a type fault appears when the expression executes (T1)
- ❌ No declared parameter or return types:
-specis optional metadata, and the language has no generic type parameters (T5) - ❌ No sum types: A tagged tuple such as
{ok, Value}or{error, Reason}is a convention, and no type checks the union (T4) - ❌ Dispatch is checked at run time:
Mod:Fun(Args)resolves when called, so a missing function fails there rather than at compile time (T3) - ✅
-specand Dialyzer give optional static analysis: A success-typing pass over the compiled BEAM files detects inconsistent calls - ✅ Atoms serve as named constants: An atom such as
ok,error, orundefinedis a distinct value, so the language needs no null
1.1.2 Construction
- ✅ All data is immutable: A term cannot be changed after it is built
- ✅ A pattern in the clause head selects the construction path: The arguments decide which clause runs
- ✅ Records and maps declare named structure:
-record(user, {name, age})gives compile-time named fields over a tuple - ✅ State is a value threaded through recursion: An OTP behaviour receives its state and returns the new state, so no object owns it
- ✅ A process is the unit of mutable state: A
gen_serverowns its state and exposes it by message - ⚠️ A record is a tuple: Field access is positional at run time, so the record definition must be shared by every module that uses it
1.1.3 Memory
- ❌ Collection is per-process and non-deterministic: A process collects its own heap on its own schedule (M2)
- ✅ A private heap confines a pause: A collection affects one process, and there is no global stop-the-world pause
- ✅ No manual deallocation: The VM reclaims memory, and a program never frees a term
- ✅ Off-heap binaries are reference counted: A binary above the heap threshold is shared between processes rather than copied
- ⚠️ A large live set costs more per collection: A process that accumulates state pays a higher collection cost
- ⚠️ An ETS table lives outside the collector: The table persists until its owner deletes it or dies
1.1.4 Error
- ❌ Exceptions are unchecked:
throw,error, andexitare not declared, so a caller cannot see the failure set from a signature (Er2) - ✅ “Let it crash” makes failure a supervision event: A supervisor restarts a failed process, and the normal path carries no recovery code
- ✅ Links and monitors define propagation:
link/1couples the fates of two processes, andmonitor/2reports a failure without dying - ✅ A pattern-match failure raises a defined error: An unmatched value raises
badmatch, and an unmatched clause raisesfunction_clause - ✅
try/catch/afteris available when recovery is local: Acatchclause separatesthrow,error, andexit - ⚠️ A restart resets process state: Supervision restores the process, not the data it held, so state must be recoverable
1.1.5 Concurrency
- ✅ The actor model is the core of the language: Processes share no memory and communicate by message
- ✅
spawn,link, andmonitorare primitives: Starting and observing a process needs no library - ✅ The BEAM schedules preemptively by reductions: A process cannot hold a scheduler indefinitely
- ✅ Messages are immutable values: A message is copied or shared by reference count, never aliased mutably
- ✅ Distribution is built into the VM:
net_kernelandepmdlet a process send to a node as if it were local - ⚠️ Distribution is designed, not automatic: Node failure, network partition, and message loss must be handled by the application
1.1.6 Platform
- ✅ The BEAM is portable and open source: The Erlang runtime runs on Linux, macOS, Windows, and BSD
- ✅ Hot code loading is a runtime feature:
code:load_file/1replaces a module while the system runs - ✅ A release bundles the runtime and the application:
release_handlerandreltoolproduce a deployable node - ✅ Distribution across nodes is a platform feature: Nodes discover each other and pass messages without an external broker
- ⚠️ The runtime must be present: An Erlang program runs on the VM, so the artefact is not a native binary on its own
1.1.7 Trade-offs (not coupling)
- The syntax is Prolog-derived: The form is unfamiliar to most developers; this is a learning property, not a coupling
- Distribution is a design task: The language supplies the transport, and the application decides the failure semantics
- Weak CPU-bound performance is a domain fit note: The VM is built for I/O concurrency, not for numeric loops
- Hot upgrade constrains state handling: The running system can change module code, so the application must supply a
code_change/3callback to migrate state - Atoms are named constants in place of null: Absence is carried by an atom convention such as
undefined - Records are tuples by design: The representation is a deliberate choice made for compactness and speed
1.2 Ecosystem coupling
1.2.1 Performance
- ✅ The BEAM handles very many concurrent I/O processes: A process is cheap and needs no thread
- ✅ BeamAsm compiles hot code: The JIT added in OTP 24 means bytecode is not always interpreted
- ✅ Per-process collection avoids a global pause: A collection affects one process
- ✅ Reduction scheduling bounds latency: A process yields after a fixed number of reductions
- ⚠️ CPU-bound computation is weak relative to native code: A tight numeric loop trails C or Rust
- ⚠️ A NIF runs on a scheduler thread: A native function that runs long blocks other processes on that scheduler
1.2.2 Ecosystem
- ✅ Hex.pm and
rebar3cover packages and builds: A registry, dependency resolution, and release builds are all present - ✅ OTP ships with the language: Supervision,
gen_server,application, and release handling are part of the distribution - ✅ Elixir libraries are reachable through interop: The two languages run on one VM and call each other directly
- ✅ The flagship projects are long-lived and battle-tested: RabbitMQ, CouchDB, Riak, and WhatsApp’s messaging stack are Erlang systems
- ✅ OTP is documented as a first-class topic: The OTP design principles document the behaviours
- ⚠️ The library count is smaller than the mainstream: Fewer general-purpose packages exist outside messaging and telecom
1.2.3 Evolution
- ✅ The language has kept backward compatibility for decades: Code written against an old OTP release continues to run
- ✅ Hot code loading decouples deployment from runtime: A module is replaced while the system runs
- ✅
code_change/3migrates state across a version change: An OTP behaviour converts old state to the new shape - ✅ OTP release handling gives a controlled upgrade path:
release_handlerapplies a version change to a running node - ⚠️ A hot upgrade must be designed: State migration and dependency order are the application’s responsibility
1.2.4 Domain
- ✅ Excellent fit for distributed, fault-tolerant systems: Supervision, distribution, and the actor model match the problem directly
- ✅ Proven in telecom and messaging: WhatsApp, RabbitMQ, and Discord’s infrastructure run on the BEAM
- ✅ Binary parsing is a strength: The bit syntax
<<A:8, B:16/little>>matches a protocol frame directly - ❌ Poor fit for CPU-bound numerical work: The VM is not built for dense numeric computation, so that work belongs in another language (D2)
- ⚠️ Distribution across nodes requires explicit design effort: The application must decide how to handle a lost node
1.2.5 Learning
- ❌ The learning curve is steep: A developer learns functional programming, the actor model, and OTP at once (L1)
- ❌ The Prolog-derived syntax is unfamiliar: The
.,,, and;delimiters and the term syntax differ from the mainstream (L1) - ⚠️ The surface is large: The language, the OTP behaviours, and the VM’s semantics are separate bodies of knowledge (L2)
- ✅ The official documentation is thorough: The Erlang reference manual and the OTP design principles are authoritative
- ✅ The community is established: Decades of accumulated material and answered questions exist
1.2.6 Syntax
- ✅ The term syntax is uniform: Every construct is a term, and a module is a sequence of forms ending in a period
- ✅ The delimiter roles are consistent: A comma separates expressions, a semicolon separates clauses, and a period ends a form
- ✅ Bit syntax is compact for protocols: A binary pattern states field widths and endianness in one expression
- ✅ Pattern matching is part of the function-head syntax: A clause states its cases directly
- ⚠️ The comma, semicolon, and period distinction is easy to get wrong: One misplaced delimiter changes the meaning of a form
- ⚠️ Atoms and variables differ only by case:
okis an atom andOkis a variable
1.2.7 Testing
- ✅ EUnit and Common Test ship with OTP: Unit tests and system tests are part of the standard distribution
- ✅ Property-based testing is available: PropEr and QuickCheck generate inputs from a property
- ✅ Tracing and
observerinspect a running system:dbg,recon, and the observer tool expose processes, messages, and scheduler load - ✅ Common Test runs a distributed suite: A test can start several nodes and drive them together
- ❌ Errors surface at run time: Dynamic typing defers a type fault to the executed path (G3)
- ⚠️ Dialyzer catches some faults and not all: Success typing proves the absence of some inconsistencies, not the presence of correctness
1.2.8 Security
- ✅ Process isolation contains a fault: A crashed process does not corrupt another process’s memory
- ✅ Messages are immutable values: No shared mutable state exists to race on
- ✅ The supervision tree bounds a failure: A restarted process returns the system to a known state
- ⚠️ The atom table has a fixed limit:
list_to_atom/1orbinary_to_atom/2on untrusted input can exhaust the table, whose default ceiling is about one million atoms - ⚠️
binary_to_term/1on untrusted input is dangerous: Decoding an external term can allocate a large structure unless thesafeoption is used - ⚠️ A NIF and a
Portreach unsafe code: Native code can break the VM’s guarantees
1.3 Engineering readiness
1.3.1 Reproducibility
- ❌ Builds are not byte-reproducible by default.
1.3.2 Supply chain
- ⚠️ Published packages carry no signed provenance by default.
- ⚠️ The package registry is a flat name space.
1.3.3 Observability
- ⚠️ No stable tracing and metrics standard.
1.3.4 Feedback
- ✅ No charged mechanism in this dimension.
1.3.5 Interop
- ✅ No charged mechanism in this dimension.
1.3.6 Governance
- ✅ No charged mechanism in this dimension.
1.4 Profile
Unscored, and kept separate from every score on purpose. The profile records what a mechanism price cannot express: what a running program costs, how well a coding model and an agent work in the language, and whether a safety-critical path exists.
1.4.1 Energy and cost
What this axis asks: what does a running program cost in energy and time?
1.4.2 AI and agent readiness
What this axis asks: how well does a coding model and an agent work in this language?
- ❌ Sparse training data: models write it unreliably [7].
1.4.3 Safety certification
What this axis asks: is there a certified or standards-qualified path to a safety-critical system?
- ⚠️ No certified or standards-qualified path for safety-critical systems.
1.5 Scoring detail
1.5.1 Core coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Type | T1 |
dynamic no static check | ✅ | −15 |
T2 |
weak static | — | 0 | |
T3 |
dynamic/duck dispatch | ✅ | −8 | |
T4 |
no sum types | ✅ | −4 | |
T5 |
no generics | ✅ | −4 | |
T6 |
no namespaces | — | 0 | |
T7 |
type erasure/reflection | — | 0 | |
T8 |
operator overloading | — | 0 | |
T9 |
inheritance required for polymorphism | — | 0 | |
T10 |
no null-safety | — | 0 | |
T11 |
no module boundary | — | 0 | |
T12 |
no effect tracking | ✅ | −8 | |
T13 |
async/sync colouring | — | 0 | |
T14 |
unchecked foreign boundary | ✅ | −4 | |
| Type subtotal | −15 (−43) | |||
| Construction | C1 |
global mutable state by default | — | 0 |
C2 |
no immutability by default | — | 0 | |
C3 |
constructors coupled to allocation | — | 0 | |
C4 |
no constructor/validation | — | 0 | |
| Construction subtotal | 0 | |||
| Memory | M1 |
manual deallocation | — | 0 |
M2 |
GC non-deterministic pauses | ✅ | −8 | |
M3 |
no RAII | — | 0 | |
M4 |
no ownership model | — | 0 | |
| Memory subtotal | −8 | |||
| Error | Er1 |
no structured path | — | 0 |
Er2 |
unchecked exceptions only | ✅ | −8 | |
Er3 |
errors ignored/non-exhaustive | — | 0 | |
Er4 |
no composable result type | — | 0 | |
| Error subtotal | −8 | |||
| Concurrency | K1 |
GIL/no parallel | — | 0 |
K2 |
shared mutable state default | — | 0 | |
K3 |
manual sync no race detection | — | 0 | |
K4 |
no structured concurrency | — | 0 | |
K5 |
no primitives | — | 0 | |
| Concurrency subtotal | 0 | |||
| Platform | P1 |
cannot function outside one vendor/domain | — | 0 |
P2 |
single-vendor but other uses | — | 0 | |
P3 |
legacy/discontinued platform | — | 0 | |
| Platform subtotal | 0 |
Core Dimensions score: 69/100 — total cost = −31 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.2 Ecosystem coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Performance | R1 |
interpreted no AOT/JIT | — | 0 |
R2 |
GC pauses | — | 0 | |
R3 |
JIT warmup | — | 0 | |
| Performance subtotal | 0 | |||
| Ecosystem | Ec1 |
no package manager | — | 0 |
Ec2 |
fragmented tooling | — | 0 | |
Ec3 |
small/niche/discontinued | — | 0 | |
Ec4 |
proprietary | — | 0 | |
Ec5 |
platform-locked tooling | — | 0 | |
Ec6 |
monoculture | — | 0 | |
Ec7 |
legacy burden | — | 0 | |
| Ecosystem subtotal | 0 | |||
| Evolution | V1 |
breaking changes | — | 0 |
V2 |
frozen | — | 0 | |
V3 |
discontinued | — | 0 | |
V4 |
glacial standards | — | 0 | |
| Evolution subtotal | 0 | |||
| Domain | D1 |
domain/vendor lock-in | — | 0 |
D2 |
poor domain fit | ✅ | −8 | |
D3 |
framework forces architecture | — | 0 | |
| Domain subtotal | −8 | |||
| Learning | L1 |
steep | ✅ | −8 |
L2 |
large surface/multi-paradigm | ✅ | −4 | |
L3 |
sparse docs | — | 0 | |
| Learning subtotal | −12 | |||
| Syntax | S1 |
verbose | — | 0 |
S2 |
inconsistent | — | 0 | |
S3 |
punctuation-heavy | — | 0 | |
| Syntax subtotal | 0 | |||
| Testing | G1 |
weak tooling | — | 0 |
G2 |
poor debugging | — | 0 | |
G3 |
runtime-only errors | ✅ | −4 | |
| Testing subtotal | −4 | |||
| Security | Q1 |
memory-unsafe | — | 0 |
Q2 |
dynamic/weak typing risk | — | 0 | |
Q3 |
known vulnerability/injection | — | 0 | |
| Security subtotal | 0 |
Meta Dimensions score: 76/100 — total cost = −24 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.3 Engineering readiness Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Reproducibility | Y1 |
build not reproducible | ✅ | −8 |
Y2 |
dependencies not pinned | — | 0 | |
| Reproducibility subtotal | −8 | |||
| Supply chain | Y3 |
no signed provenance | ✅ | −8 |
Y4 |
registry without namespacing | ✅ | −4 | |
| Supply chain subtotal | −12 | |||
| Observability | Y5 |
no stable tracing standard | ✅ | −8 |
Y6 |
no structured logging | — | 0 | |
| Observability subtotal | −8 | |||
| Feedback | Y7 |
slow build | — | 0 |
Y8 |
no fast in-editor analysis | — | 0 | |
| Feedback subtotal | 0 | |||
| Interop | Y9 |
no foreign-function interface | — | 0 |
Y10 |
no portable target | — | 0 | |
| Interop subtotal | 0 | |||
| Governance | Y11 |
single-vendor governance | — | 0 |
Y12 |
closed specification | — | 0 | |
| Governance subtotal | 0 |
Readiness Dimensions score: 72/100 — total cost = −28 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
Verdict: Erlang’s design yields moderate language-imposed coupling in the domain it was built for. The actor model removes shared memory from concurrency, immutability removes in-place mutation, and “let it crash” with OTP supervision moves recovery out of the business logic. Main strengths: processes are cheap and isolated, the BEAM schedules preemptively, per-process collection avoids a global pause, and hot code loading decouples deployment from runtime. Main weaknesses: the language is dynamically typed, so a type fault surfaces at run time; exceptions are unchecked; the syntax is unfamiliar; and distribution across nodes must be designed rather than assumed.
The formal development. The definitions, theorems, and proofs behind this page are published in:
- On the Nature of Coupling: All Coupling as Shared Resource Access — Zenodo, 2026 — doi:10.5281/zenodo.21925122
- Coupling as a Consequence of Cohesion: Decomposition, the Coupling Floor, and Domain-Imposed Coupling — Zenodo, 2026 — doi:10.5281/zenodo.21925201
- On the Nature of Cohesion: Cohesion as Correctness Relative to a Partitioning Rule — Zenodo, 2026 — doi:10.5281/zenodo.21703193