3

I came across this question about the "pyramid of doom" in F#. The accepted answer there involves using Active Patterns, however my understanding is that it can also be solved using Computation Expressions.

How can I remove the "pyramid of doom" from this code using Computation 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
sdgfsdh
  • 33,689
  • 26
  • 132
  • 245

3 Answers3

7

F# for fun and profit has an example for this specific case:

type OrElseBuilder() =
    member this.ReturnFrom(x) = x
    member this.Combine (a,b) = 
        match a with
        | Some _ -> a  // a succeeds -- use it
        | None -> b    // a fails -- use b instead
    member this.Delay(f) = f()

let orElse = new OrElseBuilder()

But if you want to use it with IDictionary you need a lookup function that returns an option:

let tryGetValue key (d:System.Collections.Generic.IDictionary<_,_>) =
    match d.TryGetValue key with
    | true, v -> Some v
    | false, _ -> None

Now here's a modified example of its usage from F# for fun and profit:

let map1 = [ ("1","One"); ("2","Two") ] |> dict
let map2 = [ ("A","Alice"); ("B","Bob") ] |> dict
let map3 = [ ("CA","California"); ("NY","New York") ] |> dict

let multiLookup key = orElse {
    return! map1 |> tryGetValue key
    return! map2 |> tryGetValue key
    return! map3 |> tryGetValue key
    }

multiLookup "A" // Some "Alice"
TheQuickBrownFox
  • 10,544
  • 1
  • 22
  • 35
  • I have accidentally downvoted your answer, please, make an edit so I can upvote it – netchkin Nov 22 '17 at 12:28
  • 1
    Note that this approach would be highly inefficient: before `Combine` can be called, all lookups need to be performed, not only up to the first successful one. – Fyodor Soikin Nov 22 '17 at 13:34
  • @FyodorSoikin thanks for pointing this out. I will not mark this as an answer since short-circuiting is important – sdgfsdh Nov 22 '17 at 14:05
  • I didn't mean to belittle the solution: it did technically answer your question as asked, and I myself have upvoted it. If you want to look for the ultimate problem, that would be your question itself: you're asking to solve a problem with a specific tool, without first making sure that it's the right tool for the problem. This is fine as an intellectual exercise, but you should be aware of drawbacks, which is why I commented in the first place. – Fyodor Soikin Nov 22 '17 at 14:17
  • I can offer you a solutions based on computation expressions that doesn't have this problem, but it would be even less readable. – Fyodor Soikin Nov 22 '17 at 14:18
  • @FyodorSoikin yes, this is an intellectual exercise. I would like to 1) understand CEs better and 2) compare solutions using different approaches – sdgfsdh Nov 22 '17 at 14:24
  • 1
    The approach that @kaefer took in his answer, using the `Bind` method instead of `Combine`, allows short-circuiting. Both answers are very instructive, though. – rmunn Nov 23 '17 at 07:33
5

The pattern I like for "pyramid of doom" removal is this:

1) Create a lazy collection of inputs 2) Map them with a computation function 3) skip all the computations that yield unacceptable results 4) pick the first one that matches your criteria.

This approach, however, does not use Computation Expressions

open System.Collections

let a = dict [1, "hello1"]
let b = dict [2, "hello2"]
let c = dict [2, "hello3"]

let valueGetter (key:'TKey) (d:Generic.IDictionary<'TKey, 'TVal>) =
    (
        match d.TryGetValue(key) with
        | (true, v) -> Some(v)
        | _ -> None
    )

let dicts = Seq.ofList [a; b; c] // step 1

let computation data key =
    data
    |> (Seq.map (valueGetter key)) // step 2
    |> Seq.skipWhile(fun x -> x = None) // step 3
    |> Seq.head // step 4

computation dicts 2
netchkin
  • 1,387
  • 1
  • 12
  • 21
  • I won't mark this as the answer, since I asked for CEs, but I like this approach too. It is much easier to understand coming from a C# background. – sdgfsdh Nov 22 '17 at 14:02
  • The computation function can be replaced by dicts |> Seq.tryPick (valueGetter key), or using Seq.pick if the desired functionality is to throw if no match is found. – hlo Nov 22 '17 at 14:19
5

A short-circuiting expression can be achieved if we subvert the Bind method, where we are in a position to simply ignore the rest of the computation and replace it with the successful match. Also, we can cater for the bool*string signature of the standard dictionary lookup.

type OrElseBuilder() =
    member __.Return x = x
    member __.Bind(ma, f) =
        match ma with
        | true, v -> v
        | false, _ -> f ()

let key = 2 in OrElseBuilder() {
    do! dict[1, "1"].TryGetValue key
    do! dict[2, "2"].TryGetValue key
    do! dict[3, "3"].TryGetValue key
    return "Nothing found" }
// val it : string = "2"
kaefer
  • 5,491
  • 1
  • 15
  • 20