Modules: Elixir's Unit of Code Organization
In Elixir, code is organized into modules using the defmodule keyword, and every function must live inside one. Module names follow CamelCase convention (like String or MyApp.Accounts.User), and dotted names create a namespace hierarchy rather than a literal nesting of modules. A single file can contain multiple modules, though the common convention is one primary module per file, named to match its path under lib/.
Cricket analogy: A module is like a franchise squad in the IPL -- Mumbai Indians groups its batters, bowlers, and support staff under one name, and MyApp.Accounts.User is like naming a specific unit 'Accounts.User' within that larger organization.
Named Functions and Arity
Every named function is identified by both its name and its arity -- the number of arguments it takes -- so greet/1 and greet/2 are entirely different functions that happen to share a name. You can define multiple clauses for the same name and arity, and Elixir tries each clause's pattern from top to bottom until one matches the arguments, similar to how case selects a branch.
Cricket analogy: Just as a bowler's yorker/1 (one target: the base of off stump) and yorker/2 (target plus pace variation) are different deliveries despite sharing a name, greet/1 and greet/2 are distinct functions distinguished by arity.
When multiple clauses share the same name and arity, Elixir compares the call's arguments against each clause's patterns in the order they're written, executing the first one that matches. This lets you replace conditional branching inside a function body with separate, focused clauses -- for example, one clause matching an empty list [] and another matching [head | tail] to handle recursion.
Cricket analogy: An umpire's decision tree checks LBW conditions in a fixed order -- pitched outside leg first, then impact, then wicket-to-wicket -- stopping at the first match, just as Elixir tries function clauses top to bottom.
Default Arguments with the \\ Operator
Default argument values are declared with a \\ after a parameter, as in def greet(name, greeting \\ "Hello"). When a function has multiple clauses of the same name and arity, only one clause should declare the defaults -- Elixir requires a bodiless function head listing the defaults, with the actual implementations as separate clauses that omit \\.
Cricket analogy: A net session has a default over count set with overs \\ 6 unless the coach specifies otherwise, just as greet(name, greeting \\ "Hello") falls back to a default when the caller omits the second argument.
Private Functions and Module Attributes
Functions defined with defp are private to their module -- callable from other functions inside the same module but invisible to external callers, which is useful for internal helpers you don't want to expose as public API. Module attributes like @moduledoc, @doc, and @spec attach documentation and type information at compile time, while a custom attribute such as @max_retries 3 acts as a compile-time constant that gets inlined wherever it's referenced.
Cricket analogy: A team's internal fielding drills (defp) are practiced behind closed doors and never shown to the public, unlike the actual match-day lineup (def) that's announced to everyone.
Anonymous Functions and the Capture Operator
Anonymous functions are created with fn arg -> expr end and invoked by adding a dot before the parentheses, as in add.(1, 2). The capture operator & offers a terser shorthand -- &(&1 + &2) is equivalent to fn a, b -> a + b end -- and the same operator can capture an existing named function as a value, like &String.upcase/1, which is commonly passed directly to functions such as Enum.map/2.
Cricket analogy: Calling an anonymous function with .() is like a substitute fielder brought on for one over -- used immediately for a specific task without a permanent squad name, unlike a named player represented by &String.upcase/1.
defmodule Greeter do
@moduledoc "Simple greeting utilities."
@default_greeting "Hello"
@doc "Greets a person by name, with an optional custom greeting."
def greet(name, greeting \\ @default_greeting)
def greet(name, greeting) when is_binary(name) do
"#{greeting}, #{name}!"
end
def greet(_name, _greeting) do
{:error, :invalid_name}
end
defp shout(message), do: String.upcase(message) <> "!!!"
def greet_loudly(name) do
name
|> greet()
|> shout()
end
end
Greeter.greet("Ana") # "Hello, Ana!"
Greeter.greet("Ana", "Hi") # "Hi, Ana!"
Greeter.greet_loudly("Ana") # "HELLO, ANA!!!!"
add = fn a, b -> a + b end
add.(2, 3) # 5
double = &(&1 * 2)
Enum.map([1, 2, 3], double) # [2, 4, 6]
Enum.map(["ana", "ben"], &String.capitalize/1)
# ["Ana", "Ben"]
A function's full identity in Elixir is written as name/arity, e.g. greet/2. This is why documentation, error messages, and tools like IEx.Helpers.h/1 always refer to functions with their arity attached -- greet/1 and greet/2 can have completely different behavior.
Giving default values to more than one clause of the same function raises a compile-time error like "definitions with defaults are not grouped together". Fix it by adding one function head with no body that declares the defaults (def greet(name, greeting \\ "Hello")), then writing the actual clauses without \\.
- Modules (defmodule) group related functions under a namespace; dotted CamelCase names indicate nesting.
- A function's identity is name + arity, so greet/1 and greet/2 are unrelated as far as the compiler is concerned.
- Multiple same-name/arity clauses are matched top-to-bottom against the caller's arguments.
- Default arguments use \\ and must be declared on a single bodiless function head when multiple clauses exist.
- defp restricts a function to internal use within its defining module.
- Module attributes (@moduledoc, @doc, @spec, custom constants) add documentation, types, and compile-time values.
- Anonymous functions (fn...end, called with .()) and the capture operator (&) let you pass functions as first-class values.
Practice what you learned
1. What distinguishes `greet/1` from `greet/2` in Elixir?
2. Which keyword defines a function that is only callable from within its own module?
3. When a function has multiple clauses with the same name and arity, how does Elixir choose which one to run?
4. What is the correct way to declare defaults when a function has multiple clauses?
5. What does `&String.upcase/1` do?
Was this page helpful?
You May Also Like
The Pipe Operator
Understand how Elixir's |> pipe operator chains function calls into readable, left-to-right data transformation pipelines.
Guards and case in Elixir
Learn how Elixir's case expression and guard clauses let you branch on pattern matches with additional boolean conditions, and when to reach for cond instead.
Recursion and Enum
Explore how Elixir uses recursion for iteration and how the Enum and Stream modules provide higher-level functions built on that foundation.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics