Clementson's Blog

Bits and pieces (mostly Lisp-related) that I collect from the ether.

May 2007
Sun Mon Tue Wed Thu Fri Sat
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Apr  Jun

MapReduce Beer Song in Erlang

Sunday, May 20, 2007

In an earlier post, I created a concurrent Erlang version of the "99 Bottles of Beer" beer song. In that post, I mentioned that I was reading a beta copy of Joe Armstrong's new book Programming Erlang - Software for a Concurrent World. Well, in that book, Joe creates a simplistic version of MapReduce in Erlang. (I described MapReduce in my earlier post on "Concurrent/Parallel Programming - The Next Generation"). So, I decided to implement the beer song using Joe's Erlang version of MapReduce. (Note: Although you have to pay for a copy of Joe's book, the source code from the book is freely downloadable. If you want to try out my program below, you will need the "phofs.erl" file from that code archive.)

Basically, usage of Joe's mapreduce function is as follows:

  1. The calling program calls the mapreduce(F1, F2, Acc0, L) function passing it 4 parameters:
    1. F1: The name of the "map" function that will do whatever operation needs to be done on each list element that will be processed.
    2. F2: The name of the "reduce" function that will take all of the output from the "map" function calls and assemble them into a list to return to the caller.
    3. Acc0: The initialized value of the output list to be used by F2.
    4. L: The input list that will be processed by the F1 mapping function.
  2. For each element of the "L" input list, the mapreduce function spawns off a new "F1" mapping process.
  3. The mapreduce function also runs the "F2" reduce process to assemble all of the output from the "F1" processes into a single list.
  4. The calling program receives back the list containing all of the results from the mapreduce function and does whatever it needs to do with that list.
Basically, this can be represented as follows:

MapReduce

So, converting my concurrent Erlang version of the beer song to a MapReduce Erlang version wasn't hard at all:
-module(beersong).
-export([sing/0]).
template(0) -> "~s of beer on the wall, ~s of beer.~nGo to the store and buy some more, 99 bottles of beer on the wall.~n"; template(_N) -> "~s of beer on the wall, ~s of beer.~nTake one down and pass it around, ~s of beer on the wall.~n~n".
bottles() -> lists:seq(0,99).
phrase(0) -> ["No more bottles", "no more bottles"]; phrase(1) -> ["1 bottle", "1 bottle", "no more bottles"]; phrase(2) -> ["2 bottles", "2 bottles", "1 bottle"]; phrase(Bottle) -> lists:duplicate(2, integer_to_list(Bottle) ++ " bottles") ++ [integer_to_list(Bottle-1) ++ " bottles"].
do_map(Pid, 0) -> Pid ! {0, io_lib:format(template(0), phrase(0))}; do_map(Pid, Bottle) -> Pid ! {Bottle, io_lib:format(template(Bottle), phrase(Bottle))}.
do_reduce(Key, Verse, A) -> [{Key, Verse}|A].
sing() -> F1 = fun do_map/2, F2 = fun do_reduce/3, L1 = phofs:mapreduce(F1, F2, [], bottles()), lists:map(fun(X) -> {_, Verse} = X, io:format(Verse) end, lists:reverse(lists:sort(L1))), ok.
The do_map "Map" processes format the verse output for each of the bottles that is being processed and return a key/value tuple (consisting of the bottle# and the "verse" for that bottle). The do_reduce "Reduce" process takes all of those key/value tuples and assembles them into a list (conceivably, it could have done an insertion sort so that the output was ordered correctly, but I decided to keep things simple, so mapreduce just returns the unsorted output). The resulting list is then sorted and "sung". Fun fun! ;-)

Update-2007-05-20: Luke Gorrie pointed out that my original "TEMPLATE_0 and TEMPLATE_n macros would be just fine as functions instead of macros" - and, indeed, they are much nicer as functions! You can still see the original macros in my earlier post while the code in the current post has the function implementations.

emacs Copyright © 2007 by Bill Clementson