-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Fixes and improvements to erasure #11695
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 == b I'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 == b Then 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.erased but 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?)
@@ -925,7 +925,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { | |||
else PrintableFlags(isType) | |||
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
erased
itself
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] = erasedValue
Making 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 == b Issue: 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 A could 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
@@ -24,7 +24,7 @@ object Feature: | |||
private val scala2macros = experimental("macros") | |||
|
|||
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
erased
on classes as wellFixes #11743