Inspired by last Erlang Thursday by Steven Proctor - Functional fizzbuzz I decided to give it a try and see how a FizzBuzz implementation without a modulus operator might look like in Elixir.
Here we go:
defmodule FizzBuzz do
def print(n) do
generate
|> Stream.take(n)
|> Stream.each(&IO.puts/1)
|> Stream.run
end
def generate do
fizzes = Stream.cycle(["Fizz", "", ""])
buzzes = Stream.cycle(["Buzz", "", "", "", ""])
Stream.zip(fizzes, buzzes)
|> Stream.map(&concat/1)
|> Stream.with_index
|> Stream.map(&translate/1)
|> Stream.drop(1)
end
defp concat({f, b}), do: f <> b
defp translate({"", n}), do: to_string(n)
defp translate({str, _n}), do: str
end
It is generally straightforward, but there are some peculiarities, I’d like to
explain.
I know one of the problems in Erlang’s implementation was lack of lazy streams
and functions such as cycle
- in Elixir we don’t have this problem because
Stream
module exists. On the other hand using :lists.zipwith/3
in Erlang
allowed to handle zipping and combining terms in one go. In Elixir we need two
steps - first we use Stream.zip/2
(or Stream.with_index/1
) and
only later we can map the resulting tuple to the format we need.
The call to Stream.drop/2
is needed because in Elixir indexing starts with 0
and we want our first term to be a result of fizzbuzzing a 1
. TO avoid this
issue, we could generate a third stream to give us numbers starting with 1
,
and zip that:
def generate2 do
fizzes = Stream.cycle(["", "", "Fizz"])
buzzes = Stream.cycle(["", "", "", "", "Buzz"])
indexes = Stream.iterate(1, &(&1 + 1))
Stream.zip(fizzes, buzzes)
|> Stream.map(&concat/1)
|> Stream.zip(indexes)
|> Stream.map(&translate/1)
end
I’m not entirely sure which one is clearer, and easier to grasp.