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

Enhancing Annotations and Macros #4238

Open
RohitSaily opened this issue Jan 25, 2025 · 17 comments
Open

Enhancing Annotations and Macros #4238

RohitSaily opened this issue Jan 25, 2025 · 17 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@RohitSaily
Copy link

RohitSaily commented Jan 25, 2025

Proposal: Enhancing Annotations in Dart with Postfix and Expression Syntax

Annotations, including macros, are powerful tools for ensuring systems are robust and for code generation. However, their current usage is limited by where they can be applied in the code. This proposal aims to remove that limitation while maintaining backward compatibility and promoting a consistent and deterministic coding style.

Overview of Changes

Annotations in Expressions

Consider a scenario where there is a performance bottleneck in your code, and you want to optimize a function, c, by inlining it or using an annotation to alter its behavior—such as disabling runtime checks. The function is widely used across the codebase, but the performance-related alteration should only apply at the bottleneck.

Currently, the closest approach to achieving this is:

@noRuntimeChecks final cResult = c();
a(b(cResult));

This requires a verbose multi-line expression to specify the annotation. A more concise and readable solution would be to place the annotation directly within the function call:

a(b(@noRuntimeChecks c()));

Even better, we could place the annotation after the function call to indicate that it's secondary to the implementation logic:

a(b(c() @noRuntimeChecks));

This approach keeps the annotation close to the expression it modifies without interrupting the readability of the code. The annotation is still important, but it's less intrusive and aligns with the overall design of the code. This brings us to the other part of the proposed changes.

Postfix Annotations

One additional change proposed is the ability to define annotations that must be used in a postfix manner. This would enforce consistent and predictable code formatting, which improves readability, especially when dealing with development-time behaviors.

For example, suppose you want to log the result of a function call, validate its invariants, and ensure it's inlined. The current approach requires placing multiple annotations in front of the declaration:

@log @validateInvariants @inline final data = fetchData(id);

With postfix annotations, this could be written as:

final data = fetchData(id) @log @validateInvariants @inline;

This not only improves readability in a single line of code, but it becomes even more advantageous in larger code blocks where annotations might otherwise disrupt the flow. By placing annotations as postfixes, we ensure they are secondary details and don’t interfere with the core implementation logic.

One clear, but still simple, example for multiple lines:

Without postfix annotations

@log @validateInvariants @inline
final data1 = fetchData(id);
@log @validateInvariants @inline
final data2 = fetchData(id + 1);
@log @validateInvariants @inline
final data3 = fetchData(id + 2);

With postfix annotations

final data1 = fetchData(id) @log @validateInvariants @inline;
final data2 = fetchData(id + 1) @log @validateInvariants @inline;
final data3 = fetchData(id + 2) @log @validateInvariants @inline;

Implementation Considerations

To maintain backward compatibility and ensure predictable behavior, postfix annotations must be explicitly defined. This can be achieved through a marker type or by using a prefix annotation.

Example using an annotation:

@postfix const Null annotation = null;

Alternatively, an annotation could implement a PostfixAnnotation marker type:

class Annotation implements PostfixAnnotation
{	const Annotation();
}

These are just a few possible ways to implement this feature. The details of the implementation can vary, but the core idea is to have a clear distinction for postfix annotations.

Conclusion

Dart is already a powerful and customizable language, and adding support for postfix and expression annotations would significantly enhance its flexibility. This proposal aims to improve the readability of code by allowing annotations to be used in more natural and less disruptive ways. Additionally, I foresee this feature being widely adopted by Domain-Specific Languages (DSLs), as it would allow them to define more intuitive and natural syntax for augmenting expressions with additional behaviour.

@RohitSaily RohitSaily added the feature Proposed language feature that solves one or more problems label Jan 25, 2025
@tatumizer
Copy link

The problem is not "prefix vs postfix" IMO.
My theory is that

  • the symbol @ hurts readability - it shouts too loudly and obscures the meaning of the word.
  • When several annotations are placed in front of the identifier, it might not be obvious where the actual declaration starts. If written in a postfix form, it's not obvious where it ends.

Compare with

@[log, validateInvariants, inline] void fetchData(String id) {
  ...
}

@RohitSaily
Copy link
Author

RohitSaily commented Jan 25, 2025

Thanks for your input. I agree that @ shouts visually loudly. What I like about the example syntax you provided is that it is easy to identify the distinct components of the declaration i.e. annotations, method signature, and body are instantly visually separable. Even so, I still think the ordering is significant, especially when performing actual reading, not scanning/skimming. The method name and signature encodes the main semantics of what is going on i.e. what it does, what it takes as input, how they may relate, etc. The annotations provide finer details, much like the method body e.g. this method's result is logged, its invariants are validated, etc. These are not relevant for what the method achieves but effectively hooks for additional behaviours or checks.

@TekExplorer
Copy link

Or... Perhaps... We can do what we already do and put all annotations before the element. As in, the line before.

@tatumizer
Copy link

... or use a less flashy color scheme for annotations

@RohitSaily
Copy link
Author

@TekExplorer

Or... Perhaps... We can do what we already do and put all annotations before the element. As in, the line before.

To clarify, I’m not suggesting that the current approach is inherently flawed. There are specific cases where the proposed syntax would improve readability. This would be particularly observable when working with large codebases with numerous annotations and macros, especially ones which are really targeting expressions. Simply sticking to the status quo doesn’t address the potential for improvements in readability and expressiveness. Furthermore, the proposed changes do not prevent annotations from being used before an item it annotates.

@tatumizer

... or use a less flashy color scheme for annotations

This isn’t about not wanting to notice the annotations, which is what that suggestion implies. It’s about a potential enhancement for readability and expressiveness.

@TekExplorer
Copy link

@RohitSaily I don't really agree. Putting annotations after an expression would just get more confusing, and add ambiguity.

I think that the current simplicity makes more sense.

That said, we can definitely try to find improvements, such as expression macros, macros

@RohitSaily
Copy link
Author

Thank you for your response. I have a clearer understanding of that viewpoint now. I agree that this does increase the complexity of the language. However, I don't think the added complexity is significant enough to justify avoiding this syntax. As with any language feature, misuse could affect code quality, but I’m still not seeing how this would introduce major ambiguity or confusion. If possible, could you provide further elaboration or examples to help me understand how this could lead to ambiguity?

@TekExplorer
Copy link

Allowing annotations to come after an expression or statement makes it hard to tell that anything is happening.

Plus, it can mean that annotations can be "hidden" at the end of the expression.

Having the annotations ahead of the expression allows us to define the context of the following code before we read the code.

We can know that there's extra information to be had.

But if that information comes after, it can confuse you as your initial impression is now tainted - what you read may in fact be something entirely else thanks to the annotations.

I like what you're going for with the wrapper functions - but I think that the annotations should remain where they are.

Its basically like python decorators in that sense.

Expression macros would be way more powerful, as it may expand into who knows what.

@tatumizer
Copy link

Annotations look better with a different color:

@log @validateInvariants @inline void fetchData(String id) {
  //...
}

@TekExplorer
Copy link

TekExplorer commented Feb 9, 2025

Annotations look better with a different color:

@log @validateInvariants @inline void fetchData(String id) {
  //...
}

Perhaps, but that is a theming/tooling change, and has nothing to do with the language itself.

That said, the formatter places annotations vertically, not inline, so perhaps you want to consider how that applies instead.

They could possibly be folded away like you might a code block or imports.

@RohitSaily
Copy link
Author

@TekExplorer I appreciate the explanation, it helps me think this through more.

Allowing annotations to come after an expression or statement makes it hard to tell that anything is happening.

Plus, it can mean that annotations can be "hidden" at the end of the expression.

I agree that this can be an issue, like many language features, it is only an issue if the developer misuses it.

It makes it hard to tell anything is happening until the developer finishes reading or examining the expression/declaration. That is actually the benefit of it. It allows detailed information that is not necessary for the implementation to be moved to the side, making it secondary in content.

Having the annotations ahead of the expression allows us to define the context of the following code before we read the code.

We can know that there's extra information to be had.

But if that information comes after, it can confuse you as your initial impression is now tainted - what you read may in fact be something entirely else thanks to the annotations.

This is a good and important point. It is valuable for certain kinds of annotations or macros that are relevant to actual implementation behaviour to be placed at the forefront. Other kinds of annotations or macros can actually unnecessarily add to the cognitive context of the reader before they examine the main implementation details of the expression/declaration. This actually decreases readability.


Ultimately, it is the responsibility of developers to write readable code. Developers are limited by what the language permits and prohibits. It is in their best interest if the language provides flexibility so they can write as readable and expressive code as possible.

@RohitSaily
Copy link
Author

An additional consideration is to allow the annotation/macro definer to easily and concisely define the annotation or macro as prefix or postfix. This helps force the developer apply it appropriately and consistently.

To maintain compatibility, when it is not specified it is prefix. Postfix would have to be explicitly indicated in the annotation/macro definition somehow.

@TekExplorer
Copy link

I still don't see the benefit. When would a postfix annotation ever be the sensible thing?

@tatumizer
Copy link

One potential use case for a postfix annotation is related to #3580.

final int x @toBeAugmented;

(Some shorter word has to be found)

@RohitSaily
Copy link
Author

RohitSaily commented Feb 11, 2025

@TekExplorer

I still don't see the benefit. When would a postfix annotation ever be the sensible thing?

I have revised the proposal (the first post in this thread) to try and make the readability benefits more clear. Please take a look and let me know what you think

@tatumizer

One potential use case for a postfix annotation is related to #3580.

final int x @toBeAugmented;
(Some shorter word has to be found)

That's a good example. Imagine there were more annotations on that declaration too:

@toBeAugmented @annotationA @annotationB final int x;

It's quite the journey for the eyes to get to the point of the declaration! The following would be much better.

final int x @toBeAugmented @annotationA @annotationB;

We can immediately identify what is actually at that line (a definition of x), and can ignore the annotations if they are not of concern for the current context e.g. for a bug we are trying to identify. Names often encode significant semantics in code too, so it is clearly better to have that at the forefront.

@TekExplorer
Copy link

TekExplorer commented Feb 13, 2025

I see. I think I can understand that.

That's a good example. Imagine there were more annotations on that declaration too:

@toBeAugmented @annotationA @annotationB final int x;

It's quite the journey for the eyes to get to the point of the declaration! The following would be much better.

I actually don't agree here, since they would not be on the same line.

@toBeAugmented @annotationA @annotationB
final int x;

As you can see, you can see the declaration just fine.

I can see the point to having an annotation on the trailing end of a declaration like that, as you effectively put the marker exactly where the code will go.

Though perhaps it may be better to consider expression macros, in a sense:

class Foo {
  final int i = @someAnnotation;
}

It actually occurs to me that this can synergize with a different issue, (#3580) such that it's equivalent (in terms of actionable code) to:

class Foo {
  // Indicates that the initializer is filled in elsewhere
  final int i =;
  // type identifier = annotations*;
}

Edit: actually heh, that issue is exactly the one mentioned above

@RohitSaily
Copy link
Author

RohitSaily commented Feb 14, 2025

I agree putting the annotations on the line before improves it from the example I gave. However, now it is taking up another line. Furthermore, if the annotations are lengthy due to names and parameters, it becomes multiple lines before the declaration (depending on code formatting guidelines being used). Having the subsequent lines contain the annotations is better for readability as explained in the revised original post.

I didn't consider an annotation as a standalone expression, that is interesting. I believe it would be beneficial to have an expression which can be annotated and for multiple annotations to be applicable to the expression.

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

3 participants