Reputation: 569
On my quest to get better at F# and gain a better understanding on how Suave.io works, I've been attempting to create some reusable functions/operators for composing functions. I understand that Suave actually implements its >=> operator to work specifically for async option, but I thought I would be fun to try and generalize it.
The code below is inspired by too many sources to credit, and it works well for types I define myself, but I can't make it work for system types. Even though the type augmentations of Nullable and Option compiles fine, they aren't recognized as matching the member constraint in the bind function.
When I failed to make it work for Option, I had hoped that it might be due to Option being special in F#, which is why I tried with Nullable, but sadly, no cigar.
The relevant errors and output from fsi is in the code below in the comment.
Any help would be appreciated.
Thanks, John
open System
let inline bind (f : ^f) (v : ^v) =
(^v : (static member doBind : ^f * ^v -> ^r )(f, v))
// I'd prefer not having to use a tuple in doBind, but I've
// been unable to make multi arg member constraint work
let inline (>=>) f g = f >> (bind g)
// Example with Result
type public Result<'a,'b> =
| Success of 'a
| Error of 'b
type public Result<'a,'b> with
static member inline public doBind (f, v) =
match v with
| Success s -> f s
| Error e -> Error e
let rF a = if a > 0 then Success a else Error "less than 0"
let rG a = if a < 10 then Success a else Error "greater than 9"
let rFG = rF >=> rG
// val rFG : (int -> Result<int,string>)
//> rFG 0;;
//val it : Result<int,string> = Error "less than 0"
//> rFG 1;;
//val it : Result<int,string> = Success 1
//> rFG 10;;
//val it : Result<int,string> = Error "greater than 9"
//> rFG 9;;
//val it : Result<int,string> = Success 9
// So it works as expected for Result
// Example with Nullable
type Nullable<'T when 'T: (new : unit -> 'T) and 'T: struct and 'T:> ValueType> with
static member inline public doBind (f, v: Nullable<'T>) =
if v.HasValue then f v.Value else Nullable()
let nF a = if a > 0 then Nullable a else Nullable()
let nG a = if a < 10 then Nullable a else Nullable()
let nFG = nF >=> nG
// error FS0001: The type 'Nullable<int>' does not support the operator 'doBind'
type Core.Option<'T> with
static member inline doBind (f, v) =
match v with
| Some s -> f s
| None -> None
let oF a = if a > 0 then Some a else None
let oG a = if a < 10 then Some a else None
let oFG = oF >=> oG
// error FS0001: The type 'int option' does not support the operator 'doBind'
Upvotes: 4
Views: 194
Reputation: 26174
Why extension methods are not taken into account in static member constraints is a question that has been asked many times and surely it will continue being asked until that feature is implemented in the F# compiler.
See this related question with a link to other related questions and a link to a detailed explanation of what has to be done in the F# compiler in order to support this feature.
Now for your specific case the workaround mentioned there solves you issue and it's already implemented in FsControl.
Here's the code:
#nowarn "3186"
#r "FsControl.dll"
open FsControl.Operators
// Example with Result
type public Result<'a,'b> =
| Success of 'a
| Error of 'b
type public Result<'a,'b> with
static member Return v = Success v
static member Bind (v, f) =
match v with
| Success s -> f s
| Error e -> Error e
let rF a = if a > 0 then Success a else Error "less than 0"
let rG a = if a < 10 then Success a else Error "greater than 9"
let rFG = rF >=> rG
// val rFG : (int -> Result<int,string>)
rFG 0
//val it : Result<int,string> = Error "less than 0"
rFG 1
//val it : Result<int,string> = Success 1
rFG 10
//val it : Result<int,string> = Error "greater than 9"
rFG 9
//val it : Result<int,string> = Success 9
// So it works as expected for Result
// Example with Option
let oF a = if a > 0 then Some a else None
// val oF : a:int -> int option
let oG a = if a < 10 then Some a else None
// val oG : a:int -> int option
let oFG = oF >=> oG
// val oFG : (int -> int option)
oFG 0
// val it : int option = None
oFG 1
// val it : int option = Some 1
Anyway I would recommend using existing Choice
instead of Success/Error or implementing Success on top of Choice in your case it would be like this:
type Result<'a, 'b> = Choice<'a, 'b>
let Success x :Result<'a, 'b> = Choice1Of2 x
let Error x :Result<'a, 'b> = Choice2Of2 x
let (|Success|Error|) = function Choice1Of2 x -> Success x | Choice2Of2 x -> Error x
And then you can run your examples without writing any bind or return.
You might wonder why there is no example for Nullable
, well that's simply because Nullable
is not a monad, it only works on value types and a function is not a value type so better stick to Option
for the same functionality.
Upvotes: 3