Eliminating if
from F#
Recently I was asked by a colleague if there were a better way to write a specific method this colleague was using. It was a simple method which called a couple other methods and returned a value from them. Essentially, if two conditions met specific criteria, call one of four other methods. Oh, and it was in F#.
Naturally, as C-style programmers it's easy for us to use if
or switch
to do what we want, but for some reason when we look at functional languages we cannot seem to reason how we should replace these two constructs with match
. It must be trivial, right? We must be missing some silly detail. That's not entirely true. We're not missing anything trivial, we're just not being creative enough.
Functional languages like F# bear the advantage of being very verbose about what's going on. They're also great at implicitly typing things, and making a function read as a mathematical expression. I bolded that for a reason: if we begin to look at our code as a mathematical expression instead of code, we will hopefully see what we're missing.
Let's look at a much reduced sample of the code we were working with:
type ThingType =
Left = 0
| Right = 1
member private this.methodLeftOne =
true
member private this.methodRightOne =
false
member private this.methodLeftTwo =
false
member private this.methodRightTwo =
true
member this.MatchAndIf var thingType =
match var with
| 1 -> if thingType = ThingType.Left then this.methodLeftOne else this.methodRightOne
| 2 -> if thingType = ThingType.Left then this.methodLeftTwo else this.methodRightTwo
| _ -> false
My colleague was calling the MatchAndIf
function which was to return a boolean value from the two parameters. The code in the other four methods was a bit more complex, but I've simplified it here so we can see how things will turn out.
So, we're looking at a pretty simple bit of code: if var
and thingType
are 1
and ThingType.Left
respectively, return methodLeftOne
, if they're 1
and any other ThingType
value, return this.methodRightOne
, etc. Pretty easy to follow.
We have a slight inconsistency here, however. If thingType
is set to a non-valid value, then unexpected (well, unintended) things can happen. This is not so ideal. To fix it with this code would be a mess, now we would have if ... then ... else if ... then ... else ...
. Sure, that does what we want, but it's really ugly for F#.
Nested match
So, the first thing we might think of to rewrite it is to use a nested match
. Alright, easy enough, replace the inner if
with a match
.
member this.NestedMatch var thingType =
match var with
| 1 ->
match thingType with
| ThingType.Left -> this.methodLeftOne
| _ -> this.methodRightOne
| 2 ->
match thingType with
| ThingType.Left -> this.methodLeftTwo
| _ -> this.methodRightTwo
| _ -> false
This is obviously more F#-like. It gives us a lot more peace-of-mind, right? But we didn't fix the issue above, so let's do that.
member this.NestedMatchFixed var thingType =
match var with
| 1 ->
match thingType with
| ThingType.Left -> this.methodLeftOne
| ThingType.Right -> this.methodRightOne
| _ -> false
| 2 ->
match thingType with
| ThingType.Left -> this.methodLeftTwo
| ThingType.Right -> this.methodRightTwo
| _ -> false
| _ -> false
Wait a minute, why do we need three default (_
) cases? Ah, right, because if 1
or 2
are matched, they won't fall through to the default case, and F# will get very upset if we omit it and implicitly return false
. (That's not always a bad thing.)
Tuple match
Well, we might think to ourselves "I can just match on a Tuple
instead." Indeed that's true, let's see how that looks.
member this.TupleMatch var thingType =
match (var, thingType) with
| (1, ThingType.Left) -> this.methodLeftOne
| (1, ThingType.Right) -> this.methodRightOne
| (2, ThingType.Left) -> this.methodLeftTwo
| (2, ThingType.Right) -> this.methodRightTwo
| _ -> false
Alright, that's not bad. We've gotten a lot closer to our goal. But now we have things knowing about things they shouldn't. The TupleMatch
method does too many things inside it. It's looking for a var
of 1
or 2
and a ThingType
.
Finally Isolating Everything
The only other thing we can do to fix this (which I can tell you is the best option based on the context of what code I had) is to check thingType
in our Match
method, and pipe var
to our methodLeft
or methodRight
method (whichever is appropriate).
member private this.methodLeft var =
match var with
| 1 -> true
| _ -> false
member private this.methodRight var =
match var with
| 2 -> true
| _ -> false
member this.FinalMatch var thingType =
match thingType with
| ThingType.Left -> var |> this.methodLeft
| ThingType.Right -> var |> this.methodRight
| _ -> false
Now each method is only responsible for checking and reporting the parts it cares about. We complied with SRP and we kept it entirely functional. Each method is responsible for looking at only the code it cares about, it's not worried about what the next method down the chain is doing.