views:

276

answers:

2

Hi,

we are trying to build the Haskell-MaybeMonad sample from http://www.haskell.org/all_about_monads/html/maybemonad.html in F#.

The idea is to search for a mailaddress in two dictionaries. If one of the both lookups returns a result we look into the third one.

let bindM x k =
    match x with
    | Some value -> k value
    | None   -> None

let returnM x = Some x

type MaybeBuilder() =
     member this.Bind(x, k) = bindM x k
     member this.Return(x)  = returnM x
     member this.ReturnFrom(x) = x
     member this.Delay(f)   = f()

let maybe = MaybeBuilder()

//Sample dictionaries
let fullNamesDb = 
    [("Bill Gates", "[email protected]")    
     ("Bill Clinton", "[email protected]")
     ("Michael Jackson", "[email protected]")
     ("No Pref Guy", "[email protected]")]
       |> Map.ofList

let nickNamesDb =
    [("billy", "[email protected]")
     ("slick willy", "[email protected]")
     ("jacko", "[email protected]") ]
        |> Map.ofList

let prefsDb =
    [("[email protected]", "HTML")
     ("[email protected]", "Plain")
     ("[email protected]", "HTML")]
        |> Map.ofList


let mplus m1 m2 = if m1 <> None then m1 else m2
let (+) = mplus

let lookUp name = maybe {
    let! combined = fullNamesDb.TryFind name + nickNamesDb.TryFind name
    return! prefsDb.TryFind combined
}

let billGatesPref = lookUp "Bill Gates" |> printfn "%A" // Some "HTML"
let billyPref = lookUp "billy" |> printfn "%A" // Some "HTML"
let billClintonPref = lookUp "Bill Clinton" |> printfn "%A" // Some "Plain"
let steffenPref = lookUp "Steffen" |> printfn "%A" // None
let noPref = lookUp "No Pref Guy" |> printfn "%A" // None

System.Console.ReadKey() |> ignore

The problem is that we perform the second lookup even if the first one returns a result. The nice thing about Haskell is here, that it evaluates lazy. Now we looking for something similar in F#. We tried the following but it looks ugly and seems to break the idea of encapsulating the maybe logic in the builder:

let mplus m1 m2 = if m1 <> None then m1 else m2()
let (+) = mplus

let lookUp name = maybe {
    let! combined = fullNamesDb.TryFind name + fun _ -> nickNamesDb.TryFind name
    return! prefsDb.TryFind combined
}

Is there a better solution?

Regards, forki

+1  A: 

There's always Lazy, which is effectively what you have here but with different syntax:

let mplus m1 (m2 : Lazy<'a option>) =
    match m1 with
    | Some _ as m -> m
    | None -> m2.Force()

let (+) = mplus

let lookUp name = maybe {
    let! combined = fullNamesDb.TryFind name + lazy (nickNamesDb.TryFind name)
    return! prefsDb.TryFind combined
}
Tim Robinson
Thanks for the hint. But it seems lazyness is always reflected in the type even inside the maybe { } expression.
forki23
I can't think of a nicer way. What you're doing is like C#'s `??` coalescing operator in C#, and I don't think there's an F# equivalent.
Tim Robinson
`seq { yield fullNamesDb.TryFind name; yield nickNamesDb.TryFind name } |> Seq.collect Option.toList |> Seq.head;;`
Tim Robinson
Yes. But this is not the maybe anymore. I know that IEnumerable is "right" monad for a sequence of lazy computations. But it seems IEnumerable is a little bit too big and our Maybe monad a little bit too small.
forki23
I like @desco's idea :)
Tim Robinson
+6  A: 

You can implement additional methods Run/Combine in MaybeBuilder so result shall be the following:

let bindM x k =
match x with
| Some value -> k value
| None   -> None

let returnM x = Some x

type MaybeBuilder() =
    member this.Bind(x, k) = bindM x k
    member this.Return(x)  = returnM x
    member this.ReturnFrom(x) = x
    member this.Delay(f)   = f
    member this.Combine(a, b) = if Option.isSome a then a else b()
    member this.Run(f) = f()

let maybe = MaybeBuilder()

//Sample dictionaries (the same with original sample)
let fullNamesDb = ... 
let nickNamesDb = ...
let prefsDb = ....

let lookUp name = 
    let findName m = maybe {
        let! v = Map.tryFind name m
        return! prefsDb.TryFind v 
        }

    maybe {
        return! findName fullNamesDb
        return! findName nickNamesDb 
    }


let billGatesPref = lookUp "Bill Gates" |> printfn "%A" // Some "HTML"
let billyPref = lookUp "billy" |> printfn "%A" // Some "HTML"
let billClintonPref = lookUp "Bill Clinton" |> printfn "%A" // Some "Plain"
let steffenPref = lookUp "Steffen" |> printfn "%A" // None
let noPref = lookUp "No Pref Guy" |> printfn "%A" // None
desco
Awesome idea. That's brilliant.
forki23