Avoiding the pyramid of doom with Computation Expressions?

Viewed 306

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
3 Answers

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"

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

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"
Related