May 14th, 2009

Erlang FizzBuzz Showdown (pt2)

In my last post I documented some of the steps involved in building a simple FizzBuzz script in Erlang and compared the code to its equivalents in Python, Ruby and PHP. So far we’ve simply covered using a tail-recursive function to creat a list of numbers 1-100.

The next problem to tackle is checking whether a number is: just a number, a Fizz (divisible by 3), a Buzz (divisible by 5) or a FizzBuzz (divisible by 3 and 5). As I’m in the habit of using if statements to perform checks (I’m primarily a PHP developer) my first attempt looked like this:

fizzbuzz(I) ->
    if
        ((I rem 3 =:= 0 ) and (I rem 5 =:= 0 )) ->
            fizzbuzz;
        I rem 3 =:= 0 ->
            fizz;
        I rem 5 =:= 0 ->
            buzz;
        true ->
            I
    end.

This works just fine, but isn’t a good example of writing Erlang in a functional manner, this is more like imperative programming in style. For example, it is not dissimilar to this python function:

# python

def fizzbuzz(i):
  if i % 3 == 0 and i % 5 == 0:
    return 'Fizzbuzz'

  elif i % 3 == 0:
    return 'Fizz'

  elif i % 5 == 0:
    return 'Buzz'

  else:
    return i

In Erlang (and I assume functional programming in general) Guards rather than if statements are far more appropriate. So, using the multiple entry points for a function and combining them with a guard I ended up with this:

fizzbuzz(N) when N rem 3 == 0, N rem 5 == 0 -> "FizzBuzz";
fizzbuzz(N) when N rem 3 == 0 -> "Fizz";
fizzbuzz(N) when N rem 5 == 0 -> "Buzz";
fizzbuzz(N) -> N.

Once I’d taken on board the functional approach the FizzBuzz function has not lost any readability (if anything this is easier to follow) and has become more compact and efficient. From what I’ve seen so far: Erlang programs rarely need if statements.

The next stage in the FizzBuzz program is to combine this with my earlier range function and loop through the numbers 1-100. Attempt 1 worked like so:

combine(To) ->
  F = (fun(I) ->  fizzbuzz(I) end),
  lists:map(F,range(1,To,[])).

Again, this works: the function combine takes in the number to be counted up to (e.g. 100), then a Fun is created (basically a mini function that can be passed into other functions as an argument) that is a copy of fizzbuzz, and then uses the inbuilt module/function lists/map to apply it to each number in the list returning a new list with the appropriate changes. But again this can be condensed to make better use of Elang’s functional syntax.

combine(To) ->
  [fizzbuzz(X) || X <- range(1,To,[])].

The end result is the same but the function now uses a pattern matching/balancing style syntax to produce the new list. The process breaks down like this:

  • [] is the list notation, and using the pipes to separate the two haves of the process ([X || X <- [1,2,3,4]]) means that the result of the right hand site is passed into the left (as X, formed by breaking the list down X <- [1,2,3,4]), forming a new list one element at a time.
  • The input list is formed using the range function on the right.
  • Then the fizzbuzz function processes the incoming elements of the new list on the left.

And that almost concludes my Erlang FizzBuzz program. However, it is returning a list rather than printing one line at a time. This is the module so far:

-module(fizzbuzz).
-export([fizzbuzz/1,range/3,combine/1]).

fizzbuzz(N) when N rem 3 == 0, N rem 5 == 0 -> "FizzBuzz";
fizzbuzz(N) when N rem 3 == 0 -> "Fizz";
fizzbuzz(N) when N rem 5 == 0 -> "Buzz";
fizzbuzz(N) -> N.

range(To,To,List) -> [To|List];
range(From,To,List) when From < To -> range(From, To -1, [To|List]);
range(From,To,List) when From > To -> List.

combine(To) ->
  [fizzbuzz(X) || X <- range(1,To,[])].

And being used in the console:

Eshell V5.6.5  (abort with ^G)
1> c(fizzbuzz).
{ok,fizzbuzz}
2> fizzbuzz:combine(100).
[1,2,"Fizz",4,"Buzz","Fizz",7,8,"Fizz","Buzz",11,"Fizz",13,
 14,"FizzBuzz",16,17,"Fizz",19,"Buzz","Fizz",22,23,"Fizz",
 "Buzz",26,"Fizz",28,29|...]

Finally, this can be condensed one last time by turning the range function into a tool for applying the fizzbuzz function to each number recursively, printing the results to the console as it goes:

-module(fb_final).
-export([fizzbuzz/1,fbr/2]).

fizzbuzz(N) when N rem 3 == 0, N rem 5 == 0 ->
  io:format("FizzBuzz\n");

fizzbuzz(N) when N rem 3 == 0 ->
  io:format("Fizz\n");

fizzbuzz(N) when N rem 5 == 0 ->
  io:format("Buzz\n");

fizzbuzz(N) ->
  io:format([integer_to_list(N) | "\n"]).

fbr(To,To) -> fizzbuzz(To);
fbr(From,To) when From < To -> fizzbuzz(From), fbr(From + 1, To).

And with that I’m pretty pleased. The fb_final module makes good use of the functional approach, is concise but still fairly easy to read. In conclusion I’m enjoying Erlang a great deal.

P.S:

There is one last small modification that makes fbr/range reusable allowing higher order functions to be passed into it:

range(To,To,F) -> F(To);
range(From,To,F) when From < To -> F(From), range(From + 1, To,F).

This executes using the following console script (inserting fizzbuzz into range as a higher order function):

Eshell V5.6.5  (abort with ^G)
1> c(fb_final).
{ok,fb_final}
2> fb_final:range(1,15,fun(N) -> fb_final:fizzbuzz(N) end).
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
ok

Done and dusted! And here are some other ways of getting FizzBuzz done in a functional style.

  • Guest
    Interesting post. I have stumbled this for my friends. Hope others find it as interesting as I did.
  • Guest
    lists:seq(1,100).
  • Thanks, but as stated in Part 1: this was more to aid learning rather than using built in functionality (which I certainly will in future) - also the range function is modified to receive higher order functions, not sure that can be done with lists:seq?
  • iWantToKeepAnon
    Ah, ok there is the list comprehension. Very good. But the guard "From < To" still isn't (AFAIK) necessary. Erlang matches your function clauses top to bottom and the "range(To,To,F)" clause would break you out of your recursion.

    What is the guard for?
  • The "From < To" is simply to stop an infinite loop if the function was called backwards e.g. range(5,1).
blog comments powered by Disqus
Powered by Wordpress using the theme aav1