Concurrent Beer Song in Erlang
Monday, May 14, 2007
I've been playing around with
Erlang a bit more in the past few weeks as I've been travelling
(and have therefore had some additional free time on the plane and in
the hotel). Joe
Armstrong (the creator of Erlang) is writing a new book on Erlang
titled
Programming Erlang - Software for a Concurrent World so,
in addition to playing around with Erlang code,
I've been reading that too (it is due to be released in "dead tree" format
in July; however, you can purchase a pre-release beta of the book in
PDF format). I've made a number of
Erlang-related posts in the past; however, those posts have mostly only
referenced Erlang in the context of concurrency topics (in general) and Lisp
concurrency (more specifically). This post will deal just with
Erlang.
One of the main advantages of learning a new language (whether it's a
natural language or a
programming language) is to teach your
mind to
"think different". When I learn a new
natural language, I try
to train my mind to
think in the new langauge. This is always the hardest part of
learning a language. I find that my personality and mannerisms are
quite different when I'm speaking different languages. The same is
true when learning and using a new programming language. Erlang has a number of interesting features as a
programming language; however, the main new abstraction that one has
to come to grips with when learning the language is
concurrency oriented programming (see
paper,
slides or
movie). So, I'm constantly
trying to utilize concurrency in the example programs I'm
writing.
Since beer is always a favorite topic of mine and there is a really
nice implementation of the
"99 Bottles of Beer" beer song
in Common
Lisp, I had a look at the
Erlang versions of the song. Surprisingly, none of them used
concurrency as part of the solution. My mental image of the beer song
always involves a group of singers passing around bottles of beer
in a beer hall
(for example:
this one ;-) ),
and it seemed like the perfect opportunity to use concurrency! So, I
came up with the following solution:
-module(beersong). -export([sing/0]). -define(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"). -define(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").The program is run with
create_verse(0) -> {0, io_lib:format(?TEMPLATE_0, phrase(0))}; create_verse(Bottle) -> {Bottle, io_lib:format(?TEMPLATE_N, 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:reverse(lists:seq(0,99)).
sing() -> lists:foreach(fun spawn_singer/1, bottles()), sing_verse(99).
spawn_singer(Bottle) -> Pid = self(), spawn(fun() -> Pid ! create_verse(Bottle) end).
sing_verse(Bottle) -> receive {_, Verse} when Bottle == 0 -> io:format(Verse); {N, Verse} when Bottle == N -> io:format(Verse), sing_verse(Bottle-1) after 3000 -> io:format("Verse not received after 3 seconds - re-starting singer~n"), spawn_singer(Bottle), sing_verse(Bottle) end.
beersong:sing().which initially launches a bunch of verse-creating processes (e.g. - "singers"). It then receives messages from those processes in order to "sing" the song. If any of the verse-creating processes fails (something that can happen in real-life beer songs too!),
sing_verse will restart the failing process - this can be
simulated by changing the bottles function (so that the processes for
verses 0 & 1 won't initially be launched) to:
bottles() -> lists:reverse(lists:seq(2,99)).In addition to being a concurrent solution, this small code snippet contains examples of the following Erlang features: concurrency, message passing, receive timeouts, fault tolerance, pattern matching, tail recursion, list processing, higher order functions, anonymous functions, anonymous variables, guards, macros, BIFs. Converting the concurrent code to a distributed version of the same program (running on multiple processors) requires just a minor "tweak" (left as an exercise for the reader ;-) ).
Sure, a non-concurrent version of the code is a bit shorter:
-module(beersong). -export([sing/0]). -define(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"). -define(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").But the concurrent one is much more fun! ;-)
create_verse(0) -> io:format(?TEMPLATE_0, phrase(0)); create_verse(Bottle) -> io:format(?TEMPLATE_N, phrase(Bottle)), create_verse(Bottle-1).
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"].
sing() -> create_verse(99).

