loading...

Discussion on: What the heck is Currying anyway?

Collapse
slavius profile image
Slavius

Woah, this looks so clean.
From the code execution and efficiency point of view it's a nightmare. Instead of using full potential of your CPU you're making it sub-optimal the least by abusing function calls.
In normal situation you'd store the parameters in available registers or push them to the heap, store the stack pointer, redirect the code by updating the instruction pointer, use generic add instruction multiple times and then call return and remove the address from stack.
With curry function you're doing this multiple times increasing nesting, filling the stack (I don't want to debug this by looking at the stack trace, must look like a mess), making the code more vulnerable to thread unsafety as there is much more room when the thread can be pre-empted.
I can't see a reason why would anyone use this in production code. I assume any good compiler would inline those function calls anyways leaving you with significantly different debug and production code which might result in weird bugs that you cannot reproduce.

Collapse
slavius profile image
Slavius

Normal function in IL:

IL_0000: ldarg.0      // a
IL_0001: ldarg.1      // b
IL_0002: add
IL_0003: ldarg.2      // c
IL_0004: add
IL_0005: ldarg.3      // d
IL_0006: add
IL_0007: ret

// function call
IL_0000: ldc.i4.1
IL_0001: ldc.i4.2
IL_0002: ldc.i4.3
IL_0003: ldc.i4.4
IL_0004: call  int32 CurryDemo.BenchmarkNormalVsCurry::NormalAdd(int32, int32, int32, int32)

Curry function in IL:

IL_0000: newobj  instance void CurryDemo.BenchmarkNormalVsCurry/'<>c__DisplayClass1_0'::.ctor()
IL_0005: dup
IL_0006: ldarg.0      // a
IL_0007: stfld  int32 CurryDemo.BenchmarkNormalVsCurry/'<>c__DisplayClass1_0'::a

IL_000c: ldftn  instance class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, int32>> CurryDemo.BenchmarkNormalVsCurry/'<>c__DisplayClass1_0'::'<CurryAdd>b__0'(int32)
IL_0012: newobj  instance void class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, int32>>>::.ctor(object, native int)
IL_0017: ret

// function call
IL_0000: ldc.i4.1
IL_0001: call         class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, int32>>> CurryDemo.BenchmarkNormalVsCurry::CurryAdd(int32)
IL_0006: ldc.i4.2
IL_0007: callvirt     instance !1/*class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, int32>>*/ class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, int32>>>::Invoke(!0/*int32*/)
IL_000c: ldc.i4.3
IL_000d: callvirt     instance !1/*class [System.Runtime]System.Func`2<int32, int32>*/ class [System.Runtime]System.Func`2<int32, class [System.Runtime]System.Func`2<int32, int32>>::Invoke(!0/*int32*/)
IL_0012: ldc.i4.4
IL_0013: callvirt     instance !1/*int32*/ class [System.Runtime]System.Func`2<int32, int32>::Invoke(!0/*int32*/)

Benchmark:

// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.450 (2004/?/20H1)
AMD Ryzen 7 3800X, 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=3.1.401
  [Host]        : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT
  .NET Core 3.1 : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT

Job=.NET Core 3.1  Runtime=.NET Core 3.1

|        Method |       Mean |     Error |    StdDev |
|-------------- |-----------:|----------:|----------:|
| TestNormalAdd |  0.0203 ns | 0.0093 ns | 0.0083 ns |
|  TestCurryAdd | 33.1419 ns | 0.2941 ns | 0.2607 ns |

// * Hints *
Outliers
  BenchmarkNormalVsCurry.TestNormalAdd: .NET Core 3.1 -> 1 outlier  was  removed (1.57 ns)
  BenchmarkNormalVsCurry.TestCurryAdd: .NET Core 3.1  -> 1 outlier  was  removed, 2 outliers were detected (33.77 ns, 35.37 ns)

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 ns   : 1 Nanosecond (0.000000001 sec)

// ***** BenchmarkRunner: End *****

the program:

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<BenchmarkNormalVsCurry>();
        }
    }

    [SimpleJob(RuntimeMoniker.NetCoreApp31)]
    [RPlotExporter]
    public class BenchmarkNormalVsCurry
    {
        public static int NormalAdd(int a, int b, int c, int d)
        {
            return a + b + c + d;
        }

        public static Func<int, Func<int, Func<int, int>>> CurryAdd(int a) =>
            (b) =>
                (c) =>
                    (d) => a + b + c + d;

        [Benchmark]
        public void TestNormalAdd() => NormalAdd(1, 2, 3, 4);

        [Benchmark]
        public void TestCurryAdd() => CurryAdd(1)(2)(3)(4);
    }
Collapse
xtofl profile image
xtofl

Fantastic that you emphasize this. While FP code constructs unclutter the code, without proper tool support you just move the clutter into the runtime.

Indeed, JavaScript (nor C#) was never designed to go full throttle on functional programming concepts. Therefor the JIT compiler doesn't optimize any of it.

Languages like Haskell, F#, Erlang, Lisp, are built for this, and will handle it in a better way.

Collapse
heytulsiprasad profile image
Tulsi Prasad Author

Thanks man, for putting this out here!

I'm definitely interested to know what happens behind all the functions and about compilation. However, I'm not quite experienced at it, but hopefully this will be helpful to others and even me in future.