Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Shorthand for simple expression lambdas #3561

Closed
HaloFour opened this issue Jun 17, 2015 · 30 comments
Closed

Proposal: Shorthand for simple expression lambdas #3561

HaloFour opened this issue Jun 17, 2015 · 30 comments

Comments

@HaloFour
Copy link

This has come up a few times on CodePlex as well as here but I've not seen any formal proposal for it so I thought I'd post one just to track it and get some conversation going about it.

Many of the LINQ extension methods accept a single argument and return a value, most often the result of a simple expression stemming from that argument. In those simple cases the developer is still required to assign the parameter value to a name that cannot shadow a name used in the current scope and then immediately reference that name in the body of the closure.

var query = db.Orders
    .Where(order => order.Value > 50.0m)
    .Select(order => order.Employee)
    .OrderBy(employee => employee.Name);

I propose that a short-hand version of this common pattern where the value of the argument can be accessed through a specific sigil which must be referenced in the method body. The following uses the sigil @ and is the same as the query above:

var query = db.Orders
    .Where(@.Value > 50.0m)
    .Select(@.Employee)
    .OrderBy(@.Name);
@whoisj
Copy link

whoisj commented Jun 17, 2015

👍

@Suchiman
Copy link
Contributor

While my mind is not settled about wether this syntax is cool or ugly, let's think about these problems first:

Nesting

var query = db.Orders
    .Where(@.Items.Any(@.Weight > 20));

Overload resolution when passing Lambdas

instance.Do(@.SomeMethod(() => @.SomeProperty));

Is () => @.SomeProperty now passed as argument-less lambda or is it a lambda with one anonymous @ argument that returns lambda with a closure on @, e.g.:

instance.Do(@.SomeMethod(@ => () => @.SomeProperty));

Constant values / throwing away the argument?

var query = db.Orders
    .Where(@.true); //? member access, no
    .Where(@true); //? rather like this, though **extremely** small difference to verbatim identifier / strings
    .Where(true); //? not unique enough, overload resolution problems

Also is this allowed?

var query = db.Orders
    .Where({
        return @.Items.Any(x => true);
    });

@HaloFour
Copy link
Author

@Suchiman

I agree, it can get nasty quick. My personal opinion is that none of that is permitted and the sigil is effectively just expanded into _temp => _temp. Trying to build a fully-featured alternate syntax defeats the purpose of the short-hand and the existing lambda syntax is more than sufficient and plenty succinct.

To be a little more specific, the sigil would not represent an actual variable in scope. It could not be reused within the expression in any form. That would make nesting permitted and unambiguous (and a common scenario where I see this being useful), and the other situations non-issues since none of them would be legal.

That's my personal opinion, though.

@orthoxerox
Copy link
Contributor

Also mentioned in #3171

@RedwoodForest
Copy link

Swift calls this Shorthand Argument Names and uses the syntax $1 for the first argument, $2 for the second, etc.

@HaloFour
Copy link
Author

@lawrencejohnston Sorta, I'm not looking to reproduce Swift's syntax in the least. I actually really don't care for the idea of having some kind of indexed sigil or implicit tuple for referencing the arguments. As mentioned above the only time that I think that this syntax should apply is for those very simple cases where you need an expression rooted in the argument, otherwise the sigil cannot be used.

@RedwoodForest
Copy link

@HaloFour If C# ends up going a different direction that's fine, I just wanted to put Swift's similar feature out there as a source of inspiration/discussion.

@HaloFour
Copy link
Author

@lawrencejohnston You're right. I put this proposal up specifically to generate conversation so it's silly to quash it with my opinion. 😄

@orthoxerox
Copy link
Contributor

@Suchiman, statement-bodied lambdas should definitely not use this syntax. Nested lambdas are also a no-no (only innermost lambdas should be allowed to use @). Only simple chains should be allowed:

var data = File.ReadAllLines(@"in.txt") .Select(@.Split('\t')) .Select(new {Word = @[0], Freq = Double.Parse(@[1])}) .SelectMany( w=>LetterCombinations(w.Word) .Select(new {LC = @, Freq = w.Freq}) ) .GroupBy(@.LC) .Select(g => new {LC = g.LC, Freq = g.Sum(@.Freq)}) .OrderByDescending(@.Freq) .Select(String.Format("{0}\t{1}", @.LC, @.Freq)); File.WriteAllLines(@"out.txt", data);

Of course, now we'll have four different ways to write a delegate...

@alrz
Copy link
Member

alrz commented Aug 5, 2015

I would prefer that or its keywords for this kinda situation. for example .Select(that.Name) .

@HaloFour
Copy link
Author

HaloFour commented Aug 5, 2015

@alrz

IIRC Cω used an it keyword. Is there a benefit to requiring a new contextual keyword? Seems that it would complicate the grammar since the compiler would be required to determine if a variable/type/etc of that name was in scope (as it does with var, dynamic, etc.) and it requires more keystrokes than a simple sigil.

@orthoxerox
Copy link
Contributor

Have you thought about the case of passing Func<int, int> to something that expects Func<int>? The longhand version is () => func(x), what would the shorthand version be, @func(x)?

@HaloFour
Copy link
Author

HaloFour commented Aug 5, 2015

@orthoxerox

I have not. This proposal only really addresses those delegates that accept a single parameter and return the result of a single expression. Is there a reason that the expression () => func(x) isn't DRY or succinct?

@bondsbw
Copy link

bondsbw commented Aug 15, 2015

Not a bad idea. Maybe even a shorter syntax could work, where a dot with no LHS is used to access a member of the implicit parameter:

var query = db.Orders
    .Where(.Value > 50.0m)
    .Select(.Employee)
    .OrderBy(.Name);

Off the top of my head I can't think of any reason that would be a problem.

