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 ;-)
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
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
.