Table of Contents
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.
- Video available here: https://youtu.be/REO7moXVaz8
- Also streaming on Twitch: https://www.twitch.tv/percygrunwald
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