Skip to content

Functions and Modules | Elixir

Named functions are defined inside modules using def and defp. They are compiled and can be called from other modules (for def) or only within the defining module (for defp).

defmodule Math do
# Public function
def add(a, b) do
a + b
end
# Public function with shorthand
def subtract(a, b), do: a - b
# Private function
defp double(x), do: x * 2
# Private helper used internally
def quadruple(x), do: double(double(x))
end
iex> Math.add(2, 3)
5
iex> Math.double(4)
** (UndefinedFunctionError) function Math.double/1 is undefined (private)

Functions can have default values for parameters. Default arguments are evaluated at compile time:

defmodule Greeter do
def greet(name, greeting \\ "Hello") do
"#{greeting}, #{name}!"
end
# Multiple defaults
def connect(host, port \\ 80, protocol \\ "http") do
"#{protocol}://#{host}:#{port}"
end
end
iex> Greeter.greet("World")
"Hello, World!"
iex> Greeter.greet("World", "Hi")
"Hi, World!"
iex> Greeter.connect("example.com")
"http://example.com:80"
iex> Greeter.connect("example.com", 443, "https")
"https://example.com:443"

When using default arguments, Elixir generates multiple clause heads. This means the function can be called with different arities. Use &Greeter.greet/2 (with 2 args) or &Greeter.greet/1 (with 1 arg).

Every function is identified by both its name and arity (number of arguments). Math.add/2 and Math.add/3 are different functions:

defmodule Example do
def process(data), do: process(data, [])
def process(data, opts), do: process(data, opts, %{})
def process(data, opts, context) do
# Full implementation
{data, opts, context}
end
end
## Arity is part of the function identity
iex> &Example.process/1
#Function<...>
iex> &Example.process/2
#Function<...>
iex> &Example.process/3
#Function<...>

Named functions can have multiple clauses with different patterns. Elixir evaluates them top-to-bottom:

defmodule ListUtils do
def first([]), do: nil
def first([head | _tail]), do: head
def classify(0), do: :zero
def classify(n) when n > 0, do: :positive
def classify(_n), do: :negative
def describe({:ok, value}), do: "Success: #{inspect(value)}"
def describe({:error, reason}), do: "Error: #{inspect(reason)}"
def describe(:loading), do: "Still loading"
def describe(other), do: "Unknown: #{inspect(other)}"
end

A catch-all clause should come last. Using an unbound variable as the catch-all (which will match any value and bind it) is idiomatic, but the underscore-prefixed variable (_other) avoids unused variable warnings:

def handle({:ok, data}), do: process(data)
def handle({:error, _reason}), do: :retry
def handle(_other), do: :unhandled

Anonymous functions (also called lambdas or closures) are first-class values. They are created with fn...end or the capture shorthand &.

## Basic anonymous function
add = fn a, b -> a + b end
iex> add.(2, 3)
5
# Multi-line anonymous function
greet = fn name ->
greeting = "Hello"
"#{greeting}, #{name}!"
end
iex> greet.("Alice")
"Hello, Alice!"
# Anonymous function with pattern matching
classify = fn
n when n > 0 -> :positive
0 -> :zero
_ -> :negative
end
iex> classify.(5)
:positive
# Anonymous function with multiple clauses
handle = fn
{:ok, value} -> {:success, value}
{:error, reason} -> {:failure, reason}
end
iex> handle.({:ok, 42})
{:success, 42}

The & operator creates anonymous functions by capturing existing functions or creating shorthands:

# Capturing a named function
add = &Kernel.+/2
iex> add.(2, 3)
5
# Capturing a module function
upcase = &String.upcase/1
iex> upcase.("hello")
"HELLO"
# Shorthand: &1, &2, etc. are positional arguments
double = &(&1 * 2)
iex> double.(5)
10
add_one = &(&1 + 1)
iex> add_one.(99)
100
# Multiple arguments
swap = &{&2, &1}
iex> swap.(1, 2)
{2, 1}
# Capturing a local or imported function
map_double = &Enum.map(&1, fn x -> x * 2 end)
# This is equivalent to:
# fn list -> Enum.map(list, fn x -> x * 2 end) end

The &Module.function/arity syntax captures a named function as a function value:

# From Enum module
Enum.map([1, 2, 3], &Integer.to_string/1)
# ["1", "2", "3"]
# Capturing with &/1, &/2 etc. notation
square = &(&1 * &1)
# This is different from &Mod.fun/arity -- the former is a shorthand,
# the latter captures an existing named function
# Partial application (not built-in, but achievable)
add = fn a, b -> a + b end
add_five = &add.(5, &1)
iex> add_five.(3)
8

Anonymous functions close over their enclosing scope, capturing variable bindings:

defmodule Counter do
def make_counter(initial) do
count = initial
fn ->
count = count + 1
count
end
end
end
# Note: the above doesn"t work as expected because Elixir is immutable.
# Each call creates a new binding. For stateful closures, use processes:
defmodule StatefulCounter do
def start(initial) do
Agent.start_link(fn -> initial end, name: __MODULE__)
end
def increment do
Agent.update(__MODULE__, &(&1 + 1))
Agent.get(__MODULE__, & &1)
end
def get do
Agent.get(__MODULE__, & &1)
end
end

Modules are the basic unit of organization in Elixir. They group related functions, provide namespacing, and enable code reuse through composition.

defmodule MyApp.Calculator do
@moduledoc """
A calculator module for basic arithmetic operations.
## Examples
iex> MyApp.Calculator.add(2, 3)
5
"""
@doc """
Adds two numbers.
"""
def add(a, b), do: a + b
@doc """
Subtracts the second number from the first.
"""
def subtract(a, b), do: a - b
@doc false
defp validate_input(n) when is_number(n), do: :ok
defp validate_input(_), do: {:error, :not_a_number}
end

Module attributes are compile-time constants stored in the module:

defmodule Config do
@moduledoc "Module-level documentation"
@doc "Function-level documentation"
def my_function, do: :ok
# Constant attribute
@version "1.0.0"
def version, do: @version
# Compile-time attribute
@before_compile MyApp.Logger
# Accumulating attribute
Module.register_attribute(__MODULE__, :handlers, accumulate: true)
@handlers :json
@handlers :xml
def handlers, do: @handlers # [:json, :xml]
end

Common module attributes:

AttributePurpose
@moduledocDocumentation for the module
@docDocumentation for the next function
@doc falseHide function from documentation
@specTypespec for a function
@typeType alias
@callbackBehaviour callback definition
@implMark behaviour implementation
@behaviourDeclare a behaviour
@before_compileCallback before module is compiled
@on_definitionCallback on every function definition
@deriveAuto-implement protocol
@enforce_keysEnforce keys in struct
defmodule MyApp do
defmodule Web do
defmodule Router do
def match(path) do
# ...
end
end
defmodule Controller do
def render(template, assigns) do
# ...
end
end
end
defmodule DB do
def query(sql) do
# ...
end
end
end
# Access with full path
MyApp.Web.Router.match("/users")
# Shorthand with alias
alias MyApp.Web.Router
Router.match("/users")
# Or multiple aliases at once
alias MyApp.{Web.Router, Web.Controller}

alias creates a short name for a module:

# Basic alias
alias MyApp.User
User.find(1)
# Alias with :as option
alias MyApp.Web.{Controller, Router, as: Web}
# Router is available as Web.Router
# Controller is available as Web.Controller
# Alias all modules under a namespace
alias MyApp.Web.{Controller, Router, Middleware}
# Controller, Router, Middleware all accessible
# Scoped alias
alias MyApp.User, as: Person
Person.find(1)
# require - makes a module's macros available
require Logger
Logger.info("This works")
# use - calls __using__/1 macro on the module
use GenServer
# Equivalent to: require GenServer; GenServer.__using__(__MODULE__)
# use with options
use Ecto.Repo, otp_app: :my_app
# import - brings functions into current scope
import List, only: [flatten: 1, duplicate: 2]
import String, only: :functions
import String, except: [split: 2]
import Enum, only: :macros

Key differences:

DirectiveEffectCommon Use
requireMakes macros availablerequire Logger
useCalls __using__/1 macrouse GenServer
importBrings functions/macros into scopeimport Enum
aliasCreates short namealias MyApp.User

Structs are maps with a fixed set of fields, default values, and compile-time type checking:

defmodule User do
@moduledoc "Represents a user in the system"
@enforce_keys [:id, :name]
defstruct [:id, :name, :email, age: 0, role: :guest]
end
# Creating structs
iex> %User{id: 1, name: "Alice"}
%User{id: 1, name: "Alice", email: nil, age: 0, role: :guest}
# @enforce_keys ensures these fields must be provided
iex> %User{name: "Alice"}
** (ArgumentError) the following keys must also be given when building struct User: [:id]
# Accessing fields
iex> u = %User{id: 1, name: "Alice", age: 30}
iex> u.name
"Alice"
iex> u[:name]
"Alice"
# Updating structs (creates new struct)
iex> updated = %{u | age: 31}
%User{id: 1, name: "Alice", email: nil, age: 31, role: :guest}
# Pattern matching on structs
defmodule UserService do
def adult?(%User{age: age}) when age >= 18, do: true
def adult?(%User{}), do: false
def greet(%User{name: name, role: :admin}), do: "Welcome, Admin #{name}"
def greet(%User{name: name}), do: "Hello, #{name}"
end
# Struct comparison
iex> %User{id: 1, name: "A"} == %User{id: 1, name: "A"}
true
iex> %User{id: 1} == %User{id: 2}
false
# Checking struct type
iex> is_struct(%User{})
true
iex> is_struct(%User{}, User)
true
iex> is_struct(%{name: "Alice"}, User)
false
# Map-like operations on structs
iex> Map.keys(%User{id: 1, name: "A"})
[:id, :name, :email, :age, :role]
# Structs are maps under the hood
iex> %User{}.__struct__
User
iex> is_map(%User{})
true
FeatureStructMap
Fixed keysYesNo
Default valuesYesNo
Required keys (@enforce_keys)YesNo
Type check (%ModuleName{})YesNo
Pattern matching specificityYes (specific struct type)No
Compile-time dispatchYes (protocols)No
PerformanceSlightly faster for known keysGeneral purpose

Protocols provide polymorphic dispatch based on the type of the first argument. They are similar to interfaces or type classes.

defprotocol Size do
@doc "Returns the size of a data structure"
def size(data)
end
# Implementations for different types
defimpl Size, for: List do
def size(list), do: length(list)
end
defimpl Size, for: Map do
def size(map), do: map_size(map)
end
defimpl Size, for: Tuple do
def size(tuple), do: tuple_size(tuple)
end
defimpl Size, for: BitString do
def size(binary), do: byte_size(binary)
end
defimpl Size, for: MapSet do
def size(set), do: MapSet.size(set)
end
iex> Size.size([1, 2, 3])
3
iex> Size.size(%{a: 1, b: 2})
2
iex> Size.size({:a, :b, :c})
3
# Derive protocol for structs
defstruct [:items]
defimpl Size, for: Collection do
def size(%Collection{items: items}), do: length(items)
end
# Or derive automatically for structs
defimpl Size, for: Any do
def size(_), do: 0
end

Elixir ships with several built-in protocols:

ProtocolPurposeKey Functions
InspectString representationinspect/1
String.CharsString conversionto_string/1
EnumerableCollection operationsEnum.reduce/3, Enum.member?/2
List.CharsCharlist conversionto_charlist/1
CollectableInto collectionEnum.into/2

Structs can automatically implement protocols using @derive:

defmodule Point do
@derive [Inspect, String.Chars]
defstruct [:x, :y]
defimpl String.Chars, for: Point do
def to_string(%Point{x: x, y: y}), do: "(#{x}, #{y})"
end
end

Behaviours define a set of callbacks that a module must implement. They are similar to interfaces or abstract classes in OOP.

defmodule Storage do
@moduledoc "Behaviour for key-value storage backends"
@type key :: any()
@type value :: any()
@type error :: {:error, term()}
@callback init(opts :: keyword()) :: {:ok, state :: term()} | error()
@callback put(state :: term(), key(), value()) :: {:ok, term()} | error()
@callback get(state :: term(), key()) :: {:ok, value()} | {:error, :not_found}
@callback delete(state :: term(), key()) :: {:ok, term()} | error()
@callback list(state :: term()) :: [key()]
end
defmodule MemoryStorage do
@behaviour Storage
@impl true
def init(_opts), do: {:ok, %{}}
@impl true
def put(state, key, value), do: {:ok, Map.put(state, key, value)}
@impl true
def get(state, key) do
case Map.fetch(state, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, :not_found}
end
end
@impl true
def delete(state, key), do: {:ok, Map.delete(state, key)}
@impl true
def list(state), do: Map.keys(state)
end

The @impl attribute marks a function as implementing a behaviour callback:

@impl Storage # explicitly names the behaviour
def put(state, key, value), do: ...
@impl true # infers from @behaviour declaration
def get(state, key), do: ...
@impl false # explicitly NOT a callback
def private_helper, do: ...

Typespecs provide optional type annotations for documentation, static analysis (Dialyzer), and IDE support.

defmodule MyApp.Types do
# Public type alias
@type user_id :: String.t()
@type email :: String.t()
@type age :: non_neg_integer()
# Struct type
@type user :: %{
id: user_id(),
name: String.t(),
email: email() | nil,
age: age()
}
# Private type
@typep internal_state :: :idle | :running | :stopped
# Opaque type (type is exported but implementation is hidden)
@opaque token :: binary()
end

Function specifications declare the expected types for function parameters and return values:

defmodule Math do
@spec add(number(), number()) :: number()
def add(a, b), do: a + b
@spec safe_div(number(), number()) :: {:ok, float()} | {:error, :division_by_zero}
def safe_div(_num, 0), do: {:error, :division_by_zero}
def safe_div(num, denom), do: {:ok, num / denom}
@spec classify(number()) :: :positive | :zero | :negative
def classify(n) when n > 0, do: :positive
def classify(0), do: :zero
def classify(_n), do: :negative
@spec fetch(map(), any()) :: {:ok, any()} | {:error, :not_found}
def fetch(map, key) do
case Map.fetch(map, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, :not_found}
end
end
end
# Basic types
@spec basic_types() :: term()
# term() is the top type (any value)
# Atomic types
@spec atoms() :: atom() | boolean() | nil
# atom() includes all atoms, including :true, :false, :nil
# Numbers
@spec nums() :: number() | integer() | float() | neg_integer() | non_neg_integer() | pos_integer()
# Collections
@spec collections() ::
list(integer()) | # list of integers
[integer()] | # same as list(integer())
map(atom(), String.t()) | # map with atom keys and string values
{:ok, String.t()} | # tuple
String.t() # String.t() is an alias for binary()
defmodule Handler do
@type event :: map()
@type result :: {:ok, map()} | {:error, String.t()}
@callback handle(event()) :: result()
@callback init(keyword()) :: {:ok, term()} | {:error, String.t()}
end
# Basic comprehension
iex> for x <- [1, 2, 3], do: x * 2
[2, 4, 6]
# Multiple generators
iex> for x <- [1, 2], y <- [:a, :b], do: {x, y}
[{1, :a}, {1, :b}, {2, :a}, {2, :b}}
# With filter
iex> for x <- 1..10, rem(x, 2) == 0, do: x
[2, 4, 6, 8, 10]
# With :into (build maps, structsets, etc.)
iex> for {k, v} <- [a: 1, b: 2], into: %{}, do: {k, v}
%{a: 1, b: 2}
iex> for x <- [1, 2, 3, 2, 1], into: MapSet.new(), do: x
MapSet.new([1, 2, 3])
# With :reduce (folding)
iex> for x <- [1, 2, 3], reduce: 0, do: (acc -> acc + x)
6
# The reduce option transforms the comprehension into a reducer.
# Syntax: for pattern <- enumerable, reduce: acc, do: (acc -> new_acc)
# Practical: building a map from a list
users = [%{id: 1, name: "Alice"}, %{id: 2, name: "Bob"}]
for user <- users, into: %{}, do: {user.id, user}
# %{1 => %{id: 1, name: "Alice"}, 2 => %{id: 2, name: "Bob"}}
# Practical: filtering and transforming
for {_key, value} <- %{a: 1, b: 2, c: 3}, value > 1, into: [], do: value
# [2, 3]
# Defining a custom exception
defmodule MyApp.Error do
defexception [:message, :code]
@impl true
def message(%{message: msg, code: code}) do
"[#{code}] #{msg}"
end
end
# Raising exceptions
raise "something went wrong"
raise MyApp.Error, message: "invalid input", code: :bad_input
# Handling exceptions
try do
risky_operation()
rescue
RuntimeError -> :handled_runtime_error
ArithmeticError -> :handled_arithmetic_error
%MyApp.Error{code: code} -> {:error, code}
error in [ArgumentError, TypeError] ->
{:error, Exception.message(error)}
else
result -> {:ok, result}
after
cleanup()
end

