100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Functions and Modules in Elixir

Learn how Elixir organizes code into modules and functions, covering named functions, arity, default arguments, private functions, module attributes, and anonymous functions.

Core ConceptsBeginner9 min readJul 10, 2026
Analogies

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.

elixir
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

Was this page helpful?

Topics covered

#Programming#ElixirStudyNotes#FunctionsAndModulesInElixir#Functions#Modules#Elixir#Unit#StudyNotes#SkillVeris#ExamPrep