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

  1. Write an anonymous function that takes two numbers and returns the larger one
  2. Create a module with a function classify/1 that uses pattern matching to identify if a list is empty, has one element, or has multiple elements
  3. Write a recursive function reverse/1 that reverses a list
  4. Use pipe operator to: take a string, split by spaces, filter words longer than 3 chars, capitalize each, join with hyphens
  5. Write a higher-order function apply_n_times/3 that 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.