First F# Foray

The Melbourne Alt.Net meetup on the 27th Feb 2018 included a portion of mobbing on some basic F# code, namely FizzBuzz. I had never written a line of F# in anger before, but I certainly knew the concepts and ideals behind functional programming, so I was surprised at how the session went. Whilst I had somewhat of an idea of how I would do it, with currying, partial application, and other such concepts in mind, I couldn’t express those ideas in a mob programming session because I didn’t know the syntax.

Instead what ended up happening was a rather procedural implementation of the algorithm with a different-from-normal syntax being the only thing that was “F#” about it. It turns out though that this approach ended up being effective, as I got to see how the code went from procedure to slightly more functional, so much so that I was inspired today to finally dig in and write my own version of a solution (aided by a little bit of competitive spirit with past and current co-workers).

To the code!

One interesting aspect of F# that I wasn’t aware of is that function definitions only exist physically below where they are declared in the file. This makes sense with the let xxx = syntax for defining them then, but also means that explaining the code is best done from the bottom up.

[<EntryPoint>]
let main argv =
    fizzBuzz |> List.iter System.Console.WriteLine
    System.Console.ReadLine() |> ignore
    0

Firstly we define our entry point for the console application, which is a simple main method. The first line of the method calls the fizzBuzz function, which returns a list of strings to display. The output of that function is piped (|>) to List.iter which iterates through the list, and calls the function passed in once for each item. In this case we display the item on the console with the normal .NET console class.

The next line does a ReadLine from the console to pause the output, but since F# requires all return values to be used in some way, we have to pipe it to ignore.

Finally the main method returns 0.

let fizzBuzz =
    [1..100] |> List.map fizzBuzzChecks

Now we define out fizzBuzz function and its pretty straight forward. We create a simple range, from 1 to 100, and map that list over the fizzBuzzChecks method. Map is the act of taking a list of items, and for each one, converting it into a different type. In this case we convert ints from the range, to strings for display.

let fizzBuzzChecks n =
    None 
    |> fizz n 
    |> buzz n 
    |> Option.defaultValue (string n)

The fizzBuzzChecks method is where the core algorithm is, and it again uses piping to do its job. It starts by creating an Option<string> that is None as a seed value, and then pipes that option to the fizz function, the buzz function, and finally the Option.defaultValue function in turn.

The defaultValue function will look at the option being passed in, and if its None will output the parameter, which is the string value of the number being evaluated, otherwise will return the value of the option. Because of type inference we, and the compiler, know that fizz and buzz have to take an int, an option<string> and return an option<string>.

let fizz n result = is 3 "fizz" n result
let buzz n result = is 5 "buzz" n result

Fizz and buzz are simple helper methods that check if n is divisible by 3 or 5, and return “fizz” or “buzz” respectively. The important thing to note here is that both function take two parameters, but above we only supplied one. This is partial application, and it means that instead of calling the function fizz with two parameters, the code above fizz n instead returns a function that takes one parameter (the option being passed through the pipe) and uses n as the other parameter.

This is where the power and expressiveness of F# comes in and leaves the pipeline of the main algorithm very simple and readable.

Finall the is function is a quick helper function:

let is divisor outputString n result =
    if n % divisor = 0 then
        Some ( (result |> Option.defaultValue("")) + outputString)
    else
        result

This is where the actual check for remainder occurs, though it does look more complicated. Essentially all it does though is one of three things:

  • If the number passed in matches the divisor then we know we need to return outputString. First we check the option that is passed in though, and either prepend it to the outputString, of its None simply ignore it by prepending an empty string ("").
  • Otherwise if the current number isn’t a match, we simply pass the result along the chain. This will either be empty, or have a value, but it doesn’t matter.

The full code resulting is:

let is divisor outputString n result =
    if n % divisor = 0 then
        Some ( (result |> Option.defaultValue("")) + outputString)
    else
        result

let fizz n result = is 3 "fizz" n result
let buzz n result = is 5 "buzz" n result

let fizzBuzzChecks n =
    None 
    |> fizz n 
    |> buzz n 
    |> Option.defaultValue (string n)
      
let fizzBuzz =
    [1..100] |> List.map fizzBuzzChecks

[<EntryPoint>]
let main argv =
    fizzBuzz |> List.iter System.Console.WriteLine
    System.Console.ReadLine() |> ignore
    0

The is method is not ideal and the concatenating and resolving the option in particular looks a bit hairy, but this code meets the main goal that I had for it, and that is that the algorithm can be expanded with minimal effort, and without changing supporting code.

For example to add a case for “Bang” if the number is divisible by 7 we would simply add in to our pipeline before the Option.defaultValue like so:

let fizzBuzzChecks n =
    None 
    |> fizz n 
    |> buzz n 
    |> is 7 "bang" n
    |> Option.defaultValue (string n)

We could of course create a helper method for it like we did with fizz and buzz, but the key point is that without changing our core algorithm, merely adding in another building block into the pipeline, we get new functionality.

That sort of expressiveness and simplicity is really attractive I think, and its relatively straightforward to achieve with a little bit of partial application, and ensuring that we always pass the result option along the chain.

Written on February 27, 2018

Share this post on Twitter, Facebook.