Functions
Functions are first-class citizens in Elixir. Learn anonymous functions, named functions, pattern matching in functions, and function composition.
Anonymous Functions
Functions defined inline, stored in variables.
Basic Syntax
# Define
add = fn a, b -> a + b end
# Call (note the dot)
add.(1, 2) # => 3
# Multiple parameters
greet = fn name -> "Hello, #{name}!" end
greet.("Alice") # => "Hello, Alice!"
# No parameters
say_hello = fn -> "Hello!" end
say_hello.() # => "Hello!"
Shorthand: Capture Operator (&)
# Verbose
add = fn a, b -> a + b end
# Shorthand
add = &(&1 + &2)
# &1, &2, etc. are numbered parameters
# More examples
double = &(&1 * 2)
double.(5) # => 10
is_even = &(rem(&1, 2) == 0)
is_even.(4) # => true
Capturing Named Functions
# Capture existing function
uppercase = &String.upcase/1
uppercase.("hello") # => "HELLO"
# Use in Enum
Enum.map([1, 2, 3], &(&1 * 2))
# => [2, 4, 6]
# Capture with partial application
increment = &(&1 + 1)
Enum.map([1, 2, 3], increment)
# => [2, 3, 4]
Multiple Clauses
handle_result = fn
{:ok, result} -> "Success: #{result}"
{:error, reason} -> "Error: #{reason}"
end
handle_result.({:ok, 42}) # => "Success: 42"
handle_result.({:error, "Failed"}) # => "Error: Failed"
Closures
Anonymous functions capture their environment:
multiplier = fn factor ->
fn number -> number * factor end
end
times_two = multiplier.(2)
times_three = multiplier.(3)
times_two.(5) # => 10
times_three.(5) # => 15
Named Functions
Functions defined in modules.
Basic Module and Function
defmodule Math do
def add(a, b) do
a + b
end
def subtract(a, b) do
a - b
end
end
Math.add(5, 3) # => 8
Math.subtract(10, 4) # => 6
One-line Functions
defmodule Math do
def add(a, b), do: a + b
def subtract(a, b), do: a - b
def multiply(a, b), do: a * b
end
Private Functions
defmodule Calculator do
def calculate(a, b, operation) do
case operation do
:add -> add(a, b)
:multiply -> multiply(a, b)
end
end
# Private - only callable within module
defp add(a, b), do: a + b
defp multiply(a, b), do: a * b
end
Calculator.calculate(3, 4, :add) # => 7
Calculator.add(3, 4) # ** (UndefinedFunctionError)
Pattern Matching in Functions
Define multiple function clauses with different patterns:
Basic Pattern Matching
defmodule Greeter do
def hello("Alice"), do: "Hey Alice!"
def hello("Bob"), do: "Good to see you, Bob"
def hello(name), do: "Hello, #{name}"
end
Greeter.hello("Alice") # => "Hey Alice!"
Greeter.hello("Charlie") # => "Hello, Charlie"
Tuple Patterns
defmodule ResultHandler do
def handle({:ok, result}) do
"Success: #{result}"
end
def handle({:error, reason}) do
"Error: #{reason}"
end
def handle(_) do
"Unknown result"
end
end
ResultHandler.handle({:ok, 42}) # => "Success: 42"
ResultHandler.handle({:error, "fail"}) # => "Error: fail"
List Patterns
defmodule ListOps do
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
def first([]), do: nil
def first([head | _]), do: head
end
ListOps.sum([1, 2, 3, 4]) # => 10
ListOps.first([1, 2, 3]) # => 1
Map Patterns
defmodule UserGreeter do
def greet(%{name: name, age: age}) when age >= 18 do
"Hello adult #{name}!"
end
def greet(%{name: name}) do
"Hello #{name}!"
end
end
UserGreeter.greet(%{name: "Alice", age: 30})
# => "Hello adult Alice!"
Guard Clauses
Add conditions to pattern matches:
Basic Guards
defmodule Number do
def sign(n) when n > 0, do: :positive
def sign(n) when n < 0, do: :negative
def sign(0), do: :zero
end
Number.sign(5) # => :positive
Number.sign(-3) # => :negative
Number.sign(0) # => :zero
Multiple Guards
defmodule Classifier do
def classify(n) when is_integer(n) and n > 0 do
:positive_integer
end
def classify(n) when is_integer(n) and n < 0 do
:negative_integer
end
def classify(n) when is_float(n) do
:float
end
def classify(0), do: :zero
end
Guard Operators
# Boolean: and, or, not
# Comparison: ==, !=, ===, !==, <, >, <=, >=
# Arithmetic: +, -, *, /
# Type checks: is_integer, is_float, is_atom, is_list, etc.
# Other: length, hd, tl, elem, in
defmodule Validator do
def valid_age?(age) when is_integer(age) and age >= 0 and age <= 150 do
true
end
def valid_age?(_), do: false
end
Default Arguments
defmodule Greeter do
def hello(name, greeting \\ "Hello") do
"#{greeting}, #{name}!"
end
end
Greeter.hello("Alice") # => "Hello, Alice!"
Greeter.hello("Alice", "Hi") # => "Hi, Alice!"
Greeter.hello("Alice", "Howdy") # => "Howdy, Alice!"
Multiple Defaults
defmodule Format do
def format(string, prefix \\ "", suffix \\ "") do
"#{prefix}#{string}#{suffix}"
end
end
Format.format("hello") # => "hello"
Format.format("hello", ">>>") # => ">>>hello"
Format.format("hello", ">>>", "<<<") # => ">>>hello<<<"
Default with Pattern Matching
When using defaults with pattern matching, define a function head:
defmodule Calculator do
def add(a, b \\ 0)
def add(a, b), do: a + b
end
Calculator.add(5) # => 5
Calculator.add(5, 3) # => 8
Pipe Operator (|>)
Chain function calls - output of left becomes first argument of right.
Basic Piping
# Without pipe
String.upcase(String.trim(" hello "))
# With pipe
" hello "
|> String.trim()
|> String.upcase()
# => "HELLO"
Multi-step Transformation
"the quick brown fox"
|> String.split()
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
# => "The Quick Brown Fox"
With Enum
[1, 2, 3, 4, 5]
|> Enum.filter(&(rem(&1, 2) == 0))
|> Enum.map(&(&1 * 2))
|> Enum.sum()
# => 12
Piping into Custom Functions
defmodule Pipeline do
def add_tax(price), do: price * 1.1
def format_price(price), do: "$#{:erlang.float_to_binary(price, decimals: 2)}"
end
100
|> Pipeline.add_tax()
|> Pipeline.format_price()
# => "$110.00"
Pipe to Different Argument Position
By default, pipes to first argument. Use anonymous function for other positions:
# Pipe to second argument
"hello"
|> (&String.split("a b c", &1)).()
# Can be complex, consider refactoring
# Better: wrap in a function
defmodule StringUtils do
def split_by(string, separator), do: String.split(string, separator)
end
"hello"
|> StringUtils.split_by("l")
Higher-Order Functions
Functions that take or return functions.
Functions as Arguments
defmodule Math do
def apply_operation(a, b, operation) do
operation.(a, b)
end
end
add = fn a, b -> a + b end
multiply = fn a, b -> a * b end
Math.apply_operation(3, 4, add) # => 7
Math.apply_operation(3, 4, multiply) # => 12
Functions that Return Functions
defmodule Multiplier do
def create(factor) do
fn number -> number * factor end
end
end
times_two = Multiplier.create(2)
times_ten = Multiplier.create(10)
times_two.(5) # => 10
times_ten.(5) # => 50
Practical Example: Custom Validator
defmodule Validator do
def create_validator(min, max) do
fn value ->
cond do
value < min -> {:error, "Too small"}
value > max -> {:error, "Too large"}
true -> {:ok, value}
end
end
end
end
age_validator = Validator.create_validator(0, 150)
age_validator.(25) # => {:ok, 25}
age_validator.(200) # => {:error, "Too large"}
Recursion
Since Elixir is immutable, recursion replaces traditional loops.
Basic Recursion
defmodule Count do
def up(0), do: IO.puts("Blastoff!")
def up(n) do
IO.puts(n)
up(n - 1)
end
end
Count.up(5)
# 5
# 4
# 3
# 2
# 1
# Blastoff!
List Recursion
defmodule ListOps do
# Base case: empty list
def length([]), do: 0
# Recursive case
def length([_ | tail]), do: 1 + length(tail)
end
ListOps.length([1, 2, 3, 4]) # => 4
Tail Recursion
More efficient - uses accumulator:
defmodule ListOps do
def sum(list), do: sum(list, 0)
defp sum([], acc), do: acc
defp sum([head | tail], acc) do
sum(tail, acc + head)
end
end
ListOps.sum([1, 2, 3, 4]) # => 10
Practical Example: Factorial
defmodule Math do
# Non-tail recursive
def factorial(0), do: 1
def factorial(n) when n > 0 do
n * factorial(n - 1)
end
# Tail recursive (better)
def factorial_tail(n), do: factorial_tail(n, 1)
defp factorial_tail(0, acc), do: acc
defp factorial_tail(n, acc) when n > 0 do
factorial_tail(n - 1, n * acc)
end
end
Math.factorial(5) # => 120
Math.factorial_tail(5) # => 120
Function Arity
Functions identified by name and number of parameters:
defmodule Example do
def greet(name), do: "Hello, #{name}!" # greet/1
def greet(first, last), do: "Hello, #{first} #{last}!" # greet/2
end
Example.greet("Alice") # Calls greet/1
Example.greet("Alice", "Bob") # Calls greet/2
# Reference by arity
&Example.greet/1
&Example.greet/2
Documentation
Use @doc to document functions:
defmodule Calculator do
@doc """
Adds two numbers together.
## Examples
iex> Calculator.add(2, 3)
5
iex> Calculator.add(-1, 1)
0
"""
def add(a, b), do: a + b
end
Practical Examples
FizzBuzz
defmodule FizzBuzz do
def of(n) when rem(n, 15) == 0, do: "FizzBuzz"
def of(n) when rem(n, 3) == 0, do: "Fizz"
def of(n) when rem(n, 5) == 0, do: "Buzz"
def of(n), do: n
def run(limit) do
1..limit
|> Enum.map(&of/1)
|> Enum.each(&IO.puts/1)
end
end
FizzBuzz.run(15)
List Operations
defmodule MyList do
def map([], _func), do: []
def map([head | tail], func) do
[func.(head) | map(tail, func)]
end
def filter([], _func), do: []
def filter([head | tail], func) do
if func.(head) do
[head | filter(tail, func)]
else
filter(tail, func)
end
end
end
MyList.map([1, 2, 3], &(&1 * 2))
# => [2, 4, 6]
MyList.filter([1, 2, 3, 4], &(rem(&1, 2) == 0))
# => [2, 4]
Data Pipeline
defmodule DataProcessor do
def process(data) do
data
|> clean()
|> validate()
|> transform()
|> save()
end
defp clean(data) do
Enum.map(data, &String.trim/1)
end
defp validate(data) do
Enum.filter(data, &(String.length(&1) > 0))
end
defp transform(data) do
Enum.map(data, &String.upcase/1)
end
defp save(data) do
Enum.each(data, &IO.puts/1)
data
end
end
Exercises
- Write an anonymous function that takes two numbers and returns the larger one
- Create a module with a function
classify/1that uses pattern matching to identify if a list is empty, has one element, or has multiple elements - Write a recursive function
reverse/1that reverses a list - Use pipe operator to: take a string, split by spaces, filter words longer than 3 chars, capitalize each, join with hyphens
- Write a higher-order function
apply_n_times/3that applies a function to a value n times
# Solutions
# 1. Max function
max_fn = fn a, b -> if a > b, do: a, else: b end
# Or: max_fn = &max/2
# 2. Classify list
defmodule ListClassifier do
def classify([]), do: :empty
def classify([_]), do: :single
def classify([_| _]), do: :multiple
end
# 3. Reverse list
defmodule MyList do
def reverse(list), do: reverse(list, [])
defp reverse([], acc), do: acc
defp reverse([head | tail], acc) do
reverse(tail, [head | acc])
end
end
# 4. String pipeline
"the quick brown fox jumps over the lazy dog"
|> String.split()
|> Enum.filter(&(String.length(&1) > 3))
|> Enum.map(&String.capitalize/1)
|> Enum.join("-")
# => "Quick-Brown-Jumps-Over-Lazy"
# 5. Apply n times
defmodule Fn do
def apply_n_times(func, value, 0), do: value
def apply_n_times(func, value, n) when n > 0 do
apply_n_times(func, func.(value), n - 1)
end
end
Fn.apply_n_times(&(&1 * 2), 1, 5) # => 32 (1 * 2^5)
Next Steps
Continue to 05-collections.md to master working with lists, tuples, maps, and the powerful Enum and Stream modules.