Elixir is just cool. An example with pattern matching and structs.

Essential Elixir : part 7 of 10 published on Jul 06, 2015

In the code example for this post, we are examining the statuses a contest could have. A contest can be:

  • Finalized
  • Cancelled
  • Past Deadline
  • Or eligible for a pick sheet (aka :ok)

We need to know the contest’s status because a pick sheet can not be created on a contest that is finalized, cancelled, or past the deadline.

Start at the end

For this example walk through we will start by looking at the final code. Don’t worry if you can’t follow everything. The purpose of the post is to clear up any confusion by talking through each of the different sections.

At the end we will look again at the final code. If I did a good job, it should be clear on how the code works.

defmodule ContestStatus do
  defstruct cancelled: false, finalized: false, past_deadline: false

  def check(%ContestStatus{ cancelled: true }), 
    do: :contest_cancelled
  def check(%ContestStatus{ finalized: true }), 
    do: :contest_finalized
  def check(%ContestStatus{ past_deadline: true }), 
    do: :past_deadline
  def check(%ContestStatus{}), do: :ok

  def past_deadline(nil), do: false
  def past_deadline(date), do: Ecto.DateTime.local > date

  def check(contest) do
    check(%ContestStatus{
      cancelled: contest.cancelled || false,
      finalized: contest.finalized || false,
      past_deadline: past_deadline(contest.picks_deadline)
    })
  end
end

We want to return the status so that we can use the function like this:

case ContestStatus.check(contest) do
  :ok -> create_pick_sheet(user, contest, picks)
  :contest_cancelled -> { :error, "Contest is cancelled" }
  :contest_finalized -> { :error, "Contest is finalized" }
  :past_deadline -> { :error, "Contest's deadline has passed" }
end

Let’s break down how this works.

We begin at check(contest)

We start the process with ContestStatus.check(contest). This function accepts a single parameter of any type. We then build the defined struct %ContestStatus{} and call check/1.

def check(contest) do
  check(%ContestStatus{
    cancelled: contest.cancelled || false,
    finalized: contest.finalized || false,
    past_deadline: past_deadline(contest.picks_deadline)
  })
end

This time since the parameter is a %ContestStatus type, it will be eligible for the pattern matching of the following functions:

def check(%ContestStatus{ cancelled: true }), 
    do: :contest_cancelled
def check(%ContestStatus{ finalized: true }), 
    do: :contest_finalized
def check(%ContestStatus{ past_deadline: true }), 
    do: :past_deadline
def check(%ContestStatus{}), do: :ok

Quick detour to look at our struct

You might have asked what is %ContestStatus? This is a struct we defined at the top of our module.

defmodule ContestStatus do
  defstruct cancelled: false, finalized: false, past_deadline: false
  ...

We define a struct specifically so that we can use it for pattern matching on our functions. It has not special powers or methods beyond being a defined struct to pattern match against.

For more information on structs: http://elixir-lang.org/getting-started/structs.html#defining-structs

Another detour to past_deadline

Before we look at the other functions let’s take a quick detour to past_deadline/1. We want this function to return true or false depending on the deadline for the contest.

If we pass nil to the past_deadline/1 function, we will return false. We do this because in our scenario a contest without a deadline is never passed its deadline.

def past_deadline(nil), do: false

If a deadline is defined, we will check against the current time.

def past_deadline(date), do: Ecto.DateTime.local > date

Putting it together it looks like:

def past_deadline(nil), do: false
def past_deadline(date), do: Ecto.DateTime.local > date

Back to checking statuses

Just like past_deadline/1 our check/1 function will use pattern matching against our %ContestStatus struct.

If the cancelled value on our struct is true, we don’t care about anything else and immmediately know the status is :contest_cancelled.

With Elixir, we can just match on that single value on the struct.

def check(%ContestStatus{ cancelled: true }), 
    do: :contest_cancelled

If the cancelled value is not true, then Elixir will look for another function to invoke since the struct doesn’t match the above function.

Just like cancelled, if the contes is finalized then we want to return :contest_finalized.

def check(%ContestStatus{ finalized: true }), 
    do: :contest_finalized

Rinse. Repeat with past_deadline.

def check(%ContestStatus{ past_deadline: true }), 
    do: :past_deadline

If none of those patterns match, we know the contest status is :ok so will return that.

def check(%ContestStatus{}), do: :ok

So if you put it all together, you can follow how Elixir will walk through each of the functions and pattern match the input to find the correct function to invoke.

def check(%ContestStatus{ cancelled: true }), 
    do: :contest_cancelled
def check(%ContestStatus{ finalized: true }), 
    do: :contest_finalized
def check(%ContestStatus{ past_deadline: true }), 
    do: :past_deadline
def check(%ContestStatus{}), do: :ok

Viewing it all together

Now when you view the code, hopefully you can see how the pattern matching on functions communicates the branches / different statuses a contest can have.

defmodule ContestStatus do
  defstruct cancelled: false, finalized: false, past_deadline: false

  def check(%ContestStatus{ cancelled: true }), 
    do: :contest_cancelled
  def check(%ContestStatus{ finalized: true }), 
    do: :contest_finalized
  def check(%ContestStatus{ past_deadline: true }), 
    do: :past_deadline
  def check(%ContestStatus{}), do: :ok

  def past_deadline(nil), do: false
  def past_deadline(date), do: Ecto.DateTime.local > date

  def check(contest) do
    check(%ContestStatus{
      cancelled: contest.cancelled || false,
      finalized: contest.finalized || false,
      past_deadline: past_deadline(contest.picks_deadline)
    })
  end
end

The pattern matching on function inputs in Elixir creates code that easily communicates how different states should be handled. I find the above code terse yet extremely expressive.

Next: Printing to the Console with Elixir