Formatting F# functions the right way

Date: 2023-10-25 | create | fsharp | formatting |

I've been using F# for years as my primary (non-work) language - coding everything from visual simulations, to scalable backends, to containerized webscrapers.

But while I love F# for its ergonomics and simplicity, one thing always bugged me: the formatting. The reason is that F# has semantic indentation (or it treats indentation like it means something kinda like Python). I struggled for years to get my function definitions semi-readable and just last week stumbled on the official F# code formatting guidelines which revealed that I'd been doing it wrong this entire time.

In the rest of this post, we'll explore what I was doing wrong and how it's supposed to be done to hopefully save you years of frustration. =')

Fsharp Functions

F# is a weird language. IMO it's one of the best languages but it does that by doing a lot of things differently than other languages.

  • Minimal boilerplate
  • Parameters are curryable
  • Indentations matter (like Python)

Here's a simple example:

let dashString a b =
    $"{a} - {b}"

let dashOneTwo = dashString 1 2 // Prints 1 - 2

This is a function definition that takes in two parameters (a and b) and then returns the values separated by a string.

Really simple, right?! Yes for simple things but let's show what happens when we try to add type signatures.

Fsharp Functions in the Wild

Okay so in the real world, we're gonna have a ton of different functions often with a bunch of different parameters and often we want to make those functions params typed so we can establish a strict contract between caller / callee (and get the most our of our type system).

Here's a contrived example to show how verbose this can get. It's basically the same as the above function but now fully typed and made extra long for illustration:

let getVeryLongString (aVeryLongParameterName: string) (anotherVeryLongParameterName: string) (yetAnotherLongParameterName: string) (youGetTheIdeaByNow: string) : string = 
  $"{aVeryLongParameterName} - {anotherVeryLongParameterName} - {yetAnotherLongParameterName} - {youGetTheIdeaByNow}"

This is extremely hard to read as it's turned into a jumbled mess of parameter names and typings. Moreover this is so long that it often gets auto wrapped which makes reading it even more confusing considering indentation matters in this language.

In most languages, you can simply deal with this by moving the params to a new line. Here's an example in Python:

def get_very_long_string(
  aVeryLongParameterName: string,
  anotherVeryLongParameterName: string,
  yetAnotherLongParameterName: string,
  youGetTheIdeaByNow: string
) -> string:

But in F# you couldn't do that as the syntax turns it into a tuple. Which is sick when you want a tuple. Bad when you don't.

I struggled for a long time to tame this unreadableness by trying various strategies to get the params on new lines. The most egregious of these experiments was probably trying to utilize open parentheses to bypass F#'s indentation semantics:

let getVeryLongString (
  aVeryLongParameterName: string) (
  anotherVeryLongParameterName: string) (
  yetAnotherLongParameterName: string) (
  youGetTheIdeaByNow: string) : string = 
  $"{aVeryLongParameterName} - {anotherVeryLongParameterName} - {yetAnotherLongParameterName} - {youGetTheIdeaByNow}"

While perhaps more readable for param names, this looks like a nightmare to type out so eventually I went back to the horizontal method.

How to actually format Fsharp Functions

So for quite some time, I just gritted my teeth and dealt with it. The rest of the language was so good that it was worth it. But it was always frustrating to me. Then I stumbled on the official F# formatting guidelines and realized there was a better way.

Note: I largely blame how long it took me to realize I was formatting things wrong on my hobbyist / solo usage of F#. I'm certain if I was on a team using this at work / that reviewed my code I'd have quickly been told there was a better way. Alas.

F# understands it introduces weird limitations by both having indentation semantics and minimizing boilerplate (the parenthesis and brackets you'd use in most languages to symbolize params vs other). So it actually just goes all in and says indents are the primary semantics.

  • Parameters can be defined on new indented lines
  • Sub parameters can also be defined on sub indented lines

Properly indented F# func from above:

let getVeryLongString 
  (aVeryLongParameterName: string) 
  (anotherVeryLongParameterName: string) 
  (yetAnotherLongParameterName: string) 
  (youGetTheIdeaByNow: string) 
  : string 
  $"{aVeryLongParameterName} - {anotherVeryLongParameterName} - {yetAnotherLongParameterName} - {youGetTheIdeaByNow}"

Same / similar rules apply for records:

type my_record = 
    aString: string
    bString: string 
    cString: string

Am I frustrated w myself for badly formatting F# for years? Yes

Do I dislike F# because of it? No, I actually am more impressed with it. It's a formatting style I've never seen before and it... makes sense.


Anyway thanks for reading my rant. If you're into F#, you might also like:

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.