-
Notifications
You must be signed in to change notification settings - Fork 842
Description
I was discussing this with @TIHan today and we want to record this issue, this is just a sketch.
For code like this:
let makeThing(f1: int -> int*string, f2: byte -> byte*string) = ....
let f (z: string) =
let g x = (x,z)
makeThing(g, g)the F# type checker generalises the inner function:
let f (z: string) =
let g<'T> (x: 'T) = (x,z)
MakeThing(g<int>, g<byte>)In the absence of other optimizations, and to avoid code duplication, and to make compilation localised, F# often emits let g<'T> = ... as a "first class function value" (FSharpTypeFunc) and then immediately specialises that. For example the most naive code might be a bunch of closures roughly like this:
let f (z: string) =
static type clo_g2<'T>(z:string): FSharpFunc<'T, 'T*string>
field _z: string = z
abstract Invoke(x:'T) = (x,this._z)
static type clo_g(z:string): FSharpTypeFunc
field _z: string = z
abstract Specialize<'T> = box (new clo_g2<'T>(this._z))
//MakeThing(g_int, g_byte) becomes:
MakeThing((clo_g.Specialize<int> :?> (int -> int * string)),
(clo_g.Specialize<byte> :?> (byte -> byte * string)))
Here clo_g is almost always completely superfluous, it's immediately speciailzed to two types.
But really, we should basically never (or almost never) emit FSharpTypeFunc because, as can be seen above, these things are always immediately applied to actual type arguments,
Now, IlxGen actually has a complex code path to emit "local type function closures" in the case where T is constrained (when 'T is constrained we can't use FSharpTypeFunc because that only represents unconstrained first class type functions - we only realised this late in F# 0.x when we fully connected F# to .NET generics)
We should either :
-
always emit "local" type functions (no FSharpTypeFunc), simply by deleting this one single line, or
-
still emit ILX closures and adjust the emit of
FSharpTypeFuncand subsequent closures to avoid calls to FSharpTypeFunc.Specialize and go straight to the closure class its cody would return (clo_g2)
Notes
-
Note the currently emitted code is particularly bad when multiple generic parameters are involved, when there can be several calls to Specialize.
-
Note also that a lot of this code gets generated in debug mode, and it's possible the debugging experience degrades in some ways because of that. It would likely become better if we make the generated code simpler, but should be checked
-
The call to Specialize is ultimately built here
-
The FSharpTypeFunc closure class is emitted here
Background and relation to TType_forall and Expr.TyLambda
The existence of FSharpTypeFunc at all is a relic from the 1.x era of F# when we thought the language would have to support "first class forall types", e.g. let f (x: FORALL<'T>. 'T -> 'T). These things are however easily representable in .NET programming by classes or interfaces that have generic vitrual methods, and code generall becomes much clearer when such a subtle contract is given a name.
However we never actually removed forall types or type lambdas from the F# TypedTree and pickled format. For example, you might imagine that a Val stores its information like this:
let f<'T> (x: 'T) = x
f = { genericParams: ['T]
parameters: [{name="x";type='T;attribs}]
returnType: [type='T; attribs] }but actually what happens is that the type of f when used as a first class value is stored in Val is more like this:
f = { type: FORALL 'T. 'T -> 'T
argInfos: [{"x";attribs}]
returnInfo: attribs }and the body matches this:
let f = TyLambda<'T>. Lambda(x:'T). xThe information in Val gets "split and folded back together" repeatedly in routines like GetTopValTypeInFSharpForm.
Thus the TypedTree and pickled format include Expr.TyLambda (in Expressions) and TType_forall (in types). Both are a nasty relic. Also some optimization phases may, I believe, give rise to further instances of these that are not eliminated or in a neat normal form.
As an aside, the argInfos in Val only gets stored for top level (class, module) function definitions and members, and not expression-level function definitions. This even shows in F# tooling where inner function definitions do not show metadata information like argument names.
As an aside, this whole story really stems from the early implementation of F# as a lambda-calculus-like-thing over-emphasises the view that function definitions result in first class function values.
