Sorter syntax for lambda expression with just one parameter

Topics: C# Language Design
Apr 5, 2014 at 11:44 PM
Edited Apr 6, 2014 at 12:21 AM
Most of the usages of lambdas take just one parameter, why not make the syntax sorter?

So instead of writing:
Invoices.Where(a => a.Amount > 10).Select(a => new { a.Id, inv.Customer })
we could write
Invoices.Where(@.Amount > 10).Select(new { @.Id, @.Customer })
And also improve strongly typed reflection, filling the missing memberof operator:
PropertyInfo pi = Customer.Propery(@.Id)  //Expression tree generated
Apr 5, 2014 at 11:45 PM
Timwi expressed some valid concerns on https://roslyn.codeplex.com/discussions/540883
This code is highly ambiguous. How should the compiler decide which of the following you meant?
Invoices.Where(x => x.Amount > 10).Select(new { y => y.Id, z => z.Customer })
Invoices.Where(x => x.Amount > 10).Select(y => new { y.Id, y.Customer })
x => Invoices.Where(x.Amount > 10).Select(new { x.Id, x.Customer })
Apr 6, 2014 at 12:14 AM
Just using my intuition about how the compiler works:

I think the compiler can solve the ambiguity just in the same way that it does with nested if/else: just take the closest surrounding environment expecting a lambda or if there is none fail.

In the rare scenario where the lambda is used in a method, the method has overloads, and both a lambda expression and a expression itself fit some overloads, just fail also and let the user fall back to the common lambda syntax.

In other languages, like typescript, lambda expressions can be independent literals, but not in C#. Without a surrounding environment expecting a lambda the lambda can not be converted to a delegate/expression delegate, and does not compile. While this is a weakness due to C# nominal typing, we can take advantage in this particular case.

So, at the syntactic level, an expression could be considered a lambda expression if the expression contains the @ token somewhere inside of it and has a surrounding environment expecting a lambda.

By surrounding environment expecting a lambda, I mean something like
Where(@.Id > 2)
Func<Invoice, bool> condition = @.Id > 2
new Func<Invoice, bool>(@.Id > 2)
By surrounding environment expecting a lambda, I mean this think that tells the lambda whether it should be a delegate or an expression, and provide the type information to enable type inference. I haven't found the proper name in the C# language specification, but the concept is there.
7.15 Anonymous function expressions
An anonymous function is an expression that represents an “in-line” method definition. An anonymous function does not have a value or type in and of itself, but is convertible to a compatible delegate or expression tree type. The evaluation of an anonymous function conversion depends on the target type of the conversion: If it is a delegate type, the conversion evaluates to a delegate value referencing the method which the anonymous function defines. If it is an expression tree type, the conversion evaluates to an expression tree which represents the structure of the method as an object structure.
Apr 7, 2014 at 10:54 PM
Would you propose the same for methods with just one parameter? And indexers?
Apr 7, 2014 at 11:13 PM
I don't understand what you mean, could you elaborate on this?
Apr 7, 2014 at 11:33 PM
If I understand it correctly, you are proposing some sort of shorthand for single parameter lambdas, right?

Would this be restricted only to those lambdas?
Apr 7, 2014 at 11:51 PM
Yes, lambdas of one parameter are very common.

When you use them, you have to invent a name that doesn't collide with any other name in the scope, just for using it after that and throwing it away.

And also there will be a code reduction of 5 character per lambda
a => a.Name
@.Name
Its a good syntax, the only problem I see for being accepted is that there are already many different lambda flavors.

Here is a family picture :)
delegate(Person a){ return a.Name; }
(Person a) =>{ return a.Name; }
(a) =>{ return a.Name; }
(a) => a.Name
a => a.Name
@.Name
Lambdas are the building block for libraries to write pseudo-syntax. The sorter the better.

I don't see a similar problem with methods or indexers. Do you?
Apr 7, 2014 at 11:58 PM
_ => _.Name
delegate(Person _){ return _.Name; }
(Person _) =>{ return a.Name; }
(_) =>{ return _.Name; }
(_) => _.Name
_ => _.Name
Apr 8, 2014 at 6:20 AM
Edited Apr 8, 2014 at 6:20 AM
Of course you could write
_ => _.Name
But I usually use _ for ignored parameters,
i => i.Name
Is even sorter, but nothing beats
@.Name
What is more important, at least in my team we try to use the initial letter that makes sense: i,j for indexes, p for person, etc... but this variables have to be free. For example:
public static string FindSimilarPersonName(Person p)
{
    return persons.Where(p2=>p2.Equals(p)).Select(p2=>p.Name); 
}
Look how, in the where operator, i was forced to take the ugly ´p2´ name because ´p´ was already taken.

Additionally, I introduce a bug in the select because I used ´p´ instead of ´p2´.

But with the new syntax:
public static string FindSimilarPersonName(Person p)
{
      return persons.Where(@.Equals(p)).Select(@.Name); 
}
It's sorter, but is not just about that. It's mainly about removing the names you have to keep in your head and reducing bugs.
Apr 8, 2014 at 6:21 AM
How do you feel about:
var c1 = objects.Select(@);
var c2 = booleans.Where(@).Select(@);
?
Apr 8, 2014 at 7:12 AM
I feel fine :) Looks strange because is strange code:
var c1 = objects.Select(a=>a);
var c2 = booleans.Where(a=>a).Select(a=>a);
Is also strange code.
Apr 8, 2014 at 7:49 AM
@Olmo
not really. Consider these two: flags.Where(@).Count(); and flags.Where(!@).Count(). These code snippets compute the count of set and reset flags respectively. I still believe that flags.Where(f => true).Count() and flags.Where(f => false).Count() are better.
Apr 8, 2014 at 8:49 AM
flags.Where(@).Count();
Will be compiled to
flags.Where(f => f).Count();
not
flags.Where(f => true).Count();
Similarly
flags.Where(!@).Count();
will be compiled to
flags.Where(f => !f).Count();
not
flags.Where(f => false).Count();
I don't see any problem
Apr 8, 2014 at 9:07 AM
Edited Apr 8, 2014 at 9:11 AM
@Olmo,
sure, my code snippets aren't exactly equal, that's true. I'm just trying to point out that both .Where(@) and .Where(!@) implicitly implement the predicates having no explicit notation where the first one is actually always true and the second is actually always false. This looks somewhat magical.
Apr 8, 2014 at 9:17 AM
Edited Apr 8, 2014 at 10:07 AM
You mean that you would like to write something like this?
flags.Where(true).Count();
flags.Where(false).Count();
Or that, at least, you are uncomfortable with the asymmetry?

Yup, is a corner case, but I think that will be going too far. I will keep it simple:

Lambda expression can be any expression in surrounding environment expecting a lambda that contain at least one @ and no nested lambdas.
Apr 8, 2014 at 9:57 AM
Olmo wrote:
Or that, at least, you are uncomfortable with the asymmetry?
Yes, this is probably exactly what I mean. :)
Apr 8, 2014 at 10:02 AM
Edited Apr 8, 2014 at 10:07 AM
Olmo, you still haven’t addressed the elephant in the room here. You need to define how the compiler is going to determine the beginning and end of a lambda expression. There is far too much ambiguity in your proposal.

Consider the following example:
myEnumerable.Select(GetDelegate(@.name))
Assume for the sake of this example that GetDelegate has two overloads; one takes a delegate as a parameter, the other a string.

How is the compiler supposed to decide which of the following three you mean?
myEnumerable.Select(GetDelegate(x => x.name))
myEnumerable.Select(x => GetDelegate(x.name))
x => myEnumerable.Select(GetDelegate(x.name))
Clearly, all three are valid interpretations of the code and all three yield valid programs that should compile, but do different things.

