4

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'
JJJ
  • 509
  • 1
  • 6
  • 14
  • 4
    Extension members aren't considered for statically resolved type constraints. Just not a feature of F#, period. [Please cast your vote on uservoice](https://fslang.uservoice.com/forums/245727-f-language/suggestions/5664242-simulate-higher-kinded-polymorphism). – Fyodor Soikin Aug 17 '16 at 22:00
  • 1
    As Fyodor mentioned, this is not supported. While I think this would actually be a nice extension to F#, I'm kind of curious about what use cases you had mind for this? I find Suave nice because it is quite simple and not overly abstract, so generalizing the operators does not sound like something that would go towards better F# code - though it's fun to just explore it, of course. – Tomas Petricek Aug 17 '16 at 22:36
  • @FyodorSoikin Thanks, that explains it. What an annoying limitation, 3 votes have been cast :) – JJJ Aug 18 '16 at 05:01
  • @TomasPetricek No use case yet, just playing. Back in my C++ days I used templates to their full extent, mostly to good effect. I particularly enjoyed using Loki, which kinda took things to their limits. I've always been a little annoyed by generics in C#, they lack power, so I am/was hoping that F# might rectify that. – JJJ Aug 18 '16 at 05:09
  • 2
    @JJJ I'm not sure - you can stretch the F# static constraints a bit (like FsControl does), but I don't think it's what they're designed for. _[rant] FsControl is amazing achievemnt, but I would not really use it in practice, because it gets close to the rough edges - and there is usually more idiomatic solution to the original problem. In other words, I think taking concepts from one language (C++ templates or Haskell type classes) and trying to use them in another language (F#) does not always lead to very clean code. [/rant]_ – Tomas Petricek Aug 18 '16 at 12:47
  • 1
    @TomasPetricek Don't worry, I'm a pragmatic guy. I just enjoy exploring the language. Maybe if F# had a stronger, more consistent story for meta-programming, people wouldn't try to bring in stuff from everywhere else. – JJJ Aug 18 '16 at 18:08

1 Answers1

3

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.

Community
  • 1
  • 1
Gus
  • 25,839
  • 2
  • 51
  • 76
  • Thank you for the detailed answer. I'll check out the links and FsControl in particular. – JJJ Aug 18 '16 at 07:34
  • @JJJ In fact you don't even need to implement Success on top of Choice (see the updated answer). Anyway that's certainly a good idea, because you'll reuse all existing functionality and increase compatibility with other libraries. – Gus Aug 19 '16 at 06:29