moarboilerplate
moarboilerplate

Reputation: 1643

How does nesting f# functions differ from not nesting them when nesting is optional?

I understand the benefit of nesting functions when you want to return a closure. But when a function is never used outside of the function it is defined in, is there a cost to keeping it nested?

Consider:

let private validate number = 
    number > 100

let validateAndPrint (i : int) = 
    printfn "%i Greater than 100: %s" i ((validate i).ToString())

Versus:

let validateAndPrint (i : int) = 
    let validate number = 
        number > 100
    printfn "%i Greater than 100: %s" i ((validate i).ToString())

The concern here is usually when I have a few of these inner functions and I only need to call one of them based on an execution path (it's usually not prohibitive to keep these functions private or nested and test the permutations).

validateAndPrint could also be rewritten more tdd-friendly to take in the validate function:

let validateAndPrint validate i = ...

..but if validation's signature contains an internal or private type then there is the consideration of making everything public and your parameter list blowing up.

Any crucial differences?

Upvotes: 4

Views: 193

Answers (1)

V.B.
V.B.

Reputation: 6382

In Release mode, IL is identical and both versions of validate are inlined in this particular example.

You cannot rely on inlining always, e.g. if a function is "big" then compiler could choose not to inline it (this is true for methods, I am not 100% sure this is true for F# nested functions. It is not possible to mark them inline and force inlining, - need to consult F# specs if such functions behave like methods or are always inlined in Release). In the nested case in Debug mode, there is additional FSharpFunc allocation and callvirt. Without nesting it is just a static call without allocations, which is the cheapest of all possible implementations.

So in general, if nesting is truly optional it is safer to avoid it, because even if a function is not inlined it is always called as a static method. However, it matters only when you call this code several millions times per second.

Not nested, static call on IL_0019:

.method public static 
    void validateAndPrint (
        int32 i
    ) cil managed 
{
    // Method begins at RVA 0x2104
    // Code size 51 (0x33)
    .maxstack 5
    .locals init (
        [0] class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>,
        [1] bool
    )

    IL_0000: ldstr "%i Greater than 100: %s"
    IL_0005: newobj instance void class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [mscorlib]System.Tuple`2<int32, string>>::.ctor(string)
    IL_000a: call !!0 [FSharp.Core]Microsoft.FSharp.Core.ExtraTopLevelOperators::PrintFormatLine<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>>(class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>)
    IL_000f: stloc.0
    IL_0010: nop
    IL_0011: ldloc.0
    IL_0012: newobj instance void FSSO.Test/validateAndPrint@8::.ctor(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>)
    IL_0017: ldarg.0
    IL_0018: ldarg.0
    IL_0019: call bool FSSO.Test::validate(int32)
    IL_001e: stloc.1
    IL_001f: ldloca.s 1
    IL_0021: constrained. [mscorlib]System.Boolean
    IL_0027: callvirt instance string [mscorlib]System.Object::ToString()
    IL_002c: call !!0 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, string>::InvokeFast<class [FSharp.Core]Microsoft.FSharp.Core.Unit>(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!1, !!0>>, !0, !1)
    IL_0031: pop
    IL_0032: ret
} // end of method Test::validateAndPrint

Nested, allocation on IL_0018 and callvirt on IL_0020:

.method public static 
    void validateAndPrint (
        int32 i
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 58 (0x3a)
    .maxstack 6
    .locals init (
        [0] class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, bool> validate,
        [1] class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>,
        [2] bool
    )

    IL_0000: newobj instance void FSSO.Test2/validate@13::.ctor()
    IL_0005: stloc.0
    IL_0006: ldstr "%i Greater than 100: %s"
    IL_000b: newobj instance void class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [mscorlib]System.Tuple`2<int32, string>>::.ctor(string)
    IL_0010: call !!0 [FSharp.Core]Microsoft.FSharp.Core.ExtraTopLevelOperators::PrintFormatLine<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>>(class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>)
    IL_0015: stloc.1
    IL_0016: nop
    IL_0017: ldloc.1
    IL_0018: newobj instance void FSSO.Test2/'validateAndPrint@14-2'::.ctor(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>)
    IL_001d: ldarg.0
    IL_001e: ldloc.0
    IL_001f: ldarg.0
    IL_0020: callvirt instance !1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, bool>::Invoke(!0)
    IL_0025: stloc.2
    IL_0026: ldloca.s 2
    IL_0028: constrained. [mscorlib]System.Boolean
    IL_002e: callvirt instance string [mscorlib]System.Object::ToString()
    IL_0033: call !!0 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, string>::InvokeFast<class [FSharp.Core]Microsoft.FSharp.Core.Unit>(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!1, !!0>>, !0, !1)
    IL_0038: pop
    IL_0039: ret
} // end of method Test2::validateAndPrint

Release mode, inlined comparison on IL_0020:

.method public static 
    void validateAndPrint (
        int32 i
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 57 (0x39)
    .maxstack 6
    .locals init (
        [0] class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>,
        [1] class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>,
        [2] bool
    )

    IL_0000: ldstr "%i Greater than 100: %s"
    IL_0005: newobj instance void class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [mscorlib]System.Tuple`2<int32, string>>::.ctor(string)
    IL_000a: stloc.1
    IL_000b: call class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out()
    IL_0010: ldloc.1
    IL_0011: call !!0 [FSharp.Core]Microsoft.FSharp.Core.PrintfModule::PrintFormatLineToTextWriter<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>>(class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>)
    IL_0016: stloc.0
    IL_0017: nop
    IL_0018: ldloc.0
    IL_0019: newobj instance void FSSO.Test2/'validateAndPrint@14-2'::.ctor(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string, class [FSharp.Core]Microsoft.FSharp.Core.Unit>>)
    IL_001e: ldarg.0
    IL_001f: ldarg.0
    IL_0020: ldc.i4.s 100
    IL_0022: cgt
    IL_0024: stloc.2
    IL_0025: ldloca.s 2
    IL_0027: constrained. [mscorlib]System.Boolean
    IL_002d: callvirt instance string [mscorlib]System.Object::ToString()
    IL_0032: call !!0 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, string>::InvokeFast<class [FSharp.Core]Microsoft.FSharp.Core.Unit>(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!1, !!0>>, !0, !1)
    IL_0037: pop
    IL_0038: ret
} // end of method Test2::validateAndPrint

Upvotes: 8

Related Questions