Railway oriented programming: Carbonated edition
Three ways to implement FizzBuzz
Last updated
Was this helpful?
Three ways to implement FizzBuzz
Last updated
Was this helpful?
As a follow up to the post, I thought I'd apply the same technique to the problem, and compare it with other implementations.
A large part of this post was directly stolen from inspired by , with some additional ideas from .
As a reminder, here are the requirements for the FizzBuzz problem:
And here is a basic F# solution:
I have defined a function fizzBuzz
that, given an integer i
, uses match
with when
clauses to do the various tests, and then prints the appropriate value.
Simple and straightforward, and fine for a quick hack, but there are a number of problems with this implementation.
First, note that we had to have a special case for "fifteen". We couldn't just reuse the code from the "three" and "five" cases. And this means that if we want to add another case, such as "seven", we also need to add special cases for all the combinations as well (that is "21", "35" and "105"). And of course, adding more numbers would lead to a combinatorial explosion of cases.
Second, the order of matching is important. If the "fifteen" case had come last in the list of patterns, the code would have run correctly, but not actually met the requirements. And again, if we need to add new cases, we must always remember to put the largest ones first to ensure correctness. This is just the kind of thing that causes subtle bugs.
Let's try another implementation, where we reuse the code for the "three" and "five" case, and eliminate the need for a "fifteen" case altogether:
In this implementation, the printed value for "15" will be correct, because both the "3" and "5" cases will be used. And also we don't have to worry about order -- as much, anyway.
But -- these branches are no longer independent, so we have to track whether any branch has been used at all, so that we can handle the default case. And that has led to the mutable variable. Mutables are a code smell in F#, so this implementation is not ideal.
However, this version does have the advantage that it can be easily refactored to support multiple factors, not just 3 and 5.
Below is a version that does just this. We pass in a list of "rules" to fizzBuzz
. Each rule consists of a factor and a corresponding label to print out. The fizzBuzz
function then just iterates through these rules, processing each in turn.
If we want additional numbers to be processed, we just add them to the list of rules, like this:
To sum up, we have created a very imperative implementation that would be almost the same in C#. It's flexible, but the mutable variable is a bit of a code smell. Is there another way?
In this next version, we'll look at using the "pipeline" model, where data is fed through a series of functions to arrive at a final result.
In this design, I envision a pipeline of functions, one to handle the "three" case, one to handle the "five" case, and so on. And at the end, the appropriate label is spat out, ready to be printed.
Here is some pseudocode to demonstrate the concept:
As an additional requirement, we want the pipeline to have no side effects. This means that the intermediate functions must not print anything. They must instead pass any generated labels down the pipe to the end, and only at that point print the results.
As a first step, we need to define what data will be fed down the pipe.
Let's start with the first function, called handleThreeCase
in the pseudocode above. What is its input, and what is its output?
Obviously, the input is the integer being processed. But the output could be the string "Fizz" if we're lucky. Or the original integer if we're not.
So now let's think about the input to the second function, handleFiveCase
. It needs the integer as well. But in the case of "15" it also needs the string "Fizz" as well, so it can append "Buzz" to it.
Finally, the handleAllOtherCases
function converts the int to a string, but only if "Fizz" or "Buzz" have not been generated yet.
It's quite obvious then, that the data structure needs to contain both the integer being processed and the "label so far".
The next question is: how do we know if an upstream function has created a label? The handleAllOtherCases
needs to know this in order to determine whether it needs to do anything.
One way would be to use an empty string (or, horrors, a null string), but let's be good and use a string option
.
So, here's the final data type that we will be using:
With this data structure, we can define how handleThreeCase
and handleFiveCase
will work.
First, test the input int i
to see if it is divisible by the factor.
If it is divisible, look at the label
-- if it is None
, then replace it with Some "Fizz"
or Some "Buzz"
.
If the label already has a value, then append "Buzz" (or whatever) to it.
If the input is not divisible by the factor, just pass on the data unchanged.
The design for the handleAllOtherCases
function is slightly different:
Look at the label -- if it is not None
, then a previous function has created a label, so do nothing.
But if the label is None
, replace it with the string representation of the integer.
Here's the code -- I will call it labelOrDefault
:
Now that we have the components, we can assemble the pipeline:
Note that we have to create an initial record using {i=i; label=None}
for passing into the first function (carbonate 3 "Fizz"
).
Finally, here is all the code put together:
Creating a new record type can be useful as a form of documentation, but in a case like this, it would probably be more idiomatic to just use a tuple rather than creating a special data structure.
So here's a modified implementation that uses tuples.
As an exercise, try to find all the code that had to be changed.
In the tuple code above, I have also replaced the explicit Option matching code match .. Some .. None
with some built-in Option functions, map
and defaultArg
.
Here are the changes in carbonate
:
and in labelOrDefault
:
You might be wondering about the strange looking |> defaultArg <|
idiom.
I am using it because the option is the first parameter to defaultArg
, not the second, so a normal partial application won't work. But "bi-directional" piping does work, hence the strange looking code.
Here's what I mean:
Our carbonate
function is generic for any factor, so we can easily extend the code to support "rules" just as in the earlier imperative version.
But one issue seems to be that we have hard-coded the "3" and "5" cases into the pipeline, like this:
How can we dynamically add new functions into the pipeline?
The answer is quite simple. We dynamically create a function for each rule, and then combine all these functions into one using composition.
Here's a snippet to demonstrate:
Each rule is mapped into a function. And then the list of functions is combined into one single function using >>
.
Putting it all together, we have this final implementation:
Comparing this "pipeline" version with the previous imperative version, the design is much more functional. There are no mutables and there are no side-effects anywhere (except in the final printf
statement).
There is a subtle bug in the use of List.reduce
, however. Can you see what it is?** For a discussion of the problem and the fix, see the postscript at the bottom of this page.
** Hint: try an empty list of rules.
As a quick reminder, in "railway oriented programming" (a.k.a the "Either" monad), we define a union type with two cases: "Success" and "Failure", each representing a different "track". We then connect a set of "two-track" functions together to make the railway.
Most of the functions we actually use are what I called "switch" or "points" functions, with a one track input, but a two-track output, one for the Success case, and one for the Failure case. These switch functions are converted into two-track functions using a glue function called "bind".
Here is a module containing definitions of the functions we will need.
I am using the Choice
type here, which is built into the F# core library. But I have created some helpers to make it look like the Success/Failure type: an active pattern and two constructors.
Now, how will we adapt FizzBuzz to this?
Let's start by doing the obvious thing: defining "carbonation" as success, and an unmatched integer as a failure.
In other words, the Success track contains the labels, and the Failure track contains the ints.
Our carbonate
"switch" function will therefore look like this:
This implementation is similar to the one used in the pipeline design discussed above, but it is cleaner because the input is just an int, not a record or a tuple.
Next, we need to connect the components together. The logic will be:
if the int is already carbonated, ignore it
if the int is not carbonated, connect it to the input of the next switch function
Here is the implementation:
Another way of writing this is to use the either
function we defined in the library module:
Make sure you understand that both of these implementations do exactly the same thing!
Next, we can create our "two-track" pipeline, like this:
This is superficially similar to the "one-track" pipeline, but actually uses a different technique. The switches are connected together through composition (>>
) rather than piping (|>
).
As a result, the fizzBuzz
function does not have an int parameter -- we are defining a function by combining other functions. There is no data anywhere.
A few other things have changed as well:
We have had to reintroduce the explicit test for the "15" case. This is because we have only two tracks (success or failure).
There is no "half-finished track" that allows the "5" case to add to the output of the "3" case.
The labelOrDefault
function from the previous example has been replaced with either
. In the Success case, a string is printed. In the Failure case, an int is printed.
Here is the complete implementation:
We defined carbonation as "success"in the example above -- it seems the natural thing to do, surely. But if you recall, in the railway oriented programming model, "success" means that data should be passed on to the next function, while "failure" means to bypass all the intermediate functions and go straight to the end.
For FizzBuzz, the "bypass all the intermediate functions" track is the track with the carbonated labels, while the "pass on to next function" track is the one with integers.
So we should really reverse the tracks: "Failure" now means carbonation, while "Success" means no carbonation.
By doing this, we also get to reuse the pre-defined bind
function, rather than having to write our own connect
function.
Here's the code with the tracks switched around:
The fact that we can swap the tracks so easily implies that that maybe there is a weakness in the design. Are we trying to use a design that doesn't fit?
Why does one track have to be "Success" and another track "Failure" anyway? It doesn't seem to make much difference.
So, why don't we keep the two-track idea, but get rid of the "Success" and "Failure" labels.
Instead, we can call one track "Carbonated" and the other "Uncarbonated".
To make this work, we can define an active pattern and constructor methods, just as we did for "Success/Failure".
In this case, if FizzBuzz was our domain, then our functions could now use the domain-friendly terminology of carbonated
and uncarbonated
rather than "success" or "failure".
Note that, as before, the connect
function can be rewritten using either
(or we can just use the predefined bind
as before):
Here's all the code in one module:
There are some problems with the version we have so far:
The "15" test is ugly. Can we get rid of it and reuse the "3" and "5" cases?
The "3" and "5" cases are hard-coded. Can we make this more dynamic?
As it happens, we can kill two birds with one stone and address both of these issues at once.
The trick is to define a "append" or "concat" function for combining two functions. Once we can add two functions this way, we can continue and add as many as we like.
So given that we have two carbonation functions, how do we concat them?
Well, here are the possible cases:
If they both have carbonated outputs, we concatenate the labels into a new carbonated label.
If one has a carbonated output and the other doesn't, then we use the carbonated one.
If neither has a carbonated output, then we use either uncarbonated one (they will be the same).
Here's the code:
As an aside, notice that this code is almost like math, with uncarbonated
playing the role of "zero", like this:
This is not a coincidence! We will see this kind of thing pop up over and over in functional code. I'll be talking about this in a future post.
Anyway, with this "concat" function in place, we can rewrite the main fizzBuzz
like this:
The two carbonate
functions are added and then passed to either
as before.
Here is the complete code:
With this addition logic available, we can easily refactor the code to use rules. Just as with the earlier "pipeline" implementation, we can use reduce
to add all the rules together at once.
Here's the version with rules:
In this post, we've seen three different implementations:
An imperative version that used mutable values and mixed side-effects with logic.
A "pipeline" version that passed a data structure through a series of functions.
A "railway oriented" version that had two separate tracks, and used "addition" to combine functions in parallel.
In my opinion, the imperative version is the worst design. Even though it was easy to hack out quickly, it has a number of problems that make it brittle and error-prone.
Of the two functional versions, I think the "railway oriented" version is cleaner, for this problem at least.
By using the Choice
type rather than a tuple or special record, we made the code more elegant thoughout. You can see the difference if you compare the pipeline version of carbonate
with the railway oriented version of carbonate
.
In other situations, of course, the railway oriented approach might not work, and the pipeline approach might be better. I hope this post has given some useful insight into both.
Be careful with List.reduce
-- it will fail with empty lists. So if you have an empty rule set, the code will throw a System.ArgumentException
.
In the pipeline case, you can see this by adding the following snippet to the module:
The fix is to replace List.reduce
with List.fold
. List.fold
requires an additional parameter: the initial (or "zero") value. In this case, we can use the identity function, id
, as the initial value.
Here is the fixed version of the code:
Similarly, in the railway oriented example, we had:
which should be corrected to:
where zero
is the "default" function to use if the list is empty.
As an exercise, define the zero function for this case. (Hint: we have actually already defined it under another name).
Given this design, here's the implementation. It's a generic function that I will call carbonate
(after ) because it works with both "Fizz" and "Buzz":
The pipeline version is a perfectly adequate functional implementation of FizzBuzz, but for fun, let's see if we can use the "two-track" design described in the post.
If you are doing domain driven design, it is good practice to write code that uses the appropriate rather than language that is not applicable.
Instead of combining all the "switch" functions in series, we can "add" them together in parallel. In the post, we used this technique for combining validation functions. For FizzBuzz we are going to use it for doing all the factors at once.
If you're a FizzBuzz fan, check out the page, which has yet another variant of the problem.