Functions and Modules | Elixir
Named Functions
Section titled “Named Functions”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).
def and defp
Section titled “def and 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)5iex> Math.double(4)** (UndefinedFunctionError) function Math.double/1 is undefined (private)Default Arguments
Section titled “Default Arguments”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}" endend
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).
Function Arity
Section titled “Function Arity”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} endend
## Arity is part of the function identityiex> &Example.process/1#Function<...>iex> &Example.process/2#Function<...>iex> &Example.process/3#Function<...>Multiple Clauses and Pattern Matching
Section titled “Multiple Clauses and Pattern Matching”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)}"endThe Catch-all Clause
Section titled “The Catch-all Clause”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: :retrydef handle(_other), do: :unhandledAnonymous Functions
Section titled “Anonymous Functions”Anonymous functions (also called lambdas or closures) are first-class values. They are created with fn...end or the capture shorthand &.
fn…end Syntax
Section titled “fn…end Syntax”## Basic anonymous functionadd = fn a, b -> a + b endiex> add.(2, 3)5
# Multi-line anonymous functiongreet = fn name -> greeting = "Hello" "#{greeting}, #{name}!"endiex> greet.("Alice")"Hello, Alice!"
# Anonymous function with pattern matchingclassify = fn n when n > 0 -> :positive 0 -> :zero _ -> :negativeendiex> classify.(5):positive
# Anonymous function with multiple clauseshandle = fn {:ok, value} -> {:success, value} {:error, reason} -> {:failure, reason}endiex> handle.({:ok, 42}){:success, 42}Shorthand Capture (&)
Section titled “Shorthand Capture (&)”The & operator creates anonymous functions by capturing existing functions or creating shorthands:
# Capturing a named functionadd = &Kernel.+/2iex> add.(2, 3)5
# Capturing a module functionupcase = &String.upcase/1iex> upcase.("hello")"HELLO"
# Shorthand: &1, &2, etc. are positional argumentsdouble = &(&1 * 2)iex> double.(5)10
add_one = &(&1 + 1)iex> add_one.(99)100
# Multiple argumentsswap = &{&2, &1}iex> swap.(1, 2){2, 1}
# Capturing a local or imported functionmap_double = &Enum.map(&1, fn x -> x * 2 end)# This is equivalent to:# fn list -> Enum.map(list, fn x -> x * 2 end) endCapturing Module Functions
Section titled “Capturing Module Functions”The &Module.function/arity syntax captures a named function as a function value:
# From Enum moduleEnum.map([1, 2, 3], &Integer.to_string/1)# ["1", "2", "3"]
# Capturing with &/1, &/2 etc. notationsquare = &(&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 endadd_five = &add.(5, &1)iex> add_five.(3)8Closures
Section titled “Closures”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 endend
# 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) endendModule Definitions
Section titled “Module Definitions”defmodule
Section titled “defmodule”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}endModule Attributes
Section titled “Module Attributes”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]endCommon module attributes:
| Attribute | Purpose |
|---|---|
@moduledoc | Documentation for the module |
@doc | Documentation for the next function |
@doc false | Hide function from documentation |
@spec | Typespec for a function |
@type | Type alias |
@callback | Behaviour callback definition |
@impl | Mark behaviour implementation |
@behaviour | Declare a behaviour |
@before_compile | Callback before module is compiled |
@on_definition | Callback on every function definition |
@derive | Auto-implement protocol |
@enforce_keys | Enforce keys in struct |
Nested Modules
Section titled “Nested Modules”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 endend
# Access with full pathMyApp.Web.Router.match("/users")
# Shorthand with aliasalias MyApp.Web.RouterRouter.match("/users")
# Or multiple aliases at oncealias MyApp.{Web.Router, Web.Controller}alias creates a short name for a module:
# Basic aliasalias MyApp.UserUser.find(1)
# Alias with :as optionalias MyApp.Web.{Controller, Router, as: Web}# Router is available as Web.Router# Controller is available as Web.Controller
# Alias all modules under a namespacealias MyApp.Web.{Controller, Router, Middleware}# Controller, Router, Middleware all accessible
# Scoped aliasalias MyApp.User, as: PersonPerson.find(1)require, use, and import
Section titled “require, use, and import”# require - makes a module's macros availablerequire LoggerLogger.info("This works")
# use - calls __using__/1 macro on the moduleuse GenServer# Equivalent to: require GenServer; GenServer.__using__(__MODULE__)
# use with optionsuse Ecto.Repo, otp_app: :my_app
# import - brings functions into current scopeimport List, only: [flatten: 1, duplicate: 2]import String, only: :functionsimport String, except: [split: 2]import Enum, only: :macrosKey differences:
| Directive | Effect | Common Use |
|---|---|---|
require | Makes macros available | require Logger |
use | Calls __using__/1 macro | use GenServer |
import | Brings functions/macros into scope | import Enum |
alias | Creates short name | alias MyApp.User |
Structs
Section titled “Structs”defstruct
Section titled “defstruct”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 structsiex> %User{id: 1, name: "Alice"}%User{id: 1, name: "Alice", email: nil, age: 0, role: :guest}
# @enforce_keys ensures these fields must be providediex> %User{name: "Alice"}** (ArgumentError) the following keys must also be given when building struct User: [:id]
# Accessing fieldsiex> 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 structsdefmodule 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 comparisoniex> %User{id: 1, name: "A"} == %User{id: 1, name: "A"}trueiex> %User{id: 1} == %User{id: 2}false
# Checking struct typeiex> is_struct(%User{})trueiex> is_struct(%User{}, User)trueiex> is_struct(%{name: "Alice"}, User)false
# Map-like operations on structsiex> Map.keys(%User{id: 1, name: "A"})[:id, :name, :email, :age, :role]
# Structs are maps under the hoodiex> %User{}.__struct__Useriex> is_map(%User{})trueStructs vs Maps
Section titled “Structs vs Maps”| Feature | Struct | Map |
|---|---|---|
| Fixed keys | Yes | No |
| Default values | Yes | No |
Required keys (@enforce_keys) | Yes | No |
Type check (%ModuleName{}) | Yes | No |
| Pattern matching specificity | Yes (specific struct type) | No |
| Compile-time dispatch | Yes (protocols) | No |
| Performance | Slightly faster for known keys | General purpose |
Protocols
Section titled “Protocols”Protocols provide polymorphic dispatch based on the type of the first argument. They are similar to interfaces or type classes.
Defining a Protocol
Section titled “Defining a Protocol”defprotocol Size do @doc "Returns the size of a data structure" def size(data)end
# Implementations for different typesdefimpl 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)endUsing Protocols
Section titled “Using Protocols”iex> Size.size([1, 2, 3])3iex> Size.size(%{a: 1, b: 2})2iex> Size.size({:a, :b, :c})3
# Derive protocol for structsdefstruct [:items]
defimpl Size, for: Collection do def size(%Collection{items: items}), do: length(items)end
# Or derive automatically for structsdefimpl Size, for: Any do def size(_), do: 0endBuilt-in Protocols
Section titled “Built-in Protocols”Elixir ships with several built-in protocols:
| Protocol | Purpose | Key Functions |
|---|---|---|
Inspect | String representation | inspect/1 |
String.Chars | String conversion | to_string/1 |
Enumerable | Collection operations | Enum.reduce/3, Enum.member?/2 |
List.Chars | Charlist conversion | to_charlist/1 |
Collectable | Into collection | Enum.into/2 |
@derive
Section titled “@derive”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})" endendBehaviours
Section titled “Behaviours”Behaviours define a set of callbacks that a module must implement. They are similar to interfaces or abstract classes in OOP.
Defining a Behaviour
Section titled “Defining a Behaviour”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()]endImplementing a Behaviour
Section titled “Implementing a Behaviour”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)endThe @impl attribute marks a function as implementing a behaviour callback:
@impl Storage # explicitly names the behaviourdef put(state, key, value), do: ...
@impl true # infers from @behaviour declarationdef get(state, key), do: ...
@impl false # explicitly NOT a callbackdef private_helper, do: ...Typespecs
Section titled “Typespecs”Typespecs provide optional type annotations for documentation, static analysis (Dialyzer), and IDE support.
@type, @typep, @opaque
Section titled “@type, @typep, @opaque”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()endFunction 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 endendBuilt-in Types
Section titled “Built-in Types”# 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()@callback with Typespecs
Section titled “@callback with Typespecs”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()}endComprehensions
Section titled “Comprehensions”for Comprehensions
Section titled “for Comprehensions”# Basic comprehensioniex> for x <- [1, 2, 3], do: x * 2[2, 4, 6]
# Multiple generatorsiex> for x <- [1, 2], y <- [:a, :b], do: {x, y}[{1, :a}, {1, :b}, {2, :a}, {2, :b}}
# With filteriex> 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: xMapSet.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 listusers = [%{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 transformingfor {_key, value} <- %{a: 1, b: 2, c: 3}, value > 1, into: [], do: value# [2, 3]Exception Handling
Section titled “Exception Handling”raise and try/rescue
Section titled “raise and try/rescue”# Defining a custom exceptiondefmodule MyApp.Error do defexception [:message, :code]
@impl true def message(%{message: msg, code: code}) do "[#{code}] #{msg}" endend
# Raising exceptionsraise "something went wrong"raise MyApp.Error, message: "invalid input", code: :bad_input
# Handling exceptionstry 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()endtry/rescue vs pattern matching
Section titled “try/rescue vs pattern matching”In Elixir, exceptions are used sparingly. The preferred pattern is to return {:ok, result} or {:error, reason} tuples and use pattern matching:
# Preferred patterncase 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 _ -> :errorendflowchart 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]Summary
Section titled “Summary”Functions and modules are the organizational backbone of Elixir:
deffor public functions,defpfor private- Anonymous functions with
fn...endor&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
@callbackand@impl - Typespecs with
@typeand@specadd optional static type checking
Intuition
Section titled “Intuition”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.
Common Mistakes
Section titled “Common Mistakes”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.
Cross-References
Section titled “Cross-References”- Basics and Pattern Matching - How pattern matching enables multi-clause function dispatch
- Concurrency and OTP - How GenServer and supervisors use modules to structure concurrent processes
- Metaprogramming - How macros extend module functionality at compile time