Post

Core Functional Programming Concepts in Elixir

Core Functional Programming Concepts in Elixir

Functional programming is at the heart of Elixir. Unlike traditional imperative programming where code executes sequentially and often mutates data along the way, Elixir takes a different approach: one that emphasizes immutable data, predictable behavior, and function composition. This makes Elixir not only powerful but also well-suited for building scalable and concurrent systems.

In this post, I’ll go over the core functional programming concepts in Elixir and explain each in detail and show why they matter.

1. Immutability

In Elixir, once you bind a value to a variable, that value cannot be changed. This is called immutability, and it’s one of the foundational principles of functional programming, and it’s pretty cool.

1
2
x = 1
x = x + 1  # Rebinds x, doesn’t mutate it

In this example, we’re not actually modifying the value of x; we’re creating a new value and rebinding it to x. The original 1 is untouched.

Why does this matter? Because immutability eliminates a whole class of bugs related to shared state. In multi-threaded or distributed applications, mutable state can cause unpredictable behavior. But with immutability, data doesn’t change, and therefore, doesn’t surprise you.

Imagine having to debug a program where a variable randomly changes value somewhere deep in the code. Immutability prevents that from happening in the first place.

2. Pure Functions

A pure function is one that always produces the same output given the same input, and it does not cause any side effects.

1
2
3
defmodule Math do
  def add(a, b), do: a + b
end

Calling Math.add(2, 3) will always return 5, and it won’t change any external state, write to a database, or print to the console.

Since pure functions are easy to test and reason about, you don’t need to know what the rest of the system is doing. All you need to do is just focus on input and output.

Compare this to impure functions, which may behave differently depending on things like the current time, a global variable, or whether a file exists. Pure functions isolate complexity.

3. Recursion

In Elixir, you won’t find for or while loops like you would in other languages. Instead, you use “recursion.” Recursion simply refers to functions that call themselves to iterate.

1
2
3
4
5
6
7
8
defmodule Counter do
  def countdown(0), do: IO.puts("Done!")

  def countdown(n) do
    IO.puts(n)
    countdown(n - 1)
  end
end

Calling Counter.countdown(5) will print the numbers 5 through 1, then print “Done!”.

Recursion may feel strange at first, but it’s incredibly powerful once you get the hang of it. And Elixir helps out with tail-call optimization, which means you can safely recurse without blowing the stack, as long as the recursive call is the last thing the function does.

Recursion becomes particularly useful for working with lists or deeply nested data, as we’ll see next.

4. First-Class & Higher-Order Functions

In Elixir, functions are treated just like any other piece of data. You can assign them to variables, pass them as arguments, or even return them from other functions.

1
2
add = fn a, b -> a + b end
add.(2, 3) # => 5

You can also define higher-order functions, which are basically functions that take other functions as arguments:

1
def apply_twice(func, value), do: func.(func.(value))

This allows for very expressive and reusable code. Imagine having a suite of transformation functions and being able to apply them dynamically based on user input or configuration.

In short: functions are not second-class citizens in Elixir. They are fully flexible and composable.

5. Pattern Matching

Pattern matching is one of the most elegant features of Elixir. It lets you match and destructure data in a readable way:

1
2
{a, b} = {1, 2}  # a = 1, b = 2
[a | rest] = [1, 2, 3]  # a = 1, rest = [2, 3]

Even more powerfully, you can use pattern matching in function heads:

1
def greet(%{name: name}), do: "Hello, #{name}!"

It’s not just syntax sugar—it’s a way to enforce data shape and make your code safer and easier to read.

Pattern matching is also tightly integrated with control flow, using case, cond, and with expressions, enabling expressive branching logic.

6. Pipelines

One of Elixir’s most loved features (and one I personally enjoy using!) is the pipeline operator (|>), which allows you to chain function calls in a clean, readable way.

Instead of:

1
String.reverse(String.upcase("elixir"))

You can write:

1
2
3
"elixir"
|> String.upcase()
|> String.reverse()

This mirrors how we often think about transformations in our heads: take this, do that, then do the next thing. Pipelines promote a left-to-right flow of data and make nested function calls easier to follow.

This is especially useful in real-world projects where data flows through multiple processing steps—think: parsing, transforming, filtering, and rendering.

7. Functions as the Unit of Composition

In Elixir, everything revolves around functions. Even control structures like if, case, and cond are expressions—they return values.

1
value = if condition, do: "yes", else: "no"

This allows you to build up complex behavior through composition of small, understandable parts. Instead of massive classes or objects with lots of internal state, you create small, testable modules with clear input/output behavior.

By learning to compose functions, you’ll write code that’s easier to test, reason about, and reuse.

Final Thoughts

Functional programming in Elixir isn’t just a style—it’s the foundation for writing reliable, concurrent, and maintainable code. Concepts like immutability, recursion, and pattern matching might feel unfamiliar at first, but they quickly become intuitive with practice.

Once you embrace these patterns, you’ll find that Elixir helps you write cleaner and more robust software, whether you’re building APIs, processing data, or orchestrating real-time systems.

This post is licensed under CC BY 4.0 by the author.