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:
- Pass the input into
pounds_to_ounces/1
- Store the result in a variable named
ounces
- Pass
ounces
intoounces_to_grams/1
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.
More Content
If you liked this, you might also like some of my other posts. If you want to be notified of my new posts, follow me on Dev or subscribe to my brief monthly newsletter.
- What's the best questions you've been asked in a job interview?
- What was the first program you wrote?
- The weird quirk of JavaScript arrays (that you should never use)
- Does Elixir have for loops?
- Learn Elixir with me!
- Project Tours: Bread Ratio Calculator
- Changing Emoji Skin Tones Programmatically
- I made my first svg animation!
- 5 tips for publishing your first npm package
- 4 Hugo Beginner Mistakes
Top comments (0)