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.