dart-lang / language

Design of the Dart language
Other
2.66k stars 205 forks source link

Is there a reason why Iterable<E>.whereType<T> does not bound T to `extends E`? #4134

Closed FabrizioG202 closed 37 minutes ago

FabrizioG202 commented 7 hours ago

This is really just a minor inconvenience with the Iterable's API.

Take for example the following code snippet:

class WithInt {
  WithInt(this.a);
  final int a;
}

void main() {
  final objects = <WithInt>[
    WithInt(0),
    WithInt(1),
    WithInt(2),
    WithInt(3),
    WithInt(4)
  ];
  final sum = objects.whereType<int>().fold(0.0, (a, b) => a + b);
  print(sum);
}

We should know at compile time that the filtered iterable will always be empty since int is not a subtype of WithInt. This is a trivial example, but I found out that in more complex scenarios, especially when adding wrapper classes (such as WithInt) the code still compiles fine but suddenly filtering always yields no results.

I was wondering, since dart supports syntax like <T extends E> while declaring type parameters on functions, why is this not used in the signature of the whereType method? Having something like that would make it so that the code above does not compile. Moreover, it looks like it should be a relatively easy fix.

Is there a reason behind this design?

mateusfccp commented 7 hours ago

You don't always want this behavior. Bounding T to T extends E would prevent one to check against a supertype instead of a subtype.

Consider this example:

void main() {
  final fooList = <Foo>[];
  fooList.add(FooAB());
  fooList.add(FooA());
  fooList.add(FooB());

  print(ol.whereType2<B>());
}

abstract interface class A {}

abstract interface class B {}

final class Foo {}

final class FooAB extends Foo implements A, B {}

final class FooA extends Foo implements A {}

final class FooB extends Foo implements B {}

extension <E> on Iterable<E> {
  Iterable<T> whereType2<T extends E>() sync* {
    final iterator = this.iterator;

    while (iterator.moveNext()) {
      if (iterator.current case final T current) {
        yield current;
      }
    }
  }
}

This program does not compile, because of the bound. Removing the bound (or using whereType directly) makes it work as intended.

tatumizer commented 7 hours ago

You don't need a complicated example to demonstrate the phenomenon.

  if ("Hello" is int) { // no warning
     print(0);
   }

in contrast

  if (!true) { // warning: dead code blah blah

  }