Skip to content

Type error in function invocation (generic type is inferred to Null instead of actual type) #43253

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
arturaz opened this issue Aug 30, 2020 · 4 comments

Comments

@arturaz
Copy link

arturaz commented Aug 30, 2020

$ dart --version
Dart SDK version: 2.10.0-7.2.beta (beta) (Mon Aug 17 11:01:01 2020 +0200) on "windows_x64"

Expected output

Some(foo)

Actual output

Unhandled exception:
type '() => Option<String>' is not a subtype of type '() => Option<Null>' of 'ifNone'
#0      Option.orElse (package:zowo_lib/option_bug.dart)
#1      main (package:zowo_lib/option_bug.dart:27:27)
#2      _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:301:19)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:168:12)

Repro code

import 'dart:io';

class Option<A> {
  final A unsafeGet;

  const Option.some(this.unsafeGet);
  const Option.none() : unsafeGet = null;
  @override String toString() => isSome ? "Some($unsafeGet)" : "None";

  bool get isSome => unsafeGet != null;

  Option<A> orElse(Option<A> Function() ifNone) => isSome ? this : ifNone();
}

Option<A> Some<A>(A a) => Option.some(a);

extension MapExts<K, V> on Map<K, V> {
  Option<V> get(K key) => containsKey(key) ? Some(this[key]) : const Option.none();
}

final map = {"foo": "bar"};

void main() {
  final noneStr = map.get("baz");
  final someStr = noneStr.orElse(() => map.get("foo"));
  stdout.write(someStr);
}

@simolus3
Copy link
Contributor

simolus3 commented Aug 30, 2020

The problem is const Option.none(). Since it's constant, it can't use the V type parameter which would depend on the map you're passing to get. In your case, the type parameter of const Option.none() is inferred to be Null. You can verify this by printing noneStr.runtimeType - it's Option<Null>.

Your function has a type of Option<String> Function(), which is not a subtype of the Option<Null> Function() that would be expected by Option<Null>.orElse(). So, you're getting a type error at runtime. You can fix this by simply removing the const keyword in MapExts.

@arturaz
Copy link
Author

arturaz commented Aug 30, 2020

Thanks for clarifying.

Though I would still say that it is highly unintuitive and seems like it is leaking implementation details of the VM into the userspace code.

It is very confusing when the code compiles and says "yup, this will be Option<A>, no worries" and fails in runtime. I guess one can reason about it once you know how Dart does things under the hood. Personally I would rather emit a compilation error in the get function telling me that const isn't allowed here.

@arturaz
Copy link
Author

arturaz commented Aug 30, 2020

Turns out it does complain if you specify type arguments explicitly...

image

It's just that Option<Null> is assignable to Option<V>. Which according to https://dart.dev/faq#q-why-are-generics-covariant is "reasonable" (to which I disagree). Case closed I guess.

Thanks for the help!

@arturaz arturaz closed this as completed Aug 30, 2020
@simolus3
Copy link
Contributor

Though I would still say that it is highly unintuitive

Agreed, this is a tricky error to spot (even for very experienced Dart developers, see google/quiver-dart@41b4f69 for instance).

seems like it is leaking implementation details of the VM into the userspace code

This is not the VM, it's the Dart compiler behaving like specified. You'll get the same result with dart2js and ddc.

It's just that Option<Null> is assignable to Option<V>

Time to 👍 dart-lang/language#213 then :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants