-
Notifications
You must be signed in to change notification settings - Fork 1.1k
How to Introduce a Parametric Top Type? #10662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Just to be clear: you're not aiming to do any of this for 3.0.0, are you? |
I don't think 3.0.0 is feasible. The maximal thing we could do before 3.0 is released is something under a -Y flag, analogous to |
This is amazing and I love where it's going. I also don't want it in 3.0.0. 😃 Obviously my interests are aligned with the maximally useful case, but clearly there are some significant barriers to work out to make that possible. An alternative type variable syntax might be one way to improve this, though I'm not sure if that's better than explicit bounds. The real problem is the network effect. Everything that can bound with Top would need to, otherwise it would be very difficult to use in downstream signatures. That alone implies that the only meaningfully useful incarnation of this is the maximal one, and the compatibility story would need to be carefully sorted out. |
Given that Maximal Solution is breaking lots of things anyway - doesn't it makes sense to also rename |
I think the truly maximal approach would be to also make traits and classes by default extend It seems completely acceptable to me that Scala 2 signatures be interpreted as bounding types between |
As @IndiscriminateCoding alluded, there are really two questions here. One is the added "expressivity" to the type hierarchy and what the defaults are (which things by default have things like equals and type testing). The other is whether the expressivity is achieved by adding a new type above Any, or by moving some of Any's current power down into an immediate subtype. One difference is if I take e.g. a parameter with the explicit type It seems that it would be easier for learning at least, if the names convey as much as possible. According to that argument, Any should include everything, and there could be a new subtype. Perhaps |
@nafg I tend to agree. It seems the only practicable solution is the maximal one. Every other scheme incurs too much friction between mismatching bounds. But then it's better to leave the extreme types as So we'd have: abstract class Any:
def asInstanceOf
abstract class Classable extends Any:
def isInstanceOf
def getClass
def ==
def !=
def ##
def equals
def hashCode
def toString
class AnyVal extends Classable
class Object extends Classable For migration there would be an implicit conversion from |
Since this is not going in 3.0.0, and it will break TASTy compatibility, it won't be included before 3.T. At that point, binary compat with Scala 2 will be dropped, and therefore the question of how to interpret Scala 2 code is moot: we won't interpret Scala 2 code. For the same reason, the Intermediate Solution is pointless. It's equivalent to the Minimal Solution. Java compatibility is a different problem. It's not only about type parameters coming from Java, but also values of type
|
How would it break TASTy compatibility? |
@odersky How should I understand the name Collections are probably one of the biggest roadblock. They shouldn't require Operations like These collection changes would obviously break TASTy compatibility. |
Yes, something that has a
Sure. But before that we can also leave collections as they are in 2.13. They would be unsafe in the new model, but that's OK for a while. |
How about |
May I ask why we even need it? Is the goal to have a parametric top type?
Maybe Sébastien already suggested something similar, I’m not sure, but could we explicitly store in TASTy the type bounds
I think it would be worth experimenting with a really empty top type (with no members). Personally, I’m not sure |
Some lessons learned from experimenting with it in #10670
But the biggest win in terms of parametricity is achieved anyway by doing (1). (1) prevents us from full featured access including mutations after downcasting. An example of code that would be prevented is in #7314: val imm = IArray(1,2,3) // supposedly immutable
imm match
case a: Array[Int] => a(0) = 0 By comparison, (2) just prevents certain oracles. I.e. say we have def f[A](x: A, y: A): A With (1), we know that This means there's a realistic possibility to get better parametricity by just implementing (1). If we want to do that, then we might want to put everything in now, and just turn on the warning for later. #10670 has shown that this can be done with a very high degree of backwards compatibility. The advantage is that people using opaque types (or abstract types or type parameters, really) can do so with a better degree of safety by opting for stricter checking. How should we deal with the awkward squad in (2) then? For equality and hashCode the ultimate solution would be to make it typeclass-based. We might leave |
One problem is the name |
First of all, really cool that you're making time to see if this could, at least partly, fit partly into 3.0 already! I'm a completely unknown outsider here, but I've been following the Dotty development since 2018 now. Something which I've been looking at is the possibility of migrating the Spores and LaCasa projects to Dotty/Scala 3 and in especially LaCasa, a weak First, I think there are many things which could benefit from a reformed type hierarchy, as has been discussed elsewere. But I think also that this is heavily related to other changes like Explicit Null and Multiversal Equality. If we would take a step back for a second and pretend that we're reinventing the language, and don't have to care about migration pains and how the current collections look, etc, this could be taken to the extreme: Let me dream:What is the absolutely weakest EDIT: This would actually completely solve this if that's desirable:
Moving on from the extreme to the radical:Opaque types opens up the possibility to decouple types in general from what their runtime representation on the JVM is. The methods of trait Equals extends Top:
/** A method that should be called from every well-designed equals method that is open to be overridden in a subclass. */
abstract def canEqual(that: Equals): Boolean
/** Defined equality; abstract here */
def equals(that: Equals): Boolean
/** Semantic equality between values */
final def == (that: Equals): Boolean = this equals that
/** Semantic inequality between values */
final def != (that: Equals): Boolean = !(this == that)
trait Hashable extends Top:
/** Hash code; abstract here */
def hashCode: Int
trait Showable extends Top:
/** Textual representation; abstract here */
def toString: String = …
trait RefEq extends Equals:
final def eq(that: RefEq): Boolean = …
// reference equality
final def ne(that: RefEq): Boolean = !(this eq that)
/** Semantic equality between values */
final def == (that: RefEq): Boolean =
if (null eq this) null eq that else this equals that
/** Semantic inequality between values */
final def != (that: RefEq): Boolean = !(this == that)
trait Synchronizable:
def synchronized[T](body: => T): T // execute `body` in while locking `this`.
trait Scrutable extends Top:
/** Type test; needs to be inlined to work as given */
def isInstanceOf[a]: Boolean
/** Type cast; needs to be inlined to work as given */
def asInstanceOf[A]: A = this match {
case x: A => x
case _ => if (this eq null) this
else throw new ClassCastException()
}
def getClass...
class Object extends AnyRef
with RefEq
with Hashable
with Showable
with Synchronizable
with Scrutable It would also make sense to make Getting back to reality:From a principle of least privilege, I think it would be very desirable to be able to express what you need and nothing more when defining a polymorphic method. It would also be very nice to be able to explicitly define what if any of the Object methods are supported on a class. How to migrate:I think regardless of how much of the hierarchy is changed, there is going to be migration headaches. Any changes will probably have to reside under language flags, otherwise Some ideas: type Any = Equals & Hashable & Showable & Scrutable Classes and traits could extend
Bounds on type parameters and abstract/opaque types is probably the hardest nut to crack. I think regardless of whether something similar to what i described above could be implemented or something less exotic, a solution which maybe could be explored is if bounds could be inferred by the compiler. Without any data to point to, my hunch is that for a significant amount of cases Any is an excessive upper bound. But I don't know if wildcard types or anything else makes that impossible. |
How about |
|
|
It's still far from ideal if you can't put whatever you want in a simple |
Yes, that's true. There's going to have to be implicit conversions. And we don't want to enable Equals, etc simply by putting something into a List and extracting it. The best solution by far would be moving towards type class solutions for |
New developments: |
I've been giving this some more thought. As @odersky already said, moving equality and Question: What if we moved all of these functions out of trait Show[-T]:
extension (self: T) def toString: String
trait Hashable[-T]:
extension (self: T):
def `##`: Int
def hashCode: Int
trait Eq[-T]:
extension (self: T):
def equals(that: T): Boolean
/** Semantic equality between values */
def == (that: T): Boolean = self equals that
/** Semantic inequality between values */
def != (that: T): Boolean = !(self == that)
trait RefEq[-T] extends Eq[T]:
extension (self: T):
/** reference equality */
final def eq(that: T): Boolean = self.asInstanceOf[Object] eq that.asInstanceOf[Object]
final def ne(that: T): Boolean = !(self eq that)
override def == (that: T): Boolean =
if null eq self.asInstanceOf[Object]
then null eq that.asInstanceOf[Object]
else self equals that Then we could provide these instances in Predef: given Show[Any]:
extension (self: Any):
def toString: String =
self.asInstanceOf[Object].toString
given Hashable[Any]:
extension (self: Any):
def `##`: Int = self.asInstanceOf[AnyVal].##
def hashCode: Int = self.asInstanceOf[Object].hashCode
given Eq[Any]:
extension (self: Any):
def equals(that: Any) =
self.asInstanceOf[Object] equals that.asInstanceOf[Object]
given RefEq[AnyRef]:
extension (self: AnyRef):
def equals(that: AnyRef) =
self.asInstanceOf[Object] equals that.asInstanceOf[Object]
An extension method for debugging could be used to bypass the extension (self: Any):
def debugString: String = self.asInstanceOf[Object].toString
I also think this is a very good point.
Alternative definition using extension method:
So, it would actually make a lot of sense if it was actually "defined" closer to this: extension [R](self: Any)
def asInstanceOf: R = self match {
case x: R @unchecked => x
case _ => if (this eq null) this
else throw new ClassCastException()
}
extension [R](self: Byte | Short | Char | Int | Long | Float | Double):
def asInstanceOf: R = self.to<R>
extension [R](self: Null):
def asInstanceOf: R = <default value> This way, Could this be a realistic approach, or are there issues here I'm not seeing? |
We'd like to introduce a parametric top type (call it
Top
for now) with the following properties:Top
is a supertype ofAny
.Any
is the only class directly inheriting fromTop
.Top
isasInstanceOf
X
against a patternP
, bothX
andP
must be instances ofAny
.The question is where
Top
would replaceAny
in the rest of the language. There are several choices.Minimal Solution
Top
is just another type. We have to opt in when using it. For instance, we could writeThis would close the hole described in #7314.
This solution is maximally backwards compatible with current Scala. But it has the downside that abstract or opaque types with
Top
as upper bound are not very useful. They cannot be passed to any regular unbounded type parameter, since such parameters would have upper boundAny
.Intermediate Solution
Opaque types have
Top
as default upper bound. Type parameters and abstract types stay as they are. This would still be backwards compatible with Scala-2, since opaque types are a new construct. But the problems of usability of opaque types remain.Maximal Solution
Unbounded abstract types, opaque types, and type parameters have
Top
as upper bound, instead ofAny
.This means
==
, equals, hashCode, ortoString
defined inAny
on such types. On the other hand, we could re-introduce these methods as extension methods for these types, which was not possible before.This is ultimately the most clear and useful solution, but it poses a lot of problems for migration and interoperability.
Migration: Lots of code will break. Think of HashMaps, for instance. We could alleviate some of the breakage by adding an implicit conversion from
Top
toAny
which would warn each time it is inserted. Not sure how much this would help.Interoperability: How should we interpret a Java or Scala-2 unbounded type parameter? If we assume bounds
Nothing..Any
this would again hamper the usefulness of Scala 3 parametric types. I.e. we could not even doxs.map(f)
ifxs
is of typeList[T]
andT
is unbounded. This looks unacceptable. If we assume boundsNothing..Top
, this means that we accept that the abstraction might be broken in Scala-2 or Java code. This is bad, but better than the alternative. One additional problem is that when reading Scala-2 code we not not even know whether a type parameter is bounded or whether it has explicitly givenNothing..Any
bounds. The two are indistinguishable in the pickle format. So we'd have to widen these bounds unconditionally toNothing..Top
.Questions
Top
?The text was updated successfully, but these errors were encountered: