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 |>.

Create a free account to save this page, take notes and track your progress. Sign in

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 use def, private ones defp.
  • 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/else chains.
  • Guards (when weight > 0) add conditions to a clause.
  • Default arguments: def cost(weight, zone \\ :domestic).
  • Anonymous functions: fn x -> x * 2 end or 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

Multi-clause functions, guards and pipelines
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; use double.(4).
  • Piping into the wrong argument. |> always fills the first argument; use then/2 or a helper when you need another position.
  • String.to_atom on user input. Atoms aren't garbage collected; use String.to_existing_atom or explicit mapping for untrusted data.

Key points

  • defmodule, def (public), defp (private); functions are name/arity.
  • Multiple clauses + pattern matching + guards replace conditionals.
  • Default arguments with \\; anonymous functions with fn or &.
  • |> 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.

Was this page helpful?