Some guidelines for readable F# code

When learning a new programming language it isn’t enough to know the syntax, you must also take the time to learn the idioms and styles for the language. Unfortunately those idioms and styles develop over years and F# still hasn’t had its ‘official v1'.0’ release. So where do we start?

We can begin by looking at F#'s roots - C# and OCaml - for guidance:

Once you start looking at those docs however, you'll begin to notice some conflicting advice. The problem isn't just that F# is both functional and object-oriented.  Good design guidelines are hard to pin down because F# can be used to write applications ranging from simple scripts to line-of-buisiness apps to physics simulations.

So I won't set out to write the ultimate set of coding guidelines for F#, but I will at the very least try to start the dialog.

For this post I would like to focus on readability. (Caveat Emptor, I completely reserve the right to flip-flop on any of these style issues; you have been warned.)

 

F# Don'ts

Let's start with the things you shouldn't do. If you want to write obfuscated F# code, here's the way to start.

Don't open Namespaces without a fully-qualified path

The semantics of open are that once you open a namespace, you can then open any nested namespaces without fully-qualifying them. If you do this however, you lose any ability to determine what 'open X' is actually referring too. Worse yet, you can run into namespace collisions. Recommendation: don't do this.

 open System
open Collections
open Generic

// System.Collections.Generic.List<T>
let x = new List<string>()

Don't outscope values

In F# it is possible to introduce new values outscoping existing ones but since everything is immutable this doesn't modify the value, it simply creates a new one with the same name. To the novice F# developer, the following code would print both "Hello" and "Goodbye" however it does not.

 let greet() =
    
    let message = "Hello"
    
    let printGreeting() = printfn "%s Bob" message
    
    // prints "Hello Bob"
    printGreeting()
    
    let message = "Goodbye"
    
    // prints "Hello Bob"
    printGreeting()

F# Code to Avoid

These aren't necessarily bad things, but you need to be careful when using these concepts.

Avoid mutable function values

Passing around a mutable function value enables some interesting functional programming patterns, but also enables new classes of bugs. You should avoid mutable function values unless they have a very limited scope.

 let mutable mutableFunc = fun x y -> x + y

mutableFunc <- (fun x y -> x * y * -1)
mutableFunc <- (fun x y -> List.length [x .. y])
mutableFunc <- (fun x y -> (box x).ToString().Length + y)

mutableFunc 4 5

Avoid abusing type abbreviations

Type abbreviations simplify code and increase readability, but if you do it too much you are just introducing new types that need to be remembered by the person maintaining your code.

 type string_string_dictionary = Dictionary<string, string>

Avoid abusing partial function application (currying)

Just like the previous bullet, currying is a great feature when used judiciously. Just be aware that at some point it is possible to go overboard and make your code impossible to debug.

 let moveFunc x y = System.IO.File.Move(x, y)

let moveFileX = moveFunc @"D:\Documents\FileX.fs"

moveFileX "E:\NewFileLocation\FileX.fs"

F# Code to Consider

These are guidelines which may not always be applicable, but you should consider using them when the situation arises.

Prefer Sequence Expressions to Seq.unfold

There certainly is a place for Seq.unfold, but Sequence Expressions are much easier to read.

 // Fibonacci numbers - 1, 1, 2, 3, 5, 8, 13, ...
let seq_unfold = 
    Seq.unfold 
        (fun (idx, last1, last2) -> 
            if idx >= 20 then 
                None 
            else 
                Some(last1 + last2, (idx + 1, last1 + last2, last1))) 
        (0, 0, 1)

// vs.

let sequence_expression =
    seq {
        let lastStep = ref (0, 1)
        for i in 1 .. 20 do
            let x, y = !lastStep
            yield x + y
            do lastStep := (x + y, x) }

 

F# Dos

These are things you should always do to aid readability of your F# programs.

Use the Pipe-Forward operator wherever possible

If you find yourself stringing together a lot of operations, don't write in the lame-imperative way. Use the pipe-forward operator to simplify the code. (Do this only within reason, since you can't store intermediate results long chains of piped functions are actually very hard to debug.)

 let folderSize baseFolder =
    baseFolder
    |> filesUnderFolder
    |> Seq.map getFileInfo
    |> Seq.map getFileSize
    |> Seq.fold (+) 0L 
    |> convertBytesToMB

Add comments to Union Type data tags if necessary

Currently in F# you cannot name data associated with Union Type data tags, be sure to add a comment around the declaration site so that other programs know what the tag is expected to carry AND the order that data is expected to come in.

 type MotorizedTransport =
    // Model, Year
    | SUV   of string * int
    // Model, Year, Carrying capacity
    | Truck of string * int * int