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: -
- 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).
- 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:
- 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.
- 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?