Your first Elixir Project (Part 2)

by Adam Davis

Table of contents:

Before we get started…

This is part 2 of a series for building your first project in Elixir. If you’re brand new to Elixir, start with my overview of the language.

This post assumes you’ve already completed parts 0 and 1 of this series already.

Now, where were we?

At the end of Part 1, we had written and tested a few functions for converting between units of measure.

We had also added some error handling for invalid input and wrote some tests for that too.

In this part, we’ll be re-writing some of our existing code to utilize type guards. We’ll also be writing some new functions and using pipes.

Our existing code

If you’ve been following along from the beginning, you should have a kilograms_to_grams/1 function that looks like this:

def kilograms_to_grams(x) do
  if is_number(x) do
    x * 1000
  else
    {:error, "invalid input"}
  end
end

There’s nothing wrong with this as-is, but Elixir has some tools to help us eliminate some of this nesting.

Using guards, we can limit our function to only run when the input is valid. Here’s what that would look like:

def kilograms_to_grams(x) when is_number(x) and x >= 0 do
  x * 1000
end

Guards start with when and are followed by a boolean expression. Only when the expression is true will the function execute.

To test this, let’s see what happens when we run our tests with mix test and the invalid input tests are reached:

1) test kilograms to grams handles invalid input (UnitConverterTest)
     test/unit_converter_test.exs:15
     ** (FunctionClauseError) no function clause matching in UnitConverter.kilograms_to_grams/1

From this message, we can see that it never reached the inside of the function. Instead, we get an error that there was no matching function clause.

Handling invalid input

To make sure we still handle invalid input, we need to define another function with the same name and arity, but without the guards:

def kilograms_to_grams(x) when is_number(x) and x >= 0 do
  x * 1000
end

def kilograms_to_grams(x) do
  {:error, "invalid input"}
end

When a call to kilograms_to_grams/1 is made, it will call our first function if the input is a number and greater than or equal to zero. Otherwise, it will default to our error function.

Let’s make sure we did this right and run our tests again:

....

Finished in 0.1 seconds
1 doctest, 3 tests, 0 failures

Great, we still pass our tests!

But a closer look will reveal that we also get a syntax warning:

warning: variable "x" is unused (if the variable is not meant to be used, prefix it with an underscore)

Ignoring function parameters

We need to accept a parameter in order for it to be a fallback for our guarded version of kilograms_to_grams/1. Removing the parameter would cause us to instead have kilograms_to_grams/0, which would break our intended control flow.

But we also don’t need to do anything with this parameter when it doesn’t meet the criteria of our guard.

Luckily, the warning tells us how to fix the issue. Prefixing a parameter name with an underscore will tell the compiler that we need to accept the parameter, but that it won’t be used within the function.

def kilograms_to_grams(x) when is_number(x) and x >= 0 do
  x * 1000
end

def kilograms_to_grams(_x) do
  {:error, "invalid input"}
end

If we run the tests again now, the warning should be resolved.

Adding more conversions

Now that we have this conversion working, let’s follow the same pattern for converting ounces to grams and pounds to ounces. Because this code is essentially the same as what we’re using in kilograms_to_grams/1 but with different conversion factors, feel free to just copy and paste these tests and functions.

Add these tests to unit_converter_test.exs:

test "converts ounces to grams" do
  assert Float.floor(UnitConverter.ounces_to_grams(10), 4) == 283.4951
  assert UnitConverter.ounces_to_grams(0) == 0
  assert Float.floor(UnitConverter.ounces_to_grams(1.5), 4) == 42.5242
end

test "ounces to grams handles invalid input" do
  assert UnitConverter.ounces_to_grams("hello there") == {:error, "invalid input"}
  assert UnitConverter.ounces_to_grams(-1) == {:error, "invalid input"}
  assert UnitConverter.ounces_to_grams([1, 2, 3]) == {:error, "invalid input"}
  assert UnitConverter.ounces_to_grams(:invalid) == {:error, "invalid input"}
end

test "converts pounds to ounces" do
  assert UnitConverter.pounds_to_ounces(10) == 160
  assert UnitConverter.pounds_to_ounces(0) == 0
  assert UnitConverter.pounds_to_ounces(1.5) == 24
end

test "pounds to ounces handles invalid input" do
  assert UnitConverter.pounds_to_ounces("hello there") == {:error, "invalid input"}
  assert UnitConverter.pounds_to_ounces(-1) == {:error, "invalid input"}
  assert UnitConverter.pounds_to_ounces([1, 2, 3]) == {:error, "invalid input"}
  assert UnitConverter.pounds_to_ounces(:invalid) == {:error, "invalid input"}
end

Note that these tests make use of [Float.floor/2](https://hexdocs.pm/elixir/1.13/Float.html#floor/2) to round the results of ounces_to_grams/1 for testing.

Add these functions to unit_converter.ex:

def ounces_to_grams(x) when is_number(x) and x >= 0 do
  x * 28.34952
end

def ounces_to_grams(_x) do
  {:error, "invalid input"}
end

def pounds_to_ounces(x) when is_number(x) and x>= 0 do
  x * 16
end

def pounds_to_ounces(_x) do
  {:error, "invalid input"}
end

Changing things up a bit

Next we’re going to write a function to convert pounds to grams. But instead of looking up the conversion factor and plugging it into a function, we’re going to leverage our existing functions pounds_to_ounces/1 and ounces_to_grams/1.

We’ll start the same way by writing some simple tests:

test "converts pounds to grams" do
  assert Float.floor(UnitConverter.pounds_to_grams(10), 2) == 4535.92
  assert UnitConverter.pounds_to_grams(0) == 0
  assert Float.floor(UnitConverter.pounds_to_grams(1.5), 2) == 680.38
end

test "pounds to grams handles invalid input" do
  assert UnitConverter.pounds_to_ounces("hello there") == {:error, "invalid input"}
  assert UnitConverter.pounds_to_ounces(-1) == {:error, "invalid input"}
  assert UnitConverter.pounds_to_ounces([1, 2, 3]) == {:error, "invalid input"}
  assert UnitConverter.pounds_to_ounces(:invalid) == {:error, "invalid input"}
end

Then we’ll write our function utilizing the same techniques we’ve used above:

def pounds_to_grams(x) when is_number(x) and x >= 0 do
  ounces = pounds_to_ounces(x)
  ounces_to_grams(ounces)
end

def pounds_to_grams(_x) do
  {:error, "invalid input"}
end

The only difference here is that instead of multiplying or dividing our input value, we:

Alternatively, we could nest the function calls:

def pounds_to_grams(x) when is_number(x) and x >= 0 do
  ounces_to_grams(pounds_to_ounces(x))
end

But there’s a better way

The above code is fine, but Elixir has an operator that lets you avoid nested function calls or creating variables that are only used once.

Pipes

The pipe operator, |>, works by passing the return value of one call as the first parameter in the next call.

Here’s the above function rewritten using pipes:

def pounds_to_grams(x) when is_number(x) and x >= 0 do
  pounds_to_ounces(x)
  |> ounces_to_grams()
end

First, pounds_to_ounces(x) is evaluated. Then, the |> operator passes the return value into the first (and only) parameter of ounces_to_grams/1.

While this concept may feel foreign at first, it’s really helpful for when you need to make sequential calls to modify some data.

Adding pipes to our tests

We can also eliminate some function call nesting in our tests for ounces_to_grams/1 and pounds_to_grams/1.

test "converts ounces to grams" do
  assert UnitConverter.ounces_to_grams(10) |> Float.floor(4) == 283.4951
  assert UnitConverter.ounces_to_grams(0) == 0
  assert UnitConverter.ounces_to_grams(1.5) |> Float.floor(4) == 42.5242
end
test "converts pounds to grams" do
  assert UnitConverter.pounds_to_grams(10) |> Float.floor(2) == 4535.92
  assert UnitConverter.pounds_to_grams(0) == 0
  assert UnitConverter.pounds_to_grams(1.5) |> Float.floor(2) == 680.38
end

You did it!

Now you know how to use type guards and pipes in Elixir. You’ve also written a small module that you can add more of your own functions to.

To make sure you don’t miss the next part of this series or any of my other Elixir content, subscribe to my monthly newsletter, follow me on Dev, or follow me on Twitter.

Adam Davis

Share this post: