dotnet / vblang

The home for design of the Visual Basic .NET programming language and runtime library.
288 stars 64 forks source link

More concise syntax for lambdas #371

Open jrmoreno1 opened 5 years ago

jrmoreno1 commented 5 years ago

While VB’s use of the full-length Function keyword makes the language easier to approach and learn, it makes it harder to actual work with.

I use linq a lot and when chaining methods the length of the line quickly becomes an issue. I suggest using a conditional contextual keyword Fn, which would be the equivalent of Function if there was no Fn in scope as a compromise between providing context and conciseness and a breaking change.

I am familiar with the history, http://www.panopticoncentral.net/2006/12/08/lambda-expressions-part-i-syntax/, https://visualstudio.uservoice.com/forums/121579-visual-studio-ide/suggestions/3741764-allow-c-style-lambda-expression-syntax-in-vb-net, but feel that a shorter syntax is really needed.

bandleader commented 5 years ago

Allowing Fn instead of Function would indeed make my code shorter & easier to read. That said, I proposed in #256 using Scala's underscore syntax, which fits perfectly into VB in my opinion:


Dim names = custList.Select(Function(cust) cust.Name")
'becomes:
Dim names = custList.Select(_.Name)```

The `Fn` keyword was actually suggested there by a commenter. However, this was due to parsing concerns, which I later investigated and found to be unfounded.
bandleader commented 5 years ago

This is not a 1:1 duplicate of #256, but I think discussion should nevertheless continue there.

jrmoreno1 commented 5 years ago

@bandleader: while I could probably live with the symbol heavy usage described in #256, I think it’s too unVBish too get adopted. This is a more limited proposal, and I believe it has a slightly better chance of being adopted. Ps I was the commenter that suggested Fn

voronoipotato commented 5 years ago

Has there been any progress on this issue? It's friction point when trying to use the watch window in particular. If Fn is too brief, F# currently uses Fun which would still be an improvement over function while maintaining the VB look and feel as the name is mostly non-idiomatic.

gafter commented 5 years ago

@voronoipotato I am not aware of any progress. Ate you working on a protorype?

voronoipotato commented 5 years ago

I was messing with it yeah. I don't know how far I'll get.

voronoipotato commented 5 years ago

Making an alias like this requires a change to roslyn\src\Compilers\VisualBasic\Portable\Syntax\Syntax.xml is kinda where I am right now.

AnthonyDGreen commented 5 years ago

@voronoipotato I don't think it's an issue of being too brief, per se. At times I'd considered the shorter compromise of just f(x) ...

VB has roots in math actually and that would be fine, it's just more about the value, the precedent, and the ambiguities.

One problem, aside from f being a valid identifier, is that if you parenthesize the expression (no matter what identifier you pick) it becomes a legal invocation again fun(x)(y) actually could just be indexing the result of a call. Similarly if you're combining the feature with a With block you have to be able to distinguish f() .WithBlockProperty and f().ReturnValueProperty. The simple case of f(x) x.ToString() is actually pretty easy. That's never legal code today so.

Having a syntax that conditionally means a lambda but might mean a lambda based on imports is... rough.

I'm curious about why some VB developers prefer using lambdas to using the query comprehension syntax which is comparatively more expressive than in C# (more operators like Take/Skip/Skip While/Distinct, etc) and is almost always shorter?

Also, is this most a bother with LINQ-style methods (Select/Where) specifically or does it bother you as much any time you pass a lambda to any method? So, is this problematic for you:

Private SomeField As New Lazy(Of BigClass)(Function() MakeBigClass(size:=1024, SynchronizationContext.Current))

Could you share some of the ugliest code you've written with the current design?

pricerc commented 5 years ago

I'm curious about why some VB developers prefer using lambdas to using the query comprehension syntax which is comparatively more expressive than in C# (more operators like Take/Skip/Skip While/Distinct, etc) and is almost always shorter?

A couple of thoughts on this:

  1. Because there are very few examples 'in the wild' of how to build and use them query constructs, and so people don't know a) that they can and b) how. I do a lot in the SQL world, and so queries make sense to me, but declarative set-based programming (which is how I see LINQ) requires a mental shift that people probably don't see the need for if they can use lambdas to do the same thing without re-thinking the problem as a set-based problem.

  2. Even in C#, I never see the 'query' style in examples; I only ever see 'fluent' lambda constructs (It's almost as if they didn't really want query syntax and so left out many concepts that VB included). And so anyone translating samples from C# will be getting lambdas, not queries.

pricerc commented 5 years ago

Could you share some of the ugliest code you've written with the current design?

Given my dislike of anonymous methods (in general), having had the pain of debugging event-driven UI code on Windows Mobile; I'm happier to just code a named method.

I'm tempted to suggest that, actually, the current style should be left in its ugly state, specifically to encourage people to use named methods, not anonymous ones. It's not really a lot of work, and if you've ever had to deal with many-layers deep stack trace of anonymous methods, you learn to appreciate the named ones.

bandleader commented 5 years ago

@AnthonyDGreen I'm curious about why some VB developers prefer using lambdas to using the query comprehension syntax

Personally for two reasons:

  1. Readability. Code that is not essential to the goal at hand is (to me) noise and reduces readability. In the query: From cust In context.GetCustomers() Where cust.ID = id, the clause From cust In doesn't do anything but signify to the compiler that we're starting a LINQ query and declare the variable name we're going to use in it. The real intent is context.GetCustomers() Where cust.ID = id. So From cust In is noise.
  2. Parens. LINQ queries always seem to need parens around them to work properly. I could search my code to see where I've had to do this, but one example that comes to mind is when you need to call a method afterwards: context.GetCustomers().Where(Function(cust) cust.ID = id).ToArray() works fine, but in query syntax you need parens: (From cust In context.GetCustomers() Where cust.ID = id).ToArray() ...and I often want to do ToArray() (I get why LINQ was created lazy but often I don't want that), or other further processing.

I can see two approaches to solve this problem:

  1. More concise syntax for lambdas, as requested in this issue and by me in #256 -- this removes the need for query syntax altogether, and also has the benefit of working well for C# too. We would write (for instance) context.GetCustomers().Where(_.ID = id) which is concise and removes all "non-intentful" (noise) code.
  2. Improve query syntax a bit. a. Instead of making us say From cust In, allow the variable name to be implicit -- maybe it like in Swift? e.g. From context.GetCustomers Where it.ID = id This is similar to property Set(value) where the identifier can be omitted (just Set) and defaults to value. This would only work for the vast majority of (my) queries; for the few nested queries and multi-Select queries that I have, I would still specify the variable name. b. As well, perhaps certain clauses (Where, Select, etc.) could turn the left-side expression into a query implicitly, with no From necessary. Then we could just do context.GetCustomers Where it.ID = id Select it.Avatar or similar. I think this was suggested in a separate issue, but becomes much more doable with implicit identifiers as above. I get why it' easier to force a keyword like From though. c. Also, help us with the post-processing stuff -- it would be nice if we could have a clause ToArray or something. context.GetCustomers() Where it.ID = id ToArray is much easier than going back and adding parens: (context.GetCustomers() Where it.ID = id).ToArray() Or alternatively Select Array at the end, but to me that's long. It would be nice to solve this in a more general way, maybe Run clause that ends the query and takes the place of the dot operator? I'm not sure this is a good idea, but maybe it could be refined: context.GetCustomers() Where it.ID = id Run ToArray() context.GetCustomers() Where it.ID = id Run ToList().Count()

I think some of these improvements to query syntax were discussed in previous issues and should be discussed there. I also think that improvements to concise lambda syntax should be discussed in #256.

Thank you

bandleader commented 5 years ago

Given my dislike of anonymous methods (in general), having had the pain of debugging event-driven UI code on Windows Mobile; I'm happier to just code a named method.

@pricerc I was initially extremely surprised that you don't see the value of lambdas (aka anonymous functions) hear you and prefer named methods, but then I saw your other comment

I do a lot in the SQL world, and so queries make sense to me, but declarative set-based programming (which is how I see LINQ) requires a mental shift that people probably don't see the need for if they can use lambdas to do the same thing without re-thinking the problem as a set-based problem.

Do you mean that you aren't used to passing around code, the concept of first-class functions, and using the framework's LINQ methods such as .Select(...), .Where(...), etc.?

voronoipotato commented 5 years ago

Lambdas are great when the syntax isn't too wordy. Underscore accomplishes some of what I want. However because it doesn't cover all of it even if we had it I would still want a non-laborious syntax for lambdas. LINQ queries are fine when they fit what I'm doing but sometimes they don't. There's a reason we need both and one of the most important is that we do things with the Linq methods that we simply would never do with a database query. I very rarely ever use the query style because it's typically further from my mental model of what I'm doing with them. Sure sometimes I'm thinking in a SQL declarative way and LINQ queries are a great fit for that, but other times I'm thinking in terms of types and transformations in a "Map/Filter/Reduce/Bind" way.

bandleader commented 5 years ago

@voronoipotato 100%; I think lambdas need to be fixed even if queries would be painless. I just think lambdas could be better to the extent that query syntax would be much less necessary.

voronoipotato commented 5 years ago

I updated my post because I didn't realize you were reading it 🐋

pricerc commented 5 years ago

Given my dislike of anonymous methods (in general), having had the pain of debugging event-driven UI code on Windows Mobile; I'm happier to just code a named method.

@pricerc I was initially extremely surprised that you don't see the value of lambdas (aka anonymous functions) hear you and prefer named methods, but then I saw your other comment

Yes, my dislike for them comes from painful experience. YMMV, but for me, I've found the initial small overhead of coding an actual method makes for much easier debugging and maintenance later. Note, that it's still a general dislike, and I do use them, especially in the C# work that I do, but pretty much only for 'one-liners'. If it needs more code than I can put on one line, then I will usually refactor it out into its own named method.

I do a lot in the SQL world, and so queries make sense to me, but declarative set-based programming (which is how I see LINQ) requires a mental shift that people probably don't see the need for if they can use lambdas to do the same thing without re-thinking the problem as a set-based problem.

Do you mean that you aren't used to passing around code, the concept of first-class functions, and using the framework's LINQ methods such as .Select(...), .Where(...), etc.?

I think that's a different topic (that I'll come back to). I was responding to the question about why people don't use the query comprehension syntax, and I was suggesting that it was because they don't need to, and that the learning curve is too high for people who don't otherwise 'get' the concept of a declarative query, because a) there are few examples to help someone learn from, and b) there are many examples (mostly in C#) using the things you mention: piped LINQ methods, lambdas and first-class functions.

Conceptually at least, a query comprehension syntax (like SQL or VB's) is the developer telling the compiler what they want done with a set of items, not how to do it. This is a concept that I've seen even very good developers struggle with, because a) they struggle with sets and b) they don't trust a query compiler to 'do the right thing', I think that same psychology also plays a part in why people still use cursors with relational databases. On top of this mental shift, it also requires the developer to use a syntax that is different from the rest of their program.

On the other hand, using piped LINQ methods are the developer explicitly telling the compiler not only what to do, but also exactly how they want it done. And using syntax that looks like the rest of their code.

In theory, different generations of the compiler could implement completely different code for the same query (like automatically turn a simple query into a multi-threaded query), but I'd expect them to always produce (nearly) the same code for piped methods.

Back to responding to your question: I am definitely 'old school'. I did my first formal developer training in 1985 using Turbo Pascal, and learnt COBOL in 1989. And I still have a 3.5" floppy disk somewhere with an X86 assembler code assignment I did for a university course.

So, I don't know that I'm used to anything! But I would say that I'm unconvinced by some of the more modern techniques (and by some of the suggestions on this forum), mostly because I see them adding limited, if any, value to the long-term maintainability of a code base.

My perspective is undoubtedly flavoured by my 'chequered' programming past, and by the line of work I find myself in, where I do much more maintenance of old code than I do writing of new code.

AnthonyDGreen commented 5 years ago

@pricerc,

I get where you're coming from. In Roslyn when I'm debugging code and it's coming from some generic handler in an async method coming from an iterator... it's not the debugging experience I've come to know and love. I don't have any concrete ideas how to fix it though, but I have to hope it can be fixed one day.

pricerc commented 5 years ago
  • Readability. Code that is not essential to the goal at hand is (to me) noise and reduces readability. In the query: From cust In context.GetCustomers() Where cust.ID = id, the clause From cust In doesn't do anything but signify to the compiler that we're starting a LINQ query and declare the variable name we're going to use in it. The real intent is context.GetCustomers() Where cust.ID = id. So From cust In is noise.
  • Parens. LINQ queries always seem to need parens around them to work properly. I could search my code to see where I've had to do this, but one example that comes to mind is when you need to call a method afterwards: context.GetCustomers().Where(Function(cust) cust.ID = id).ToArray() works fine, but in query syntax you need parens: (From cust In context.GetCustomers() Where cust.ID = id).ToArray() ...and I often want to do ToArray() (I get why LINQ was created lazy but often I don't want that), or other further processing.

For me the weird thing here (which I think is addressed in a different proposal), is when I'm feeding that query into a for each loop, I need to do for each cust in (from cust in context.GetCustomers()...)

perhaps your ideas around handling the To<Type> postfix methods could be addressed by extending the proposal around casting using As <Type> (#59) : Dim CustArray = From cust In GetCustomers ... As Array

bandleader commented 5 years ago

@pricerc For me the weird thing here (which I think is addressed in a different proposal), is when I'm feeding that query into a for each loop, I need to do for each cust in (from cust in context.GetCustomers()...)

Also true (though we couldn't reuse cust there anyway -- that identifier is scoped only to the loop -- but the implicit it would help here).

@pricerc Perhaps your ideas around handling the To<Type> postfix methods could be addressed by extending the proposal around casting using As <Type> (#59) : Dim CustArray = From cust In GetCustomers ... As Array

No, because this would only work if: 1) An implicit conversion is defined for IEnumerable<T> and Array<T> -- which IMHO is a very bad idea 2) AND proposal #59 ends up using implicit conversions (à la CType, unlike DirectCast) -- something it should IMHO not do, at least in its basic form

pricerc commented 5 years ago
  • An implicit conversion is defined for IEnumerable<T> and Array<T> -- which IMHO is a very bad idea

Don't you need that for (From cust In context.GetCustomers() Where cust.ID = id).ToArray() ?

  • AND proposal #59 ends up using implicit conversions (à la CType, unlike DirectCast) -- something it should IMHO not do, at least in its basic form

I understood #59 to be the opposite - adopting the C#-style casting as opposed to the VB 'library' casting?

pricerc commented 5 years ago

Also true (though we couldn't reuse cust there anyway

which just made it extra annoying :)

bandleader commented 5 years ago

Don't you need that for (From cust In context.GetCustomers() Where cust.ID = id).ToArray()

No, ToArray is an independent method that iterates through an IEnumerable and puts the result in an array -- not connected to type conversion. (In actuality, the type conversion you suggested would have to call the ToArray method.)

pricerc commented 5 years ago

Don't you need that for (From cust In context.GetCustomers() Where cust.ID = id).ToArray()

No, ToArray is an independent method that iterates through an IEnumerable and puts the result in an array -- not connected to type conversion. (In actuality, the type conversion you suggested would have to call the ToArray method.)

Ok, I see what I did. I was spitballing, and not expanding my thoughts properly.

I was thinking more that 'concept of' the proposal could be 'extended' so that, when using the query syntax, you could As Array instead of ToArray(). Not so much that it be used as a casting operator (although conceptually, it kind-of is). And I suppose this could be extended to any of the LINQ To methods.

Dim newArray = From c In Customers where c.IsActive Order By c.Name Select c As Array
Dim newList = From c In Customers where c.IsActive Order By c.Name Select c As List

(I can already see some holes in there, but hopefully the idea makes sense.)

bandleader commented 5 years ago

@pricerc Got it. Interesting idea. Yeah, I can see a hole: it would be unclear what to do if the type was already an array or list. A casting/conversion operator would be expected to return the same object, whereas current IEnumerable<T>.ToArray() (or .ToList()) iterates through the collection again, creating a copy (and potentially re-executing side-effects).

In any case, I don't see the advantage that this (making a special case for As T casting) has over the other, arguably much simpler way (making a special clause for query syntax). (And outside of query syntax, we can just use ToArray etc. and don't need a new feature.) I don't mind if the the query syntax feature uses the As keyword; sure.

Thanks for your feedback.

I do think that we should really move discussions surrounding lambda syntax to #256, and discussions surrounding improving the query syntax to a new issue. I can't seem to find another issue that discusses query syntax, except for #104 (incidentally -- the need for #104 would be obviated by my suggestions here). Should I create a new issue for that, @AnthonyDGreen?

pricerc commented 5 years ago

it would be unclear what to do if the type was already an array or list.

Ahh, but isn't the type of (From ...) always IEnumerable ?

I think you saw the problem in a different way than I did.

I was thinking more that it is ambiguous whether the As <Type> clause is applying to the whole query or to the Selected item; it's a parenthetical question, that I'm sure could be addressed:


Dim newList = (From c In Customers where c.IsActive Order By c.Name Select c) As List
' or
Dim newList = From c In Customers where c.IsActive Order By c.Name Select (c As List)

Happy to move the discussion, but thought it would be helpful to have this response in this thread.

bandleader commented 5 years ago

@pricerc If you support #59, that we use As T for casting, in all normal expressions around VB, and you also propose what you did above that As Array and As List introduce the special behavior of calling ToArray or ToList, then you introduce inconsistent behavior (As T is a cast, equivalent to DirectCast(..., T), whereas As Array is a call to ToArray which creates a copy and introduces side effects, unlike DirectCast).

If you don't support #59, and are instead proposing that As List / As Array work only in queries, and not for general casting in normal expressions around VB, then you don't have that problem, but in that case To Array / To List seem more natural to me than As Array / As List. (Might still conflict somewhat with #25)

pricerc commented 5 years ago

@bandleader

I don't think it would need to be inconsistent, although I guess that might depend on what kind of consistency you like (which I've learnt is also subjective!).

My main concern is around consistency of experience, in which I think VB has an advantage over C#.

Let me explain.

One of the little things I like about VB, is that it uses parentheses for both method calls and indexes/indexers. To me, this is 'consistent', because as a high-level programmer, there is no practical difference between them - I'm asking an object to give me some data based on some parameter. e.g.:

x = SomeCollection[i];
x = SomeCollection(i);

which one of these is correct depends on what SomeCollection is, and how it's implemented, and I think that's an unhelpful distinction that we've avoided in VB - a compiler is quite smart enough to tell the difference, there's no need for a human to, and so VB offers consistency of experience.

In this thread, we've been talking about converting the output of a query expression from IEnumerable into a 'concrete' collection (List, Array, etc).

You're suggesting that there is a necessary distinction of experience between this, and converting an instance of an object to some other type. I would argue that there is a case to be made that both of these are "Type Conversion" constructs, and that they are deserving of the same consistency of experience that we have for indexes and methods parameters.

Note: at this point, I'm not arguing the merits of the case, just saying there's a case to be made. I might go and ask the question in #59.

AdamSpeight2008 commented 5 years ago

@AnthonyDGreen
Imagine if we could alias generic delegates / functions with variable args.

Imports f = Func(Of ... )

Then we could then use xs.Where(f(x) x Mod 2 == 0)

AdamSpeight2008 commented 5 years ago

@bandleader Think of .ToArray , ToList as get the answer(s) now.

bandleader commented 5 years ago

My main concern is around consistency of experience.

@pricerc I assume you mean that it's appropriate to do implicit type conversions from enumerables to arrays/lists, because VB developers do indeed conflate "iterating through an enumerable and creating an array/list" with "casting from an enumerable to an array/list."

My problem is that .ToList() also exists on existing arrays/lists (since they implement IEnumerable), and here they naively treat the list as an enumerable, and iterate through the whole thing, and create and return a new list.

As List would therefore be inconsistent with .ToList(), which in my opinion is confusing.

'Currently:
Dim someQuery = From cust In context.GetCustomers() Where cust.Active Select cust.ID
Dim origList = someQuery.ToList()
'The next line returns the same list (just like DirectCast/CType). Referential equality is preserved.
Dim list1 = origList As List
Debug.Print(list1 Is origList) 'TRUE
'The next line iterates over the list and creates a NEW list, somewhere else in memory.
Dim list2 = origList.ToList()
Debug.Print(list2 Is origList) 'FALSE

While I can't speak for all VB developers, I personally don't think of .ToList() as a cast at all, and would not want an implicit type conversion. Moreover, I think that "a type conversion that potentially executes side effects" is always an anti-pattern.

bandleader commented 5 years ago

@pricerc I posted this more succinctly in #59 -- please continue any As List discussion there. I'll still leave my reply here up for now, as it responds more directly to your post.

Thank you!