Learnings from playing with FizzBuzz for 3 days
As I wrote in a previous post1, I've been playing with Coding Katas to practice my TDD/Object-Oriented Design/Functional Programming skills.
And I write several solutions for a very simple Kata (FizzBuzz
) in 3
days, here is what I got:
Elixir: Data transformation pipeline
This is my Elixir solution for FizzBuzz
:
defmodule FizzBuzz do def convert(number) do "" |> maybe_concat(rem(number, 3) == 0, "Fizz") |> maybe_concat(rem(number, 5) == 0, "Buzz") |> fallback_if_empty(to_string(number)) end defp maybe_concat(result, true, sound), do: result <> sound defp maybe_concat(result, false, _sound), do: result defp fallback_if_empty("", sound), do: sound defp fallback_if_empty(result, _sound), do: result end
Actually, this is my last attempt for FizzBuzz
problem. I put it at
the first because it's so simple and it literally blew my mind2.
And I think this solution also is a great example for explaining Elixir's core: Data Transformation.
As Dave Thomas said in Programming Elixir:
The goal of Elixir is data-transforming.
FizzBuzz.convert/1
is just converting a string (""
) into another string ("Fizz"
or""
), and another string , and so on.|>
operator makes data transformations explicit.All these transformations were put together using the "pipe operator" (
|>
), which is the perfect match for data-transformation: data pipes in, new data pipes out.Elixir code tries to be declarative, not imperative
Finally, if we look at the private methods, they are all declarative:
- First, do pattern matching on the inputs
- Then, based on the inputs value, generate some new data as the output
In Ruby, I always find myself to push the logic out of the code and into the data, something like:
SOUNDS = { 3: 'Fizz', 5: 'Buzz' } def sound(factor) SOUNDS[factor] end
But with pattern matching, I can do this directly in the function declaration level and don't need to do this by myself anymore. And I find that I use Map in Elixir way less than I would use Hash in Ruby.
(And I'm also curious if it's still necessary to do that in Elixir. Please leave your thoughts in the comment.)
If we think a little bit further, in Ruby, we often initialize different classes based on the input and provides different behavior by leveraging the message dispatching (I call this
Class Level Dispatch
).But with pattern matching, we can send messages to different methods based on the input value (I call this
Method Level Dispatch
).Apparently,
Method Level Dispatch
has smaller granularity thanClass Level Dispatch
, so that we can write shorter code with it because we can push more message dispatching work to the programming language itself3.
Ruby: Data Transformation by Hand
Of course, we can implement the same algorithm in Ruby:
class FizzBuzz def convert(number) result = "" result = maybe_concate(result, number % 3 == 0, "Fizz") result = maybe_concate(result, number % 5 == 0, "Buzz") result = fallback_if_empty(result, number.to_s) result end def maybe_concate(result, concate?, sound) if concate? result <> sound else result end end def fallback_if_empty(result, fallback) if result.empty? fallback else result end end end
But it's not as clear as the Elixir solution:
- Ruby doesn't have Pattern Matching yet4. So we need to handle different execution paths by conditional statements.
- Ruby doesn't have the pipe operator. So we need a temporary variable to manually manage this state.
Ruby: Chain of Responsibility
I also wrote a more complex solution in Ruby. (Because I want to try
the Chain of Responsibility pattern and FizzBuzz
seems to be a good
fit.)
SoundsChain
class SoundsChain def initialize(sound, fallback) @sound = sound @fallback = fallback end def for(number) if sound.convertable? sound.for(number) else fallback.for(number) end end private attr_reader :sound end
ConcatenatableSound
class ConcatenatableSound def initialize(sounds) @sounds = sounds end def convertable?(number) sounds.any? { |sound| sound.convertable?(number) } end def for(number) sounds(number).join end private attr_reader :sounds def sounds(number) sounds.map { |sound| sound.for(number) } end end
FactorSound
class FactorSound def initialize(factor, sound) @factor = factor @sound = sound end def convertable?(number) number % sound == 0 end def for(number) if convertable?(number) sound else '' end end private attr_reader :sound end
StringSound
class StringSound def convertable?(_number) true end def for(number) number.to_s end end
We have 2 chains here:
SoundsChain
- It will return the sound if this number is convertable by current sound, otherwise it will ask fallback sound to convert the number.
ConcatenatableSound
- This class is not strictly a Chain of Responsibility, since the
#for
method is concatenating all the converted results in this chain.
It is a complex solution. But we can see a absolutely clean interface here:
convertable?
- Test if this
number
can be convert by this object. for
- Return the converted sound for this number if possible.
Summary
FizzBuzz
is a pretty simple problem that can be solved in many
different ways, like:
- Pass in
Proc
as#for
and#convertable?
- Use instance variable as the internal state to convert sounds
- etc.
I'm not saying that Elixir's solution is better than Ruby's solution.
It's just that FizzBuzz
is a perfect data transforming question
that's a perfect fit for Elixir.
I'm also not saying that Chain of Responsibility is a better solution
for FizzBuzz
. We can see that it's more complex and needs more lines
of code to implement.
It's pretty interesting to do coding katas in these ways to explore different solutions. Give it a try and maybe you can find more useful things from a simple problem.
Footnotes:
I also mentioned this idea in 我看程序语言的历史、现在与将来 - dsdshome
Pattern Matching might be added in Ruby 3 -- from RubyConf 2017: Opening Keynote - Good Change, Bad Change by Yukihiro Matsumoto - YouTube