Pavel Voronin
Pavel Voronin

Reputation: 13983

Why does F# compiler prefer to generate closed implementations of FSharpFunc types?

For this code:

module Module =
    let func x y z = 0

    [<EntryPoint>]
    let main args =
        func 1
        func 1 1
        0

Decompilation yields:

[CompilationMapping(SourceConstructFlags.Module)]
public static class Main
{
    [CompilationMapping(SourceConstructFlags.Module)]
    public static class Module
    {
        [Serializable]
        internal sealed class main@30 : OptimizedClosures.FSharpFunc<object, object, int>
        {
            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            [CompilerGenerated]
            [DebuggerNonUserCode]
            public int x;

            [CompilerGenerated]
            [DebuggerNonUserCode]
            internal main@30(int x)
            {
                this.x = x;
            }

            public override int Invoke(object y, object z)
            {
                return func(x, y, z);
            }
        }

        [Serializable]
        internal sealed class main@31-1 : FSharpFunc<object, int>
        {
            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            [CompilerGenerated]
            [DebuggerNonUserCode]
            public int x;

            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            [CompilerGenerated]
            [DebuggerNonUserCode]
            public int y;

            [CompilerGenerated]
            [DebuggerNonUserCode]
            internal main@31-1(int x, int y)
            {
                this.x = x;
                this.y = y;
            }

            public override int Invoke(object z)
            {
                return func(x, y, z);
            }
        }

        [CompilationArgumentCounts(new int[]
        {
            1,
            1,
            1
        })]
        public static int func<a, b, c>(a x, b y, c z)
        {
            return 0;
        }

        [EntryPoint]
        public static int main(string[] args)
        {
            int x = 1;
            new main@30(x);
            int x2 = 1;
            int y = 1;
            new main@31-1(x2, y);
            return 0;
        }
    }

    public static a Dump<a>(a arg00)
    {
        return arg00.Dump();
    }
}

It generates a concrete type, that is generic parameters are provided at type definition. Why is not this done at the point of construction? I also noticed that types are generated in the module where call occurs, not where func is defined.

Having let func x y z = ... we need implementations of types to cover all possibilities:

FSharpFunc<T1,FSharpFunc<T2,T3,TReturn>>
FSharpFunc<T1,T2,FSharpFunc<T3,TReturn>>
FSharpFunc<T1,FSharpFunc<T2,FsharpFunc<T3,TReturn>>>

Compiler could generate all possible combinations in the same place, where function is defined, closing only for parameters with inferenced types.

You could argue that for the list of 7 args the set of types going to be quite large, but types like FSharpFunc<T1,T2,..,Tn, FSharpFunc<...>> are a mere optimazation. And FSharpFunc supports up to six generic types, then compiler has to switch to FSharpFun<T1,T2,T3,T4,T5,FSharp<...>>.

Upvotes: 3

Views: 194

Answers (1)

As pointed out by Fyodor it's not function creation that makes the compiler generating the hidden classes. The hidden classes are used to implement partial application.

In F# a partial application and lambdas are implemented as a compiler generated class that extends an abstract class. C# lambdas rely on delegates instead. IIRC Java and Scala use a similar technique to F# as JVM doesn't have delegates.

I suspect the F# compiler generates a class per partial application because it's simpler than collecting all partial applications and coalesce the identical ones.

It also helps the debuggability of F# programs as the name hints where the partial application was done: main@31-1 => In the main function at row 31. This name if included in logs or performance runs can help identifying what partial application is causing problems.

This comes at the cost of increasing the size of the F# assembly file as noted in a comment by Pavel.

Upvotes: 2

Related Questions