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 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)
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
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!
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: