Parallel Map Beer Song in Erlang
Friday, June 1, 2007
When learning a new language, it's important to learn the
"idioms"
that are peculiar to that language. Since I'm learning Erlang, I'm
especially interested in the concurrency-related idioms. Hence, my
recent exercises in translating the the
"99 Bottles of Beer" beer song into both a
concurrent Erlang solution (using standard Erlang
spawn/send/receive code) and a
MapReduce Erlang version.
Joe Armstrong
commented (a year ago) on how easy it was to convert
Erlang's map into a pmap (parallel map)
command that would map across multiple processes. He goes into this a
bit more in his new book
Programming Erlang - Software for a Concurrent World and I
was interested in reading more about some of the issues that he
discussed. So, I did some google'ing and came across a
blog post that discussed parallel-map
and parallel-foreach in Erlang. That led to another
interesting blog post on an implementation of a parallel map that
uses job queues to make a more efficient "spread" of the map processes
across multiple processors. Both articles were interesting; however,
one great little tidbit came out of one of the comments to the first blog
posting.
Luke Gorrie posted his version of parallel map:
parmap(F, L) ->
Parent = self(),
[receive {Pid, Result} -> Result end || Pid <- [spawn(fun() -> Parent ! {self(), F(X)} end) || X <- L]].
What a gem!
Luke's function utilizes
list comprehensions to do the send/receive/gather all in one
function. An excellent example of
brainfsck coding! ;-) This is an idiom that I'm sure I'll be using
in many situations now that I've seen it.So, I tweaked my MapReduce version of the beersong to utilize Luke's
parmap (parallel map) function (eliminating the need
for the MapReduce phofs module and steamlining the code a
bit more):-module(beersong3). -export([sing/0]).Fun fun fun! ;-)
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".
create_verse(0) -> {0, io_lib:format(template(0), phrase(0))}; create_verse(Bottle) -> {Bottle, io_lib:format(template(Bottle), phrase(Bottle))}.
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"].
bottles() -> lists:seq(0,99).
parmap(F, L) -> Parent = self(), [receive {Pid, Result} -> Result end || Pid <- [spawn(fun() -> Parent ! {self(), F(X)} end) || X <- L]].
sing() -> L = lists:reverse(lists:sort(parmap(fun(N) -> create_verse(N) end, bottles()))), lists:foreach(fun io:format/1, [Verse || {_Bottle, Verse} <- L]).
Update-2007-06-03: Luke Gorrie sent me an email (and subsequently converted it to a blog post) with some comments about his
parmap code. Unfortunately, while it's a very cute
piece of code, it doesn't handle errors very well so shouldn't be used
in production. Luke provides a better implementation of a parallel map
function on
his blog.Also, Dominique Boucher provided (via Marc Feeley) a pmap implementation in Termite Scheme.

