Suppose I want to do repeated map lookups.
In C#, I can use return
for a "flat" control-flow:
Thing v = null;
if (a.TryGetValue(key, out v))
{
return v;
}
if (b.TryGetValue(key, out v))
{
return v;
}
if (c.TryGetValue(key, out v))
{
return v;
}
return defaultValue;
It's a bit ugly, but quite readable.
In F#, which I am less familiar with, I would use match
expressions:
match a.TryGetValue(key) with
| (true, v) -> v
| _ ->
match b.TryGetValue(key) with
| (true, v) -> v
| _ ->
match c.TryGetValue(key) with
| (true, v) -> v
| _ -> defaultValue
This feels wrong - the code gets more and more nested with each map.
Does F# provide a way to "flatten" this code?
You could slightly change the semantics and run all TryGetValue
calls up-front. Then you need just one flat pattern match, because you can pattern match on all the results at the same time and use the or pattern (written using |
) to select the first one that succeeded:
match a.TryGetValue(key), b.TryGetValue(key), c.TryGetValue(key) with
| (true, v), _, _
| _, (true, v), _
| _, _, (true, v) -> v
| _ -> defaultValue
This flattens the pattern matching, but you might be doing unnecessary lookups (which probably is not such a big deal, but it's worth noting that this is a change of semantics).
Another option is to use an active pattern - you can define a parameterized active pattern that pattern matches on a dictionary, takes the key as an input parameter and performs the lookup:
let (|Lookup|_|) key (d:System.Collections.Generic.IDictionary<_, _>) =
match d.TryGetValue(key) with
| true, v -> Some v
| _ -> None
Now you can write pattern Lookup <key> <pat>
which matches a dictionary when it contains a value matching pattern <pat>
with the key <key>
. Using this, you can rewrite your pattern matching as:
match a, b, c with
| Lookup key v, _, _
| _, Lookup key v, _
| _, _, Lookup key v -> v
| _ -> defaultValue
The way F# compiler handles this is that it will run the patterns one after another and match the first one that succeeds - so if the first one succeeds, only one lookup gets performed.
When control flow becomes a pain, it is sometimes helpful to transform the problem. Say you have this:
let a = [ (1, "a"); (2, "b") ] |> dict
let b = [ (42, "foo"); (7, "bar") ] |> dict
let key = 8
let defaultValue = "defaultValue"
Then the following shows the intent behind the computation: keep trying to get a value, if all fail, use the default.
[ a; b ]
|> Seq.tryPick (fun d -> let (s, v) = d.TryGetValue key in if s then Some v else None)
|> defaultArg <| defaultValue
The more dictionaries you have, the bigger the benefit.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With