Your first Elixir Project (Part 1)
Table of contents:
Let’s write some Elixir!
But before we start…
This tutorial series is designed to introduce developers to the Elixir programming language and get them up-and-running with a simple project.
If this is the first you’ve heard of Elixir, start with my post that gives an overview of the language.
This post assumes that you’ve already worked through part 0 of this tutorial series, which includes instructions for installing Elixir and creating the template project.
Running the template project
Open a new terminal window and run the command iex
. Now, you should be in the elixir REPL environment.
However, just because you’re in the Elixir REPL doesn’t automatically mean that you can run your code. iex(1)> UnitConverter.hello()
gives an error.
Because Elixir is a compiled language, the function will be undefined until we compile it. For compiling, we have a couple options:
- To compile a single file, run
c
followed by a string for the file path:c "lib/unit_converter.ex"
. After making changes to the code, you would then re-compile the module by runningr
followed by the name of the module:r UnitConverter
- Alternatively, if you are working within a project, you can open an iex shell with
iex -S mix
at the project’s root directory. This will compile all modules in the project, allowing you to recompile all of them at once with therecompile
command.
While method 1 works just fine, I recommend using method 2 as I find it to be more convenient.
After compiling your code with either of the two methods, run UnitConverter.hello()
again in iex. If all went well, you should see :world
printed as the result.
The value has a colon at the beginning because it uses the atom data type. We’re not going to go over the details of elixir’s data types in this post, but here’s the relevant documentation if you want to learn more.
Here’s a recap of what just happened:
- The file
lib/unit_converter.ex
has a module calledUnitConverter
- You compiled that module and accessed it using
iex
, which is the REPL environment for elixir - You ran the
hello
function in theUnitConverter
module using dot notation - That function returned the value
:world
, which caused that value to be printed to the terminal
“But wait,” you might be saying to yourself. “How did that value get returned? I don’t see a return
statement!”
And you’d be right, there is no return statement. That’s because, in elixir, functions automatically return the value of their last statement.
If you want to test that principle out for yourself, try adding another value before or after :hello
and see what happens (don’t forget to recompile before running the code again).
def hello do
:world
:worlds
end
# returns :worlds
def hello do
:world
10
end
# returns 10
def hello do
"there"
:world
end
# returns :world
Writing some code
Now that we’re a bit more acquainted with how to compile and run elixir code, let’s start writing some.
Throughout this tutorial, we’re going to be writing functions that convert between units of measurement.
For our first function, we’ll convert kilograms to grams:
def kilograms_to_grams(x) do
x * 1000
end
All this function does is multiply its input parameter x
by 1000 and return that value.
Here’s some things to note about this function:
def
lets elixir know that you’re writing a functiondo
signifies the end of the function header and the beginning of the function bodyend
tells elixir you’ve reached the end of the code block- The common convention is to use snake case for elixir function names, meaning everything is lowercase and there are underscores between each word
Now let’s run this function.
You should still have iex
open from earlier. If you don’t, just run iex -S mix
. Save your code and run recompile
in iex
. Then run UnitConverter.kilograms_to_grams(10)
. If everything worked, you should see the output 10000
.
🔥 hot tip 🔥
If you’re tired of typing UnitConverter
before every function call, you can run import UnitConverter
in iex
to call them without prefixing the module name. More information can be found on this page of the documentation. You only need to import a module once. If you run recompile
, the updated code will be imported.
Time to test 🎊
Now that we’ve verified our function works for at least some values, it’s time to write some unit tests so we can cover edge cases and ensure that the function continues to work after we make more changes.
In your text editor, open the file test/unit_converter_tests.exs
. This is where test cases are written.
You should see an existing test for the hello
function that was included in the file:
test "greets the world" do
assert UnitConverter.hello() == :world
end
Let’s break this test down:
- Tests blocks start with the
test
keyword "greets the world"
is the name of test, which is used to provide helpful logging about which tests passedassert
is a function that causes the test to fail if its argument evaluates tofalse
. Note that this function does not require parentheses- All that’s really going on here is that our test is verifying whether the value returned by
UnitConverter.hello()
is equal to:world
Now that we’ve gone over what this test does, let’s actually run it!
Open a new terminal window and run the command mix test
. If you haven’t changed the hello
function, then you should get a successful result that looks like this:
..
Finished in 0.06 seconds
1 doctest, 1 test, 0 failures
Now let’s try writing a test for the function we wrote above. From earlier, we know that we want UnitConverter.kilograms_to_gram(10)
to return 10000
so we’ll start there.
test "converts kilograms to grams" do
assert UnitConverter.kilograms_to_grams(10) == 10000
end
Next, run the test using mix test
and you should see a successful result that looks like this:
...
Finished in 0.06 seconds
1 doctest, 2 tests, 0 failures
Before we add more functionality, let’s write some more test cases to ensure our function works for other situations.
test "converts kilograms to grams" do
assert UnitConverter.kilograms_to_grams(10) == 10000
assert UnitConverter.kilograms_to_grams(0) == 0
assert UnitConverter.kilograms_to_grams(1.5) == 1500
end
test "kilograms to grams handles invalid input" do
assert UnitConverter.kilograms_to_grams("hello there") == {:error, "invalid input"}
assert UnitConverter.kilograms_to_grams(-1) == {:error, "invalid input"}
assert UnitConverter.kilograms_to_grams([1, 2, 3]) == {:error, "invalid input"}
assert UnitConverter.kilograms_to_grams(:invalid) == {:error, "invalid input"}
end
There’s a few things going on in the updated test, so let’s break it down:
- We added two new cases with the
"converts kilograms to grams"
test. These cases cover zero and decimal inputs. - Next we created the test
"kilograms to grams handles invalid input"
which, as the name implies, has cases for different invalid inputs. - The value
{:error, "invalid input"}
probably looks unfamiliar. In elixir, the common convention for errors is that a tuple is returned. The first value in the tuple is:error
, and the second value is a message that explains the error.
If we run the tests again with mix test
, we can see that we haven’t handled everything we need to handle:
1) test kilograms to grams handles invalid input (UnitConverterTest)
test/unit_converter_test.exs:15
** (ArithmeticError) bad argument in arithmetic expression: "hello there" * 1000
code: assert UnitConverter.kilograms_to_grams("hello there") == {:error, "invalid input"}
stacktrace:
:erlang.*("hello there", 1000)
(unit_converter 0.1.0) lib/unit_converter.ex:20: UnitConverter.kilograms_to_grams/1
test/unit_converter_test.exs:16: (test)
...
Finished in 0.1 seconds
1 doctest, 3 tests, 1 failure
From this output, we can see that we get an ArithmeticError
when we try to call kilograms_to_grams("hello there")
.
If you take a look back at that function in lib/unit_converter.ex
you can see that we’re using the *
operator, which does not accept strings as an argument.
To ensure we only allow numbers to reach that point, we can check the type of the value in an if statement, using the is_number
function. (For more information, see the docs here)
def kilograms_to_grams(x) do
if is_number(x) do
x * 1000
else
{:error, "invalid input"}
end
end
Now let’s run the tests again and see what we get:
1) test kilograms to grams handles invalid input (UnitConverterTest)
test/unit_converter_test.exs:15
Assertion with == failed
code: assert UnitConverter.kilograms_to_grams(-1) == {:error, "invalid input"}
left: -1000
right: {:error, "invalid input"}
stacktrace:
test/unit_converter_test.exs:17: (test)
...
Finished in 0.08 seconds
1 doctest, 3 tests, 1 failure
The test are still failing, but we get a different failure now. Progress!
The problem now is that we aren’t checking to see if the input is non-negative. To fix this, we can add and x >= 0
to our condition in the if
statement.
def kilograms_to_grams(x) do
if is_number(x) and x >= 0 do
x * 1000
else
{:error, "invalid input"}
end
end
If you run the tests again, they should all pass!
....
Finished in 0.09 seconds
1 doctest, 3 tests, 0 failures
You did it!
You’ve now written your first elixir function and passed your unit tests! If you’d like to continue working on this project, I’ll be releasing a part 2 where you will…
- Learn how to use the pipe operator!
- Use type guards to separate validation from functionality!
- Perform basic pattern matching!
To make sure you don’t miss out, follow me on DEV or subscribe to my monthly newsletter.
If you liked this post, click here to subscribe to my mailing list!