F# Tip 4: When (not) to use point-free style
Brian Berns

Brian Berns @shimmer

About: Functional programming enthusiast focused on F# .NET applications. #fsharp

Location:
US
Joined:
Dec 11, 2019

F# Tip 4: When (not) to use point-free style

Publish Date: Jan 6 '20
8 1

Inspired by a blog post by Eirik Tsarpalis.

Let's write a small F# function that safely takes the square root of a number. Both the argument and the result should be wrapped in an Option, and the function should return None when the argument is None or negative. Something like this:

let safeSqrt (xOpt : Option<float>) : Option<float> =
   // implementation?
Enter fullscreen mode Exit fullscreen mode

How would you implement this function? One approach is to use "bare-metal" pattern matching:

let safeSqrt xOpt =
    match xOpt with
        | Some x when x >= 0.0 -> sqrt x |> Some
        | _ -> None
Enter fullscreen mode Exit fullscreen mode

That's easy to understand, but a bit verbose. Personally, I'd get tired quickly reading a lot of functions in that style. Let's try the opposite extreme instead, using a totally "point-free" approach:

let safeSqrt =
    Option.filter ((<=) 0.0) >> Option.map sqrt
Enter fullscreen mode Exit fullscreen mode

Well, that's certainly shorter, but is it actually better? It's not clear that safeSqrt is even a function any more, because it doesn't have an argument. Is there perhaps a middle ground?

Option comes with a bevy of composable higher-order functions (like filter and map) that every F# developer should be comfortable with, so it makes sense to use them instead of low-level pattern matching. However, having an explicit argument to the safeSqrt function helps a lot with readability, because it gives the function a "protaganist" that we can follow. The function then becomes a story about transformations applied to that protagonist. So with that in mind, here's another version of the function:

let safeSqrt xOpt =
    xOpt
        |> Option.filter (fun x -> x >= 0.0)
        |> Option.map (fun x -> sqrt x)
Enter fullscreen mode Exit fullscreen mode

This approach makes it clear that our function is a good citizen of the Option monad. We can easily trace the adventures of xOpt as it moves through the steps of the function via the pipe operator. In particular, I think fun x -> x >= 0.0 is a lot clearer than (<=) 0.0. In fact, the latter looks like it could mean "numbers that are not positive" when it is in fact the opposite. On the other hand, fun x -> sqrt x seems a bit wordy when we could just use sqrt point-free:

let safeSqrt xOpt =
    xOpt
        |> Option.filter (fun x -> x >= 0.0)
        |> Option.map sqrt
Enter fullscreen mode Exit fullscreen mode

To my eye, this last version is the best because it uses higher-order functions with both lambdas and point-free functions where appropriate.

With this result in mind, here are some guidelines to keep in mind when trying to write clear, idiomatic F# code:

  • Consider replacing raw function composition (>> and <<) with pipe operators (|> and <|) in order to give the input an explicit name.
  • Avoid currying infix operators, such as <= and >=, especially when it means flipping arguments unnaturally. Use lambdas instead.
  • Go ahead and use simple one-argument functions (like sqrt) without points in order to shorten code.

What do you think? Are there other guidelines you prefer? Let me know in the comments!

Comments 1 total

  • Oleg Alexander 🇺🇦
    Oleg Alexander 🇺🇦Aug 26, 2022

    Thank you for posting this, Brian! I recently went on a binge attempting to code in a 100% point free style. At the end of this experiment I came to the same conclusions and rules as you did. Namely that the point-free style often works well in HOFs but shouldn't be used elsewhere. I have one more rule (though I'm still unsure about its practicality) which is to fit functions on one line--similar to what you'd see in APL/BQN code. So your function would look like this:

    let safeSqrt xOpt = Option.filter (fun x -> x >= 0.0) xOpt |> Option.map sqrt
    
    Enter fullscreen mode Exit fullscreen mode
Add comment