No history yet

Advanced Functional Patterns

Functions That Match

In Elixir, pattern matching isn't just for variables. It's a powerful tool for controlling the flow of your programs directly within function definitions. Instead of writing a single function with complex if or case statements inside, you can define multiple function clauses with the same name. Elixir will execute the first clause whose arguments match the values passed in.

This lets you handle different scenarios with separate, clean functions. For example, we can write a function to describe a list. One clause handles an empty list, another handles a list with one item, and a third handles any other list.

defmodule Greeter do
  # Clause for an empty list
  def describe([]), do: "This is an empty list."

  # Clause for a list with a single element
  def describe([item]), do: "This list has one item: #{item}."

  # Clause for a list with a head and a tail (more than one item)
  def describe([head | _tail]), do: "This list starts with #{head}."
end

Greeter.describe([])
#=> "This is an empty list."

Greeter.describe([:apple])
#=> "This list has one item: apple."

Greeter.describe([1, 2, 3])
#=> "This list starts with 1."

Using multiple function clauses with pattern matching makes your code more declarative. You describe what the data looks like for each case, rather than writing instructions to check it.

Efficient Recursion

Recursion, where a function calls itself, is a fundamental pattern in functional programming. A poorly written recursive function can be inefficient, consuming more and more memory until the system crashes. This happens in body recursion, where the recursive call is not the very last thing the function does.

# Body-recursive sum: INEFFICIENT
defmodule Math do
  def sum_list([]), do: 0
  def sum_list([head | tail]) do
    # The addition happens *after* the recursive call returns.
    head + sum_list(tail)
  end
end

The BEAM virtual machine, which Elixir runs on, includes a crucial feature called Tail Call Optimisation (TCO). If the very last action a function performs is to call itself, the BEAM reuses the current stack frame instead of creating a new one. This makes the recursion as memory-efficient as a standard loop.

To achieve this, we use an accumulator argument to pass the running total down through the calls.

# Tail-recursive sum: EFFICIENT
defmodule Math do
  # Public function to start the process
  def sum_list(list), do: do_sum_list(list, 0)

  # Private helper function that uses an accumulator
  defp do_sum_list([], acc), do: acc
  defp do_sum_list([head | tail], acc) do
    # The recursive call is the very last thing that happens.
    do_sum_list(tail, head + acc)
  end
end

Math.sum_list([1, 2, 3])
#=> 6

Eager vs Lazy Collections

Elixir provides two primary modules for working with collections: Enum and Stream. The key difference lies in how they execute.

Enum is eager. When you use an Enum function, it processes the entire collection immediately and returns the result. This is great for most collections, but can be inefficient if you're dealing with very large, or even infinite, datasets.

# Enum processes the entire list at each step.
[1, 2, 3, 4, 5]
|> Enum.map(&(&1 * 10))   #=> [10, 20, 30, 40, 50] -> new list created
|> Enum.filter(&(&1 > 25))  #=> [30, 40, 50] -> another new list created

Stream is lazy. It sets up a series of computations but doesn't execute them until you explicitly ask for the result, for example by passing the stream to an Enum function. Instead of creating intermediate collections at each step, Stream processes each element through the entire pipeline one at a time.

Lazy evaluation with Stream is perfect for handling large files, network resources, or infinite sequences without consuming all your memory.

# Stream builds a computation, but does no work yet.
stream = 1..1_000_000
|> Stream.map(&(&1 * 10))
|> Stream.filter(&(&1 > 25))

# The work happens here, as we pull items from the stream.
stream |> Enum.take(3)
#=> [30, 40, 50]

Composing with the Pipe

As you've already seen, the pipe operator |> is a cornerstone of idiomatic Elixir. It takes the result of the expression on its left and passes it as the first argument to the function on its right. This allows you to chain functions together in a highly readable sequence that flows from left to right.

Without the pipe, code becomes a series of nested function calls that are difficult to read from the inside out.

# Without the pipe operator (hard to read)
Enum.sum(Enum.map(Enum.filter(1..10, &(&1 > 5)), &(&1 * 2)))

With the pipe operator, the same logic becomes a clear, step-by-step transformation of data.

# With the pipe operator (clear and sequential)
1..10
|> Enum.filter(&(&1 > 5))  #=> [6, 7, 8, 9, 10]
|> Enum.map(&(&1 * 2))     #=> [12, 14, 16, 18, 20]
|> Enum.sum()              #=> 80

Now, let's test your understanding of these advanced functional patterns.

Quiz Questions 1/5

What is the primary advantage of using multiple function clauses with the same name in Elixir?

Quiz Questions 2/5

For Tail Call Optimisation (TCO) to be applied to a recursive function, which of the following must be true?

Mastering these patterns—pattern matching in function heads, tail-call recursion, lazy streams, and the pipe operator—is key to writing clean, efficient, and maintainable Elixir code.