This post is written as a set of Literate Commits. The goal of this style is to show you how this program came together from beginning to end. Each commit in the project is represented by a section of the article. Click each section’s header to see the commit on Github, or check out the repository and follow along.
Christmas in August
Today’s an exciting day! We’ll be tackling an entirely new set of code katas using an entirely different language! We’ll be working on the first challenge in the Advent of Code series.
In the same vein as one of our recent posts, we’ll be using the Elixir language to solve this challenge.
This first commit is the result of mix new advent_of_code_01
and creates a base
Elixir project.
config/config.exs
+use Mix.Config
lib/advent_of_code_01.ex
+defmodule AdventOfCode01 do +end
mix.exs
+defmodule AdventOfCode01.Mixfile do + use Mix.Project + + def project do + [app: :advent_of_code_01, + version: "0.1.0", + elixir: "~> 1.3", + build_embedded: Mix.env == :prod, + start_permanent: Mix.env == :prod, + deps: deps()] + end + + def application do + [applications: [:logger]] + end + + defp deps do + [] + end +end
test/advent_of_code_01_test.exs
+defmodule AdventOfCode01Test do + use ExUnit.Case + doctest AdventOfCode01 + + test "the truth" do + assert 1 + 1 == 2 + end +end
test/test_helper.exs
+ExUnit.start()
Watching Tests
Out of the box, Elixir comes with a fantastic unit testing
framework.
ExUnit can be run against our current project by running the mix test
command. Unfortunately, this runs the test suite one time and quits. It doesn’t
watch our project and rerun our suite when it detects changes.
Thankfully, the mix_test_watch
dependency does exactly that. We can
add it to our project, and run the mix test.watch
command. Our test
suite will rerun on every file change.
Now that our test suite is up and running, we’ll remove the example test Elixir provided for us.
mix.exs
... defp deps do - [] + [ + {:mix_test_watch, "~> 0.2", only: :dev} + ] end
test/advent_of_code_01_test.exs
... doctest AdventOfCode01 - - test "the truth" do - assert 1 + 1 == 2 - end end
Our First Test
To get things going, let’s write the simplest test we can think of. Amazingly, tests for Elixir functions can be written within the documentation for the function itself. For example, we can write our first test like this:
iex> AdventOfCode01.which_floor "("
1
Elixir will tease these tests out of the function docs and run them as part of our test suite.
We can make this first test pass by simply returning 1
from our new
which_floor
function:
def which_floor(directions) do
1
end
And with that, our test suite flips back to green.
lib/advent_of_code_01.ex
defmodule AdventOfCode01 do + + @doc """ + Determines which floor Santa will end up on. + + ## Examples + + iex> AdventOfCode01.which_floor "(" + 1 + + """ + def which_floor(directions) do + 1 + end + end
Jumping Forward
Our next test is slightly more complicated:
iex> AdventOfCode01.which_floor "(("
2
Normally, we might take a bit more time to flesh out more naive,
intemediary solutions, but this is a fairly easy problem to solve. We
want to split our directions
string into its component characters,
map
those characters into numbers (1
in this case), and then sum
the result:
directions
|> String.split("", trim: true)
|> Enum.map(&handle_direction/1)
|> Enum.sum()
Our handle_direction
private function expects to recieve a "("
as
its input and always returns a 1
:
defp handle_direction("("), do: 1
With those changes, our suite returns to green.
lib/advent_of_code_01.ex
... + iex> AdventOfCode01.which_floor "((" + 2 + """ def which_floor(directions) do - 1 + directions + |> String.split("", trim: true) + |> Enum.map(&handle_direction/1) + |> Enum.sum() end + defp handle_direction("("), do: 1 + end
Handling All Matches
Let’s add one last test that excercises the ability to process “down” directions:
iex> AdventOfCode01.which_floor("()(")
1
After adding this test, our suite fails. It complains that it
can’t find a “function clause matching in
AdventOfCode01.handle_direction/1
”.
This is because we’re only handling “up” directions ("("
) in the
handle_direction
function:
defp handle_direction("("), do: 1
Let’s add a new match for “down” directions:
defp handle_direction(")"), do: -1
After adding that new function definition to our module, our tests flip back to green. Victory!
lib/advent_of_code_01.ex
... + iex> AdventOfCode01.which_floor "()(" + 1 + """ ... defp handle_direction("("), do: 1 + defp handle_direction(")"), do: -1
Final Thoughts
Elixir is a beautiful language with lots of exciting features. In this example, we got a taste of transformation pipelines and how pattern matching can be used to write expressive control flow structures.
The first class treatment of documentation and testing is a welcome surprise coming from an ecosystem where testing seems to be an afterthought.
Around here we’re big fans of using small practice problems and code katas to learn new languages and paradigms. Expect to see Elixir making appearances in upcoming Literate Commit posts!