Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Improve my nested matches in F# [duplicate]

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?

like image 622
sdgfsdh Avatar asked Dec 06 '22 13:12

sdgfsdh


2 Answers

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.

like image 101
Tomas Petricek Avatar answered Dec 08 '22 03:12

Tomas Petricek


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.

like image 42
CaringDev Avatar answered Dec 08 '22 01:12

CaringDev