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

Early Return Macro #9

Open
cds-reis opened this issue Sep 14, 2024 · 7 comments
Open

Early Return Macro #9

cds-reis opened this issue Sep 14, 2024 · 7 comments
Labels
enhancement New feature or request macro

Comments

@cds-reis
Copy link
Contributor

Alright, this is a proposition that I don't know if it's possible with the current state of macros in Dart. This is based on my experience using the rust_core package, specially talking about early return.

Proposition

Create a dart macro for a function that has the return type of either Result or Option, that can simplify the use of the early return operator:

class User {
  final String email;
  final int age;
  final bool isVerified;

  const User(this.email, this.age, this.isVerified);
}

@EarlyReturn()
Result<User, int> buildUser(int userId) {
  final (email, age) = getUser(userId)[$]; // Early Return operator used here
  final isVerified = isUserVerified(email)[$]; // Early Return operator used here

  return User(email, age, isVerified);
}

As you can see, we used the early return operator without explicitly wrapping it in an Early Return expression. Currently, this function works like this:

@EarlyReturn()
Result<User, int> buildUser(int userId) {
  return Result(($) {
    final (email, age) = getUser(userId)[$]; // Early Return operator used here
    final isVerified = isUserVerified(email)[$]; // Early Return operator used here

    return User(email, age, isVerified);
  }
}

This would make it so you don't need to add the early return expression to get an early return key, while also providing the same safety guarantees that an explicit early return expression would give.

How to do this

I'm going to be honest, I have 0 idea if it is possible. I am not yet familiar enough with macros to say how it could be done, but I think this is how it could.

Let's call this part of the function body:

final (email, age) = getUser(userId)[$]; // Early Return operator used here
final isVerified = isUserVerified(email)[$]; // Early Return operator used here

return User(email, age, isVerified);

What the macro would have to do is to get the body and wrap it inside the early return expression:

@EarlyReturn()
Result<User, int> buildUser(int userId) {
  return Result(($) {
    // body
  }
}

The body would be able to access the early return operator without excplicity typing the early return context.

Again, 0 idea if it can be done, but it would be very helpful.

@mcmah309
Copy link
Owner

mcmah309 commented Sep 14, 2024

Sounds like a good idea! (Also not super familiar with the macro system) Based on my experience with custom build_runners, I think this should be possible with macros.

My only concern is how this effects formatting. I don't know if a method annotated with a macro can still be formatted and what that would mean for the user experience.

I'd also suggest that [$] can be changed to just $. Also the name @EarlyReturn is certainly correct but if users are going to be using this on a lot of functions, maybe something like @$ might be more concise? Or something else

@cds-reis
Copy link
Contributor Author

AFAIK the formatting is expected to work normally. I didn't understand the $ though, are you proposing that the macro can automatically transform the $ into a [$]?

@mcmah309
Copy link
Owner

mcmah309 commented Sep 14, 2024

are you proposing that the macro can automatically transform the $ into a [$]?

Yeah, and the initial implementation should be easy/efficient to implement - "Any $ at the end of an Ident or ) or ], transform into [$]". This should work for 99% of cases without conflict, and I'd expect all cases a user would ever write.

So in the end result you would be able to do something like

@$()
Result<User, int> buildUser(int userId) {
  final email = getUser(userId)$.email;
  final isVerified = isUserVerified(email)$; 

  ...
}

@mcmah309
Copy link
Owner

mcmah309 commented Sep 14, 2024

Note to future implementer: Worth investing if inlining the try-catch may lead to optimizations. e.g generated code

Result<User, int> buildUser(int userId) {
    final $ = _ResultEarlyReturnKey<F>._();
    try {
      // Body
    } on _ResultEarlyReturnNotification<F> catch (notification) {
      return notification.value;
    }
}

@mcmah309
Copy link
Owner

For a later addition, it may even be possible to skip try-catch all together. e.g. final x = expr$ to

var $res = expr;
switch ($res) {
    case Err():
        return $res.into();
    case Ok():
}
final x = $res.ok;

@mcmah309
Copy link
Owner

mcmah309 commented Sep 14, 2024

This may a good opportunity to revisit #3

By adding a $ field (with an "@internal" metadata annotation so it is never used outside the macro). e.g.

@$()
Result<User, int> buildUser(int userId) {
  final email = getUser(userId).$.email;
  final isVerified = isUserVerified(email).$; 

  ...
}

generates

Result<User, int> buildUser(int userId) {
    try {
      // ignore: internal
      final email = getUser(userId).$.email;
      // ignore: internal
      final isVerified = isUserVerified(email).$; 

    ...
    } on _ResultEarlyReturnNotification<F> catch (notification) {
      return notification.value;
    }
}

Edit: This may cause some issues when mixing Option and Result. A solution may be a custom lint for each, of which, the ignore is applied only in the correct context. Instead of @internal - @result_safe_context_needed/// ignore: result_safe_context_needed and @option_safe_context_needed/// ignore: option_safe_context_needed. These should be compiler errors if not ignored. Based on the return type of the annotated function, the appropriate // ignore: ... flag will be applied all uses of .$ in scope.

@cds-reis
Copy link
Contributor Author

The most important thing imo it's to not let the early return be used outside the safe context of an early return expression. Other then that, I like the idea of being able to ditch the try-catch entirely

@mcmah309 mcmah309 added enhancement New feature or request macro labels Sep 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request macro
Projects
None yet
Development

No branches or pull requests

2 participants