Skip to content

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

Closed
odersky opened this issue Dec 5, 2020 · 23 comments · Fixed by #10670
Closed

How to Introduce a Parametric Top Type? #10662

odersky opened this issue Dec 5, 2020 · 23 comments · Fixed by #10670
Assignees
Milestone

Comments

@odersky
Copy link
Contributor

odersky commented Dec 5, 2020

We'd like to introduce a parametric top type (call it Top for now) with the following properties:

  • Top is a supertype of Any.
  • Any is the only class directly inheriting from Top.
  • The only member of Top is asInstanceOf
  • To check a scrutinee X against a pattern P, both X and P must be instances of Any.

The question is where Top would replace Any 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 write

opaque type IArray[+T] <: Top = Array[T]

This 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 bound Any.

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 of Any.
This means

  1. we cannot pattern match on such types anymore
  2. we cannot use the ==, equals, hashCode, or toString defined in Any 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 to Any 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 do xs.map(f) if xs is of type List[T] and T is unbounded. This looks unacceptable. If we assume bounds Nothing..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 given Nothing..Any bounds. The two are indistinguishable in the pickle format. So we'd have to widen these bounds unconditionally to Nothing..Top.

Questions

  • Is it at all feasible to introduce Top?
  • What is the best solution (or maybe we need a sequence of steps)?
  • What is the migration story?
  • What timeline is realistic for this?
@sjrd
Copy link
Member

sjrd commented Dec 5, 2020

Just to be clear: you're not aiming to do any of this for 3.0.0, are you?

@odersky
Copy link
Contributor Author

odersky commented Dec 5, 2020

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 -Yexplicit-nulls. Even that is a loong stretch.

@djspiewak
Copy link

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.

@IndiscriminateCoding
Copy link

Given that Maximal Solution is breaking lots of things anyway - doesn't it makes sense to also rename Any -> Object and Top -> Any?

@LPTK
Copy link
Contributor

LPTK commented Dec 5, 2020

I think the truly maximal approach would be to also make traits and classes by default extend Top instead of AnyRef. Most traits are not meant to be matched anyway, and it would be nice to opt out of having equals and co. defined by default on every class. Case classes, sealed traits and enums would obviously still extend AnyRef by default.

It seems completely acceptable to me that Scala 2 signatures be interpreted as bounding types between Nothing..Top instead of Nothing..Any. It's a small unsoundness to pay for providing a reasonable migration story towards a great improvement of the language.

@nafg
Copy link

nafg commented Dec 6, 2020

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 Any, am I affected?

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 Scrutable or something in that direction?

@odersky
Copy link
Contributor Author

odersky commented Dec 6, 2020

@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 Any and Nothing. So Any is the new Top. Most Any methods would go to a new trait or abstract class, Scrutable (or Inspectable or Classable/Classed/Classified?`).

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 Any to Classable that issues a warning. Another useful addition would be an extension method toString on Any. toString is pervasively used and relatively harmless. If it's not wanted for some code the extension method could be unimported,

@sjrd
Copy link
Member

sjrd commented Dec 6, 2020

How should we interpret a Java or Scala-2 unbounded type parameter?

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 Object. Realistically, we'll have to map them to Any (was Top) I think.

toString() will probably have to be allowed in some way or another. Or are we also going to prevent using Anys in string interpolation? That would also mean no way to print the contents of collections, and in general significantly reduce the ability to do print-based debugging.

@odersky
Copy link
Contributor Author

odersky commented Dec 6, 2020

How would it break TASTy compatibility?

@LPTK
Copy link
Contributor

LPTK commented Dec 6, 2020

@odersky How should I understand the name Classable? Something that can be "classed"? It sounds a bit weird.

Collections are probably one of the biggest roadblock. They shouldn't require Classable type arguments, otherwise they will become unusable (can't make a list of values of unbounded generic type, for instance).

Operations like distinct should probably be made to require a type class for equals and hashCode (which could have a default implementation for all Classable types), and collection types like Set and Map likewise. This may be a good thing as it will allow not using multiversal equality, which could improve performance.

These collection changes would obviously break TASTy compatibility.

@odersky
Copy link
Contributor Author

odersky commented Dec 6, 2020

@odersky How should I understand the name Classable? Something that can be "classed"? It sounds a bit weird.

Yes, something that has a getClass. Not sure about what's the best name myself.

These collection changes would obviously break TASTy compatibility.

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.

@odersky
Copy link
Contributor Author

odersky commented Dec 6, 2020

How about Classy?

@julienrf
Copy link
Contributor

julienrf commented Dec 6, 2020

  • The only member of Top is asInstanceOf

May I ask why we even need it? Is the goal to have a parametric top type?

  • Interoperability: How should we interpret a Java or Scala-2 unbounded type parameter?

Maybe Sébastien already suggested something similar, I’m not sure, but could we explicitly store in TASTy the type bounds Nothing..Top of unbounded type parameters compiled with Scala 3? When reading Scala 2 classfiles, we would use the bounds Nothing..Any.

  • Migration: Lots of code will break. Think of HashMaps, for instance.

toString() will probably have to be allowed in some way or another. Or are we also going to prevent using Anys in string interpolation? That would also mean no way to print the contents of collections, and in general significantly reduce the ability to do print-based debugging.

I think it would be worth experimenting with a really empty top type (with no members). Personally, I’m not sure toString does more good than harm. Of course, this would mean redesigning the collections to take an implicit Show[A] to print the collection elements, and to take an implicit CanEqual[K] (or Hash[K]) to compare Map keys. But this could be an interesting experiment.

@odersky
Copy link
Contributor Author

odersky commented Dec 7, 2020

Some lessons learned from experimenting with it in #10670

  1. Adding a class Scrutable and putting isInstanceOf like behavior in it is quite feasible. In Add Matchable trait #10670, there are three operations that are now only possible on Scrutable instances: getClass, isInstanceOf, and deconstruction with constructor patterns or typed patterns. An implicit conversion maps from Any instances to Scrutable instances. The conversion can be tuned to be on only for certain language versions or under certain flags.

  2. It would be much harder to migrate equality, hashCode, and toString. The problem is that these operations are recursive, so they cannot really be expressed as a member of a universal trait. This affects all case classes. For instance, :: and with it List cannot have a sound equality since the element type of lists is not bounded by Scrutable. Nor can they have hashCodes and toStrings if these operations are moved to Scrutable. So moving these operations to Scrutable would affect fundamentally what code we guarantee in case classes and how we construct libraries. I do not see a way to do this without causing huge migration pain.

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 f will return either x or y (always assuming no casts or unchecked matches). With full-featured parametricity we'd know in addition that f will either always return x or always return y. But that guarantee holds only in a pure functional language, where no external inputs are allowed. If f had access to the current time, or a simple random generator, or could read a mutable variable, that second guarantee would be destroyed anyway. So it seems to me that for getting useful theorems for free (1) is much more important than (2).

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 equals and hashCode as they are in Java (we don't really have a choice there), but could work to re-interpret == and ##. Or invent other equality and hashcode operators (===, ### anyone?), but I would really prefer if we could avoid that. For toString, I believe it is useful as an inspection tool for debugging, so I'd leave it where it is, and invest in a separate typeclass-based show instead.

@odersky
Copy link
Contributor Author

odersky commented Dec 7, 2020

One problem is the name Scrutable which is somewhat cute for experts, but unfriendly for beginners. Since this will be in every diagram of top-level classes it's important to have a friendly name. So, maybe Classy is not so bad? Or are there other candidates?

@mbloms
Copy link
Contributor

mbloms commented Dec 7, 2020

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 Top type would help tremendously. For this reason I've been giving this quite a lot of thought, and even though I would've hoped to think through the edge cases a bit more, I would like to share my thoughts, which I hope are considered helpful:

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 Top type one could think of? I would say an interesting type which is currently impossible to express in Scala's type system is a reference which is stack local. I'll call it Borrowed for lack of a better name.
Borrowed can only be passed as parameters and returned from functions, but never be saved to fields or variables. If we had that, permissions could be guaranteed never to be saved on the heap, and borrowing patterns from Rust could be emulated. (To be clear, I would never suggest this would be the default in any case.)
This is extreme, but let me at least dream. It could be interesting to know what other cases are impossible to express in the current type system.

EDIT: This would actually completely solve this if that's desirable:

With (1), we know that f will return either x or y (always assuming no casts or unchecked matches). With full-featured parametricity we'd know in addition that f will either always return x or always return y. But that guarantee holds only in a pure functional language, where no external inputs are allowed. If f had access to the current time, or a simple random generator, or could read a mutable variable, that second guarantee would be destroyed anyway. So it seems to me that for getting useful theorems for free (1) is much more important than (2).

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.
In a bigger perspective, there is no reason every class must inherit Java's Object. Any already erase to Object in interops with Java, so there is precedence for pretending things that Java interprets as Object is something weaker/safer in Scala.

The methods of Any could all be factored out to traits that inherit Top:

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...

Object could be an subtype of AnyRef instead of an alias:

class Object extends AnyRef
  with RefEq
  with Hashable
  with Showable
  with Synchronizable
  with Scrutable

It would also make sense to make Null a subtype of Object. Everything coming from Java, legacy Scala, or explicitly extending Object would be a subtype of Object. It makes sense that things coming from Java and legacy Scala can be nullable.

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:
If we want to keep Any, it could be an alias like this:

type Any = Equals & Hashable & Showable & Scrutable

Classes and traits could extend Object by default.
We could have unsafe implicit conversions that print warnings:

given unsafeAsAny: Conversion[Top,Any]

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.

@mbloms
Copy link
Contributor

mbloms commented Dec 7, 2020

One problem is the name Scrutable which is somewhat cute for experts, but unfriendly for beginners. Since this will be in every diagram of top-level classes it's important to have a friendly name. So, maybe Classy is not so bad? Or are there other candidates?

How about Matchable?

@mbloms
Copy link
Contributor

mbloms commented Dec 7, 2020

...If we give the elements of :: an upper bound Showable & Hashable & Equals, the recursive definitions of toString and equals work as before!...
EDIT: I didn't think this one through. I misunderstood what Martin meant above.

@arturopala
Copy link
Contributor

One problem is the name Scrutable which is somewhat cute for experts, but unfriendly for beginners. Since this will be in every diagram of top-level classes it's important to have a friendly name. So, maybe Classy is not so bad? Or are there other candidates?

AnyClass ?

@Jasper-M
Copy link
Contributor

Jasper-M commented Dec 7, 2020

If we give the elements of :: an upper bound Showable & Hashable & Equals, the recursive definitions of toString and equals work as before!

It's still far from ideal if you can't put whatever you want in a simple List.

@mbloms
Copy link
Contributor

mbloms commented Dec 7, 2020

If we give the elements of :: an upper bound Showable & Hashable & Equals, the recursive definitions of toString and equals work as before!

It's still far from ideal if you can't put anything you want in a simple List.

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 Show and CanEqual and Hashable. This is actually just as hard as trying to define a recursive clone method on a generic class.

@odersky
Copy link
Contributor Author

odersky commented Dec 7, 2020

New developments: Scrutable has been renamed to Matchable and it's now a trait instead of a class. So the diagram of toplevel classes is as before. The only new thing is that AnyVal and AnyRef both inherit an additional trait Matchable, which contains getClass and isInstanceOf.

@odersky odersky linked a pull request Dec 7, 2020 that will close this issue
@mbloms
Copy link
Contributor

mbloms commented Dec 8, 2020

we cannot use the ==, equals, hashCode, or toString defined in Any on such types. On the other hand, we could re-introduce these methods as extension methods for these types, which was not possible before.

Another useful addition would be an extension method toString on Any. toString is pervasively used and relatively harmless. If it's not wanted for some code the extension method could be unimported,

It would be much harder to migrate equality, hashCode, and toString. The problem is that these operations are recursive, so they cannot really be expressed as a member of a universal trait.

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.

I've been giving this some more thought. As @odersky already said, moving equality and toString to a universal trait doesn't really help. It would mean a migration headache without any real benefit. For List for example, there would have to be one List[T <: Showable] which extends Showable and one List which doesn't. It's a problem better solved using type classes.

Question: What if we moved all of these functions out of Any and made them extension methods on type classes instead? If we provide type class instances on Any and AnyRef, this change would make little to no difference now, but would make moving towards a type class approach in the future easier? Example:

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]

For toString, I believe it is useful as an inspection tool for debugging

An extension method for debugging could be used to bypass the Show instance entirely:

extension (self: Any):
  def debugString: String = self.asInstanceOf[Object].toString
  • The only member of Top is asInstanceOf

May I ask why we even need it? Is the goal to have a parametric top type?

I also think this is a very good point. asInstanceOf is the source of massive bike shedding in virtually every discussion regarding the type system. While I understand why it's desirable or even necessary to have a universal escape hatch, asInstanceOf with it's double role as a type converter actually behaves more like an overloaded function than a method with dynamic dispatch:

scala> val lst: List[Int] = List(1,2,3,4)
val lst: List[Int] = List(1, 2, 3, 4)

scala> val xs: List[Any] = lst
val xs: List[Any] = List(1, 2, 3, 4)

scala> lst.map(_.asInstanceOf[Double])
val res0: List[Double] = List(1.0, 2.0, 3.0, 4.0)

scala> xs.map(_.asInstanceOf[Double])
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Double (java.lang.Integer and java.lang.Double are in module java.base of loader 'bootstrap')
  at scala.runtime.BoxesRunTime.unboxToDouble(BoxesRunTime.java:112)
  at $anonfun$res1$1(<console>:1)
  at $anonfun$res1$1$adapted(<console>:1)
  at scala.collection.immutable.List.map(List.scala:246)
  ... 33 elided

Alternative definition using extension method:

scala> object pre:
     |   extension [R >: Double <: Double](self: Int) def alsInstanzVon: R = self.toDouble
     |   extension [R](self: Any) def alsInstanzVon: R = self.asInstanceOf[R]
     | 
// defined object pre

scala> import pre._

scala> val lst: List[Int] = List(1,2,3)
val lst: List[Int] = List(1, 2, 3)

scala> val xs: List[Any] = lst
val xs: List[Any] = List(1, 2, 3)

scala> lst.map(_.alsInstanzVon[Double])
val res0: List[Double] = List(1.0, 2.0, 3.0)

scala> xs.map(_.alsInstanzVon[Double])                                                                                                                                              
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Double (java.lang.Integer and java.lang.Double are in module java.base of loader 'bootstrap')
        at scala.runtime.BoxesRunTime.unboxToDouble(BoxesRunTime.java:112)
        at rs$line$6$.$init$$$anonfun$1(rs$line$6:1)
        at scala.collection.immutable.List.map(List.scala:246)
        at rs$line$6$.<clinit>(rs$line$6:1)
        at rs$line$6.res1(rs$line$6)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at dotty.tools.repl.Rendering.$anonfun$3(Rendering.scala:84)
        at scala.Option.map(Option.scala:242)
        at dotty.tools.repl.Rendering.valueOf(Rendering.scala:84)
        at dotty.tools.repl.Rendering.renderVal(Rendering.scala:121)
        at dotty.tools.repl.ReplDriver.$anonfun$13(ReplDriver.scala:310)
        at scala.collection.immutable.List.flatMap(List.scala:293)
        at scala.collection.immutable.List.flatMap(List.scala:79)
        at dotty.tools.repl.ReplDriver.extractAndFormatMembers$2(ReplDriver.scala:310)
        at dotty.tools.repl.ReplDriver.renderDefinitions$$anonfun$2(ReplDriver.scala:331)
        at scala.Option.map(Option.scala:242)
        at dotty.tools.repl.ReplDriver.renderDefinitions(ReplDriver.scala:334)
        at dotty.tools.repl.ReplDriver.compile$$anonfun$2(ReplDriver.scala:253)
        at scala.util.Either.fold(Either.scala:189)
        at dotty.tools.repl.ReplDriver.compile(ReplDriver.scala:269)
        at dotty.tools.repl.ReplDriver.interpret(ReplDriver.scala:197)
        at dotty.tools.repl.ReplDriver.loop$1(ReplDriver.scala:130)
        at dotty.tools.repl.ReplDriver.runUntilQuit$$anonfun$1(ReplDriver.scala:133)
        at dotty.tools.repl.ReplDriver.withRedirectedOutput(ReplDriver.scala:152)
        at dotty.tools.repl.ReplDriver.runUntilQuit(ReplDriver.scala:133)
        at dotty.tools.repl.Main$.main(Main.scala:6)
        at dotty.tools.repl.Main.main(Main.scala)

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, Any becomes a truly parametric top type with no members, without actually incurring any migration headaches. The extension methods won't clash with user definitions in classes, because "real" methods take precedence, and sometime in the future the default Any instances could be dropped.

Could this be a realistic approach, or are there issues here I'm not seeing?

@odersky odersky added this to the 3.0.0-M3 milestone Dec 10, 2020
@odersky odersky removed this from the 3.0.0-M3 milestone Dec 14, 2020
@Kordyjan Kordyjan added this to the 3.0.0 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.