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

void type can be used as an generic #53179

Closed
Vorkytaka opened this issue Aug 10, 2023 · 8 comments
Closed

void type can be used as an generic #53179

Vorkytaka opened this issue Aug 10, 2023 · 8 comments
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language).

Comments

@Vorkytaka
Copy link

Hello!

As we know, void is the type that indicates that a value shouldn't be used.
For example:

void a = 10;
print(a);

This code won't be compiled because of error

Error: This expression has type 'void' and can't be used.
  print(a);

But, if we will use void a with a generic function, then this will compile and even work.

void f<T>(T a) {
  print(T);
  print(a.runtimeType);
  print(a);
}

void main() {
  void a = 10;
  f(a);
}

This code will print us this output:

void
int
10

So, as we can see, generic type is void, but this works fine.

But, if we will change generic in the function to the <T extends Object> or will use dynamic a instead of generic, then we will get expected type errors.

Also, if we will use <T extends Object?>, then it also compile fine.

So, is this an bug and compiler should know that void should not be used as a generic, or this is expected behaviour?

@wwwhttpru
Copy link

Here's another problem, maybe they are related.

import 'dart:async';

void main() async {
  final testX = TestX();

  try {
    testX.init().ignore();

    await testX.isInitialized.timeout(
      const Duration(seconds: 4),
      onTimeout: () {
        return testX.init();
      },
    );
  } on Object catch (error, sk) {
    print('Error: $error, \n sk: $sk');
  }
}

class TestX {
  final _completer = Completer<bool>();

  TestX();

  Future<void> init() async {
    if (_completer.isCompleted) {
      return;
    }

    await Future<void>.delayed(const Duration(seconds: 5));
    _completer.complete(true);
  }

  Future<void> get isInitialized => _completer.future;
}

here is what is output to the console:

Error: type '() => Future<void>' is not a subtype of type '(() => FutureOr<bool>)?' of 'onTimeout', 
 sk: #0      Future.timeout (dart:async/future_impl.dart)
#1      main (package:untitled/main.dart:11:31)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)

@lrhn
Copy link
Member

lrhn commented Aug 11, 2023

Everything here looks like it's working as intended.

The void type is a top type, a supertype of both Object and bool, and not assignable to either.

It's special cased such that a static type of void has extra restrictions on how its value can be used, but that doesn't apply to a genetic type argument which happens to be bound to void in some calls, but not necessarily all.
The void protection is compile time only, and at runtime, void is just another way to write Object? in almost all cases.

@mkustermann mkustermann added the area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). label Aug 11, 2023
@eernstg
Copy link
Member

eernstg commented Aug 11, 2023

@Vorkytaka wrote:

But, if we will use void a with a generic function, then this will compile and even work.

void f<T>(T a) {
  print(T);
  print(a.runtimeType);
  print(a);
}

void main() {
  void a = 10;
  f(a);
}

As @lrhn already mentioned, the rules that prevent (or rather: introduce some resistance against) using a value of type void are specific to that type, no such rules are applied to expressions whose type is a type variable, even in the case where the actual value of the type variable could actually be (or is) the type void.

There is some further discussions about this design choice in the section about 'Void Soundness' in the language specification. In short, we decided that this kind of protection must not give rise to any run-time costs in code where the type void does not occur directly as the type of an expression (in particular: there's no protection when that type is a type variable). For an expression whose type is void there is no run-time cost: Usages of that value are rejected at compile time. So there are lots of ways to gain access to the value of a void expression, and the reason is that we don't want to pay for this feature at run time.

This is indeed working as intended.

PS: The language specification updates about null safety are still in review, so the description in the language specification fits the pre-null-safety version of Dart.

@eernstg
Copy link
Member

eernstg commented Aug 11, 2023

@wwwhttpru wrote:

Here's another problem, maybe they are related.

The error that occurs in your program is caused by dynamically checked covariance.

See dart-lang/language#524 for an approach, declaration-site variance, that allows this kind of situation to be detected statically (and vote for that issue if you want support for this kind of static checking). Note, though, that the given example cannot directly use declaration-site variance because it requires changes to some classes that are provided by the platform (such as Future), and breaking changes to platform classes are not easy to perform.

The problem in the example program is that the static type of testX.isInitialized is Future<void>, and hence the timeout method expects an actual 2nd argument of type FutureOr<void> Function()?. The function literal () { return testX.init(); } which is passed as this actual argument has a return type of Future<void>, which is fine according to the static analysis.

However, the run-time type of testX.isInitialized is actually Future<bool>, so the real 2nd parameter type of timeout is FutureOr<bool> Function()?, which is not a supertype of the statically known parameter type (so we're now asking for a run-time error).

In fact, you're passing an object of type Future<void> Function() where an object of type Future<bool> Function()? is required, and that's a type error. It's a run-time type error because it wasn't reported by the static analysis (but it is detected at compile time, and the compiler generates code to check the types and throw an exception if there is a type error).

(Declaration-site variance is all about reporting that error at compile time rather than generating code to throw an exception at run time.)

If Future<T> had been invariant in T (that is, class Future<inout T> ..., assuming #524; except that we don't get to edit the class Future like that) then it would have been a compile-time error for isInitialized to have a return type of Future<void>, and the situation where a Future<bool> is statically typed as a Future<void> would never occur.

So this is a completely different topic, but still working as designed. ;-)

@lrhn
Copy link
Member

lrhn commented Aug 13, 2023

If Future<T> had been invariant in T

(Just to be up-front here: if I cannot make Future covariant, I won't consider the variance feature complete.)

@Vorkytaka
Copy link
Author

Vorkytaka commented Aug 13, 2023

Ok, I've got that this is expected for the compiler.
And I agree that checks around void should not cost us some runtime performance.

For me, still, pretty strange that i cannot use void variable in place, but can use it as an argument of function with generic.

void a = 10;
print(a); // compile time error
f(a); // no error at all

It's looks like we should not have ability to use it in this case too.
But, as long as it expected behaviour i'm close the issue. :)

@eernstg
Copy link
Member

eernstg commented Aug 14, 2023

@Vorkytaka wrote:

f(a); // no error at all

This is because it is not an error to pass the value of a void expression as an actual parameter when the parameter type is void (specified here), and because f(a) is inferred as f<void>(a), and the parameter type of f<void> is void.

@eernstg
Copy link
Member

eernstg commented Aug 14, 2023

@lrhn wrote:

(Just to be up-front here: if I cannot make Future covariant, I won't consider the variance feature complete.)

Like this?:

abstract interface class Future<out T> {
  factory Future(FutureOr<T> computation()) =>
      throw 'Not implemented';
  factory Future.microtask(FutureOr<T> computation()) =>
      throw 'Not implemented';
  factory Future.sync(FutureOr<T> computation()) =>
      throw 'Implementation omitted';
  factory Future.value([FutureOr<T>? value]) =>
      throw 'Implementation omitted';
  factory Future.error(Object error, [StackTrace? stackTrace]) =>
      throw 'Implementation omitted';
  factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) =>
      throw 'Implementation omitted';

  Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});
  Future<T> catchError(Function onError, {bool test(Object error)?});
  Future<T> whenComplete(FutureOr<void> action());
  Stream<T> asStream();
  Future<T> timeout(Duration timeLimit, {covariant FutureOr<T> onTimeout()?});
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language).
Projects
None yet
Development

No branches or pull requests

5 participants