Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error on Extension Methods when Inlining

Tags:

f#

I want to extend some system types and later use them via inlining

type System.String with  
    member this.foo n = this + "!" + n 

type System.Boolean with  
    member this.foo n = sprintf "%A!%A" this n 

Now I call these extension methods

let x = "foo".foo "bar"
let y = true.foo "bar"

which gives me this

- val x : System.String = "foobar"
- val y : string = "true!"bar""

All fine and dandy - but now I want to wrap the call to .foo into an inline function

let inline foo n v = (^T : (member foo : ^N  -> ^S) v, n)
let z = foo "bar" "baz" 

Only now I get a compiler error telling me that

> The type 'string' does not support the operator 'foo':

well ... it does!

Can somebody explain whats going on?

like image 539
robkuz Avatar asked Dec 14 '22 05:12

robkuz


2 Answers

Extension methods are not taken into account in static member constraints (possible duplicate of this) and this is a general problem when you want to implement generic code using member constraints and make it work also with already defined or primitive types.

See the user voice request, also the workarounds mentioned here and Don Syme's explanation of why it's complicated to implement it in the F# compiler.

If you follow the links there you will see currently the way to workaround it basically involves creating an intermediate type and overloads for all the known types and a generic one for the extensions.

This is a very basic example of how to workaround it:

type Foo = Foo with
    static member ($) (Foo, this:int)    = fun (n:int) -> this + n 
    static member ($) (Foo, this:string) = fun n -> this + "!" + n 
    static member ($) (Foo, this:bool)   = fun n -> sprintf "%A!%A" this n 

let inline foo this n = (Foo $ this) n

//Now you can create your own types with its implementation of ($) Foo.

type MyType() =
    static member ($) (Foo, this) = 
        fun n -> printfn "You called foo on MyType with n = %A" n; MyType()

let x = foo "hello" "world"
let y = foo true "world"
let z = foo (MyType()) "world"

You can enhance it by adding an explicit generic overload for new types:

// define the extensions

type System.String with  
    member this.foo n = this + "!" + n 

type System.Boolean with  
    member this.foo n = sprintf "%A!%A" this n 

// Once finished with the extensions put them in a class
// where the first overload should be the generic version.
type Foo = Foo with
    static member inline ($) (Foo, this) = fun n -> (^T : (member foo : ^N -> ^S) this, n)
    static member ($) (Foo, this:string) = fun n -> this.foo n 
    static member ($) (Foo, this:bool)   = fun n -> this.foo n
    // Add other overloads
    static member ($) (Foo, this:int)    = fun n -> this + n 

let inline foo this n = (Foo $ this) n

//later you can define any type with foo
type MyType() =
    member this.foo n = printfn "You called foo on MyType with n = %A" n; MyType()

// and everything will work
let x = foo "hello" "world"
let y = foo true "world"
let z = foo (MyType()) "world"

You can further refine it by writing the static constraints by hand and using a member instead of an operator (see an example here),

At the end of the day you will end up with something like this generic append function from FsControl.

like image 71
Gus Avatar answered Dec 25 '22 23:12

Gus


Statically resolved type constraints do not support extension methods. It's just not a feature of F#.

If you would like F# to gain support for higher-kinded polymorphism, you can vote for it on user voice.

like image 20
Fyodor Soikin Avatar answered Dec 26 '22 00:12

Fyodor Soikin