FizzBuzz kata in F#: stage 2 by Mark Seemann
In my previous post I walked you through stage 1 of the FizzBuzz kata. In this post I'll walk you through stage 2 of the kata, where new requirements are introduced (see the kata itself for details). This makes the implementation much more complex.
Unit test #
In order to meet the new requirements, I first modified and expanded my existing test cases:
[<Theory>] [<InlineData(1, "1")>] [<InlineData(2, "2")>] [<InlineData(3, "Fizz")>] [<InlineData(4, "4")>] [<InlineData(5, "Buzz")>] [<InlineData(6, "Fizz")>] [<InlineData(7, "7")>] [<InlineData(8, "8")>] [<InlineData(9, "Fizz")>] [<InlineData(10, "Buzz")>] [<InlineData(11, "11")>] [<InlineData(12, "Fizz")>] [<InlineData(13, "Fizz")>] [<InlineData(14, "14")>] [<InlineData(15, "FizzBuzz")>] [<InlineData(16, "16")>] [<InlineData(17, "17")>] [<InlineData(18, "Fizz")>] [<InlineData(19, "19")>] [<InlineData(20, "Buzz")>] [<InlineData(30, "FizzBuzz")>] [<InlineData(31, "Fizz")>] [<InlineData(32, "Fizz")>] [<InlineData(33, "Fizz")>] [<InlineData(34, "Fizz")>] [<InlineData(35, "FizzBuzz")>] [<InlineData(36, "Fizz")>] [<InlineData(37, "Fizz")>] [<InlineData(38, "Fizz")>] [<InlineData(39, "Fizz")>] [<InlineData(50, "Buzz")>] [<InlineData(51, "FizzBuzz")>] [<InlineData(52, "Buzz")>] [<InlineData(53, "FizzBuzz")>] [<InlineData(54, "FizzBuzz")>] [<InlineData(55, "Buzz")>] [<InlineData(56, "Buzz")>] [<InlineData(57, "FizzBuzz")>] [<InlineData(58, "Buzz")>] [<InlineData(59, "Buzz")>] let FizzBuzzReturnsCorrectResult number expected = number |> FizzBuzz |> should equal expected
This is the same test code as before, only with new or modified test data.
Implementation #
Compared with the stage 1 implementation, my implementation to meet the new requirements is much more complex. First, I'll post the entire code listing and then walk you through the details:
let FizzBuzz number = let arithmeticFizzBuzz number = seq { if number % 3 = 0 then yield "Fizz" if number % 5 = 0 then yield "Buzz" } let digitalFizzBuzz digit = seq { if digit = 3 then yield "Fizz" if digit = 5 then yield "Buzz" } let rec digitize number = seq { yield number % 10 let aTenth = number / 10 if aTenth >= 1 then yield! digitize aTenth } let arithmeticFizzBuzzes = number |> arithmeticFizzBuzz let digitalFizzBuzzes = number |> digitize |> Seq.collect digitalFizzBuzz let fizzOrBuzz = arithmeticFizzBuzzes |> Seq.append digitalFizzBuzzes |> Seq.distinct |> Seq.toArray |> Array.sort |> Array.rev |> String.Concat if fizzOrBuzz = "" then number.ToString() else fizzOrBuzz
First of all, you may wonder where the original implementation went. According to the requirements, the function must still 'Fizz' or 'Buzz' when a number is divisible by 3 or 5. This is handled by the nested arithmeticFizzBuzz function:
let arithmeticFizzBuzz number = seq { if number % 3 = 0 then yield "Fizz" if number % 5 = 0 then yield "Buzz" }
The seq symbol specifies a sequence expression, which means that everything within the curly brackets is expected to produce parts of a sequence. It works a bit like the yield keyword in C#.
Due to F#'s strong type inference, the type of the function is int -> seq<string>, which means that it takes an integer as input and returns a sequence of strings. In C# an equivalent signature would be IEnumerable<string> arithmeticFizzBuzz(int number). This function produces a sequence of strings depending on the input.
- 1 produces an empty sequence.
- 2 produces an empty sequence.
- 3 produces a sequence containing the single string "Fizz".
- 4 produces an empty seqence.
- 5 produces a sequence containing the single string "Buzz".
- 6 produces a sequence containing the single string "Fizz".
- 15 produces a sequence containing the strings "Fizz" and "Buzz" (in that order).
That doesn't quite sound like the original requirements, but the trick will be to concatenate the strings. Thus, an empty sequence will be "", "Fizz" will be "Fizz", "Buzz" will be "Buzz", but "Fizz" and "Buzz" will become "FizzBuzz".
The digitalFizzBuzz function works in much the same way, but expects only a single digit.
let digitalFizzBuzz digit = seq { if digit = 3 then yield "Fizz" if digit = 5 then yield "Buzz" }
- 1 produces an empty sequence.
- 2 produces an empty sequence.
- 3 produces a sequence containing the single string "Fizz".
- 4 produces an empty seqence.
- 5 produces a sequence containing the single string "Buzz".
- 6 produces an empty sequence.
In order to be able to apply the new rule of Fizzing and Buzzing if a digit is 3 or 5, it's necessary to split a number into digits. This is done by the recursive digitize function:
let rec digitize number = seq { yield number % 10 let aTenth = number / 10 if aTenth >= 1 then yield! digitize aTenth }
This function works recursively by first yielding the rest of a division by 10, and then calling itself recursively with a tenth of the original number. Since the number is an integer, the division simply still produces an integer. The function produces a sequence of digits, but in a sort of backwards way.
- 1 produces a sequence containing 1.
- 2 produces a sequence containing 2.
- 12 produces a sequence containing 2 followed by 1.
- 23 produces a sequence containing 3 followed by 2.
- 148 produces 8, 4, 1.
This provides all the building blocks. To get the arithmetic (original) FizzBuzzes, the number is piped into the arithmeticFizzBuzz function:
let arithmeticFizzBuzzes = number |> arithmeticFizzBuzz
In order to get the digital (new) FizzBuzzes, the number is first piped into the digitize function, and the resulting sequence of digits is then piped to the digitalFizzBuzz function by way of the Seq.collection function.
let digitalFizzBuzzes = number
|> digitize
|> Seq.collect digitalFizzBuzz
The Seq.collect function is a built-in function that takes a sequence of elements (in this case a sequence of digits) and for each element calls a method that produces a sequence of elements, and then concatenates all the produced sequences. As an example, consider the number 53.
Calling digitize with the number 53 produces the sequence { 3; 5 }. Calling digitalFizzBuzz with 3 produces the sequence { "Fizz" } and calling digitalFizzBuzz with 5 produces { "Buzz" }. Seq.collect concatenates these two sequences to produce the single sequence { "Fizz"; "Buzz" }.
Now we have two sequences of "Fizz" or "Buzz" strings - one produced by the old, arithmetic function, and one produced by the new, digital function. These two sequences can now be merged and ordered with the purpose of producing a single string:
let fizzOrBuzz = arithmeticFizzBuzzes
|> Seq.append digitalFizzBuzzes
|> Seq.distinct
|> Seq.toArray
|> Array.sort
|> Array.rev
|> String.Concat
First, the Seq.append function simply concatenates the two sequences into a single sequence. This could potentially result in a sequence like this: { "Fizz"; "Buzz"; "Fizz" }. The Seq.distinct function gets rid of the duplicates, but the ordering may be wrong - the sequence may look like this: { "Buzz"; "Fizz" }. This can be fixed by sorting the sequence, but sorting alphabetically would always put "Buzz" before "Fizz" so it's also necessary to reverse the sequence. There's no function in the Seq module which can reverse a sequence, so first the Seq.toArray function is used to convert the sequence to an array. After sorting and reversing the array, the result is one of four arrays: [], [ "Fizz" ], [ "Buzz" ], or [ "Fizz"; "Buzz" ]. The last step is to concatenate these string arrays to a single string using the String.Concat BCL method.
If there were no Fizzes or Buzzes, the string will be empty, in which case the number is converted to a string and returned; otherwise, the fizzOrBuzz string is returned.
if fizzOrBuzz = "" then number.ToString() else fizzOrBuzz
To print the FizzBuzz list for numbers from 1 to 100 the same solution as before can be used.
What I like about Functional Programming is that data just flows through the function. There's not state and no mutation - only operations on sequences of data.