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: Method Cascading, or the .. operator #8947

Closed
manixrock opened this issue Feb 19, 2016 · 22 comments
Closed

Proposal: Method Cascading, or the .. operator #8947

manixrock opened this issue Feb 19, 2016 · 22 comments

Comments

@manixrock
Copy link

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:

myTokenTable.Add("span");
myTokenTable.Add("div");
myTokenTable.Add("blockquote");
myTokenTable.Add("p");
myTokenTable.Add("small");
// ... many more here ...
myTokenTable.Add("blink");

You might want to write this as

myTokenTable
    .Add("span")
    .Add("div")
    .Add("blockquote")
    .Add("p")
    .Add("small")
    // ... many more here ...
    .Add("blink");

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:

myTokenTable
    ..Add("span")
    ..Add("div")
    ..Add("blockquote")
    ..Add("p")
    ..Add("small")
    // ... many more here ...
    ..Add("blink");

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):

String s = (new StringBuilder()
    ..Append('There are ')
    ..Append(beersCount)
    ..Append(' beers on the wall, ')
    ..Append(beersCount)
    ..Append(' number of beers...')
    ).ToString();

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:

TimeMarker createMarkerAhead(TimeMarker marker, TimeSpan ahead, Action movedAction) {
    var aheadMarker = new TimeMarker(marker);
    aheadMarker.Time += ahead;
    aheadMarker.Moved += movedAction;
    return aheadMarker;
}

Which is quite long. We could do it in one line using the object initializer syntax:

TimeMarker createMarkerAhead(TimeMarker marker, TimeSpan ahead, Action movedAction) {
    var aheadMarker = new TimeMarker(marker) { Time = marker.Time + ahead };
    aheadMarker.Moved += movedAction;
    return aheadMarker;
}

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:

TimeMarker createMarkerAhead(TimeMarker marker, TimeSpan ahead, Action movedAction) {
    return new TimeMarker(marker)
        ..Time += ahead
        ..Moved += movedAction;
}

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:

var topStudent = topStudents.FirstOrDefault(student => student.Grade >= 8.5)
    ?..Grant += 1000;

watchedEpisodes
    ?..[1] = true
    ?..[4] = true
    ?..[7] = true;

Even more examples of use cases here: http://news.dartlang.org/2012/02/method-cascades-in-dart-posted-by-gilad.html

@Joe4evr
Copy link

Joe4evr commented Feb 19, 2016

Your StringBuilder example is flawed, as the .Append() and .AppendLine() methods already return the StringBuilder instance to provide a fluent API.

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.

@HaloFour
Copy link

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 void. It could potentially do the same if the method returns a different type, however I would be concerned about situations where you start with a concrete type and the methods return interfaces that represent the affected values. I don't think that cascades fit into the currently push towards more immutability.

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 foo.bar = baz.fizz..buzz(); do? That syntax just looks unattractive and unintuitive.

@MgSam
Copy link

MgSam commented Feb 21, 2016

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 .. feature- it's written the old fashioned way; by returning this after each call.

To me this seems like a solution in search of a problem.

@vladd
Copy link

vladd commented Feb 22, 2016

Aren't you really proposing another form of with operator?

@bondsbw
Copy link

bondsbw commented Feb 22, 2016

@MgSam

Is it particularly cumbersome to write a fluent API in C# right now?

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 outline invokes some custom DSL toolchain on the supplied block.

@alrz
Copy link
Member

alrz commented Feb 22, 2016

Head meets the desk.

@Unknown6656
Copy link

^LOLzed about comment above

I think, that the Proposal is wonderful, but it is in my view and extended with-operator (which C# is of course missing)...
I would -however- like to see an other operator token than .. -- Maybe some sort of pipe? Or other symbols/tokens, which are (not yet) so much in use, like §, ¯, \, ::, etc.

@manixrock
Copy link
Author

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 .. operator:

Thread StartBackgroundThread(ThreadStart threadStart, string name = null)
{
    return new Thread(threadStart) { Name = name, IsBackground = true }
        ..Start();
}

@manixrock
Copy link
Author

HaloFour pointed out a need for clarification for when the .. operator is used without whitespace. For example, let's consider the following expression:

var result = Foo.MakeAFoo()..Bar="Hello"..Baz=1234.ToString();

First, the .. operator should not be nestable, so multiple usages of it will always apply to the first expression.

Second, C# is not whitespace insensitive. For example 1234.ToString() is a valid expression, while the expression 1234 .ToString() is not (notice the space before the dot). So the presence or absence of whitespace between 1234 and .ToString() determines whether it applies to 1234 or the root expression.

Edit: Seems I was wrong about the whitespace. Thanks @HaloFour.

@HaloFour
Copy link

@manixrock

Replying here since the other issue is closed.

The .. operator should not be nestable so there's only even one level of it, so any subsequent usages of .. will apply to the same expression as the previous one, eliminating any ambiguity.

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 with.

For example 1234.ToString() is a valid expression, while 1234 .ToString() is not (notice the space before the dot).

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();

@manixrock
Copy link
Author

@HeloFour by "the first expression" I mean the first expression on which the first .. is used.

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";

@manixrock
Copy link
Author

I wonder if the operator could also be used to create shorter lambdas. For example ..Bar without any assignment would translate to a => a.Bar.

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 .. expressions would return a Func if we introduce single-argument {}-delimited lambdas with .Abc returning the field Abc of the only argument.

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.

@manixrock manixrock reopened this Feb 27, 2016
@manixrock
Copy link
Author

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?

@bondsbw
Copy link

bondsbw commented Mar 5, 2016

It shows Open at the top.

@manixrock
Copy link
Author

@bondsbw Got it, thanks.

@aluanhaddad
Copy link

aluanhaddad commented Jun 22, 2016

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 . was added by accident when someone was breaking a method chain across multiple lines...

@rsmckinney
Copy link

rsmckinney commented Aug 19, 2016

This proposal trades little value for a pretty hefty cost; readability actually suffers in most of the examples I've seen. For instance:

    var aheadMarker = new TimeMarker(marker) { Time = marker.Time + ahead };
    aheadMarker.Moved += movedAction;
    return aheadMarker;

vs.

    return new TimeMarker(marker)
        ..Time += ahead
        ..Moved += movedAction;

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.

    var am = new TimeMarker(marker);
      am.Time += ahead
      am.Moved += movedAction;
    return am;

That's not so bad, right?

@Unknown6656
Copy link

@rsmckinney I can understand your concern, but I actually find the proposal more readable.
I would suggest some sort of pipe-symol instead of .., as this would add more intuitiveness (but it should not be confused with function-piping).
I think the with-keyword would be the best alternative.....

@rsmckinney
Copy link

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!

@bondsbw
Copy link

bondsbw commented Aug 22, 2016

a strong candidate for stackoverflow hits

A good point. Ever tried searching for .. on SO, Google, or Bing?

@iDaN5x
Copy link

iDaN5x commented Jun 9, 2017

Dart lang implemented the cascading operator identically to this proposal.
I could only find one related question in StackOverflow, which suggests it is not that confusing after all.

With language design, smaller is notoriously mistaken for more concise.

You could ask the same for constructor initialization syntax...

I'm in favor of this suggestion - it's simple, useful & elegant.

@jcouv
Copy link
Member

jcouv commented Oct 22, 2017

Language design issues are now in csharplang. I'll go ahead and close.
dotnet/csharplang#781 seems to be the most related.

@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