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

Nested cascades. #3043

Open
lrhn opened this issue May 4, 2023 · 8 comments
Open

Nested cascades. #3043

lrhn opened this issue May 4, 2023 · 8 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented May 4, 2023

It's not possible to nest cascades, because the .. always continues the outer cascade, and there is no delimiter. That is, you cannot do:

  new Cons()
    ..car = 42
    ..cdr = Cons()
      ..car = 7
      ..cdr = Nil();

and have the last two cascacdes apply to the second Cons().

So, what if we allow you to use more dots!

  new Cons()
    ..car = 42
    ..cdr = Cons()
      ...car = 7
      ...cdr = Nil();

Every time you need to nest, you just use a cascade operator with one more dot. There is no upper limit (readability becomes an issue for other reasons before you reach six-seven dots, where it probably starts being hard to count).

We can either allow any larger number of dots, so you can do a..b....c and skip using three dots, or we can require always incrementing by one.

The most pleasant is probably to allow any increment, so changing the outermost .. to a single . when removing the next-to-last member access, won't immediately make the rest of the expression invalid.
We can always provide infos and quick-fixes to canonicalize the number of dots. Or perhaps even to increment the number in anticipation of adding an extra level at the outside.

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label May 4, 2023
@leafpetersen
Copy link
Member

I have to say that the kotlin scope functions really do work nicely for this. Your code above would look something like:

new Cons().apply {
  car = 42;
  cdr = Cons().apply {
    car = 7;
    cdr = Nil();
  }
}

@Kayes-Islam
Copy link

In C# we would do:

new Cons 
{
  Car = 42;
  Cdr = new Cons 
  {
    Car = 7;
    Cdr = new Nil();
  }
}

Very close to a JSON. If we had something close to this, I'd totally use a dart configuration file for my dartlang projects over a JSON/YAML.

@munificent
Copy link
Member

It's not possible to nest cascades, because the .. always continues the outer cascade, and there is no delimiter.

There is a delimiter, you just have to put it in a totally unintuitive place:

new Cons()
  ..car = 42
  ..cdr = (Cons()
    ..car = 7
    ..cdr = Nil());

I see code internally like this surprisingly often even though it's not what I would consider readable.

I would be delighted to have better support for nested cascades. But not like what you propose here. :)

If I recall, @jacob314's original proposal was:

new Cons().{
  car = 42,
  cdr = Cons().{
    car = 7,
    cdr = Nil()
  }
}

That looks pretty nice to me. I could see us maybe using parentheses instead of braces.

Here's an example I found in the wild today:

return AppendReq()
  ..proposedMessage = (AppendReq_ProposedMessage()
    ..id = (UUID()..string = event.uuid.uuid)
    ..data = event.data
    ..customMetadata = event.metadata
    ..metadata.addAll({
      Metadata.Type: event.type,
      Metadata.ContentType: event.contentType,
    }));

With your proposal, I think that would be:

return AppendReq()
  ..proposedMessage = AppendReq_ProposedMessage()
    ...id = UUID()....string = event.uuid.uuid
    ...data = event.data
    ...customMetadata = event.metadata
    ...metadata.addAll({
      Metadata.Type: event.type,
      Metadata.ContentType: event.contentType,
    });

With Jacob's syntax, it would be:

return AppendReq().{
  proposedMessage = AppendReq_ProposedMessage().{
    id = UUID().{ string = event.uuid.uuid },
    data = event.data,
    customMetadata = event.metadata,
    metadata.addAll({
      Metadata.Type: event.type,
      Metadata.ContentType: event.contentType,
    })
  }
};

@jakemac53
Copy link
Contributor

Note that the new package:checks relies heavily on cascades and would also benefit from something here

@lrhn
Copy link
Member Author

lrhn commented May 5, 2023

The example here actually can be done with parentheses, because it creates a new object.
If it was just writing existing objects, that wouldn't work:

foo
  ..bar1
    ...baz = 42
    ...qux = 37
  ..bar2
    ...baz = 87
    ...qux = 117;

This one cannot be helper with parentheses.

@eernstg
Copy link
Member

eernstg commented May 8, 2023

@leafpetersen wrote:

I have to say that the kotlin scope functions really do work nicely for this.

Right. I'm tempted to mention that we do have a Dart proposal, anonymous methods, with a similar purpose and appearance:

new Cons()..{
  car = 42;
  cdr = Cons()..{
    car = 7;
    cdr = Nil();
  }
}

These approaches differ in a few ways:

The Kotlin functions accept a lambda as an argument. It may or may not be possible to inline both the scope function and the lambda such that the construct as a whole is just as fast as the statements in the body/bodies. Anonymous methods are designed such that this kind of inlining is definitely possible.

Some of the Kotlin scope functions return the so-called context object (that's the receiver of the scope function invocation, like new Cons() above), namely apply and also. Others return the result returned by the lambda invoked on the context object: let, run, and with. With anonymous methods, this choice is made by using .. to return the receiver (as in the examples above) and . to return the result returned from the block of code.

var theCons = Cons()..{ car = 1; cdr = Nil(); print('Hello!'); }; // We just want the side effects.
var theBlockResult = StringBuffer('').{ write('Hello, '; write('world!'); return toString(); }

Note that it is in line with other parts of Dart to use Cons()..{ /*some code*/ } to yield the fresh instance of Cons(), and Cons().{ /*some code*/ } to yield the result returned by /*some code*/. Also, of course, we can use ?. and ?.. if needed, again with the same semantics for ?. and ?.. as in regular method invocations.

Some of the Kotlin scope functions provide access to the context object using the keyword this (which may be omitted, as in instance member blocks), namely run, with, and apply; others provide access to the context object as the formal parameter it (which is declared by default when no parameters are specified, similar to this proposal). This distinction is made based on the context type of the lambda (the context type may be a function type of the kind whose argument is accessed as this, or it may be a regular function type, and regular functions have an argument named it whose type is inferred from the context type).

Anonymous methods by default use this to access the receiver ('context object'), which means that we don't need to look up the parameter type of a scope function in order to determine whether it is this or it. (I tend to think it's a little bit dangerous to change the scope rules of the lambda so radically, based on the parameter type of a different function). Anonymous methods also allow an explicit declaration of the parameter, so we can do Cons()..(it){ it.car = 1; it.cdr = Nil(); } if needed (for instance in order to avoid name clashes when several anonymous methods are nested).

Of course, Kotlin scope functions are more general than Dart anonymous methods, because you can write your own scope functions to do whatever you want. However, I think the above remarks support the claim that the anonymous methods mechanism can easily do the things that the Kotlin community have expressed using a specific well-known set of scope functions.

@jamesderlin
Copy link

I dislike the "more dots" approach because it looks like the spread operator and IMO would hurt readability.

@bsutton
Copy link

bsutton commented Sep 25, 2023

Just came looking for a solution to this problem and having read all of the alternate syntaxes, the original proposed by @lrhn reads the cleanest.
It is the only one that you read and go 'yes I know exactly what that is doing'. The rest I had to stop and parse to understand what was going on.

I don't agree that this will cause any confusion with the spread operator as the context is always different.

a = [...listOfDependencies];

a =  Pubspec()
   ..dependencies
      ...append(x)
      ...append(x)
   .dependencies;

a = [...listOfDependencies, Pubspec()
     ..dependencies
         ...append(x)
         ...append(x)
     .dependancies]

That last line got away from me but now imagine if you had to add '{''s into the line.

The core point is, that even though I used the 'new' cascade operator in an array, it was still clear what was going on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

8 participants