-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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: Method Cascading, or the .. operator #8947
Comments
Your Other than that, I can certainly see the value in this proposal, and it shouldn't be significantly hard to implement I think, since the compiler could emit the same kind of calls as for an Object Initializer. |
I often write my APIs to follow this kind of convention. However, baking it into the language so that it happens to work for types where it has not been explicitly designed I think is a bad idea. For example, the following would not return what the user expected: string foo = "Hello World!";
string bar = foo.Replace("Hello", "Goodbye")
..Replace("World", "Nurse");
Debug.Assert(bar == "Goodbye Nurse!"); // nope, it's "Hello Nurse!" The compiler could warn/fail if the method being chained returns something other than As for how you're attempting to chain property assignments or event subscriptions, that I can't see working due to the whitespace in the assignment. What exactly would |
Is it particularly cumbersome to write a fluent API in C# right now? If a type is useful with one, then it likely already has such methods or you could easily make your own extension methods. JQuery is a poor example- JavaScript has no such To me this seems like a solution in search of a problem. |
Aren't you really proposing another form of |
Yes. And trying to add features around it (like you normally would do via dynamic dispatch and other OOP mechanisms) can be much more difficult than a non-fluent version of the API. Because of this, I now regret utilizing a fairly popular fluent C# library over a non-fluent one. But I don't know that small language improvements will make it substantially less cumbersome. Fluent APIs are a symptom that C# doesn't support embedded DSLs to the degree that is needed. If we want to embed DSLs, we should be able to really embed them with full compiler and tools support. Something like this, which I also mentioned here: var toc = outline {
Table of Contents
1. Before you begin
2. Introduction
3. Compiler concepts
a. Lexer
b. Parser
c. Checker
d. Emitter
4. Creating your own language
a. Design
b. Optimization
c. Tooling
}; where |
Head meets the desk. |
^LOLzed about comment above I think, that the Proposal is wonderful, but it is in my view and extended |
Another real-life example I stumbled upon today: Thread StartBackgroundThread(ThreadStart threadStart, string name = null)
{
var thread = new Thread(threadStart) { Name = name, IsBackground = true };
thread.Start();
return thread;
} And with Thread StartBackgroundThread(ThreadStart threadStart, string name = null)
{
return new Thread(threadStart) { Name = name, IsBackground = true }
..Start();
} |
HaloFour pointed out a need for clarification for when the var result = Foo.MakeAFoo()..Bar="Hello"..Baz=1234.ToString(); First, the
Edit: Seems I was wrong about the whitespace. Thanks @HaloFour. |
Replying here since the other issue is closed.
What defines a "level"? The top-most expression? If you couldn't use this operator within an existing expression that would kill much of the use of it. Otherwise this seems like just a very hobbled form of
Incorrect. The following is perfectly legal C#, although the formatter in VS will fight you. string s1 = 1234.ToString();
string s2 = 1234. ToString();
string s3 = 1234 .ToString();
string s4 = 1234 . ToString();
string s5 = 1234
.ToString();
string s6 = 1234.
ToString();
string s7 = 1234
.
ToString(); |
@HeloFour by "the first expression" I mean the first expression on which the first Regarding the whitespace issue I was incorrect. However we can use curly braces to cover both use cases: // .Baz is assigned "1234", result is Foo.MakeAFoo()
var result = Foo.MakeAFoo()..Bar="Hello"..Baz=1234.ToString();
// .Baz is assigned 1234, result is Foo.MakeAFoo().ToString()
var result = Foo.MakeAFoo(){..Bar="Hello"..Baz=1234}.ToString();
// same as above, maybe a bit more readable
var result = Foo.MakeAFoo(){ ..Bar = "Hello" }{ ..Baz = 1234 }.ToString();
// same as above, maybe a bit more readable
var result = Foo.MakeAFoo(){ ..Bar = "Hello", ..Baz = 1234 }.ToString(); It even looks a bit like the object initializer. The operator is usable with any existing expression. var form = (someCondition ? form1 : form2)
..Text = "Best Form"
..TopLabel.Text = "Best Label"; |
I wonder if the operator could also be used to create shorter lambdas. For example This could be used for easier to read Linq queries: var topStudentNames = students
.OrderByDescending(student => student.Grade)
.Take(5)
.Select(student => student.Name); Would become: var topStudentNames = students
.OrderByDescending(..Grade)
.Take(5)
.Select(..Name); And it could work equally well with the null-checking version: var dogOwners = dogs.Select(?..Owner); Alternatively, all Func<Dog, Person> ownerGetter = ..Owner;
Func<Dog, Dog> winnerMarker = { ..IsWinningDog = true };
Func<Dog, bool> ageCheck = { .Age > 7 };
// expanded versions
Func<Dog, Person> ownerGetter = dog => dog.Owner;
Func<Dog, Dog> winnerMarker = dog => { dog.IsWinningDog = true; return dog; };
Func<Dog, bool> ageCheck = dog => { return dog.Age > 7; };
// when appended to an expression they run and return the original value
var dog1 = getWinningDog()..IsWinningDog = true;
var dog2 = getWinningDog() { ..IsWinningDog = true }; // same
var dog3 = getWinningDog() { .IsWinningDog = true }; // same
var dog4 = getWinningDog() { winnerMarker }; // same This would also probably work nicely with immutable structs allowing for short copy-and-change expressions. |
I accidentally closed this issue a week ago by mistake. I reopened it soon after, but I haven't seen any comments since. How can I check that the issue is indeed open? |
It shows Open at the top. |
@bondsbw Got it, thanks. |
This example of an unexpected result provided by @HaloFour string foo = "Hello World!";
string bar = foo.Replace("Hello", "Goodbye")
..Replace("World", "Nurse");
Debug.Assert(bar == "Goodbye Nurse!"); // nope, it's "Hello Nurse!" Pretty much makes this a nonstarter for me. Is anyone not going to be confused by this? Imagine the interactions with LINQ where an extra |
This proposal trades little value for a pretty hefty cost; readability actually suffers in most of the examples I've seen. For instance:
vs.
Sure the second one is smaller, but I'm forced to stare a bit harder at it to comprehend what's happening. Readability trumps writability every time. My preference would be something like a 'with' statement; it more clearly signals what is going on. But, in either case, I'm not sure there's enough evidence to suggest there'd be a significant improvement in readability. In other words I'd rather declare a variable and use it repeatedly as the LHS of method invocations. It's not much more verbose and it's well understood and clearly readable.
That's not so bad, right? |
@rsmckinney I can understand your concern, but I actually find the proposal more readable. |
With language design, smaller is notoriously mistaken for more concise. In my view this feature falls into that category. Either way my point here is there doesn't appear to be enough value from this operator to justify adding it to the language. First, I'm not convinced the frequency of use is there. How often would the average programmer use this? My guess is pretty infrequently, probably on the order of switch statements, thus a strong candidate for stackoverflow hits especially for readers of this syntax. Above all, in my view the code is less readable, others may disagree, so let's say there is no gain or loss in readability, a lateral change. So why complicate the language with another member access operator? There's no performance gain, no readability improvement, just a tad smaller code to write. The math is not in favor. All this said, it's still an interesting idea! |
A good point. Ever tried searching for |
Dart lang implemented the cascading operator identically to this proposal.
You could ask the same for constructor initialization syntax... I'm in favor of this suggestion - it's simple, useful & elegant. |
Language design issues are now in csharplang. I'll go ahead and close. |
Method cascading operators are already used in languages such as Dart. Many examples here are taken from their documentation.
Wiki page: https://en.wikipedia.org/wiki/Method_cascading
Usually, fluent interfaces rely on method chaining. Say you want to add a large number of elements to a list:
You might want to write this as
but this requires that Add() return the receiver, myTokenTable, instead of the element you just added. The API designer has to plan for this, it won't work for existing libraries, and it may conflict with other use cases. With cascades, no one needs to plan ahead or make this sort of tradeoff. The Add() method can do its usual thing and return its arguments. However, you can get a chaining effect using cascades:
Here, ".." is the cascaded method invocation operation. The ".." syntax invokes a method (or setter or getter) but discards the result, and returns the original receiver instead.
Another example (edit: added parenthesis to eliminate ambiguity):
The success of frameworks like jQuery show that method call chaining is easy to learn, intuitive and makes for easier to read code.
If we had a
struct TimeMarker { DateTime Time; event Action Moved; }
class, and wanted to create a modified clone of an existing marker, we would have to do something like:Which is quite long. We could do it in one line using the object initializer syntax:
But this sets the
Time
variable twice, and doesn't really make it any easier to read. The method cascading operators would allow chaining calls:This seems especially useful for creating form components and assigning event listeners at the spot of creation, instead of on the lines after.
Companion operators
?..
and?..[]
could also be added for safe access:Even more examples of use cases here: http://news.dartlang.org/2012/02/method-cascades-in-dart-posted-by-gilad.html
The text was updated successfully, but these errors were encountered: