Search code examples
f#monadscomputation-expressionfsharpx

Computation expressions vs applicative functors and what not


Not entirely sure the title describes it ok, but I do have about the following code:

paket.dependencies:

source https://www.nuget.org/api/v2
nuget fsharpx.extras
nuget mongodb.driver

some.fsx:

#r @".\packages\MongoDB.Bson\lib\net45\MongoDB.Bson.dll"
#r @".\packages\MongoDB.Driver\lib\net45\MongoDB.Driver.dll"
#r @".\packages\MongoDB.Driver.Core\lib\net45\MongoDB.Driver.Core.dll"

#r @".\packages\FSharpX.Extras\lib\net45\FSharpX.Extras.dll"


open MongoDB
open MongoDB.Driver
open MongoDB.Bson 
open MongoDB.Bson.Serialization

open FSharpx.Choice

let private createClient (connectString:string) = MongoClient(connectString)
let CreateClient = protect createClient

let private getDb name (client:IMongoClient) = client.GetDatabase(name)
let GetDB1 name client =
    choose {
        let! c = client
        return! (protect (getDb name) c)
    }

let GetDB2 name (client:Choice<IMongoClient, exn>) =
    protect (getDb name)
    <!> client

The point for this "excersise" was to write GetDB2 so that it does the same as GetDB1 but use operators (applicatives?), but I am at the moment not able to turn my head to manage this.

The above code compiles, but the signatures for GetDB1 and GetDB2 are not equal, and Im obviously doing something not right.

val GetDB1 :
  name:string ->
    client:Choice<#MongoDB.Driver.IMongoClient,exn> ->
      Choice<MongoDB.Driver.IMongoDatabase,exn>

val GetDB2 :
  name:string ->
    client:Choice<MongoDB.Driver.IMongoClient,exn> ->
      Choice<Choice<MongoDB.Driver.IMongoDatabase,exn>,exn>

I have tried several versions and orders of doing things in GetDB2 but I more or less always end at same signature as above.

The general idea I initially had was to write small function doing the stuff they should and then add exception handling (protect) and then "wrap" and "unwrap" accordingly.

That might of course not be entirely right idea too.

Are someone able to point me in some directions here for further studies, code examples or anything? Any comments of any type are in fact welcome at this point ;-)

FSharpx doc

Addendum

I think the following should be about same as above, but without the mongodb dependencies.

#r @".\packages\FSharpX.Extras\lib\net45\FSharpX.Extras.dll"

type DataBase = 
    {
        Name: string
    }

type Client = 
    {
        connectString: string
    } with member this.GetDatabase name = {
                        Name = name
                    }

open FSharpx.Choice
let private createClient (connectString:string) = {
  connectString= connectString
}

let CreateClient = protect createClient

let private getDb name (client:Client) = client.GetDatabase name

let GetDB1 name client =
    choose {
        let! c = client
        return! (protect (getDb name) c)
    }

let GetDB2 name client =
    protect (getDb name)
    <!> client

Solution

  • You are getting the compounding of types here because you have used the <!> operator, which is map. That is defined something like this:

    let map f = function
        | Choice1Of2 value = Choice1Of2 (f value)
        | Choice2Of2 fail  = Choice2Of2 fail
    

    This has the signature ('T -> 'U) -> Choice<'T,'Failure> -> Choice<'U,'Failure>, i.e. the function f is used as a map inside the choice type. For example:

    map (sprintf "%d")
    

    has type Choice<int, 'Failure> -> Choice<string, 'Failure>. This is good for applying functions which do not use the Choice type - there is only one possible point of failure, and that happened before the call to map.

    Your next function, however, produces a Choice type, but it takes a non-Choice type. This means that you want the errors to propagate through - if there is an error in the value, then choose that. If the value is fine, but there's an error in the function, then use that. If everything is successful, use that. This requires the two error types to be the same, which for you they are (exn).

    This is describing the bind operation, defined like this:

    let bind f = function
        | Choice1Of2 value = f value
        | Choice2Of2 fail  = Choice2Of2 fail
    

    with signature ('T -> Choice<'U,'Failure>) -> Choice<'T,'Failure> -> Choice<'U,'Failure>.

    Note that bind is very similar to map, except that the latter raises the result into a Choice1Of2 - the mapped function is always successful.

    In FSharpX, you can access bind by the |>-like operator >>=, or the <|-like operator <<=.

    Finally, protect is a fancy way of catching a thrown exception into a Choice2Of2 exn. It is similar to map in that the passed function is of type 'T -> 'U, but the function can also throw an exception and the passed type is not a Choice. protect is defined something like this:

    let protect f x =
        try
            Choice1Of2 (f x)
        with
            exn -> Choice2Of2 exn
    

    so its signature is ('T -> 'U) -> 'T -> Choice<'U, exn>.

    For more information on how everything is implemented, see the source of this computation expression.


    Putting this all together, we can see why your example went wrong.

    • getDb name is a function Client -> DataBase
    • protect (getDb name) is a function Client -> Choice<DataBase, exn>
    • map (protect (getDb name)) is therefore a function Choice<Client, exn> -> Choice<Choice<DataBase, exn>, 'Failure>, because map works inside Choice.

    What you want, though, is

    let GetDB name client =
        bind (protect (getDb name)) client
    

    or in operator form,

    let GetDB name client = client >>= protect (getDb name)
    

    In general, if your mapping function has signature 'T -> 'U you want map. If it has 'T -> Choice<'U, 'Failure>, you want bind.