DEV Community

Cover image for Your first Elixir Project (Part 1)
Adam Davis
Adam Davis

Posted on • Originally published at brewinstallbuzzwords.com

Your first Elixir Project (Part 1)

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:

  1. 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 running r followed by the name of the module: r UnitConverter
  2. 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 the recompile 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 called UnitConverter
  • You compiled that module and accessed it using iex, which is the REPL environment for elixir
  • You ran the hello function in the UnitConverter 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. def lets elixir know that you're writing a function
  2. do signifies the end of the function header and the beginning of the function body
  3. end tells elixir you've reached the end of the code block
  4. 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
Enter fullscreen mode Exit fullscreen mode

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 passed
  • assert is a function that causes the test to fail if its argument evaluates to false. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If you run the tests again, they should all pass!

....

Finished in 0.09 seconds
1 doctest, 3 tests, 0 failures
Enter fullscreen mode Exit fullscreen mode

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.

More content

If you liked this, you might also like some of my other posts:

Discussion (0)