99 Bottles of Beer (Haskell)

From LiteratePrograms

Jump to: navigation, search
Other implementations: Alice ML | Erlang | Haskell | Inform 7 | Java | OCaml | Perl | Python | Ruby

A typical verse of the 99 bottles of beer song is:

99 bottles of beer on the wall, 99 bottles of beer.
Take one down and pass it around, 98 bottles of beer on the wall.

The verse is then repeated starting with the new count for the number of bottles left on the wall:

98 bottles of beer on the wall, 98 bottles of beer.
Take one down and pass it around, 97 bottles of beer on the wall.

The last three verses (two, one and zero bottles of beer on the wall) are special cases:

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.

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

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.

Note that when we get to one bottle of beer the verse changes from the plural bottles to the singular bottle, and the final line No more bottles of beer on the wall! of the song is different from all the preceding verses. The very last verse is also different from all the others.

The implementation

Because the song is so structured, with only the number of bottles changing in most verses, and pluralization and the next action to take changing in a few other verses, the song can easily be structured as a loop. Here, we use a List monad to iterate over the range of numbers and collect the lines to print in a list. Because this is Haskell, we like to modularize and separate the logic of the program from the actual I/O operations; and someone else can decide what they want to do with the resulting list lazily.

In countdown function, we make use of two other auxiliary functions, bottleCount and nextAction, which will compute for us the correct phrase to sing and the correct next action to take, given the number of bottles of beer we have on hand.

countdown :: Integer -> [String]
countdown n = do i <- [n,n-1..0]
                 let b    = bottleCount i
                     b1   = bottleCount (i-1)
                     next = nextAction i
                 [(b ++ " on the wall, " ++ b ++ "."),
                  (next ++ ", " ++ b1 ++ " on the wall."),

So let us look at these helper functions. The first function, bottleCount, is fairly straight-forward, given the number of bottles of beer on hand, and returns the properly pluralized phase for the song. The only tricky points here are the last pattern match condition. In the first three conditions, we've covered all of the non-negative numbers of beer, which is all we would expect to encounter. However, adding a case to handle negative numbers allows us to easily process the final verse of the song.

Note that in each verse, the end of the second line tells us how many bottles we will have after taking one down. This works fine, so long as we have at least one bottle to take down, the first three cases of bottleCount handle that properly. But in the last verse, we no longer take down a bottle, as we have none, so instead, we head to the store to restock, back up to our original number of beers. If we add a case that handles negative beers, then we can compute the last half of the second line of each verse in the same way, by calling bottleCount with one less beer than we currently have. And so when we are out of beers, we will call it with an argument of -1, triggering this case. Now then, notice how this case works. As we have thrown out the value of the argument in this case, we do not use it in the construction of the phrase. Instead, we will use Haskell's closure properties to return a phrase using a value of n which comes from outside this function. As you will see, this value is the original number of beers we started with.

bottleCount 0 = "No more bottles of beer"
bottleCount 1 = "1 bottle of beer"
bottleCount n | n > 0 = show n ++ " bottles of beer"
bottleCount _ = show n ++ " bottles of beer"

Next, we have nextAction. This function is about as simple as it gets. If the number of beers we have on hand is zero, then we go to the store for more. Otherwise, if we still have some left, we take it down and pass it around.

nextAction 0 = "Go to the store, buy some more"
nextAction _ = "Take one down, pass it around"

Finally we put it all together with the wrapper function bottles. we pass in to bottles the number of bottles of beer we have to start with (sure, most folk start with 99, but we can start with 2 or 17, or 999999 if we so choose -- flexibility is a good thing). We then locally define our helper functions bottleCount, nextAction, and countdown (note that this is where the final case of bottleCount gets its value for n), and then we do a quick sanity check on the number of beers we would like to start with. If that number is zero or less, then we raise an exception, and inform the caller that we are worried about the health of his liver. Otherwise, if we have some beer to tork with, we begin the countdown.

bottles :: Integer -> [String]
bottles n | n > 0     = countdown n
          | otherwise = error "Not even a single bottle of beer?  You must have a drinking problem..."
  where countdown

Finally, we offer a sample invocation that will test our function with the traditional 99 beers. We run putStrLn on each line, which will print each string in the list returned by bottles as a line.

main :: IO ()
main = mapM_ putStrLn (bottles 99)