F# is giving me some trouble with its type inference rules. I'm writing a simple computation builder but can't get my generic type variable constraints right.
The code that I would want looks as follows in C#:
class FinallyBuilder<TZ>
{
readonly Action<TZ> finallyAction;
public FinallyBuilder(Action<TZ> finallyAction)
{
this.finallyAction = finallyAction;
}
public TB Bind<TA, TB>(TA x, Func<TA, TB> cont) where TA : TZ
{ // ^^^^^^^^^^^^^
try // this is what gives me a headache
{ // in the F# version
return cont(x);
}
finally
{
finallyAction(x);
}
}
}
The best (but non-compiling code) I've come up with for the F# version so far is:
type FinallyBuilder<′z> (finallyAction : ′z -> unit) =
member this.Bind (x : ′a) (cont : ′a -> ′b) =
try cont x
finally finallyAction (x :> ′z) // cast illegal due to missing constraint
// Note: ' changed to ′ to avoid bad syntax highlighting here on SO.
Unfortunately, I have no clue how I would translate the where TA : TZ
type constraint on the Bind
method. I thought it should be something like ′a when ′a :> ′z
, but the F# compiler doesn't like this anywhere and I always end up with some generic type variable constrained to another.
Could someone please show me the correct F# code?
Background: My goal is to be able to write an F# custom workflow like this:
let cleanup = new FinallyBuilder (fun x -> ...)
cleanup {
let! x = ... // x and y will be passed to the above lambda function at
let! y = ... // the end of this block; x and y can have different types!
}
where T : new() Means that the type T must have a parameter-less constructor. Having this constraint will allow you to do something like T field = new T(); in your code which you wouldn't be able to do otherwise. You then combine the two using a comma to get: where T : class, new()
It will give a compile-time error if you try to instantiate a generic type using a type that is not allowed by the specified constraints. You can specify one or more constraints on the generic type using the where clause after the generic type name.
Object, you'll apply constraints to the type parameter. For example, the base class constraint tells the compiler that only objects of this type or derived from this type will be used as type arguments.
Interface Type Constraint You can constrain the generic type by interface, thereby allowing only classes that implement that interface or classes that inherit from classes that implement the interface as the type parameter.
I don't think it is possible to write constraint like this in F# (although I'm not exactly sure why). Anyway, syntactically, you'd want to write something like this (as Brian suggests):
type FinallyBuilder<'T> (finallyAction : 'T -> unit) =
member this.Bind<'A, 'B when 'A :> 'T>(x : 'A) (cont : 'A -> 'B) = //'
try cont x
finally finallyAction (x :> 'T)
Unfortunately, this gives the following error:
error FS0698: Invalid constraint: the type used for the constraint is sealed, which means the constraint could only be satisfied by at most one solution
This seems to be the same case as the one discussed in this mailing list. Where Don Syme says the following:
This is a restriction imposed to make F# type inference tractable. In particular, the type on the right of a subtype constraint must be nominal. Note constraints of the form 'A :> 'B are always eagerly solved to 'A = 'B, as specified in section 14.5.2 (Solving Subtype Constraints) of the F# specification.
You can always solve this by using obj
in the function passed to your builder.
EDIT: Even when you use obj
, the values bound using let!
will have more specific types (when calling finallyAction
, F# will automatically cast the value of some type parameter to obj
):
type FinallyBuilder(finallyAction : obj -> unit) =
member x.Bind(v, f) =
try f v
finally finallyAction v
member x.Return(v) = v
let cleanup = FinallyBuilder(printfn "%A")
let res =
cleanup { let! a = new System.Random()
let! b = "hello"
return 3 }
It will be something like
...Bind<'A when 'A :> 'Z>...
but let me code it up to ensure that's exactly right...
Ah, it looks like it would be this:
type FinallyBuilder<'z> (finallyAction : 'z -> unit) =
member this.Bind<'a, 'b when 'a :> 'z> (x : 'a, cont : 'a -> 'b) : 'b =
try cont x
finally finallyAction x //(x :> 'z)// illegal
except that
http://cs.hubfs.net/forums/thread/10527.aspx
points out that F# does not do contraints of the form "T1 :> T2" where both are type variables (it assumes T1 = T2). However this might be ok for your case, what exactly did you plan to use as concrete instantiations of Z
? There is probably a simple workaround or some less-generic code that will meet the scenario. For example, I wonder if this works:
type FinallyBuilder<'z> (finallyAction : 'z -> unit) =
member this.Bind<'b> (x : 'z, cont : 'z -> 'b) : 'b = //'
try cont x
finally finallyAction x
It seems to:
type FinallyBuilder<'z> (finallyAction : 'z -> unit) =
member this.Bind<'b> (x : 'z, cont : 'z -> 'b) : 'b = // '
try cont x
finally finallyAction x
member this.Zero() = ()
[<AbstractClass>]
type Animal() =
abstract Speak : unit -> unit
let cleanup = FinallyBuilder (fun (a:Animal) -> a.Speak())
type Dog() =
inherit Animal()
override this.Speak() = printfn "woof"
type Cat() =
inherit Animal()
override this.Speak() = printfn "meow"
cleanup {
let! d = new Dog()
let! c = new Cat()
printfn "done"
}
// prints done meow woof
Oh, I see, but d
and c
now have type Animal
. Hm, let me see if there is any remaining cleverness in me...
Well, obviously you can do
type FinallyBuilder<'z> (finallyAction : 'z -> unit) =
member this.Bind<'a,'b> (x : 'a, cont : 'a -> 'b) : 'b = // '
try cont x
finally finallyAction (x |> box |> unbox)
member this.Zero() = ()
which throws away type safety (will throw a cast exception at runtime if the thing is not finallyActionable).
Or you can make type-specific builders:
type FinallyBuilderAnimal (finallyAction : Animal -> unit) =
member this.Bind<'a,'b when 'a:>Animal>(x : 'a, cont : 'a -> 'b) : 'b = //'
try cont x
finally finallyAction x
member this.Zero() = ()
let cleanup = FinallyBuilderAnimal (fun a -> a.Speak())
But I think I am out of other clever ideas.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With