5

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?

sdgfsdh
  • 33,689
  • 26
  • 132
  • 245

3 Answers3

13

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.

Igor Brejc
  • 18,714
  • 13
  • 76
  • 95
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
4

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.

CaringDev
  • 8,391
  • 1
  • 24
  • 43
2

You can also declare a TryGet function that applies a given function f to key only if the variable ok is false. If ok has become true we do nothing, we just return the given couple as input.

let ATryGetValue key =
    if key>20 then (true,2) else (false,-2);
let BTryGetValue key =
    if key>10 then (true,1) else (false,-1);
let CTryGetValue key =
    if key>0 then (true,0) else (false,0);


let tryGet f (ok, key) =
    if not ok then
        match f key with
        | (true, v) -> (true, v)
        | _ -> (false, key)
    else 
        (ok,key)

let res key =
    tryGet CTryGetValue (tryGet BTryGetValue (tryGet ATryGetValue (false, key)))


printfn "%A" (res 40)