Skip to content

Augmentation ordering dependence with "with" and "implements" #4337

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

Closed
munificent opened this issue Apr 25, 2025 · 6 comments
Closed

Augmentation ordering dependence with "with" and "implements" #4337

munificent opened this issue Apr 25, 2025 · 6 comments
Labels
augmentations Issues related to the augmentations proposal. question Further information is requested

Comments

@munificent
Copy link
Member

I'm working on simplifying augmentations now that we don't need to support full macro generality.

My recollection from a previous language meeting is that we want eliminate augmented and allowing an augmentation to wrap existing code. The goal is to make it so that an introductory declaration and all of its augmentations form a non-overlapping union of disjoint declarations. That way, we don't have to define any sort of augmentation application order.

Disallowing an augmentation from replacing/wrapping an existing (non-"augmentation abstract") member covers much of this.

But augmentations are also allowed to append to a type declaration's with and implements clauses. I'm not sure if the order of types in an implements clause is user-visible, but the order of mixins in a with clause certainly is if they happen to override the same instance member.

Any thoughts on how we should handle this in augmentations? We do know that some code generators have use cases for adding to the implements and with clauses, so disallowing augmentations from touching those entirely is probably too restrictive.

@munificent munificent added augmentations Issues related to the augmentations proposal. question Further information is requested labels Apr 25, 2025
@rrousselGit
Copy link

A tad off topic but:

My recollection from a previous language meeting is that we want eliminate augmented and allowing an augmentation to wrap existing code. The goal is to make it so that an introductory declaration and all of its augmentations form a non-overlapping union of disjoint declarations. That way, we don't have to define any sort of augmentation application order.

Is there a dedicated issue about this? Because honnesty if my understanding is correct, the value of augmentation libraries would significantly decrease.

I feel like one of the core value of augments is patterns such as:

@log
int fn(int arg) => arg * 2;

...

augment int fn(int arg) {
  print('Calling fn');
  final result = augmented();
  print('Returned $result');
  return result;
}

And same thing with encapsulating fields into getters/setters.

If we can't do that, I don't see the point in having augmentations tbh.

@munificent
Copy link
Member Author

A tad off topic but:

My recollection from a previous language meeting is that we want eliminate augmented and allowing an augmentation to wrap existing code. The goal is to make it so that an introductory declaration and all of its augmentations form a non-overlapping union of disjoint declarations. That way, we don't have to define any sort of augmentation application order.

Is there a dedicated issue about this?

I don't think so. #4256 is the general issue for simplifying the feature.

Because honesty if my understanding is correct, the value of augmentation libraries would significantly decrease.

Yes, not allowing wrapping members does reduce the expressiveness of the feature.

If we can't do that, I don't see the point in having augmentations tbh.

Looking around at some of the most widely used code generators, I don't actually see many uses of wrapping existing code. It's mostly adding new declarations and new members. So an augmentation feature that didn't let you wrap existing code would be less expressive, but still seems to cover many use cases.

@rrousselGit
Copy link

Looking around at some of the most widely used code generators, I don't actually see many uses of wrapping existing code. It's mostly adding new declarations and new members. So an augmentation feature that didn't let you wrap existing code would be less expressive, but still seems to cover many use cases.

That's a chicken-and-egg problem.
Parts don't let you wrap existing code, so you won't find existing code-generators doing it. They have to find a workaround.

I certainly came accross many use-cases for code wrapping over the years. Although many of which are just difficult to implement today.


Ultimately, what are we trying to solve here?
Because if augmentations don't enable anything new and just change a little bit the generated code, why do we need them?

For instance, Dart will most likely get static extensions at some point with how popular the feature request is.
So to add a new constructor to a class, chances are we could do:

static extension on MyClass {
  factory MyClass.fromJson(Map json) {
    ...
  }
}

extension on MyClass {
  Map toJson() => ...
}

And if static extensions can replace the synthetic MyClass() with a non-synthetic one like so:

class Foo {
  // Implicit Foo() ctor
}

static extension on Foo {
  factory Foo(); // Explicitly define the default ctor
}

Then we could support data classes purely with extensions:

@data
abstract class Person {
  String get name;
  int get age;
}

// Generated
static extension on Person {
  factory Person({required this.name, required this.age}) = _PersonImpl;
}

class _PersonImpl extends Person {
  _PersonImpl({required this.name, ...});
  final String name;
  final int age;
}

extension on Person {
  Person copyWith({...}) => ...
}

That would be enough to remove a lot of _$Foo

There would be some corner cases. But that's no different than with augmentations as currently discussed ; which would also be limited in some aspects.
And if those limitations are too limited, new Dart features could be helpful too. For example, some people requested traits

So to double down: What is the purpose of augments if we can't wrap a member/declaration? :)

@munificent
Copy link
Member Author

Looking around at some of the most widely used code generators, I don't actually see many uses of wrapping existing code. It's mostly adding new declarations and new members. So an augmentation feature that didn't let you wrap existing code would be less expressive, but still seems to cover many use cases.

That's a chicken-and-egg problem. Parts don't let you wrap existing code, so you won't find existing code-generators doing it. They have to find a workaround.

When I looked at existing uses of code generators, I tried to figure out what augmentation features they would use if those features existed. You're right that there are probably code generators that don't exist at all because the capabilities to enable them aren't there.

Ultimately, what are we trying to solve here? Because if augmentations don't enable anything new and just change a little bit the generated code, why do we need them?

We're trying to improve the user experience of packages that provide or use code generators.

For instance, Dart will most likely get static extensions at some point with how popular the feature request is.

Personally, I don't think static extensions are a great fit for adding what should feel like "real" members onto a class.

So to double down: What is the purpose of augments if we can't wrap a member/declaration? :)

I think we're trying to figure out what the right scope for the feature is so we can finish it off and ship it. Without macros, it doesn't need to be as maximal as it was originally designed. It's a really complex feature. So we're trying to figure out if there is a way to scope it down such that users still get most of the most useful functionality at a fraction of the implementation cost on our end.

@rrousselGit
Copy link

My feeling is that the discussions regarding augmentations are backward. Usually, a problem is expressed. Then various proposals to solve that issue are made.
But here, it feels like we have a feature that was already worked on, and we're trying to retrofit it into the language while trying to guess if it'll improve the language or not.

It doesn't sound like there's a clear list of problems we're trying to solve.


If we're looking at solving codegen pain points, I'd say that wrapping top-level functions and existing class members is definitely an important one.

For starter, I write code-generators as a workaround to limitations in language/
Anything that Dart doesn't allow us or is really inconvenient is a possible target for code-gen.

One of such problems is function composition. Think middlewares, dependency-injection to inject a parameter, function currying, ...
#4281 is one of the features I wish Dart had the most, if I had to pick. Yet codegen struggles quite a bit to solve that problem.

The initial augmentation proposal at least solved some of it.
We could write:

@log
@myMiddleware
void fn() {}

Although it wouldn't solve currying/DI like:

@widget
Widget example(BuildContext context) => ...

void main() {
  runApp(example()); // The `context` parameter was injected by @widget
}

With this discussion, it sounds like even the @log/@middleware wouldn't be doable.

@munificent
Copy link
Member Author

We talked about this on the language team and we're OK with continuing to specify augmentation application order, so I can go ahead and close this.

The somewhat off-topic discussion about augmentations in general is a good one. It's probably best to continue it on #4256. Though, for what it's worth, we're still leaning towards eliminating augmented. It's not that it's not useful, it's just that we're trying to focus our effort on features that are probably more immediately impactful for users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
augmentations Issues related to the augmentations proposal. question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants