Some thoughts after writing my first program in Elixir

I wrote my very first program in Elixir as a learning exercise.

The program is a simple implementation of the Fizz-Buzz Test.

I felt like sharing my first encounter with Elixir; perhaps it will be useful for fellow beginners.

My main goals for this project were to:

  • Try out Visual Studio Code with ElixirLS.
  • Learn how to create a new project using mix.
  • Get an understanding of the files which are scaffolded by mix.
  • Learn how to use ExUnit for testing functions and module documentation.
  • Get some exposure to the language itself, to get comfortable with the syntax.

I intentionally kept all of the commits in the repo instead of squashing them together, so that I could study them to learn from my mistakes. My meandering is exposed for the world to see :blush:

A couple of things that surprised me:

  • It was extremely easy to create a new project and grok ExUnit, due to its explicit nature.
  • Visual Studio Code with LanguageLS was a really great experience.
  • I discovered a surprising bug the very first time I ran my tests.
  • Having tests really sped up my development cycle, as I didn’t need to drop into iex.

Some difficulties I stumbled upon:

  • I had to spend some time to get the ordering of the case clauses correct.
  • For a while, I struggled with matching 0, until I discovered how easy it was ({0, 0, 0}).
  • It doesn’t seem like Elixir has an is_range() guard, so I had to find a different solution.
    • I first tried to implement my own is_range() guard with defguardp, but gave up on that when I wasn’t able to pattern-match or call a local function within the guard definition.
  • I wasn’t sure whether to use Enum.map() or list comprehension.
    • Both approaches worked, but I opted for Enum.map() because it looked nicer.

One other thing that’s bugging me is this code:

n when is_list(n) ->
  Enum.map(n, fn n -> check_number(n) end)

n = %Range{} ->
  Enum.map(n, fn n -> check_number(n) end)

Because there is duplication; only the case differs. I haven’t figured out (yet) how to either match both of those clauses at once. Maybe I will extract that line to a private helper function.

Other than that, it was pretty much straightforward and an enjoyable experience.

A few areas I want to look into next:

  • Adding specifications to functions using @spec.
    • Figure out if I can add type specs for multiple input values.
7 Likes

:wave::+1:

Want some unsolicited advice?

I’d split check/1 into multiple functions:

def check(number) when is_integer(number) do
  check_number(n)
end

def check(numbers) when is_list(numbers) or is_range(numbers) do
  Enum.map(n, fn n -> check_number(n) end)
end
3 Likes

Oopsie, is_range guard doesn’t exist in elixir, you can match on it as a struct then.

1 Like

Yes, of course! I greatly appreciate any advice I can get. :+1:

This is probably a good idea. I’m also not sure if it’s a good idea to have the same function take different types of inputs (integers, lists and ranges). That’s something that’s bugging me.

def check(number) when is_integer(number) do
  check_number(n)
end

def check(numbers) when is_list(numbers) do
  check_numbers(numbers)
end

def check(%Range{} = numbers) do
  check_numbers(numbers)
end

defp check_numbers(numbers) do
  Enum.map(n, fn n -> check_number(n) end)
end
3 Likes

Indeed! I learned that the hard way. Tried (and failed) at implementing my own guard (details in OP).

Ah, yes! Neat. Thanks for the input.

I forgot that I could “overload” functions by giving them the same name, even if they all take one argument. Mental baggage from working with other languages.

Why this at all? You have another clause that checks for integers first, so you have sorted them out already, just call Enum.map with whatever n you have now.

2 Likes

Hmmm, yeah—good point. Thanks for the advice; I’ll try it out.

The reason why I did it like that, was because I thought I should be as explicit and excluding as possible, in case somebody were to pass it a different kind of type with a different “shape” which implements the Enumerable protocol, e.g. a map or something like that.

Bear in mind that I’m totally new to this :slight_smile:

Then maybe you can fail in check/1 for an invalid input (non-integer) and call it from check/1 for lists and ranges?

  @type result :: integer | :fizz | :buzz | :fizz_buzz

  @spec check(integer) :: result
  def check(number) when is_integer(number) do
    case {rem(number, 3), rem(number, 5), number} do
      {0, 0, 0} -> 0
      {0, 0, _} -> :fizz_buzz
      {0, _, _} -> :fizz
      {_, 0, _} -> :buzz
      {_, _, _} -> number
    end
  end

  @spec check(list | Range.t()) :: [result] # can actually be arbitrary nested
  def check(numbers) do
    Enum.map(numbers, fn number -> check(number) end)
  end
2 Likes

According to the rules of fizzbuzz, shouldn’t 0 return fizzbuzz?

Here’s my attempt at reorganizing your code:

defmodule Try do
  def fizzbuzz(number) when is_integer(number) do
    convert(number, rem(number,3), rem(number, 5))
  end
  def fizzbuzz(numbers) do
    Enum.map(numbers, &convert(&1, rem(&1,3), rem(&1,5)) )
  end

  defp convert(_,0,0), do: :fizz_buzz
  defp convert(_,0,_), do: :fizz
  defp convert(_,_,0), do: :buzz
  defp convert(number,_,_), do: number

end

iex(13)> c "my.exs"                   
warning: redefining module Try (current version defined in memory)
  my.exs:1
warning: redefining module Lists (current version defined in memory)
  my.exs:36
[Lists, Try]

iex(14)> Try.fizzbuzz 0
:fizz_buzz

iex(15)> Try.fizzbuzz [1, 2, 3, 4, 5, 15]
[1, 2, :fizz, 4, :buzz, :fizz_buzz]

iex(16)> Try.fizzbuzz 1..15              
[1, 2, :fizz, 4, :buzz, :fizz, 7, 8, :fizz, :buzz, 11, :fizz, 13, 14,
 :fizz_buzz]

iex(17)> 

In erlang, multiple function clauses are preferred over case statements.

I thought I should be as explicit and excluding as possible

In erlang that’s considered good practice because it makes the code easier to read: you don’t have to try to guess what types of arguments fall through to the second function clause.

And, I learned something from you: %Range{}. I had to go searching for what that meant. Apparently, a range is defined as a struct and %Range{} will match any range. Thanks!

3 Likes

I couldn’t find any mention of how to handle zero on the site I was reading, but I thought it would make more sense to treat zero in a special way, because its unlike all other numbers. It’s easy enough to make zero retur :fizz_buzz, though—By just removing the first case.

Awesome, thanks for showing me an alternative approach! That’s a neat way of doing it.

Yes, I read about that in “Programming Erlang” but forgot about it. Thanks for the reminder :smile:

Yeah, it actually took me more than an hour to figure out how to do that! After beating my head, trying to implement my own is_range() guard. Had to go digging in the documentation.

I’m glad I could “teach” you something by coincidence :slight_smile:

I’ve read Programming Erlang too, and I’m new to elixir as well.

1 Like

Whoa, that stuff you’re doing up there with @type and pipes, and down there in check()with @spec—That looks like magic to me right now :joy: Haven’t encountered that in my book yet.

But I’m intrigued and I’ll definitely look into that!

Very cool! I’ll follow your posts to learn from your learning :slight_smile:

I used @type and @spec in that snippet mostly as documentation (to show which check/1 expects what), neither of them directly affects the check/1's execution.

1 Like

Aha, I see what it’s doing now. Nifty!

Also, thank for showing me how to spec for two types at once.

It would crash, in exactly the same way it would crash today when one passes a keyword list…

1 Like

Yes, good point.

Would you say there is a point in trying to prevent something from crashing in a case such as this, or is the “let it crash” mantra applicable here?

I do think so. I’m fine with crashing, especially when the caller is passing in values which violates the contract.

1 Like