Description
F# currently never emits "direct" delegates - it only ever emits delegates that go via a closure object.
As background, .NET IL permits the emit of direct delegates, e.g. for the code:
delegate D of someArg: int -> int
type C() =
static method M(arg:int) = 1
System.Func<int,int>(C.M)
F# currently always emits a closure. Instead it is possible to emit
ldnull // load null because method is static
ldftn void C::M(int)
newobj instance void class System.Func`2<int, int>::.ctor(object, native int)
That is, a direct delegate is not a closure but rather a delegate with delegate.TargetMethod known to be the exact MethodInfo for the target.
This needs careful specification and is best done as an RFC as the differences are, in some sense, observable to reflection.
The basic situation is
SomeDelegateType<...>(expr)
The cases we care about are where the target is a "known" function or method, for example:
System.Action<_,_>(handler) // non-eta-expanded known target
System.Action<_,_>(fun a b -> handler a b) // eta-expanded known target
System.Action<_,_>(fun a b -> handler (a,b)) // eta-expanded known target with different currying/tupling but same compiled representation
System.Action<_,_>(SomeType.StaticMethod) // same with a static method
System.Action<_,_>(fun a b -> SomeType.StaticMethod a b) // same with a static method
System.Action<_,_>(fun a b -> SomeType.StaticMethod(a,b)) // same with a static method
System.Action<_,_>(SomeType<...>.StaticMethod<...>) // same with a generic static method
System.Action<_,_>(fun a b -> SomeType<...>.StaticMethod<...> a b) // same with a generic static method
System.Action<_,_>(fun a b -> SomeType<...>.StaticMethod<...>(a,b)) // same with a generic static method
System.Action<_,_>(someObject.InstanceMethod) // same with an instance method
System.Action<_,_>(fun a b -> someObject.InstanceMethod a b) // same with an instance method
System.Action<_,_>(fun a b -> someObject.InstanceMethod(a, b)) // same with an instance method
System.Action<_,_>(fun a b -> someObject.InstanceMethod<...> a b) // same with a generic instance method
System.Action<_,_>(fun a b -> someObject.InstanceMethod<...>(a, b)) // same with a generic instance method
Arguably we may also care about partial applications - here a closure is needed but the argument names generated for the closure could be those drawn from the elided arguments of the obvious target:
System.Action<_,_>(handler arg1 arg2) // non-eta-expanded known target via partial application
System.Action<_,_>(SomeType.StaticMethod arg1) // non-eta-expanded known target via partial application
The questions are
- When do we guarantee to create a direct delegate?
- If we don't, do we guarantee to at least create a closure with specific argument names
We have to consider these questions for both Debug and Release code.
The proposal for the cases above is:
-
Direct delegate?
- For non-eta-expanded - currently no - proposal is to change this to "yes" for both debug and release code
- For eta-expanded - currently no - proposal is to change this to "yes" for release code. The user has made the lambdas explicit, however release code can be more efficient eliminating intermediate closures. For debug code we would still generate a closure with the user-given argument names.
-
If no direct delegate, then what are the closure argument names?
- For non-eta-expanded - currently you get a closure with funny argument names "delegateArg0" etc. The proposal is to always generate a direct delegate, so there is nothing more to do beyond that
- For non-eta-expanded partial applications - currently you get a closure with funny argument names "delegateArg0" etc. The proposal is to use the argument names drawn from the obvious target
- For eta-expanded - currently we use the argument names derived from the patterns in the source code. The proposal is to leave this unchanged for debug code. In release code we will generate a direct delegate.
-
Are these changes visible in quotations?
- The proposal is that quotations wouldn't change.
-
Are these changes visible to FCS?
- The proposal is that FCS reported expressions won't change.
This is in theory a potential breaking change because user code doing inspection on delegate value .TargetMethod may detect the difference. However within the context of the language spec it's a reasonable change as the TargetMethod is currently a closure and so inspection is not particularly useful, and the language spec gives no guarantees about this situation. For this reason it is reasonable to make this change via an RFC and /langversion
The transformation would be implemented in the F# optimizer and Ilxgen.
Pros and Cons
The advantages of making this adjustment to F# are efficiency and more direct translation of F# code
The disadvantages of making this adjustment to F# are it's work.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): S
Related suggestions: (put links to related suggestions here)
Affidavit (please submit!)
Please tick this by placing a cross in the box:
- This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
- I have searched both open and closed suggestions on this site and believe this is not a duplicate
- This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.
Please tick all that apply:
- This is not a breaking change to the F# language design
- I or my company would be willing to help implement and/or test this
For Readers
If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.