DEV Community

Brian Berns
Brian Berns

Posted on

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

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!

Top comments (1)

Collapse
 
theolegarchy profile image
Oleg Alexander πŸ‡ΊπŸ‡¦

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