Anatomy of an Erlang file
Erlang is a functional, concurrent programming language designed for building scalable and fault-tolerant systems, running on the BEAM VM.
File extensions: .erl
Every part of the example below is labelled and explained. This page is one of 55 annotated tours on AnatomyOf, a free, open-source project by LunarWerx Studios.
What is inside an Erlang file
Module declaration
Defines the module name, must match the filename.
Every Erlang source file is a module, declared with -module(name). as (conventionally) the first form in the file. The atom given must match the filename minus its .erl extension, so math_utils.erl must declare -module(math_utils). -- the compiler will refuse to link things up otherwise. Modules are the unit of code loading on the BEAM: the runtime can hot-swap a new version of a module into a live system while old processes keep running the previous version, which is the basis of Erlang's famous "upgrade a phone switch without dropping a call" trick.
Export attribute
Specifies which functions are publicly visible.
By default every function in a module is private. -export([area/1, sum_list/1]). lists the functions callable from outside, each written as name/arity since Erlang distinguishes functions by their argument count -- area/1 and area/2 are entirely different functions that happen to share a name. Everything left out of the export list stays a private helper, callable only from within the same module. This mirrors the modest, deliberate surface area that BEAM-based systems favor: expose the minimum, keep the rest free to change.
Comment
Starts with %, ignored by the compiler.
A % marks the rest of the line as a comment; there is no block-comment syntax, so multi-line explanations just stack single-line comments. By convention %% is used for comments documenting a whole section or function, while a lone % trails an individual line -- a style convention, not a compiler rule. Erlang has no docstrings built into the language the way some newer BEAM languages do, so comments (plus tools like EDoc) are the traditional way to document a module's intent.
Module attribute
Metadata about the module (-author, -vsn, etc.).
Attributes like -author("Erlang Developer"). or -vsn(1). attach metadata to the compiled module, retrievable at runtime via Module:module_info(). They have no effect on behavior -- they are read by tooling, documentation generators, and the occasional nostalgic developer scrolling to the top of a file. A module can carry any number of custom attributes beyond the well-known ones; the compiler stores whatever you declare, whether or not any tool reads it back.
Record definition
Defines a named, structured data type.
-record(point, {x = 0, y = 0}). defines a compile-time template for tuples with named fields and defaults. Under the hood a record is still an ordinary tagged tuple -- #point{x=1, y=2} compiles down to {point, 1, 2} -- so records are purely a readability layer the compiler erases before the BEAM ever sees it. Because that expansion happens at compile time, record definitions must be visible wherever they are used, typically via a shared .hrl header file included with -include("records.hrl").; there is no runtime record registry to consult.
Function definition (area/1)
A function with multiple clauses, separated by ;.
A function can have several clauses with the same name and arity, each ending in ; except the last, which ends in .. At call time Erlang tries each clause top to bottom and runs the first whose head matches the argument and whose guard (if any) succeeds -- this is how area/1 picks among a square, a circle, or a point without a single if statement in sight. If no clause matches, the process crashes with a function_clause error rather than silently doing nothing -- in line with Erlang's "let it crash" philosophy, where a supervisor restarting a failed process is the expected recovery path, not e
Pattern matching
Matching values and extracting data in function arguments.
Function heads and the = operator both perform pattern matching, not assignment: area({square, Side}) only matches a two-element tuple whose first element is the atom square, and simultaneously binds Side to the second element. A bound variable used again later must match its existing value rather than being overwritten -- Erlang variables are single-assignment within a clause. Pattern matching extends to records too, as in area(#point{x=X, y=Y}), which destructures the record's fields directly in the argument list. This is the same mechanism used in case expressions and receive blocks, making
Guard sequence (when)
Additional constraints on function clauses.
A when clause after a function head adds a boolean test that must also succeed for that clause to fire, as in when Side > 0. Guards are restricted to a safe subset of expressions -- comparisons, type tests like is_number/1, and a handful of arithmetic and boolean operators -- deliberately excluding anything with side effects, since the runtime may need to evaluate a guard speculatively. Multiple conditions can be combined with , (all must hold) or separated with ; between whole guard sequences (any may hold), giving a compact way to layer validation directly onto dispatch instead of writing it
Function body
Expressions executed when a clause matches, ends with ..
The body after -> is a sequence of expressions separated by ,, and the value of the last expression is the clause's return value -- there is no explicit return keyword anywhere in Erlang. The final clause of the function is terminated with a period . instead of the semicolon used between earlier clauses, marking the end of the whole function definition. Because everything is an expression, control constructs like case and if also produce values that can be bound directly to a variable, which is why Erlang code tends to read as nested expressions rather than sequences of statements.
Function definition (sum_list/1)
A function with a single clause.
Not every function needs multiple clauses -- sum_list(List) -> ... has exactly one, matching any argument bound to the name List with no guard at all. A single-clause function is just the degenerate case of the general multi-clause mechanism described above. Arity still matters even here: a hypothetical sum_list/2 would be a completely separate function, and both could be exported and called independently without conflict.
List comprehension
Concise syntax for creating lists based on existing lists.
[X || X <- List, is_number(X)] reads as "the list of X such that X is drawn from List and is_number(X) holds." The part after <- is a generator, and any comma-separated expressions after it are filters that must evaluate truthy for that element to be kept -- here, quietly dropping anything that snuck into the list without being a number. Comprehensions can chain multiple generators to produce combinations (handy for building all pairs from two lists) and are typically far more concise than the equivalent hand-written recursive function, without sacrificing Erlang's purely functional, no-mutati
External function call
Calling a function from another module (module:function).
Writing math:pi() or lists:sum(List) calls a function exported by another module, using the Module:Function(Args) form. This is also how the BEAM's hot code loading works under the hood: a remote call is resolved through the module's current code each time, so swapping in a new version of lists (in principle) affects every caller immediately. By contrast, a local call like area(Shape) within the same module is resolved directly at compile time. The distinction matters for long-running systems, since a process stuck in an old module version via local calls will only pick up a fresh release at i
Official Erlang site · All languages on AnatomyOf
The interactive tour needs JavaScript. Enable it to hover a callout and trace it into the code.