... such that an existing type originating from an outer scope can be further constrained:
class List[T](...) {
// this method should convey an additional constraint that `T` needs to be sortable:
fun sort() { ... }
}
works.
Prior Art
Existing solutions seem to be rather poor/ad-hoc/special-cased:
-
Java: Hoist the method from an instance method to a static method.
In this example we add the constraint Comparable
to a List
, to be able to sort it:
static <T extends Comparable<? super T>> void sort(List<T> list)
-
Scala: Add an implicit parameter Ordering
which knows how to sort the list's element type.
def sorted[B >: A](implicit ord: Ordering[B]): List[A]
-
Rust: Add the constraint Ord
in a where clause:
pub fn sort(&mut self) where T: Ord
I feel that none of those approaches is appealing – either they are workarounds (Java), expose a lot of unnecessary machinery (Scala), or feel like an afterthought (Rust).
While constraints can be added on typeclass implementations, like in Rust, I feel that it's worthwhile to be able to be implement typeclasses directly inside the class definition to avoid Rust's spread of implementation pieces across different places in the code (see #56).
The Bigger Picture
It's important to remember that the core Issue is purely a syntactic one:
We need a way to distinguish between introducing a type parameter and referring to it.
Traditionally, type parameter declarations are binary, were the left part introduces a new type parameter, while the right part refers to an existing type:
[T : Sortable[T]]
| |
| Refers to existing types
|
Introduces new type parameter
Other Approaches
Let's consider a List[T]
that should support a method sort
only if the contained value of T
supports the typeclass Sortable
.
(In the next examples I added SomeUnrelatedTypeclass
as a class-level constraint to demonstrate the syntax in a bigger context. I also elided the constructor parameters of List
.)
Starting point – this doesn't work, because we haven't added the constraint which means we cannot use the methods provided by the typeclass:
class List[T : SomeUnrelatedTypeclass](...) {
fun sort() { ... x.sortsBefore(y) ... }
}
Option: Introduce introduction-site syntax to distinguish introducing a type parameter from referring to it:
class List[type T : SomeUnrelatedTypeclass](...) {
fun sort[T : Sortable[T]]() { ... x.sortsBefore(y) ... }
}
- Bad: Increases the cost to define type parameters (which happens more often than adding additional constraints later)
- Bad: Poor readability.
Option: Lambda-style definition-site type intro
class List[T | T : SomeUnrelatedTypeclass](...) {
fun sort[T : Sortable[T]]() { ... x.sortsBefore(y) ... }
}
- Bad: Type intros without bounds look ugly:
[T|T]
(or [T|]
?)
Option: Prefix operators to distinguish between defining new/referring to existing one:
class List[T :: SomeUnrelatedTypeclass]() {
fun sort[T : Sortable[T]]() { ... x.sortsBefore(y) ... }
}
Option: Introduce referral-site syntax prefix #
to distinguish referring to a type parameter from introducing one:
class List[T](...) {
fun sort[#T : Sortable[T]]() { ... x.sortsBefore(y) ... }
}
- Bad: If we use
#
to refer to existing types, shouldn't the line be [#T : #Sortable[#T]]
?
Option: Introduce referral-site syntax self[]
to distinguish referring to a type parameter from introducing one:
class List[T](...) {
fun sort[self[T] : Sortable[T]]() { ... x.sortsBefore(y) ... }
}
Option: Introduce referral-site syntax prefix Self::
to distinguish referring to a type parameter from introducing one:
class List[T](...) {
fun sort[Self::T : Sortable[T]]() { ... x.sortsBefore(y) ... }
}
Option: Curly braces for constraints?
class List[T](...) {
fun sort{T : Sortable[T]}() { ... x.sortsBefore(y) ... }
}
- Bad: Overloads
{}
.
- Bad: Inconsistent:
Cell[T : Stringable]
vs. Cell[T]{T : Stringable}
?
Option: Outlaw the shadowing of type parameters; if the outer context scope has a type parameter T
, everything inside that scope refers to that T
.
- Bad: Breaks copy-pastability.