1
\$\begingroup\$

Today I decided to get into F#. I watched this video and had a bit of fun doing some coding examples, I can see that it will be very useful for certain programs that I write.

One thing that I was interested in are the explicit member constraints and how they might be used to help with the generic constructor limitation (see here for a summary of the limitation and why none of the current workarounds are adequate).

My requirements were: -

  1. That the solution was compile-time safe, i.e. if I add a new parameter to a constructor or method that it won't suddenly fall over at runtime. (This is a problem with a number of the existing workarounds to the problem).
  2. That I could know the real type several functions / classes away from where the instance was created. I have an example that makes this clearer.

Anyway, armed with a day of F# knowledge I went delving into explicit member contstraints and statically resolved type parameters and came up with this which seems to work fine (and is the code to review):

module FSharpTestModule =
    let inline createInstance< ^a when ^a : (static member CreateInstance : int -> ^a) >(i:int) =
        (^a : (static member CreateInstance : int -> ^a)(i))

This will call the static member CreateInstance on the type ^a of which the result will be ^a provided that ^a has a static member CreateInstance that takes an int and returns an ^a. Best described with an example:

type GoodFoo(a) =
    let _a = a 
    static member CreateInstance(a) = new GoodFoo(a)  
    member this.Add(b) = _a + b

type BadFoo() = 
    static member CreateInstance() = new BadFoo()      
    member this.Add(a, b) = a + b

type TestSimple() =
    static member public DoSomething() =
        //let badFoo =  FSharpTestModule.createInstance<BadFoo>(1)   //This line does not compile - excellent     
        let goodFoo = FSharpTestModule.createInstance<GoodFoo>(1)        
        goodFoo

Requirement 1: From this we can see that BadFoo causes a compile time error in createInstance because it has the wrong signature, which is good. Of course, if I added a new parameter to the constructor of GoodFoo, I would have to add the parameter in GoodFoo.CreateInstance which would mean that GoodFoo would cause a similar compile time error which is what I want.

Requirement 2: It is possible to to know the type a few hops back down the chain. For example, if we add the following:

type Factory<'T when 'T : (static member CreateInstance : int -> 'T)>() = 
    member inline this.Create(i : int) = FSharpTestModule.createInstance<'T>(i)

type TestIntermediate<'T when 'T : (static member CreateInstance : int -> 'T)>() =
    member inline this.DoSomething() =
        //normally you would do some things here...
        //ok, lets create the generic type 
        let factory = new Factory<'T>()
        let ffoo = factory.Create(1)
        //normally you would do some things here...
        //lets return it
        ffoo        

//This class knows about the real FGoodFoo
type Test() =
    static member public DoSomethingFirst() =
        //let badFoo = new TestIntermediate<BadFoo>()   //This line does not compile - excellent            
        let test = new TestIntermediate<GoodFoo>()
        let goodFoo = test.DoSomething()
        goodFoo

Then it can be seen that the type is known by Test. TestIntermediate and Factory have no knowledge of the real type only the constraint and the instance is created by createInstance in Factory which is a couple of hops down the chain.

So, my questions are:

  1. Is this a good approach or fraught with dangers? If so, what are they? Something I know already is that this does not play well with C#. In C# if I call createaInstance even with GoodFoo as a template type, I get a NotSupportedException and no compile time errors for BadFoo. Which makes me wonder if this is something that you need to be wary of when mixing C# and F# - classes/functions with explicit member constraints.
  2. Are there better ways of doing this (for example using delegate constraints)? I noticed that inline starts bubbling through my code, which I guess will increase the size of the resultant executable.

Response for Daniel

This is great for treating a constructor like a function, but doesn't quite fit in this case. For example:

type GoodFoo(x:int) = class end
let inline New x = (^T : (new : ^U -> ^T) x)
let f (g: int -> 'T) = g 0   

type Test2<'V>() =
    //let f (g: int -> 'Z) = g 0     
    member inline public this.DoSomething() = 
        //normally you would do some things here
        //let newFoo = f New<_,'V>
        let v = f New<_,'V>
        //normally you would do some things here
        ()

type Test() = 
    member public this.DoSomething() =
        let t2 = new Test2<GoodFoo>()
        t2.DoSomething()
        ()

will result in the error for the line type Test2<'V>() =:

The signature and implementation are not compatible because the declaration of the type parameter 'V' requires a constraint of the form ^V : (( .ctor ) : int -> ^V)

Unfortunately, it is not possible to constrain a generic by a constructor requiring an argument, which is the problem I am trying to solve. I could pass the function as a parameter, but that is already a known workaround that I was trying to avoid. Or is it possible to use this constraint in some way?

\$\endgroup\$
  • \$\begingroup\$ I don't understand why this is a problem, i.e. I think you're making this more complicated than it really is... Just pass a function. \$\endgroup\$ – Mauricio Scheffer Sep 4 '13 at 5:58
  • \$\begingroup\$ Thanks, yes I maybe I am overboiling things. Agree, passing a function is probably the best of the suggested options, but then you end up passing a function around to interim classes / methods that don't need to know about it. With this approach you still have to put a constraint on the class, but it is done in one place on the class and you don't need to think about it each time you look at the method flow. \$\endgroup\$ – acarlon Sep 4 '13 at 6:06
  • \$\begingroup\$ I would just add that this limitation is not particular to me as per the referenced link and: visualstudio.uservoice.com/forums/121579-visual-studio/… \$\endgroup\$ – acarlon Sep 6 '13 at 3:45
  • \$\begingroup\$ FYI: in F# 4.0, constructors can be passed as first-class functions, perhaps offering a different / easier solution to problems like these. \$\endgroup\$ – Abel Dec 15 '15 at 19:30
2
\$\begingroup\$

If you're trying to work around the inability to pass a constructor as a first-class function, here's a simpler approach:

let inline New x = (^T : (new : ^U -> ^T) x)

// a function that takes a 'constructor' function
let f (g: int -> 'T) = g 0

type T(i:int) = class end

// call it using the 'New' wrapper
f New<_,T> //use wildcard to have the constructor arg type inferred

UPDATE

Class type args require explicit constraints. The error is caused by the use of New implying a different set of constraints than those specified on the class (i.e. none). New is nothing more than a mechanism for passing constructors as first-class functions. If you could change your type to accept a "factory" function, you could do this:

type Test2<'V>(factory) =
    //let f (g: int -> 'Z) = g 0     
    member inline public this.DoSomething() = 
        //normally you would do some things here
        //let newFoo = f New<_,'V>
        let v : 'V = f factory
        //normally you would do some things here
        ()

type Test() = 
    member public this.DoSomething() =
        let t2 = new Test2<GoodFoo>(New)
        t2.DoSomething()
        ()
\$\endgroup\$
  • \$\begingroup\$ Thanks. That looks very interesting on first glance, I will take a closer look later on. \$\endgroup\$ – acarlon Sep 4 '13 at 15:27
  • \$\begingroup\$ I had a look at the code and tried it out, but unless I am missing something it doesn't quite fit what I need. Please see my edited question with Response for Daniel. Apologies if I am missing something obvious, I am quite new to this. \$\endgroup\$ – acarlon Sep 5 '13 at 3:05
  • \$\begingroup\$ I included a response to your update in my answer. \$\endgroup\$ – Daniel Sep 5 '13 at 14:30
  • \$\begingroup\$ Thanks Daniel, it is as I thought - can be passed around or a global instantiator. This is the best existing workaround, I was working on something a bit different which is a generic type constraint similar to the new() constraint. I would vote this up as it has been useful for me to know, but I don't have enough rep yet on code review to +1. Will vote up when I do. \$\endgroup\$ – acarlon Sep 6 '13 at 3:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.