5

Given this example code:

enum op
{ 
  add, 
  remove
}

Func<op, int> combo(string head, double tail) => 
  (op op) => 
  op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : Int32.Parse(head) - Convert.ToInt32(tail);

Console.WriteLine(combo("1", 2.5)(op.remove));

Which returns:

-1

What does the first arrow operator mean?
By specification it does not look like an expression body or a lambda operator.
Is there any reference in the C# language specification about this usage?

Boann
  • 48,794
  • 16
  • 117
  • 146
Giulio Caccin
  • 2,962
  • 6
  • 36
  • 57
  • 2
    That is an expression bodied method that returns a `Func` - which is then defined using a lambda expression – UnholySheep Oct 28 '19 at 20:31
  • 1
    The first one is for the expression body, the second one for the lambda operator. What exactly are you asking for? – Progman Oct 28 '19 at 20:31
  • 1
    In fact, it is an expression bodied method – knittl Oct 28 '19 at 20:32
  • It just shorthand, for single line function declaration e.g expression bodied method – johnny 5 Oct 28 '19 at 20:32
  • 2
    Syntax was added in C#6 for Expression Body Member: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members – BurnsBA Oct 28 '19 at 20:33
  • I found `combo("1", 2.5)(op.remove)` very disapointing since it is not a standard method call...??? It is like writing `Console.WriteLine("what")("is that")`! –  Oct 28 '19 at 20:38
  • 2
    @OlivierRogier Well then you'll hate this: `Console.WriteLine(((Func>>)(a => b => c => a + b + c))(1)(2)(3));` – Jonathon Chase Oct 28 '19 at 20:42
  • @JonathonChase Why hate? Disapointing because it the first time I see such call but I understand after 5 mins :) –  Oct 28 '19 at 20:44
  • 1
    @JonathonChase that's disgusting, I threw up a bit I think. – Trevor Oct 28 '19 at 20:44
  • 2
    @OlivierRogier It is a standard method call, it's just that the first call returns another function, which is being then called the next parameter. It does look quite ugly in C# compared to how currying can look in a functional language. – Jonathon Chase Oct 28 '19 at 20:45
  • @JonathonChase It's cool like a multidimentional collection access except that it's nested methods parameters there. –  Oct 28 '19 at 20:47

2 Answers2

13

What does the first arrow operator mean?

In newer versions of C#, you are allowed to write:

int M(int x) 
{
  return x + x;
}

as the shorter and clearer:

int M(int x) => x + x;

It is nothing more than a "syntactic sugar" that lets you write a simple method in a shorter and more direct way.

it does not look like an expression body or a lambda operator.

The left => indicates an expression body. The right => indicates a lambda. So it is somewhat confusing in your example because the thing being returned is a lambda, which also uses =>. But do not let that distract you:

Func<int, int> M() => x => x + 1;

is just a short way of writing

Func<int, int> M() 
{
  return x => x + 1;
}

Which in turn is just a short way or writing

static int Anonymous(int x)
{
  return x + 1;
}
Func<int, int> M() 
{
  return Anonymous;
}

If you find code with multiple => operators confusing you can always remove them by desugaring them into the more explicit form. It's just a short form that some people find easier to read.


Can you please write

static Func<op, int> combo(string head, double tail) =>
  (op op) => ...; 

with just one => operator.

Sure. Here are two equivalent ways to write that code.

// Block body, lambda return.
static Func<op, int> combo(string head, double tail)
{
  return (op op) => ...; 
}

// Block body, local function return:
static Func<op, int> combo(string head, double tail)
{
  op MyFunction(op)
  {
     return ...;
  }
  return MyFunction;
}

Sorry, I was not being explicit. What I meant was can you please extract the lambda into its own standalone function, like what one would do in the olden days.

Sure; that is a little more complicated. We will do it in a series of small steps:

We start with this:

Func<op, int> combo(string head, double tail) => 
  (op op) => 
    op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : 
    Int32.Parse(head) - Convert.ToInt32(tail);

Desugar the outer function:

Func<op, int> combo(string head, double tail)
{
  return (op op) => 
    op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : 
    Int32.Parse(head) - Convert.ToInt32(tail);
}

Turn the inner function into a helper:

static int Helper(string head, double tail, op op)
{
    return op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : 
    Int32.Parse(head) - Convert.ToInt32(tail);
}

Func<op, int> combo(string head, double tail)
{
  return (op op) => Helper(head, tail, op);
}

Now move the helper method to a class:

private class Closure
{
  public int Helper(string head, double tail, op op)
  {
    return op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : 
    Int32.Parse(head) - Convert.ToInt32(tail);
  }
}

Func<op, int> combo(string head, double tail)
{
  Closure c = new Closure();
  return (op op) => c.Helper(head, tail, op);
}

Now make head and tail members of closure:

private class Closure
{
  public string head;
  public double tail;
  public int Helper(op op)
  {
    return op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : 
    Int32.Parse(head) - Convert.ToInt32(tail);
  }
}

Func<op, int> combo(string head, double tail)
{
  Closure c = new Closure();
  c.head = head;
  c.tail = tail;
  return (op op) => c.Helper(op);
}

And now we can desugar the lambda:

private class Closure
{
  public string head;
  public double tail;
  public int Helper(op op)
  {
    return op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : 
    Int32.Parse(head) - Convert.ToInt32(tail);
  }
}

Func<op, int> combo(string head, double tail)
{
  Closure c = new Closure();
  c.head = head;
  c.tail = tail;
  return c.Helper;
}

And we're done.

If you decompile your code using ILDASM or sharplab.io or some such tool you will discover that this is exactly what the compiler generates for your code, except that it generates weird, illegal names for the helpers to ensure that you never accidentally call one of them by mistake.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Can you please write `static Func combo(string head, double tail) =>(op op) =>op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : Int32.Parse(head) - Convert.ToInt32(tail);` with just one `=>` operator. I tried but it just does not seem to make sense, because in all of the other Func definitions that I've used, all of the params needed are just sent in as `in` parameters and this seems like a hybrid. – Rakesh Oct 28 '19 at 21:35
  • @Rakesh `static Func combo(string head, double tail) { return (op) => op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : Int32.Parse(head) - Convert.ToInt32(tail);}` – Jonathon Chase Oct 28 '19 at 21:37
  • @JonathonChase - thanks, sorry, I was not being explicit. What I meant was can you please extract the return type `Func` into its own standalone function, like what one would do in the olden days. So `static Func combo1(string head, double tail){return MyCustomFunction}`, I'm looking for `MyCustomFunction` – Rakesh Oct 28 '19 at 21:40
  • 2
    @Rakesh: I've added a section to my answer explaining how the compiler realizes this code. – Eric Lippert Oct 28 '19 at 21:57
  • 1
    @EricLippert - yes I see that. This looks like stuff we would see out of a text-book! – Rakesh Oct 28 '19 at 22:03
  • @EricLippert - One of the things that I could not wrap my head around since yesterday is the fact that the Helper function is defined as `public op Helper(op op){..}`, i.e. is supposed to return an `op` object, but is returning an `int`, looking at the function body - so the Helper should be returning an `int`. – Rakesh Oct 29 '19 at 15:13
  • 1
    @Rakesh: That was a cut-and-paste error. Thanks for pointing it out. I'll fix it. – Eric Lippert Oct 29 '19 at 15:18
2

Function that returns function, maybe easier to understand without lambda, written in "old" way, just for explanation, think that params closure is not ideal:

        string head;
        double tail;
        private int innerFunc(op op )
        {
            return op == op.add ? Int32.Parse(head) + Convert.ToInt32(tail) : Int32.Parse(head) - Convert.ToInt32(tail);
        }

        Func<op, int> comboWihoutLambda (string headprm, double tailprm)
        {
            head = headprm;
            tail = tailprm;
            return innerFunc;

        }
MRsa
  • 666
  • 4
  • 8