JJJ
JJJ

Reputation: 569

Statically resolved types member constraints fail to recognize augmentations of system types

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

Answers (1)

Gus
Gus

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

Related Questions