You need to describe in exact detail how the parser is going to decide where to insert the lambda-expression node. You can’t defer this until method overload resolution because then method overload resolution (which is already hideous) would require post-hoc modifications to the parse tree, which in turn would potentially change which variables are captured by the lambda, which in turn requires a rerun of the anonymous-method transform (which by this point has already run)... am I beginning to communicate the monumental difficulty of your proposal?
Apr 8, 2014 at 10:12 AM
Apr 8, 2014 at 10:22 AM
Timwi:
I think the compiler can solve the ambiguity just in the same way that it does with nested if/else: just take the closest surrounding environment expecting a lambda or if there is none fail.
So in your example:
myEnumerable.Select(GetDelegate(x => x.name))  //Ambiguity resolver by taking the closest surronding... SEEL for sort.
myEnumerable.Select(x => GetDelegate(x.name))  //Not choose because ambiguity resolution, but could be written with this syntax if necessary
x => myEnumerable.Select(GetDelegate(x.name))  //I will never compile, because there's no SEEL. 
Notice that, for the ambiguity to become a real problem some stars have to align:
  • Overloads that take a lambda / a value have to exist in two nested SEEL
  • Both lambdas will have to have a similar type with respect of the operation you do with a parameter:
So in your example
myEnumerable.Select(GetDelegate(@.name))

myEnumerable.Select(a=>GetDelegate(b=>b.name))
both a and b have to be a property name in order to be confusing.

I think this is a much more unlikely scenario than nested if / else.
Apr 8, 2014 at 10:26 AM
Just to clarify, even if I said
Lambda expression can be any expression in surrounding environment expecting a lambda that contain at least one @ and no nested lambdas.
That doesn't mean that you can not used in nested lambdas, but they have to be used only in the inner-most one.

This should be valid code:
> persons.Where(p=>p.Cars.Count(@.IsElectric) > 1)
Apr 8, 2014 at 10:56 AM
@Olmo,

If you had to take one of these (https://roslyn.codeplex.com/wikipage?title=Language%20Feature%20Status) out in favor of this, which one would be?
Apr 8, 2014 at 11:12 AM
Edited Apr 8, 2014 at 11:19 AM
persons.Where(p=>p.Cars.Count(@.IsElectric) > 1)
or maybe
persons.Where(@.Cars.Count(c => c.IsElectric) > 1)
? Do I understand correctly: a matter of style comes here? Just because I couldn't write something like persons.Where(@.Cars.Count(@.IsElectric) > 1)

Also I'm not sure about sequence.Select(f => 90 / f). Would sequence.Select(90 / @) be valid here?
Apr 8, 2014 at 11:25 AM
Olmo, I’m afraid you do not appear to comprehend the complexity of your proposal. At parsing stage, the parser has no way of knowing which places “expect a lambda”. Even at compile stage, the compiler will not know this until after method overload resolution, but method overload resolution itself depends on knowing where the lambdas are. You have a chicken-and-egg problem here.

I strongly recommend that you read some of the C# language spec in order to get a feel for the level of detail and preciseness required, and then try to write a new section for the spec that would describe your feature. Then go through several examples, such as the one I gave, and see whether all the processes described in the spec (parsing, method overload resolution, type inference, anonymous method transformation) interact well and unambiguously with your feature.
Apr 8, 2014 at 11:27 AM
Edited Apr 8, 2014 at 11:51 AM
halogen
persons.Where(p=>p.Cars.Count(@.IsElectric) > 1)
// valid code

persons.Where(@.Cars.Count(c => c.IsElectric) > 1) 
// red underscore marking the @. Error: @ can not be used on lambdas with nested lambdas

persons.Where(@.Cars.Count(@.IsElectric) > 1) 
// red underscore marking the first @. Error: @ can not be used on lambdas with nested lambdas
Also I'm not sure about sequence.Select(f => 90 / f). Would sequence.Select(90 / @) be valid here?
Yes, is valid code.

It's not a matter of style, all the cases conform to my definition:
Lambda expression can be any expression in a surrounding environment expecting a lambda that contain at least one @ and no nested lambdas.
Apr 9, 2014 at 2:58 PM
On a side note, Scala has a very similar feature, using underscores.
cars.Count(_.IsElectric)
In their case, further underscores refer to the next arguments, so you can write
list.Reduce(_+_)
I don't know exactly how they choose where to insert the lambda. Maybe they stop at the first surrounding function call? Even with such a limitation, in most cases where you want to use this, the lambdas are very simple anyway.
It is certainly worth considering if the Scala approach would work well in C#.
Apr 9, 2014 at 5:06 PM
Thanks for pointing it out! I knew there were other languages but didn't find the right way to search for it.

Underscore won't work in C# because is a valid identifier. I think @ is a nice choice: Is available in every keyboard, not ambiguous with the other cases (stirng literal and identifier literal) and 'at' sounds like 'that'

I think there's another language that uses this feature, the keyword is that or this or something like that :)
Apr 9, 2014 at 5:49 PM
Edited Apr 9, 2014 at 5:50 PM
In principle I love the idea... it always seems meaningless coming up with a variable name for lambda arguments when there is only the one.

However,

myEnumerable.Select(a=>a.DoIt(b=>b.name))

this example would make it very strange indeed if only one part of that could be shorthanded. Now sure, you could come up with possibilities like it always scopes as narrow as possible, so this would work:

myEnumerable.Select(@.DoIt(@.name))

but damn would it be hard to parse.

Or you could add an extra @ for every nesting level

myEnumerable.Select(@.DoIt(@@.name))

But really it is still unpleasant to read.

So you are pretty much left with a shorthand for when there is only one parameter and the lambda doesn't nest... and I question if it is worth introducing yet more syntax just to handle that case?
Apr 9, 2014 at 6:19 PM
I think at least 50% of my lambdas are like this.
Nov 6, 2014 at 12:06 PM
I like the idea from Olmo (and would like it much more than some other upcoming features) as I work A LOT with Lambdas and as for Olmo most of mine also would fit the "one parameter and not nested" rule.

BUT I am not sure about using @, because it is already in use by Razor for something different (but I like your derivation). Maybe #, as I do not know any other usage of it?
Nov 7, 2014 at 12:51 AM
Similar thing exists both in PowerShell ($_) and Apple Swift ($0, $1), though both languages have clear way to distinguishing what's a lambda ({}).
Nov 7, 2014 at 2:18 AM
Edited Nov 7, 2014 at 2:25 AM
That is a good proposal... syntactic sugar.. but very sweet.
But IMHO it could only be worth it, if it can deal with such cases
People.Select($.DoIt(String.Concat($0.FirstName, ", ", $1.City.Where(Char.IsLetter($)))))
Having $0, $1 helps in more painful case of
(a, b) => a * b  //turning it into 
$0 * $1
Simple rules(out of pocket):
  1. You can't use parents lambda @/$ in nested Lambda. You need names for that(I don't like @@.Xyzw, better to disallow).
    This seems perfect and totally legit case
persons.Select($.DoIt($.Name))
  1. @/$ means same as @0/$0
Please, Big Heads, think more about it. :)
Nov 7, 2014 at 6:44 AM
arek_bal wrote:
That is a good proposal... syntactic sugar.. but very sweet.
But IMHO it could only be worth it, if it can deal with such cases
People.Select($.DoIt(String.Concat($0.FirstName, ", ", $1.City.Where(Char.IsLetter($)))))
I don't understand your example. What DoIt does? Where the lambda with two parameters ($0 and $1) comes from?
Nov 7, 2014 at 9:30 AM
@Olmo: Pretend you’re the compiler. If you believe that under your proposal the stated code should be invalid, you have to decide:
  • at which point in the compilation process the compiler determines it to be invalid
  • what the compiler error message should be
  • where in the code the cursor should point at the error.
Nov 7, 2014 at 10:24 AM
Edited Nov 7, 2014 at 10:24 AM
Olmo wrote:
arek_bal wrote:
People.Select($.DoIt(String.Concat($0.FirstName, ", ", $1.City.Where(Char.IsLetter($)))))
I don't understand your example. What DoIt does? Where the lambda with two parameters ($0 and $1) comes from?
You do understand. DoIt takes Lambda with two parameters.