Once you accept that values do not change and variables are just bindings, the next surprise in Elixir is how “calling a function” really works.
In many languages, you call a function, and the runtime enters a block of code.
Elixir adds a step.
Before any code runs, it chooses a function head by pattern matching, based on the shape of your arguments. Only then does the corresponding clause execute.
That difference explains why Elixir code tends to feel declarative long before you touch OTP or concurrency.
A Function Is Identified by Name and Arity
In Elixir, a function is not just its name. It is:
- module
- function name
- arity (number of arguments)
So these are different functions:
def add(a), do: a
def add(a, b), do: a + b
They are treated as:
add/1add/2
Same name, different arity, different function.
Arity is part of the function’s identity, not just a detail about how many arguments you happened to pass.
🎯 Join Groxio's Newsletter
Weekly lessons on Elixir, system design, and AI-assisted development -- plus stories from our training and mentoring sessions.
We respect your privacy. No spam, unsubscribe anytime.
Multiple Function Heads of the Same Arity Are the Same Function
This is where Elixir differs from what many developers assume at first.
When you write this:
def greet(:admin), do: "Welcome, admin"
def greet(:user), do: "Hello!"
def greet(_), do: "Hi"
You did not define three functions.
You defined one function: greet/1, with three function heads.
Elixir will choose the head that matches the argument, then run the code for that head.
Function Head vs Function Clause
This distinction matters because it makes the model explicit.
A function head is each declaration marked by def or defp:
def greet(:admin), do: "Welcome, admin"
A function clause is the executable part for that head: the code between do ... end, or the expression after do:.
So each head has a corresponding clause. The head is what gets selected; the clause is what gets executed.
Shape Selection Is Pattern Matching
When we say “Elixir chooses the head that matches the shape of the data,” the actual mechanism is pattern matching.
And pattern matching is not only about “types.” It matches on:
- type or structure (tuple vs map vs list)
- values (atoms, literals, specific tuple tags)
- and it is recursive (it matches inside nested structures)
Example:
def process({:ok, value}), do: {:processed, value}
def process({:error, _}), do: :ignored
Here, the “shape” includes both the tuple structure ({..., ...}) and the first element’s value (:ok vs :error).
You can go deeper:
def handle(%{role: :admin, account: %{status: :active}} = user), do: {:allow, user}
def handle(%{role: :admin}), do: :review
def handle(_), do: :deny
This is pattern matching as a selection mechanism: match the outer map, then match nested maps, then match specific atom values.
That matching is why Elixir can keep decision logic out of the middle of a long function body. The runtime selects which function head applies, then executes that clause. You do not scan for if blocks. You scan for which function head matches.
Why This Changes How You Design Code
In step-based languages, you often write one function and branch inside it:
def process(input)
if input.nil?
default
elsif input.admin?
special_case(input)
else
normal_case(input)
end
end
In Elixir, you tend to pull those branches up into multiple heads:
def process(nil), do: default()
def process(%{admin: true} = input), do: special_case(input)
def process(input), do: normal_case(input)
Notice what happened: the “which case is this?” logic moved into the function heads. The function bodies can stay focused on “what to do” for that case.
The most specific cases come first. General cases come later. Failures fall through naturally. Your code reads like a decision table, and the pattern matching happens before any clause executes.
What to Practice Today
Next time you are about to write an if deep inside a function, pause and ask: “Can I express this as multiple function heads of the same arity?”
This works especially well when the behavior depends on a tagged tuple like {:ok, _} vs {:error, _}, a map containing specific keys or values, or an atom that signals a state or mode.
Do not force it everywhere. But when you can move the decision into the head, Elixir code gets flatter and easier to read.
What Comes Next
Now that you have seen that function heads are chosen by pattern matching, the next missing piece is obvious: what shapes do we have available to match against?
Next, we will look at Elixir’s base data types — not as syntax to memorize, but as the building blocks that make these function heads possible.
See you in the next chapter.
— Paulo Valim & Bruce Tate
Want to Master Elixir the Way It's Actually Designed?
This post is from Bruce Tate's structured Elixir course, where you build deep understanding step by step—not through scattered tutorials, but through a proven learning path used by hundreds of developers. Stop piecing together blog posts and start learning Elixir the right way.