scala3
scala3 copied to clipboard
Typeclass experiments refactored
A refactoring of the typeclass-experiments branch according to topics.
DISCLAIMER: This is the same as the previous typeclass-experiments Pre-pre SIP. It's just that the commits are now in a more logical order instead of the chronological order of the first PR. Some part of this is currently under consideration as SIP-64. Other parts might be proposed as Pre-SIPs in the future.
The order of exposition described in the docs of this PR is different from the planned proposals of SIPs. I concentrate here not on how to sequence details, but instead want to present a vision of what is possible. For instance, the docs in this PR start with Self types and is syntax, which have turned out to be controversial and that will probably be proposed only late in the sequence of SIPs.
The PR needs a minor release since it adds experimental language imports, which did not exist before. Everything covered is under experimental. Baseline Scala is not affected.
About naming "is/Self" vs "forms/Something else": Note that the is syntax will only work for type classes that are defined in terms of Self. So the argument that existing type classes like Reader don't follow this naming convention is moot. They are not defined in terms of Self (not should they be), and they cannot be summoned with is. They can still be used in context bounds, as is the case today, of course.
The reasoning for "is/Self" is as follows. With this PR, we use context bounds X: B very much like type ascriptions x: T. They both mean similar things, that's why they have the same syntax. For instance, if m is a member of a type T and x: T, then you can write x.m. The same holds for members m of a type class C appearing in a context bound X: C: you can write X.m. Examples are X.unit or X.Element.
Now, if x: T, we pronounce that as x is (a) T. If you are a stickler, you might insist that it should be isa, not is. But is is much more common, for instance in type tests x is T in other languages. So analogously, if X: C we would pronounce that X is C. E.g. X is Monoidal, X is Numeric, X is Typeable, and so on. It's not the "is" in the meaning of identity, but the "is" in the meaning of an adjective, such as "the Sky is blue". Type classes characterize types, and in that sense play the linguistic role of adjectives. That's why, despite all objections I find is to work much better than forms or other alternatives.
In light of the adjective analogy, maybe we we should tweak the names of our example type classes some more. Should be Monoidal instead of Monoid, or Monadic instead of Monad? Maybe that's overthinking it. List is Monad is slightly off, but still quite common and easily understandable.
If you excuse a well meant comment from the peanut gallery :pray:
If we want to go ahead with using is, we should be thorough and consistent. When we have e.g.
given Int is Ord:
we should also use is like this
def minimum[T is Ord](xs: List[T]) =
or
given [T is Ord] => List[T] is Ord:
I think retiring : from context bounds is desirable. It is confusing, because it's yet another role the character : plays (besides type annotations and new block demarcations). Using is for this purpose would also be much more regular and thus easier for the newcomers to learn.
We can keep : in context bounds for backward compatibility, but all these new features should only work with is.
Note that my proposal doesn't really care whether it is is or forms or whatever else gets picked, the point is to use it everywhere, including the places where : is currently used.
(And we could even drop the [/], but that would be for another discussion)
given T is Ord => List[T] is Ord:
I think retiring
:from context bounds is desirable. It is confusing, because it's yet another role the character:plays (besides type annotations and new block demarcations).
I think this is focusing too much on small details - T: Ord is very much in spirit with xs: List[T] - X conforms to some constraint Y.
Using
isfor this purpose would also be much more regular and thus easier for the newcomers to learn.
But then you are using is as both a keyword to introduce a context bound to a type parameter declaration (T is Ord), and also as an infix-type at use-site (List[T] is Ord). This is surely more confusing
X conforms to some constraint Y
It's this vagueness which people will find confusing.
using
isas both a keyword to introduce [...] and also as an infix-type at
I am certain that is not how people will think of it. And not just the beginners, but ordinary Scala developers. They wouldn't think about the first par as using keywords and the other part as using infix types. For them, it would be the same thing.
If anything, this shows that if we really want to improve the Type Class situation in Scala, it would be desirable in the part which is currently "infix-type at use-site (List[T] is Ord)" to turn the is there into keyword as well. That should make it more robust. I'm worried that using only infix types is fragile and that the error messages won't be friendly.
One new thing over the previous PR is that we fix is the handling of Singleton. X <: Singleton is wrong and unsound in the presence of unions. We have 1 <: Singleton and 2 <: Singleton and therefore by the laws of subtyping union types, 1 | 2 <: Singleton. But that makes no sense. The proper approach is to treat Singleton as a type class. It should be X: Singleton to indicate that instances of X are all singletons. The problem is how to get there, yet keep the familiar name Singleton. Self-based type classes afre a solution. We now define singleton as
trait Singleton extends Any:
type Self
That's compatible with the previous subtyping constraints, but also allows the new context bound syntax. Furthermore, we treat Singleton as an erased class, and that means that no code will generated for witness arguments.
We now also implement precise type inference using a Precise typeclass trait. After the change to Singleton, this was relatively straightforward except we hit some complications with constant folding, where precise type variables were constrained from below with the unfolded underlying type instead of the folded constant type.
Regarding the Precise type class, does it also keep tuples precise? I think it would good to look at https://github.com/scala/scala3/pull/15765/ for test cases. It has pretty good coverage for feature interaction.
I believe the answer will be "it depends". A precise type variable gets instantiated without widening of singletons or unions at the top-level.vA variable is precise if it is declared with a Precise context bound, or if it has another precise type variable as a part of its upper bound.
Great idea to check behavior and expressiveness using the test cases of #15765.
This currently looks very weird to me, as I am used to think about givens as special vals or defs. (Anyway, probably not the place and time to discuss the syntax.)
Yes, let's discuss separately. The whole point is that given is not a val or a def and does usually not have a name. given A is TC is like declaring A extends TC but is separate from the class A. The relationship entry of is or extends does not naturally have a name.