25 January 2007

Erlang fizzbuzzery

Herein, a few different ways of implementing "a" solution to the FizzBuzz problem, showing examples of some different programming styles in Erlang solving the same problem.


Using a loop:

-module(fizzbuzz_with_loop).

-export([start/0]).

start() ->
  fizzbuzz(1,100).

fizzbuzz(N,M) when N > M -> noop;
fizzbuzz(N,M) ->
  io:format("~s~n",[fizzbuzz(N)]),
  fizzbuzz(N+1,M).

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) -> integer_to_list(N).

Compile the module with erlc fizzbuzz_loop.erl and run with erl -s fizzbuzz_loop -run init stop -noshell [1] to produce:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
...

Using foreach (foreach is used because the order of evaluation is important):
-module(fizzbuzz_with_foreach).

-export([start/0]).

start() ->
  lists:foreach(fun(X) -> io:format("~s~n",[fizzbuzz(X)]) end, lists:seq(1,100)).

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) -> integer_to_list(N).

Using list comprehension:
-module(fizzbuzz_with_list_comprehension).

-export([start/0]).

start() ->
  lists:foreach(fun(Y) -> io:format("~s~n",[Y]) end, [fizzbuzz(X) || X <- lists:seq(1,100)]).

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) -> integer_to_list(N).

Using processes:
-module(fizzbuzz_with_processes).

% export default start function for command line
-export([start/0]).

% export entry point for spawned processes
-export([fizzbuzz_process/0]).

start() ->
  Fizz_PID = spawn(fizzbuzz_with_processes,fizzbuzz_process,[]),
  lists:foreach(fun(X) -> ask_fizzbuzz(Fizz_PID,X) end, lists:seq(1,100)),
  Fizz_PID ! {done}.

ask_fizzbuzz(Fizz_PID,N) ->
  Fizz_PID ! {fizzbuzz,N,self()},
  receive {Answer} -> io:format("~s~n",[Answer]) end.

fizzbuzz_process() ->
  receive
    {fizzbuzz,N,Caller_PID} -> Caller_PID ! {fizzbuzz(N)}, fizzbuzz_process();
    {done} -> noop end.

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) -> integer_to_list(N).

With too many processes:
-module(fizzbuzz_with_too_many_processes).

% export default start function for command line
-export([start/0]).

% export entry point for spawned processes
-export([fizzbuzz_fizzer/1]).
-export([fizzbuzz_buzzer/1]).
-export([fizzbuzz_number/1]).
-export([fizzbuzz_printer/0]).

start() ->
  Printer = spawn(fizzbuzz_with_too_many_processes, fizzbuzz_printer, []),
  Processes = lists:map(fun(F) -> spawn(fizzbuzz_with_too_many_processes, F, [Printer]) end, [fizzbuzz_fizzer,fizzbuzz_buzzer,fizzbuzz_number]),
  lists:foreach(fun(X) -> lists:foreach(fun(P) -> P ! {n,X,self()}, receive {ok} -> noop end end, Processes), Printer ! {newline} end, lists:seq(1,100)),
  lists:foreach(fun(P) -> P ! {done} end, [Printer|Processes]).

fizzbuzz_fizzer(Printer) ->
  receive
    {n,N,P} ->
      if N rem 3 == 0 -> Printer ! {s,"Fizz"};
         true -> noop
      end,
      P ! {ok},
      fizzbuzz_fizzer(Printer);
    {done} -> noop end.

fizzbuzz_buzzer(Printer) ->
  receive
    {n,N,P} ->
      if N rem 5 == 0 -> Printer ! {s,"Buzz"};
         true -> noop
      end,
      P ! {ok},
      fizzbuzz_buzzer(Printer);
    {done} -> noop end.

fizzbuzz_number(Printer) ->
  receive
    {n,N,P} ->
      if (N rem 5 =/= 0) and (N rem 3 =/= 0) -> Printer ! {n,N};
         true -> noop
      end,
      P ! {ok},
      fizzbuzz_number(Printer);
    {done} -> noop end.

fizzbuzz_printer() ->
  receive
    {s,S} -> io:format("~s",[S]), fizzbuzz_printer();
    {n,N} -> io:format("~w",[N]), fizzbuzz_printer();
    {newline} -> io:format("~n",[]), fizzbuzz_printer();
    {done} -> noop end.

Why is the above so painful? Mostly because we have to wait for a reply from each fizzbuzz_X process because otherwise some of the processes will send messages to the printer out of order. To compare, see what the output is if you did not wait for an {ok}:
-module(fizzbuzz_with_asynch_processes).

% export default start function for command line
-export([start/0]).

% export entry point for spawned processes
-export([fizzbuzz_fizzer/1]).
-export([fizzbuzz_buzzer/1]).
-export([fizzbuzz_number/1]).
-export([fizzbuzz_printer/0]).

start() ->
  Printer = spawn(fizzbuzz_with_asynch_processes, fizzbuzz_printer, []),
  Processes = lists:map(fun(P) -> spawn(fizzbuzz_with_asynch_processes, P, [Printer]) end, [fizzbuzz_fizzer,fizzbuzz_buzzer,fizzbuzz_number]),
  lists:foreach(fun(N) -> lists:foreach(fun(P) -> P ! {n,N} end, Processes), Printer ! {newline} end, lists:seq(1,100)),
  lists:foreach(fun(P) -> P ! {done} end, [Printer|Processes]).

fizzbuzz_fizzer(Printer) ->
  receive
    {n,N} ->
      if N rem 3 == 0 -> Printer ! {s,"Fizz"};
         true -> noop
      end,
      fizzbuzz_fizzer(Printer);
    {done} -> noop end.

fizzbuzz_buzzer(Printer) ->
  receive
    {n,N} ->
      if N rem 5 == 0 -> Printer ! {s,"Buzz"};
         true -> noop
      end,
      fizzbuzz_buzzer(Printer);
    {done} -> noop end.

fizzbuzz_number(Printer) ->
  receive
    {n,N} when N rem 5 =/= 0, N rem 3 =/= 0 -> Printer ! {n,N}, fizzbuzz_number(Printer);
    {n,_N} -> fizzbuzz_number(Printer);
    {done} -> noop end.

fizzbuzz_printer() ->
  receive
    {s,S} -> io:format("~s",[S]), fizzbuzz_printer();
    {n,N} -> io:format("~w",[N]), fizzbuzz_printer();
    {newline} -> io:format("~n",[]), fizzbuzz_printer();
    {done} -> noop end.

The output of this last program is consistently interleaved. Moral of the story: just because you send a message to each of a set of processes in a particular order, it does not mean that each message will be received or more importantly acted upon in that particular order. Messages to a process are queued in order, but if you send a message M1 to process P1 and another message M2 to process P2, there is no guarantee that process P1 will receive the message before process P2. There is a guarantee that if you send message M1 to process P1 and another message M2 to the same process P1, M1 will be received before M2. But this is a crucial difference.

Lastly, an implementation that works, consisting of far too many processes:
-module(fizzbuzz_with_far_too_many_processes).

-export([start/0]).
-export([fizzbuzz/2]).

start() ->
  % spawn 100 processes to compute the 100 terms
  lists:map(fun(N) -> spawn(fizzbuzz_with_far_too_many_processes,fizzbuzz,[N,self()]) end, lists:seq(1,100)),
  lists:foreach(fun(T) -> io:format("~s~n",[element(2,T)]) end, [I || I <- receive_all(100)]).

receive_all(M) -> receive_all(M,[]).

receive_all(M,L) when M == length(L) -> lists:sort(L);
receive_all(M,L) ->
  receive
    {N,S} -> receive_all(M,[{N,S}|L])
  end.

fizzbuzz(N,P) when N rem 3 == 0, N rem 5 == 0 -> P ! {N,"FizzBuzz"};
fizzbuzz(N,P) when N rem 3 == 0 -> P ! {N,"Fizz"};
fizzbuzz(N,P) when N rem 5 == 0 -> P ! {N,"Buzz"};
fizzbuzz(N,P) -> P ! {N,integer_to_list(N)}.

A key thing to learn from this last listing is that erlang tuples are indexed starting at 1. But that is a gripe for another blog entry.

Notes:
[1] add -smp to the execution of the program to enable Erlang's use of multiple processors.

No comments: