Skip to content

Commit

Permalink
Document F-bounds and inference using bounds for 3.7 (#6403)
Browse files Browse the repository at this point in the history
Fixes #6258 

---------

Co-authored-by: Parker Lougheed <parlough@gmail.com>
Co-authored-by: Erik Ernst <eernst@google.com>
  • Loading branch information
3 people authored Feb 18, 2025
1 parent e001beb commit 01664e6
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 0 deletions.
18 changes: 18 additions & 0 deletions examples/misc/test/language_tour/generics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,21 @@ void main() {
}

class View {}

// #docregion f-bound
// ignore: one_member_abstracts
abstract interface class Comparable<T> {
int compareTo(T o);
}

int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
t1.compareTo(t2) + 1;

class A implements Comparable<A> {
@override
int compareTo(A other) => /*...implementation...*/ 0;
}

var useIt = compareAndOffset(A(), A());

// #enddocregion f-bound
10 changes: 10 additions & 0 deletions examples/type_system/lib/bounded/instantiate_to_bound.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ void cannotRunThis() {
c.add(2);
// #enddocregion undefined-method
}

// #docregion inference-using-bounds-2
X max<X extends Comparable<X>>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2;

void main() {
// Inferred as `max<num>(3, 7)` with the feature, fails without it.
max(3, 7);
}

// #enddocregion inference-using-bounds-2
18 changes: 18 additions & 0 deletions examples/type_system/lib/strong_analysis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,21 @@ void _miscDeclAnalyzedButNotTested() {
// #enddocregion generic-type-assignment-implied-cast
}
}

// #docregion inference-using-bounds
class A<X extends A<X>> {}

class B extends A<B> {}

class C extends B {}

void f<X extends A<X>>(X x) {}

void main() {
f(B()); // OK.
f(C()); // OK. Without using bounds, inference relying on best-effort
// approximations would fail after detecting that `C` is not a subtype of `A<C>`.
f<B>(C()); // OK.
}

// #enddocregion inference-using-bounds
26 changes: 26 additions & 0 deletions src/content/language/generics.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ an object is a List, but you can't test whether it's a `List<String>`.
When implementing a generic type,
you might want to limit the types that can be provided as arguments,
so that the argument must be a subtype of a particular type.
This restriction is called a bound.
You can do this using `extends`.

A common use case is ensuring that a type is non-nullable
Expand Down Expand Up @@ -195,6 +196,31 @@ Specifying any non-`SomeBaseClass` type results in an error:
var foo = [!Foo<Object>!]();
```

### Self-referential type parameter restrictions (F-bounds)

When using bounds to restrict parameter types, you can refer the bound
back to the type parameter itself. This creates a self-referential constraint,
or F-bound. For example:

<?code-excerpt "misc/test/language_tour/generics_test.dart (f-bound)"?>
```dart
abstract interface class Comparable<T> {
int compareTo(T o);
}
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
t1.compareTo(t2) + 1;
class A implements Comparable<A> {
@override
int compareTo(A other) => /*...implementation...*/ 0;
}
var useIt = compareAndOffset(A(), A());
```

The F-bound `T extends Comparable<T>` means `T` must be comparable to itself.
So, `A` can only be compared to other instances of the same type.

## Using generic methods

Expand Down
77 changes: 77 additions & 0 deletions src/content/language/type-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,83 @@ The return type of the closure is inferred as `int` using upward information.
Dart uses this return type as upward information when inferring the `map()`
method's type argument: `<int>`.

#### Inference using bounds

:::version-note
Inference using bounds requires a [language version][] of at least 3.7.0.
:::

With the inference using bounds feature,
Dart's type inference algorithm generates constraints by
combining existing constraints with the declared type bounds,
not just best-effort approximations.

This is especially important for [F-bounded][] types,
where inference using bounds correctly infers that, in the example below,
`X` can be bound to `B`.
Without the feature, the type argument must be specified explicitly: `f<B>(C())`:

<?code-excerpt "lib/strong_analysis.dart (inference-using-bounds)"?>
```dart
class A<X extends A<X>> {}
class B extends A<B> {}
class C extends B {}
void f<X extends A<X>>(X x) {}
void main() {
f(B()); // OK.
f(C()); // OK. Without using bounds, inference relying on best-effort
// approximations would fail after detecting that `C` is not a subtype of `A<C>`.
f<B>(C()); // OK.
}
```

Here's a more realistic example using everyday types in Dart like `int` or `num`:

<?code-excerpt "lib/bounded/instantiate_to_bound.dart (inference-using-bounds-2)"?>
```dart
X max<X extends Comparable<X>>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2;
void main() {
// Inferred as `max<num>(3, 7)` with the feature, fails without it.
max(3, 7);
}
```

With inference using bounds, Dart can *deconstruct* type arguments,
extracting type information from a generic type parameter's bound.
This allows functions like `f` in the following example to preserve both the
specific iterable type (`List` or `Set`) *and* the element type.
Before inference using bounds, this wasn't possible
without losing type safety or specific type information.

```dart
(X, Y) f<X extends Iterable<Y>, Y>(X x) => (x, x.first);
void main() {
var (myList, myInt) = f1();
myInt.whatever; // Compile-time error, `myInt` has type `int`.
var (mySet, myString) = f1({'Hello!'});
mySet.union({}); // Works, `mySet` has type `Set<String>`.
}
```

Without inference using bounds, `myInt` would have the type `dynamic`.
The previous inference algorithm wouldn't catch the incorrect expression
`myInt.whatever` at compile time, and would instead throw at run time.
Conversely, `mySet.union({})` would be a compile-time error
without inference using bounds, because the previous algorithm couldn't
preserve the information that `mySet` is a `Set`.

For more information on the inference using bounds algorithm,
read the [design document][].

[F-bounded]: /language/generics/#self-referential-type-parameter-restrictions-f-bounds
[design document]: {{site.repo.dart.lang}}/blob/main/accepted/future-releases/3009-inference-using-bounds/design-document.md#motivating-example

## Substituting types

Expand Down

0 comments on commit 01664e6

Please sign in to comment.