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