In Elixir, exceptions are used sparingly. The preferred pattern is to return {:ok, result} or {:error, reason} tuples and use pattern matching:

# Preferred pattern
case File.read("data.txt") do
{:ok, content} -> process(content)
{:error, :enoent} -> IO.puts("File not found")
{:error, reason} -> IO.puts("Error: #{reason}")
end
# vs. exception handling (avoid unless interfacing with Erlang)
try do
:gen_tcp.send(socket, data)
rescue
_ -> :error
end
flowchart TD
    A[1_Functions And Modules] --> B[Key Concepts]
    A --> C[Core Principles]
    A --> D[Practical Applications]
    B --> E[Fundamental definitions]
    C --> F[Design patterns]
    D --> G[Real-world usage]

Functions and modules are the organizational backbone of Elixir:

  • def for public functions, defp for private
  • Anonymous functions with fn...end or & shorthand
  • Modules provide namespacing with defmodule, alias, require, use, import
  • Structs add structure and type safety to maps with defstruct
  • Protocols provide polymorphism based on type dispatch
  • Behaviours define interfaces with @callback and @impl
  • Typespecs with @type and @spec add optional static type checking

Elixir modules are like toolboxes. Each toolbox has a name, contains specific tools (functions), and some tools are private (defp) so only you can use them inside the box. You can have many toolboxes, and you can combine tools from different toolboxes to build things. The module system gives you namespacing, so you never confuse a hammer from one project with a hammer from another.

Protocols are like electrical outlets. The protocol defines the shape of the outlet (what functions it must provide). Any device (data type) that fits that shape can be plugged in. A List, a Map, and a Tuple can all implement the same protocol, and the code using the protocol does not care which device is plugged in. This is polymorphism without inheritance.

Forgetting that defp functions are private and cannot be called from outside the module. Private functions defined with defp are only accessible within the defining module. Attempting to call Module.private_func/1 from another module raises UndefinedFunctionError. Use def for public APIs and defp for internal helpers.

Confusing import, alias, and use. import brings functions into the current scope (like import Enum). alias creates a short name (like alias MyApp.User). use invokes the __using__/1 macro on the target module, injecting its behaviour (like use GenServer). They are not interchangeable.

Assuming default arguments create only one function arity. When a function has default arguments, Elixir generates multiple clause heads. def greet(name, greeting \\ "Hello") creates both greet/1 and greet/2. Referencing the function as a function value requires specifying the arity: &Module.greet/1 or &Module.greet/2.