- Notifications
You must be signed in to change notification settings - Fork 1.1k
Fixes and improvements to erasure #11695
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
Conversation
7f69dc4 to e9c944d Compare 0289612 to c616203 Compare | Currently My intuition is that it would serve a similar purpose as a Phantom type, but in that case I would start with Another one that might be useful if the |
An opinion from the sidelines: I would love to use this to define compile-time-only proofs. Having some kind of proof be an Question: if an
As a user, I think (2) would be awesome 😄 |
| Btw this would be one of my first use-cases for this awesome feature: universal equality proof at compile-time guaranteed to always just translate to erased final class UnivEq[A]: inline def univEq(a: A, b: A): Boolean = a == bI'm currently in the process of defining it like this for Scala 3 without an // Declaring in a different package because inline methods aren't allowed in opaque types companions :( package internal: opaque type UnivEq[A] = Unit object UnivEq: def force[A]: UnivEq[A] = () type UnivEq[A] = internal.UnivEq[A] object UnivEq: inline def force[A]: UnivEq[A] = internal.UnivEq.force extension [A](proof: UnivEq[A]) inline def univEq(a: A, b: A): Boolean = a == bThen of course there are extension method ops but you get the idea. |
So you'll be happy to know that that's what's implemented! |
| Btw, I tried to make the import import language.experimental.erasedbut that failed the bootstrap since erased is a hard keyword under -Yerased-terms. Maybe once |
Oh wow @odersky that is fantastic news!! ❤️ |
| In case you hadn't thought of it yet, |
Yes, once we get a chance to change it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case you hadn't thought of it yet,
=:=and<:<seem like ideal first uses from Scala stdlib.
That is one of the oldest usecases we have (see old gist). But we need to be able to break the binary compatibility of the standard library to make this change. Note that to implement this we do not need erased classes unless we try to naively port the old implementation. Here is how one would start implementing it.
type <::<[-From, +To] type =::=[From, To] <: (From <::< To) erased given [X]: (X =::= X) = scala.compiletime.erasedValue extension [From](x: From) inline def cast[To](using From <::< To): To = x.asInstanceOf[To] // Safe cast because we know `From <:< To`| private val symbolLiterals: TermName = deprecated("symbolLiterals") | ||
| private val namedTypeArguments = experimental("namedTypeArguments") | ||
| private val genericNumberLiterals = experimental("genericNumberLiterals") | ||
| private val macros = experimental("macros") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| private val macros = experimental("macros") | |
| private val scala2macros = experimental("macros") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also use the name macros for Scala 3 macros in the compiler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should split this PR into 2
- Make erased an experimental language feature
- Add new
erased class, find use cases where it is needed and make sure it is sound (phantom types?)
| if (homogenizedView && mods.flags.isTypeFlags) flagMask &~= GivenOrImplicit // drop implicit/given from classes | ||
| val rawFlags = if (sym.exists) sym.flags else mods.flags | ||
| if (rawFlags.is(Param)) flagMask = flagMask &~ Given | ||
| if (rawFlags.is(Param)) flagMask = flagMask &~ Given &~ Erased |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might also want to remove Inline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, inline is per param, not per clause
| * At the start of a parameter block of a method, function or class | ||
| * In a method definition | ||
| * In a `val` definition (but not `lazy val` or `var`) | ||
| * In a `class` or `trait` definition |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have class and trait we should also have object. Maybe for later.
It can be emulated with the following code
type Foo erased val Foo: FooModule = erasedValue erased trait FooModule: /*erased*/ given Foo = erasedValue There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now I'd only do the minimum we need. Instead of erased object you can always write erased val.
| ## Erased Classes | ||
| | ||
| `erased` can also be used as a modifier for a class. An erased class is intended to be used only in erased definitions. If the type of a val definition or parameter is | ||
| a (possibly aliased, refined, or instantiated) erased class, the definition is assumed to be `erased` itself. Likewise, a method with an erased class return type is assumed to be `erased` itself. Since given instances expand to vals and defs, they are also assumed to be erased if the type they produce is an erased class. Finally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a method with an erased class return type is assumed to be
eraseditself
This should not be that case. This looks suspiciously like a rule from the old Phantom types. Return types should not affect the erasedness of a definition. It should behave as an abstract type would.
The restrictions come from the fact that if a method returns an instance of an erased class they must either make the definition erased or they will not be able to instantiate the class as it can only be instantiated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we allow this rule we would effectively be reintroducing some form of phantom type. But with fewer guarantees than the original phantom type. This sounds like a potential source of unsoundness that we would need to check in detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree. If val x: CanThrow becomes erased val x: CanThrow then def x: CanThrow should also become erased def x: CanThrow, and def x(): CanThrow should become erased def x(): CanThrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's a soundness issue since these are purely syntactic conventions, akin to simple desugarings.
| | ||
| object scalax: | ||
| @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - A using clause `(using CanThrow[${E}])`\n - A throws clause in a result type such as `X throws ${E}`\n - an enclosing `try` that catches ${E}") | ||
| erased class CanThrow[-E <: Exception] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| erased class CanThrow[-E <: Exception] | |
| type CanThrow[-E <: Exception] |
there is no point in having the ability to instantiate instances of CanThrow if the only places where we will create them is in
erased given CanThrow[Fail] = erasedValueMaking it a type makes it simpler for users by removing ways they could miss-use this abstraction. It will also compile faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Classes are sticky, types are not. That's why we use a class for erased.
| | ||
| object scalax: | ||
| erased class CanThrow[E <: Exception] | ||
| type CTF = CanThrow[Fail] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not?
type CanThrow[E <: Exception] type CTF = CanThrow[Fail]
It seems like this code can simply be translated to erased types as type UnivEq[A] object UnivEq: erased def force[A]: UnivEq[A] = erasedValue extension [A](erased proof: UnivEq[A]) inline def univEq(a: A, b: A): Boolean = a == bIssue: we cannot have extensions method on erased types (see #11743) P.S. type UnivEq[A] object UnivEq: erased def force[A]: UnivEq[A] = erasedValue inline def eq(a: A, b: A)(using erased UnivEq[A]): Boolean = a == b |
| It seems we do not have any use case for |
| Note: There is a pattern that I have seen since the early days of phantom type. Whenever we try to port a class that should be phantom types, if we try to emulate the original definitions as a class we always end up with awkward and useless code. On the other hand, when we simply started by defining the type and |
| In the current verison of
We also accidentally make the type a subtype of If we what to add phantom type we should start from |
| This is failing erased class A erased class B extends Acould not translate type <::<[-From, +To] type =::=[From, To] extends <::<[From, To] |
| I think the presumed similarity with phantom types is misleading. This is something completely different, with different rules. |
| Now that I understand what it does, the misleading part is that My current understanding is that an erased class defines a type and that any type that derives from will also be an erased type. Then if a term has an erased type it automatically becomes erased. Is that description complete? |
| Inference of erasedness of parameters does not seem to be working import scala.language.experimental.erasedTerms erased class ErasedTerm type <::<[-From, +To] <: ErasedTerm type =::=[From, To] <: (From <::< To) erased given [X]: (X =::= X) = scala.compiletime.erasedValue extension [From](x: From) inline def cast[To](using From <::< To): To = x.asInstanceOf[To] // Safe cast because we know `From <:< To` def convert[A, B](a: A)(using /*erased*/ x: A <::< B): B = // println(x) // error: OK because x should be erased // but currently x is not marked as erased which it should a.cast[B] @main def App: Unit = convert[Int, Int](3) // should not be an error |
That's because types don't generate erased vals; only erased classes do this. And types themselves cannot be erased. |
| Then how do we abstract over capabilities? I would have expected the following to work but I guess it won't erased class Capability def foo[Cap <: Capability](using c: Cap): T = ...Is I remember that this kind of code appeared with some utility functions for capabilities bit I can't remember which one at the moment. |
| Are |
Yes, in a trivial sense, like any other class.
No.
True only if you replace type by class. |
| Type parameters and union / intersection types are never erased. It's just class references as the docs state. The whole thing is just a syntactic convenience, and we want to keep it simple. Maybe we can extend it in the future if use cases demand it. |
eaae050 to ef0341b Compare | | ||
| val dependent = experimental("dependent") | ||
| val erasedTerms = experimental("erasedTerms") | ||
| val erasedDefinitions = experimental("erasedDefinitions") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Erased is not only about definitions. It is also about the term values they provide. This is completely lost with this new name. The erased class concept is also about erasing term definitions and uses as arguments. Therefore, the scope of the feature has not really changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But erased is a modifier on a definition, not a term. Anyway, people usually do not know what a term is. We'd have to say erasedExpressions, but I believe Definitions is preferable, since that's where the erased goes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that if you erase a definition you also erase its RHS, and if you erase a parameter, you also erase its argument follows naturally since it's the only thing that makes sense. But the trigger is the erased on a definition. Or parameter, but I think it's OK to include that in the meaning.
Map erased classes to empty interfaces.
ef0341b to 0af3f59 Compare
erasedon classes as wellFixes #11743