Beer Song - Exercism Elixir Solution & Tutorial


Percy Grunwald's Profile Picture

Written by Percy Grunwald

— Last Updated February 22, 2019

Today’s problem is Beer Song: implement functions to print verses of the famous “99 Bottles of Beer” song.

Live solution video

This is my daily live stream video solution to this problem.

Explanation of the solution

Function(s) to print a single verse of the beer song

All the nth verses of the song are the same except for the verses for 2, 1 and 0, the three of which are also unique to each other. We can express this in code as a long case statement. The case statement below checks for a number of 0, 1, 2 and all other numbers _:

def verse(number) do
  case number do
    0 ->
      """
      No more bottles of beer on the wall, no more bottles of beer.
      Go to the store and buy some more, 99 bottles of beer on the wall.
      """

    1 ->
      """
      1 bottle of beer on the wall, 1 bottle of beer.
      Take it down and pass it around, no more bottles of beer on the wall.
      """

    2 ->
      """
      2 bottles of beer on the wall, 2 bottles of beer.
      Take one down and pass it around, 1 bottle of beer on the wall.
      """

    _ ->
      """
      #{number} bottles of beer on the wall, #{number} bottles of beer.
      Take one down and pass it around, #{number - 1} bottles of beer on the wall.
      """
  end
end

We can also implement this as an overloading of the verse/1 function. In Elixir it’s possible to overload a function and pattern match on the arguments. For example, we can pattern match the number 0 as the argument:

def verse(0) do
  """
  No more bottles of beer on the wall, no more bottles of beer.
  Go to the store and buy some more, 99 bottles of beer on the wall.
  """
end

Or 1:

def verse(1) do
  """
  1 bottle of beer on the wall, 1 bottle of beer.
  Take it down and pass it around, no more bottles of beer on the wall.
  """
end

Or 2:

def verse(2) do
  """
  2 bottles of beer on the wall, 2 bottles of beer.
  Take one down and pass it around, 1 bottle of beer on the wall.
  """
end

For all other cases, we can just pass an argument name, which will match any value:

def verse(number) do
  """
  #{number} bottles of beer on the wall, #{number} bottles of beer.
  Take one down and pass it around, #{number - 1} bottles of beer on the wall.
  """
end

I think for this problem, using case and function overloading are equally readable (my submitted solution uses function overloading). I usually recommend using case instead of function overloading when you can express all cases in less than ~20 lines of code.

Function to print a range of verses of the beer song

The next function we needed to implement was one that could print a range of verses (e.g. verse 3 to verse 0) or the whole song if no range was passed.

We can leverage the verse/1 function we already implemented and use Enum.reduce/3 to build up our complete string, putting a newline between the verses "\n".

The extra newline that gets added in the last iteration can be removed with the String.slice/2 function. Calling String.slice(string, 0..-2) will remove just the last character from string. We could have also done something more fancy using indexes in the iteration and not putting a newline in the final iteration, but I think this would have been needlessly complex.

We set a default value for the range argument of 99..0. The default value gets used when the argument is not passed, in this case calling range() with no arguments will be the same as calling range(99..0). In Elixir you can pass default values for arguments using \\.

def lyrics(range \\ 99..0) do
  Enum.reduce(range, "", fn number, acc ->
    acc <> verse(number) <> "\n"
  end)
  |> String.slice(0..-2)
end

Full solution in text form

Here’s the full solution in text form, in case you want to look over the final product:

defmodule BeerSong do
  @doc """
  Get a single verse of the beer song
  """
  @spec verse(integer) :: String.t()
  def verse(0) do
    """
    No more bottles of beer on the wall, no more bottles of beer.
    Go to the store and buy some more, 99 bottles of beer on the wall.
    """
  end

  def verse(1) do
    """
    1 bottle of beer on the wall, 1 bottle of beer.
    Take it down and pass it around, no more bottles of beer on the wall.
    """
  end

  def verse(2) do
    """
    2 bottles of beer on the wall, 2 bottles of beer.
    Take one down and pass it around, 1 bottle of beer on the wall.
    """
  end

  def verse(number) do
    """
    #{number} bottles of beer on the wall, #{number} bottles of beer.
    Take one down and pass it around, #{number - 1} bottles of beer on the wall.
    """
  end

  @doc """
  Get the entire beer song for a given range of numbers of bottles.
  """
  @spec lyrics(Range.t()) :: String.t()
  def lyrics(range \\ 99..0) do
    Enum.reduce(range, "", fn number, acc ->
      acc <> verse(number) <> "\n"
    end)
    |> String.slice(0..-2)
  end
end

Comments