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?
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
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
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
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)
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
let safeSqrt xOpt = xOpt |> Option.filter (fun x -> x >= 0.0) |> Option.map sqrt
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 (
<<) with pipe operators (
<|) in order to give the input an explicit name.
- Avoid currying infix operators, such as
>=, 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)
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: