Anatomy of an Elixir file
Elixir is a functional, concurrent programming language built on top of the Erlang VM (BEAM), known for building scalable and maintainable applications.
File extensions: .ex, .exs
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 Elixir file
Module definition
Defines a named collection of functions, macros, etc., using defmodule.
defmodule opens a namespace that groups related functions, macros, and structs under a dotted name like MyApp.User. Unlike a class, a module is not instantiated. It is purely a compile-time grouping, and all its def-declared functions are really just functions living in that namespace. Module names compile to atoms (MyApp.User becomes :"Elixir.MyApp.User" under the hood), which is how the BEAM locates and loads the corresponding .beam bytecode file at runtime.
Moduledoc & doc attributes
Documentation for modules and functions using @moduledoc and @doc.
@moduledoc attaches documentation to the enclosing module, and @doc attaches it to the function definition immediately following it. Both accept a string (often a heredoc delimited by triple double-quotes) and are compiled into the module's bytecode as metadata rather than stripped as comments. Because the docs are real compiled data, tools like ExDoc and h MyApp.User.new/2 in iex can retrieve them at runtime or from a generated static site. Passing false to @moduledoc explicitly marks a module as internal and undocumented.
Comment
Single-line (#), ignored by the compiler.
Everything from a # to the end of the line is a comment and produces no bytecode. Elixir has no block-comment syntax, so multi-line explanations are just consecutive # lines, one per line. Comments are a separate concept from @moduledoc/@doc strings above: comments vanish at compile time, while doc attributes are retained as introspectable metadata. Idiomatic style favors doc attributes for anything a caller should be able to look up later.
Module attribute (constant)
A compile-time constant, starting with @.
Outside of @moduledoc/@doc, an @name value line registers a module attribute. A value that is evaluated once at compile time and inlined everywhere it is referenced in that module, similar to a macro constant. It is not a mutable module-level variable; reassigning @default_role later in the same module simply shadows the earlier value for subsequent reads. Because the value is baked into the compiled bytecode, module attributes are ideal for configuration-like constants (@default_role, @max_retries) but cannot be used to share mutable state at runtime: for that you would reach for Agent, GenSe
Struct definition
Defines a map with a fixed set of keys using defstruct.
defstruct declares the struct associated with the enclosing module: a bare %ModuleName{} map whose keys are fixed at compile time. Keys can be given default values (defstruct [:id, :name, role: @default_role]), and any key omitted at construction falls back to its default or nil. Unlike a plain map, a struct carries a __struct__ field naming its module, which lets Enumerable, Inspect, and other protocols dispatch differently for structs than for ordinary maps: and it is why you cannot add a key to a struct that wasn't declared in defstruct, even accidentally.
Public function (def)
A function callable from outside the module, defined with def.
def defines a function that is exported from the module and callable as MyApp.User.new(1, "Alice"). Function bodies are expressions, not statement blocks: the value of the last expression is the return value, and there is no explicit return keyword in idiomatic Elixir. Functions are identified by name *and* arity, written new/2; def new(id) and def new(id, name) are two entirely separate functions that happen to share a name, which is how Elixir gets clause-based dispatch and optional-argument-like ergonomics without argument-count juggling.
Struct instantiation
Creates a new instance of a struct.
%__MODULE__{id: id, name: name} builds a new struct value populated with the given fields; __MODULE__ is a compile-time macro that expands to whatever module it appears in, so the struct-building code stays correct even if the module is renamed. The same %Struct{...} syntax also appears in function heads for pattern matching, as in greet(%__MODULE__{name: name, role: role}). Structs, like all Elixir data, are immutable. %{user | name: "Bob"} update syntax produces a brand-new struct rather than mutating the original, and the old value remains valid and unchanged wherever else it is referenced.
Private function (defp)
A function callable only within the module, defined with defp.
defp defines a function with the same clause-matching behavior as def, but it is not exported: code outside the module cannot call it, and it will not show up in generated docs. It is the idiomatic way to factor out a helper (like get_greeting/1) without polluting the module's public API. Because visibility is a property of the definition keyword rather than a separate export list, a whole file can be scanned top-to-bottom to see exactly what is public (def) versus internal (defp) at a glance.
String interpolation
Embedding expressions in strings using #{}.
Inside a double-quoted string, #{expression} is evaluated and its result is converted to a string (via the String.Chars protocol) and spliced into place. "#{greeting}, #{name}!" builds a single binary at runtime from the surrounding literal text and the two interpolated values. Interpolation only works in double-quoted strings and sigils that support it (like ~s""); single-quoted values in Elixir are charlists, not strings, and do not interpolate the same way.
Control flow (case)
Matches a value against multiple patterns using case.
case role do ... end compares role against each pattern in order and executes the body of the first clause that matches, binding any variables the pattern introduces along the way. The underscore _ is a catch-all pattern that matches anything without binding it to a name, conventionally used as the final "else" branch. Because matching is structural, case can destructure tuples, lists, and maps directly in the clause head ({:ok, value} -> value), which is why pattern matching, not if/else chains, is the primary control-flow idiom in Elixir.
Script/top-level execution
Code executed outside of any module or function.
Code written outside a defmodule block runs immediately, top to bottom, as the file is loaded or compiled. There is no if __name__ == "__main__": equivalent needed because a .exs script file is meant to be run directly with elixir script.exs. Compiled .ex files, by contrast, are typically just modules with no top-level side effects, loaded into an application rather than executed as a script. This top-level code is where you typically see calls into the modules just defined, wiring together the pieces of a small script before it exits.
Function call
Executing a function, e.g. MyApp.User.new(1, "Alice").
A remote call is written Module.function(args); Elixir resolves MyApp.User.new/2 at compile time when possible and raises UndefinedFunctionError at runtime if no matching module/function/arity triple exists. Parentheses are optional in many contexts (IO.puts "hi" is valid), though most style guides keep them for clarity outside of pipelines. Because functions dispatch on arity as well as name, calling new/2 never risks accidentally hitting a same-named new/1 clause meant for a different use case. The compiler picks the exact match.
Pipe operator (|>)
Passes the result of the left expression as the first argument to the right.
a |> f(b) is rewritten by the compiler into f(a, b), the value on the left slots into the *first* argument position of the call on the right. Chaining pipes turns deeply nested calls like h(g(f(x))) into a linear top-to-bottom pipeline x |> f() |> g() |> h() that reads in the order the data actually flows. Because each stage is just a function call, anything callable can appear in a pipeline, including anonymous functions and remote calls like String.upcase/1; only the target position of the first argument is special-cased, every other argument is written normally.
Official Elixir site · All languages on AnatomyOf
The interactive tour needs JavaScript. Enable it to hover a callout and trace it into the code.