This would still have some limitations, each shared with the original proposal:

  • Exactly one parameter is required if using short-hand syntax
  • Nesting would be restricted in a clear way (e.g. using shorthand in the nested lambda prevents it from accessing members of an outer shorthand lambda's implicit parameter)
  • If the method takes a parameter of type Func<X,Y>, shorthand form cannot be used if there is also an overload with parameter of type Y in the same position (this would cause ambiguous invocation when no dotted members are accessed)

@orthoxerox
Copy link
Contributor

@bondsbw: There might be a problem if you want to test the value itself.
var evens = myNumbers.Where(@ % 2 == 0); will work, but your syntax won't.

@Thaina
Copy link

Thaina commented Apr 29, 2016

This is neat, I like it. But could it be @0 ? So we could @1 / @2 if it is multiple parameter

I would like to suggest # or $ too in addition to .

And could this be possible to use in normal function? We may like to change the name of param for documentation in intellisense but want to shorthand in the code

@Suchiman
Copy link
Contributor

@Thaina short answer: no
@ In this context is designed to make lambdas in very specific scenarios shorter where you don't care about the name anyway since it's clear what the name will be.

In regular Methods, this would lead to totally unreadable code, nothing is stopping you from naming your arguments p0, p1, p2 but nobody does for OBVIOUS reasons...

@Thaina
Copy link

Thaina commented Apr 29, 2016

@Suchiman In some case parameter name was included in public human readable intellisense so we may want to make it long and readable like

public T GetByIndex(int indexOfItemInTheList) // This will be shown in intellisense
{
    return list[indexOfItemInTheList];
}

But then it make the code more messy like that
Normally I would temp it to int i which I don't like to add variable to do just that. In worse case if it is large struct that copied it will be bad

@iam3yal
Copy link

iam3yal commented Oct 30, 2016

Don't like the syntax but I really like the idea.

Personally, I'd go with Swift version as @lawrencejohnston said, simply because it's more flexible even though I know this proposal is designed for the simple case.

@Suchiman Sometimes the predicate can be simple and yet depends on two/three parameters, I see no reason to restrict it to only a single parameter, a single parameter doesn't imply simplicity.

@jamesqo
Copy link
Contributor

jamesqo commented Dec 24, 2016

I noticed recently that a "discard" syntax is being added so people will be able to write this:

if (TryGetValue(out var _))
{
    // `_` is not a real variable and will not show up in IntelliSense
    Console.WriteLine("Has a value, but we don't want to know what it was.");
}

Since it looks like _ is already being repurposed, maybe we could use it as shorthand for the lambda variables too:

var query = db.Orders
    .Where(_.Value > 50.0m)
    .Select(_.Employee)
    .OrderBy(_.Name);

If there was already an existing variable _ in scope then it would just bind to that variable.

@HaloFour
Copy link
Author

@jamesqo

I don't think that would be possible given that it could change the behavior of existing code where a variable named _ is already is scope. The use of _ as the discard variable is frought with enough technicalities which will likely frustrate developers with compiler errors (or silent data loss) as it is.

@jamesqo
Copy link
Contributor

jamesqo commented Dec 24, 2016

@HaloFour

I don't think that would be possible given that it could change the behavior of existing code where a variable named _ is already is scope. The use of _ as the discard variable is frought with enough technicalities which will likely frustrate developers with compiler errors (or silent data loss) as it is.

Correct me if I'm wrong, but isn't discard contextual, i.e. it will preserve existing behavior if something named _ is in scope?

If I am, I vote for one of these (with positional parameters, which I just saw above):

db.Orders.Where(#1.Value > 50.0m); // Or #.Value > 50.0m

db.Orders.Where(&1.Value > 50.0m); // Or &.Value > 50.0m

db.Orders(@1.Value > 50.0m);

db.Orders(\1.Value > 50.0m);

@bondsbw
Copy link

bondsbw commented Dec 24, 2016

$_ is used for a similar purpose in Powershell. I could also see $1, $2, etc. for positional.

@bondsbw
Copy link

bondsbw commented Dec 24, 2016

Actually, wouldn't positional parameters be ambiguous? For example, db.Orders.Where($1.Value > 50.0m) could call either the standard overload, or the indexed overload where the index is ignored.

@HaloFour
Copy link
Author

@jamesqo

You are correct that _ does retain it's previous behavior as a proper identifier if there is a variable or field of that name in scope. That's one of the things that I find unsavory about it; it can quickly be confused as either a wildcard or an identifier depending on the code that comes before it. In my opinion it's too overloaded and I'd prefer to not stretch the contexts under which it can be used further than it already is. At least with declaration expressions the compiler will always treat _ as a wildcard. It's the short-form that suffers from the ambiguity.

Using _ as the sigil for such lambdas is probably a little safer since it doesn't resemble lambda syntax today, but the compiler would be forced to first attempt to resolve any identifiers by the name _ and then resolve overloads of the method called accepting the result of that property access, so for .OrderBy(_.Name) the compiler would have to first try to resolve .OrderBy(string). Given libraries like Dynamic LINQ those might not be terribly uncommon beasts.

@SunnyWar
Copy link

How about combining simple sigil suggestion with the swift functionality. If the sigil is used alone that it's tightly coupled and cannot propagate but if the sigil is follow by a number then it can.

@ghcs27
Copy link

ghcs27 commented May 28, 2017

I don't think positional parameters like $1 are a good idea in C#. They would provide a second fully-featured way of writing lambdas, while being vastly inferior to the current syntax (see nesting, overload resolution) and less readable.
Instead, I like the spirit of the original proposal: A shorthand for cases where indexing or naming parameters is not needed (because there is only one parameter).

This is now being discussed at dotnet/csharplang#74 dotnet/csharplang#91

@jnm2
Copy link
Contributor

jnm2 commented May 28, 2017

Actually the conversation directly moved to dotnet/csharplang#91. This issue should be closed in favour of that one.

@ghcs27
Copy link

ghcs27 commented May 29, 2017

Sorry for the mistake!

@jcouv
Copy link
Member

jcouv commented Oct 22, 2017

Closing since the issue was moved to csharplang. Thanks

@jcouv jcouv closed this as completed Oct 22, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests