Skip to content

Allow lower bounds on type parameters of functions #1674

Open
@eernstg

Description

@eernstg

This issue is a proposal for adding lower bounds on type parameters of functions and methods. They are supported in, for instance, Scala, and they are a well-established device that enables some constructs involving variance to be statically sound.

A lower bound on a type parameter of a method is a covariant position, so if we add sound variance it would be allowed to use covariant and invariant type variables in that position. Currently all class type variables are covariant, so they can all safely be used there. The syntax of type parameters would be extended to allow X super T (allowing both an upper and lower bound on the same type variable is not supported).

For example, consider this program where we are using a standard Dart approach (similar to List) where the type parameter E occurs in some contravariant positions:

abstract class Node<E> {
  ListNode<E> prepend(E element) => // Unsafe parameter type!
      ListNode(element, this);
}

class ListNode<E> extends Node<E> {
  final E head;
  final Node<E> tail;
  ListNode(this.head, this.tail);
}

class Nil<E> extends Node<E> {}

void main() {
  Node<num> node = ListNode<int>(1, Nil<int>());
  node = node.prepend(3.4); // Throws.
}

One might assume that an immutable list would be immune to the dynamic type errors associated with dynamically checked covariance (usually, those errors arise when we mutate a list), but a method like prepend shows that it can occur even with immutable classes.

However, prepend creates a new list, so we can make the choice to give it a new type argument:

abstract class Node<E> {
  ListNode<U> prepend<U>(U element) =>
      ListNode(element, this as Node<U>);
}

class ListNode<E> extends Node<E> {
  final E head;
  final Node<E> tail;
  ListNode(this.head, this.tail);
}

class Nil<E> extends Node<E> {}

void main() {
  Node<num> node = ListNode<int>(1, Nil<int>());
  node = node.prepend(3.4);
  print(node.runtimeType); // `ListNode<num>`.
}

In this case we are creating a new ListNode<num>, and it is statically safe to put 3.4 into it (there is no unsafe covariance here, because E only occurs covariantly).

Of course, we have two casts this as Node<U>, and they will fail unless E <: U. However, the statically known value Es of E satisfies E <: Es (because the classes are covariant in E), so if we ensure that Es <: U then it is also guaranteed that E <: U.

So we can ensure this with a lower bound on U:

abstract class Node<E> {
  ListNode<U> prepend<U super E>(U element) =>
      ListNode(element, this);
}

class ListNode<E> extends Node<E> {
  final E head;
  final Node<E> tail;
  ListNode(this.head, this.tail);
}

class Nil<E> extends Node<E> {}

void main() {
  Node<num> node = ListNode<int>(1, Nil<int>());
  node = node.prepend(3.4);
  print(node.runtimeType); // `ListNode<num>`.
}

This version of the code is statically safe: there are no occurrences of E in a contravariant position, so there are no dynamic type checks.

We could also consider a static approach:

abstract class Node<E> {
  static Node<U> buildNode<U>(U element, Node<U> node) =>
      ListNode<U>(element, node);
}

class ListNode<E> extends Node<E> {
  final E head;
  final Node<E> tail;
  ListNode(this.head, this.tail);
}

class Nil<E> extends Node<E> {}

void main() {
  Node<num> node = ListNode<int>(1, Nil<int>());
  node = Node.buildNode(3.4, node);
  print(node.runtimeType); // `ListNode<num>`.
}

This approach seems to be equally powerful as the approach based on a lower bound, but this is not quite true:

abstract class Node<E> {
  ListNode<U> prepend<U super E>(U element) {
    if (element is E) return ListNode<E>(element, this);
    return ListNode(element, this);
  }
}

class ListNode<E> extends Node<E> {
  final E head;
  final Node<E> tail;
  ListNode(this.head, this.tail);
}

class Nil<E> extends Node<E> {}

void main() {
  Node<num> node = ListNode<int>(1, Nil<int>());
  node = node.prepend(5);
  print(node.runtimeType); // `ListNode<int>`.
  node = node.prepend(2.4);
  print(node.runtimeType); // `ListNode<num>`.
}

This illustrates that we are able to combine the static type safety and the dynamic preservation of the more specific type argument.

Metadata

Metadata

Assignees

No one assigned

    Labels

    small-featureA small feature which is relatively cheap to implement.varianceIssues concerned with explicit variance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions