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

Allow return; in function returning FutureOr<void>. #3246

Open
hamsbrar opened this issue Jul 27, 2023 · 6 comments
Open

Allow return; in function returning FutureOr<void>. #3246

hamsbrar opened this issue Jul 27, 2023 · 6 comments
Labels
request Requests to resolve a particular developer problem

Comments

@hamsbrar
Copy link

FutureOr<void> fn1() { }
FutureOr<void> fn2() => 123;

FutureOr<void> fn3() {
  return;
// ^ Warning: The return value is missing after 'return'.
// ^ Compile Error: A value must be explicitly returned from a non-void function.
}
> dart --version
Dart SDK version: 3.0.6 (stable) (Tue Jul 11 18:49:07 2023 +0000) on "linux_x64"

It's not consistent with how void is treated at other places:

  1. There are no warnings in fn1 and fn2 and compiler doesn't complain about missing explicit return in f1 (which it believes to be a must in non-void functions when compiling fn3).
  2. There are no warnings when using return; in void function(s). So it's natural to expect it to work inside FutureOr.
    void b1() {}
    void b1() => 123;
    void b2() {
        return;
    }

Actual example:

FutureOr<void> _fnLoadTemplates(
  ComponentPath componentPath,
  ComponentBuilder componentBuilder,
  ToDartRenderNestedComponentOptions options,
) {
  var loadTemplatesResults = Bridge.instance.jsLoadTemplates(
    ToJsLoadTemplatesOptions(
      templateIdentifiers: componentBuilder.listLoadableTemplates(componentPath, options),
    ),
  );

  var promiseLoadingTemplates = loadTemplatesResults.promise;
  if (null == promiseLoadingTemplates) {
    return;
//  ~~~~~~
  }

  var completer = Completer<void>();
  promiseToFuture<Object>(promiseLoadingTemplates).then((_) => completer.complete());
  return completer.future;
}
@eernstg
Copy link
Member

eernstg commented Jul 27, 2023

This is all working as intended. But it is somewhat tricky, so I'll comment on several things.

It's not consistent with how void is treated at other places:

FutureOr<void> is not void.

It could be claimed that FutureOr<void> as a return type should be treated the same as void (because it works like Future<void> | void, which means that void is "one of the two return types" of the given function). So anyone who calls that function should ignore the returned object.

However, you could also claim that it should be treated differently, because "the other return type" is Future<void>, and that's a very different thing: Anyone who calls that function should handle the future (we even have lints saying that we should not ignore a future); the future should then be awaited somewhere (or we should call .ignore() in order to disarm exceptions that might be used to complete the future).

We can't make the distinction because the "two return types" are combined using a union (the pipe operator in Future<void> | void, which is just a way to explain what FutureOr<_> means and not actual syntax). This means that we can get one or the other, and we can't tell which one we got: Every object whatsoever can be marked as "please ignore this object" by passing it with type void, so it could always be the void branch.

I would tend to say that the very type FutureOr<void> shouldn't be used. It's a contradiction in terms to indicate that the returned object must be discarded and thus ignored, and it must also be handled. So just stop using that type, please. ;-)

That was the conceptual part. For the technical part:

The actual treatment of return e; in a function whose return type is FutureOr<void> is specified here:

\subsection{Return}
(here is a PDF, look into section 'Return').

For example, in the synchronous non-generator case (that is, a "normal" function, with no sync*, no async or async*) there are a number of rules intended to give developers a notification if they are returning a "real" result from a function with return type void. The point is that this is not useful, and most likely a bug (because the developer might think the result is being returned to a caller who would use it, but it will actually be discarded).

There are no errors for => functions, because an => function with return type void is allowed to return anything whatsoever. It's a one-liner, and void f(int i) => someObject.someMethod(i); is convenient. ;-)

However, there are no special rules for FutureOr<void>.

This means that FutureOr<void> is treated just like any other type (as opposed to void which gets special treatment). So we just check that the returned object has an appropriate type (which is any type, because FutureOr<void> is a top type, that is Object? <: FutureOr<void> <: Object? and everyObjectWhatsoever is FutureOr<void>).

But you're not allowed to use return; when there is a non-void return type, so you get an error in fn3.

There are no warnings in fn1 and fn2

With fn1, we have the rule that it is a compile-time error if the end of a function body can be reached (that is, we don't return from anywhere) and the return type of the function is potentially non-nullable, but FutureOr<void> is nullable (hence, it's not potentially non-nullable). We're returning null (implicitly), and that's OK for this return type. (It would be fine for void, too, by the way). b1 is OK, because the return type allows us to "not return anything" (which really means: return null).

With fn2 there is no error because int <: FutureOr<void>. b2 is OK (I'm assuming the names b1, b2, b3 even though it's written as b1, b1, b2) because of the special exception for => functions with return type void: The returned expression can have any type whatsoever.

With fn3 we get into the rules about return;, and it is an error to omit the returned value when the return type isn't void, dynamic, or Null. b3 is OK because of the 'Return' rules again.

@lrhn
Copy link
Member

lrhn commented Jul 27, 2023

I think we should fix it, by saying that return; is allowed if the function "may return no value", which we then define as a return type of void or any union type containing void for a "normal function", those types as future value type of an async function, and always for a generator function.

We can then define "may return a value" and allow return e; for those, which is any non-generator function with a non-void return value or future value type. (And then lint the heck out of void? wherever it occurs, just don't do that!)

(And then carve out whatever exceptions we want to allow. I want none.)

@hamsbrar
Copy link
Author

@eernstg Thanks for the explanation and suggestions:)

I'm not a language engineer and I might never fully understand intricacies and vagueries of void. But as a user, if I see void in a programming language, I'd see it as a handy type hint for functions that don't explicitily return a value.

I'd see Future<void> as a function that will return without a value, but not immediately. Instead, it might take some time to complete, like a promise for the future. I'd expect language to have right control constructs that are needed to wait for the signal when such functions complete their tasks.

I'd see Future<void> | void as a function that can behave in two ways: either it returns instantly without a value, or it returns at some point in the future, again without a value. I'd expect language to have constructs that would allow me to check whether the function has already returned or, if not, provide a way to wait for the signal indicating its completion.

If a function is declared with a void return type, I wouldn't expect to see return null inside it. Since void explicitily indicates no return value, encountering return null would seem contradictory.

Similarly, in the case of Future<void> | void, I wouldn't anticipate any return null statement either. Both Future<void> and void denote a lack of retrun value, so including return null would seem contradictory.

I'd see return; as explicitily returning from a function but without a value. For me, both return; and an implicit return are essentially the same – the function completes without specifying a return value. As a result, I'd see return; as perfectly appropriate inside functions with void or Future<void> | void return types.

I believe most people, unless bound by a particular specification or an implementation, will see things as such.

@eernstg
Copy link
Member

eernstg commented Jul 28, 2023

Thanks!

Dart has never had the notion of a procedure (that is, a behavioral abstraction where invocations do not return a value). So every function call returns a value, period. This is a good fit for dynamic invocations (which were very common in early Dart), and it avoids duplicating almost all of the language by having two kinds of functions everywhere. So we don't have the option to say that a particular function is a procedure.

Next, we'd need to choose which value to return in situations where no value is specified by the developer; in general, the answer is the null object. So dynamic f() {} and dynamic f() { return; } both work like dynamic f() { return null; }. (I'm just using the type dynamic because that allows us to do a lot of things without incurring compile-time errors).

I'd see Future<void> | void as a function that can behave in two ways: either it returns instantly without a value, or it returns at some point in the future, again without a value.

If we want to specify that a function might return a Future<void>, or it might return "without a value", and the caller should be able to make the distinction in order to await the future when there is one, and proceed directly when there is none, you should just use the type Future<void>?. This type provides the required affordances to the caller. That is, this type works.

In contrast, if we use the return type FutureOr<void> (Future<void> | void) then it is not safe to assume that "there is a value". In other words, if your words about not returning anything should be taken seriously then you can't check the returned value when we have that return type. It might not exist! If we stick to the terminology that the invocation returned without a value then we would need to introduce a new concept which is "test whether a value exists or not".

It is much more straightforward to use the special object null which was created in order to be an actual representation of the concept that "there is nothing here". It's a real object, and it can be tested using standard language mechanisms (like e == null or e is Null), and we can take it into account in the type system by using types of the form T?.

If a function is declared with a void return type, I wouldn't expect to see return null inside it.

You can do that, and that's the semantics of return; anyway, but I agree that it is probably less confusing if you use a style where void functions use return; and never return e; (return e; in a void function is an error in most cases, anyway, but you can say return print('Hello!); because print has return type void, too).

Similarly, in the case of Future<void> | void, I wouldn't anticipate any return null statement either.

That's a bad type, so I won't try to fix it.

However, if you use the return type Future<void>? then it makes sense to return a future, and it makes sense to return null, because you're sending a message to the caller in both cases: (1) here's a future, you should handle it; or (2) here is null which is an explicit signal that there is no future work to wait for.

I believe most people ... will see things as such.

When void is used as a return type, it is indeed not the same thing to say "ignore the returned value" respectively "this is a procedure, and there is no return value", but we do have some prior art:

void* is used in C and similar languages to specify that a particular pointer is "typeless", that is, it could be absolutely anything, and any specific usage is unsafe (we can have segmentation faults or bus errors, there is no lower bound).

In Dart, the value of a void expression is not unsafe, it's just an object. But the fact that some developer chose the type void for it indicates that there is no meaningful way to use that object.

This makes sense, for instance, in the case where we have a Visitor<X> class where visit returns an X, or the visitor has an instance variable of type X which can be read after the visit. Assume that we want to create a visitor that doesn't return anything (and we don't want to duplicate the code). We can then use the type Visitor<void> to indicate that the result returned by visit and/or the value of the instance variable should be ignored.

@hamsbrar
Copy link
Author

hamsbrar commented Jul 29, 2023

but you can say return print('Hello!); because print has return type void, too

True but print(e); return; is much more transparent, easy to grasp, and just looks correct.

void* is used in C and similar languages to specify that a particular pointer is "typeless", that is, it could be absolutely anything, and any specific usage is unsafe (we can have segmentation faults or bus errors, there is no lower bound).

I think void* should mean a pointer that doesn't point at all. And if void(as a return type) means "ignore the returned value" then void* could mean a pointer that "doesn't point to a value"(would be OK since I won't to try cast or deference it because that'd mean making it "point to value" or assuming what(value) it points to – a contradiction).

Of course, that's not how C's interpret void*. However, from my perspective, it appears that C's decision to use void* for typeless pointers is wrong. C might be able to justify it as any* require breaking existing code and just * is too damn hard to parse but what made it so? there's nothing inherently wrong with * so it must be C's decision to allow * to occur at certain places.

That's just my gut feeling, though – no real experience in C.

In Dart, the value of a void expression is not unsafe, it's just an object. But the fact that some developer chose the type void for it indicates that there is no meaningful way to use that object. This makes sense, for instance, in the case where we have a Visitor class where visit returns an X, or the visitor has an instance variable of type X which can be read after the visit. Assume that we want to create a visitor that doesn't return anything (and we don't want to duplicate the code).

I don't see problem with duplication and why would I? there are thousands of instances in which I cannot express things succinctly no matter what language I choose. But I do wonder, if a language(not just Dart) want to assist in avoiding duplication in this particular case, why would it be willing to choose to deviate from the expected semantics of void(which is so prevalent) rather than exploring ways to support something like Visitor<X | void>. Note, I didn't mean exactly Visitor<X | void>/VoidOr<X>.

I believe, any deviation from user's interpretation of code, no matter how slight, is just wrong and should be minimized whenever feasible.

@lrhn
Copy link
Member

lrhn commented Aug 1, 2023

This is currently working as specified.
It's a bad user experience, so we should consider whether we can change it. It's likely non-trivial to define where the return type is void-like enough that you can return void.
Or maybe not: Void-like is void, and either T? or FutureOr<T> where T is void-like. Meaning any union type where void is one of the elements of the union. (You can't write void? grammatically, but you can write FutureOr<void>?)

So moving this to be a language change request.

I also, now, agree that you should always use Future<void>? instead of FutureOr<void>, but we allow the latter, and it should preferably work the way a user expects it to. I'd expect it to allow return;.
(We should just warn about the type, if anyone writes it explicitly.)

@lrhn lrhn changed the title Can't compile: The return value is missing after return in FutureOr<void> functions Allow return; in function returning FutureOr<void>. Aug 1, 2023
@lrhn lrhn transferred this issue from dart-lang/sdk Aug 1, 2023
@lrhn lrhn added the request Requests to resolve a particular developer problem label Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

3 participants