Elixir Fundamentals: Functional Programming and OTP · Functional Elixir: Data, Pattern Matching and Functions
Functions, Modules, Guards and the Pipe Operator
Group functions in modules, write several clauses of one function that pattern-match their arguments, add guards, use default arguments and anonymous functions, and chain transformations with |>.
What you'll build
A shipping calculator module whose function has several clauses (by destination and weight), guards for validation, a private helper, and a pipeline that turns raw input into a formatted quote.
The idea
- Functions live in modules:
defmodule Shipping do ... end. Public functions usedef, private onesdefp. - A function is identified by name and arity (number of arguments):
Shipping.cost/2. - Multiple clauses: define the same function several times with different patterns; Elixir picks the first that matches. This replaces many
if/elsechains. - Guards (
when weight > 0) add conditions to a clause. - Default arguments:
def cost(weight, zone \\ :domestic). - Anonymous functions:
fn x -> x * 2 endor the capture shorthand&(&1 * 2); call them with a dot:double.(4). - The pipe operator
|>passes the result of one expression as the first argument of the next function, so data flows top to bottom:
" Hello World " |> String.trim() |> String.downcase() |> String.split()
The code
defmodule Shipping do
def cost(weight, zone \\ :domestic)
def cost(weight, _zone) when not is_number(weight) or weight <= 0,
do: {:error, :invalid_weight}
def cost(weight, :domestic) when weight <= 2, do: {:ok, 4.99}
def cost(weight, :domestic), do: {:ok, Float.round(4.99 + extra(weight - 2, 1.5), 2)}
def cost(weight, :international), do: {:ok, Float.round(14.99 + extra(weight, 3.0), 2)}
def cost(_weight, zone), do: {:error, {:unknown_zone, zone}}
defp extra(kilos, per_kilo), do: Float.round(Float.ceil(kilos * 1.0) * per_kilo, 2)
def quote(input) do
input
|> String.trim()
|> String.split(",")
|> Enum.map(&String.trim/1)
|> parse()
|> then(fn {weight, zone} -> {weight, zone, cost(weight, zone)} end)
|> format()
end
defp parse([weight, zone]), do: {String.to_float(weight), String.to_atom(zone)}
defp format({w, z, {:ok, price}}), do: "#{w} kg #{z}: $#{:erlang.float_to_binary(price, decimals: 2)}"
defp format({w, z, {:error, reason}}), do: "#{w} kg #{z}: error #{inspect(reason)}"
end
IO.inspect(Shipping.cost(1.5))
IO.inspect(Shipping.cost(3.2))
IO.inspect(Shipping.cost(3.2, :international))
IO.inspect(Shipping.cost(-1))
IO.inspect(Shipping.cost(1, :mars))
IO.puts(Shipping.quote(" 4.0, domestic "))
IO.puts(Shipping.quote("0.5,international"))
IO.puts(Shipping.quote("2.0, moon"))
double = fn x -> x * 2 end
triple = &(&1 * 3)
IO.inspect({double.(4), triple.(4), Enum.map([1, 2, 3], double)})
Output
{:ok, 4.99}
{:ok, 7.99}
{:ok, 26.99}
{:error, :invalid_weight}
{:error, {:unknown_zone, :mars}}
4.0 kg domestic: $7.99
0.5 kg international: $17.99
2.0 kg moon: error {:unknown_zone, :moon}
{8, 12, [2, 4, 6]}Run this on your computer — see “Get Started” for setup.
Common mistakes
- Clause order. Elixir tries clauses top to bottom; put specific clauses before general ones (the compiler warns about unreachable clauses).
- Calling anonymous functions without the dot.
double(4)is an error; usedouble.(4). - Piping into the wrong argument.
|>always fills the first argument; usethen/2or a helper when you need another position. String.to_atomon user input. Atoms aren't garbage collected; useString.to_existing_atomor explicit mapping for untrusted data.
Key points
defmodule,def(public),defp(private); functions arename/arity.- Multiple clauses + pattern matching + guards replace conditionals.
- Default arguments with
\\; anonymous functions withfnor&. |>pipes data into the first argument of the next call.
Further reading: Modules and functions, Anonymous functions.
Practice questions
How does Elixir choose which function clause to run?
Answer
It tries the clauses of the same name/arity from top to bottom and runs the first whose argument patterns match and whose guard (if any) is true. If none match, a FunctionClauseError is raised.
What does the pipe operator |> do?
Answer
It takes the value on its left and passes it as the first argument to the function call on its right, so a |> f(b) is f(a, b). Chains read as a top-to-bottom data transformation.
Open in the app to mark lessons complete, take notes and track your progress.