-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add enum construct #1970
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
A very nice explanation of the new feature 👍 There seems to be an inconsistency between the desugaring Rule 5 and the following code example: enum Option[+T] {
case Some(x: T)
case None extends Option[Nothing]
} If I understand correctly, the desugaring Rule 5 says that for the
Another minor question is, it seems the following code in the example expansions does not type check: object Some extends T => Option[T] {
def apply[T](x: T): Option[T] = new Some(x)
} We need to remove the part |
Well spotted. This clause should go to rule 6. I fixed it.
You are right. We need to drop the extends clause. |
In the following introductory example:
I find it a little bit confusing that in the Also, how would that interact with additional type parameters?
|
We have to disallow that. |
Keeping type parameters undefined looks more like an artifact of desugaring and Dotty's type system than a feature to me. Are there any cases where this would actually be useful?
OTOH, covariant type parameters look very useful and are common in immutable data structures. Could this case be simplified?
How about automatically filling in unused type parameters in cases as their lower (covariant) or upper (contravariant) bounds and only leaving invariant type parameters undefined?
Instead of only exposing Java enums to Scala in this way, Is there a well-defined subset of Scala enumerations that can be compiled to proper Java enums for the best efficiency and Java interop on the JVM? |
I'm proposing modification to the enum class Option[+T] extends Serializable {
def isDefined: Boolean
}
object Option {
def apply[T](x: T) = if (x != null) Some(x) else None
case Some[T](x: T) { // <-- changed
def isDefined = true
}
case None extends Option[Nothing] {
def isDefined = false
}
} In this case the T is obviously bound in the scope. It still desugars to the same thing, but I feel it's more regular and it allows to rename the type argument: enum class Option[+T] extends Serializable {
def isDefined: Boolean
}
object Option {
def apply[T](x: T) = if (x != null) Some(x) else None
case Some[U](x: U) extends Option[U] { // <-- changed
def isDefined = true
}
case None extends Option[Nothing] {
def isDefined = false
}
} |
On the meeting, we've also proposed an additional rule: enum class Option[+T] extends Serializable {
def isDefined: Boolean
}
object Option {
def apply[T](x: T) = if (x != null) Some(x) else None
case Some(x: Int) extends AnyRef { // <-- Not part of enum
def isDefined = true
}
case None extends Option[Nothing] {
def isDefined = false
}
} |
@DarkDimius I think this is still insufficient because it is still (a little bit) confusing that the Despite these inconveniences, I think that the shorter syntax is a huge benefit, so I find it acceptable to have just |
One more point discussed on the dotty meeting: there should be additional limitation that no other class can extend |
Sealed classes give less guarantees. The point of this addition is that you cannot get equivalent guarantees from sealed classes.
Given currently proposed rules it can be expressed, you simply need to write it explicitly using the longer vesion. |
Am I understanding correctly that the following occurs? enum IntWrapper {
case W(i:Int)
case N
}
val i = IntWrapper(1)
some match {
case (w:W) =>
w.copy(i = 2)
.copy(i = 3) //this line won't compile because the previous copy returned an IntWrapper
case N => ???
} If so then it seems like |
That's a good argument. I dropped |
AFAICT, any |
For enumerations I would love to see a |
i'd probably never use a naked oh, and if there is a |
This looks great! I don't think the long form is an improvement, though. The enum Either[+L, +R] {
def fold[Z](f: L => Z, g: R => Z): Z
case Left(value: L) {
def fold[Z](f: L => Z, g: Nothing => Z) = f(value)
}
case Right(value: R) {
def fold[Z](f: Nothing => Z, g: R => Z) = g(value)
}
} I don't see any issues here. I agree with Stefan that generics should be handled automatically by default, and have the type parameter missing and filled in as Nothing if the type is not referenced. If you want something else, you can do it explicitly. case Right[+L, +R](value: R) extends Either[L, R] |
Currently this is looking great! I wrote Enumeratum and would be happy to see something like this baked into the language :) Just a few thoughts/questions based on feedback I've received in the past:
|
Compiling to Java enums has some downsides:
This suggests to me that we need an opt-in (or maybe an opt-out) annotation for this compilation strategy. |
Java enums are exposed the the Scala typechecker as though they were constant value definitions: scala> symbolOf[java.lang.annotation.RetentionPolicy].companionModule.info.decls.toList.take(3).map(_.initialize.defString).mkString("\n")
res21: String =
final val SOURCE: java.lang.annotation.RetentionPolicy(SOURCE)
final val CLASS: java.lang.annotation.RetentionPolicy(CLASS)
final val RUNTIME: java.lang.annotation.RetentionPolicy(RUNTIME)
scala> showRaw(symbolOf[java.lang.annotation.RetentionPolicy].companionModule.info.decls.toList.head.info.resultType)
res24: String = ConstantType(Constant(TermName("SOURCE"))) This is something of an implementation detail, but is needed:
The enums from this proposal will need a similar approach, and I think that should be specced. |
@Ichoran The long form is intended to allow for
A played with various variants but found none that was clearer than what was eventually proposed. If one is worried about scoping of the type parameter one could specify that the long form is a single syntactic construct
and specify that any type parameters in |
What about making |
@retronym Thanks for the analysis wrt Java enums. It seems like an opt-in is the best way to do it. How about we take inheritance from
Then there would be no surprise that we cannot redefine Also, can you suggest spec language for the constantness part? |
I agree. This would also be required for most of the useful generic programming stuff we want to do (e.g. automatically generate serializers/deserializers for enumerations based on their name rather than their ordinal). |
@szeiger I agree it would be nice if we could fill in extremal types of co/contravariant enum types, i.e. expand
to
But maybe it's too much magic? Have to think about it some more. |
Agreed. But that means we'd have to design that feature with the generic programming stuff, because it would likely end up on the type level? Not sure abut this point. |
Sorry, that should have been |
For those interested, I fleshed out some implementation of my above proposal, as an experiment. See issue #2055. |
I would love this to be implemented but one thing that has to be supported is the ability to implement Visitor pattern dynamics and have more in the enum than simply the object itself. For explanation look at my post on Stack overflow: http://stackoverflow.com/questions/43152963 This is one of the best ways to leverage enums in a code base to avoid having complex switch logic. |
I integrated @szeiger's proposal
It's now number 3 of the new desugaring rules. The rules is complicated, but it's one stumbling block less for defining simple ADTs. |
`copy` should always return the type of it's rhs. The discussion of scala#1970 concluded that no special treatment for enums is needed.
Based on the discussion in scala#1970, enumeration objects now have three public members: - valueOf: Map[Int, E] - withName: Map[String, E] - values: Iterable[E] Also, the variance of case type parameters is now the same as in the corresponding type parameter of the enum class.
Is there a mapping from this java enum to this proposal? This is a purposely convoluted example of what you can do in Java...
syntactically, the translation would be the following, but It does not appear to be valid:
What I am highlighting here are a few features of java enums:
Scanning the examples in this issue, I did not see any examples where the enum had constructor parameters. I could be blind though. There is a lot of talk about encoding GADT's more simply, which I will certainly use, but not much talk about encoding multiple instances of simple stuff, like the canonical java "Planet" example (https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html), where the only difference is in the values embedded in the enum instances -- no behavior difference. |
A description and an implementation of a translation to Java enums is still work to do. Since I am not very current on the details of java enums, I would appreciate help from others here. I don't think it should be a requirement that we can support all features of java enums, though. Specifically, the example above passes the string associated with a case as a parameter (at least that's how I understand it, I might be wrong here). That's not supported in the proposal, you'd have to override toString instead.
enum classes can have parameters but then enum cases need to use the usual extends syntax to pass them. There's no shorthand for this, as in Java enums. |
@scottcarey I have added a Scala version of the planet example to the description above. Thanks for pointing me to the Java version! |
I made two changes to the proposal.
|
Supporting every possible quirk of Java enums is not necessary, but there is some significant overlap to take advantage of. The best way to think about Java enums is to consider that the singleton pattern in java is: enum ThereCanBe {
ONLY_ONE;
} (as recommended by EffectiveJava for a decade but ignored by bloggers talking about advantages Scala has over Java) Decorate the enum with whatever compiler-known data (member variables) and behavior (methods) you want. With that in mind, this proposal is essentially providing two things that I see:
Both cases can lead to improved performance via dispatching pattern matches with a switch over the ordinal, rather than cascaded instanceof checks. In the first case, where everything is a singleton, this maps neatly to java enums as far as I can see.
I have thought that top-level scala So as this approaches the time where the bytecode encoding to enums is considered, consider it for simple top level objects to. enum ThereCanBe {
case OnlyOne
} de-sugars to roghly the following if I am reading things right: sealed abstract class ThereCanBe
object ThereCanBe {
val OnlyOne = new ThereCanBe {}
} Which can be encoded as a Java enum: enum ThereCanBe {
OnlyOne;
} which isn't that different than an ordinary object singleton object ThereCanBe {
val stuff = "stuff"
} Which encodes as: enum ThereCanBe {
$Instance;
private final String stuff = "stuff";
public String stuff() {
return stuff;
}
} After all, an |
I have made one more tweak: enum utility methods are emitted only if there are some singleton cases and the enum class is not generic. This avoids generation of utility methods for types such as List and Option. The general rationale is that, if the enum class is generic, the utility methods would lose type precision. E.g. |
@scottcarey Interesting idea, to encode singletons as enumerations. Maybe we can use this for the Scala translation, but we'd need a lot of experimentation to find out whether it's beneficial. Note that top-level objects are already heavily optimized. |
Java (language) enumerations may not have a custom superclass, so Otherwise, the encoding is pretty similar to scala objects. public enum Test {
T1
}
|
@odersky One really useful feature in Java is the |
There was a late change in the proposal. It now demands that all type parameters of cases are given explicitly, following @LPTK's proposal of nominal correspondence in that respect. It's more verbose but also makes it clearer what happens. Example: Previously, you wrote:
Now you have to write:
|
@odersky what are the benefits of this? |
@notxcain, fixing oddities in scoping rules. Like those: #1970 (comment) |
Were the significant whitespace proposal to be accepted then it would appear that
IOW, the first level of indentation in an enum block would imply |
Change enum scheme to correspond to new description in issue #1970
@retronym Also note that the java encoding of enums sets a special flag on the generated class, ACC_ENUM. This triggers a lot of special handling in the JVM. Constructors can't be called, even with reflection / unsafe. Serialization is automatic and can not be overridden. From JLS 8.9
However, although there are no standard ways to break the guarantee above, and the well known http://jqno.nl/post/2015/02/28/hacking-java-enums/ tl;dr -- the bytecode emitted can potentially leverage ACC_ENUM in certain cases to gain a tighter guarantee that singleton enums are actually singletons (per classloader, of course). |
This is very exciting! I wonder if we could allow the following syntax (proposed here as /** Typelevel function to compute the index of the first occurrence of type [[X]] in [[L]]. */
enum IndexOf[L <: HList, X] extends DepFn0 {
type Out <: Nat
type Aux[L <: HList, X, N <: Nat] = IndexOf[L, X] { type Out = N }
implicit case IndexOf0[T <: HList, X] extends Aux[X :: T, X, _0] {
type Out = _0
def apply() = Nat._0
}
implicit case IndexOfN[H, T <: HList, X, I <: Nat](implicit i: Aux[T, X, I])
extends Aux[H :: T, X, Succ[I]] {
type Out = Succ[I]
def apply() = Succ[I]
}
} |
I've scanned around this topic looking for a clear rationale on why the suggestion is to have three separate constructs ( edit: This would also avoid the "scope crossing" type parameters mentioned here |
enum was merged a while ago so closing this issue, http://contributors.scala-lang.org/ is a better place to have follow-up discussions. |
Introduction
This is a proposal to add an
enum
construct to Scala's syntax. The construct is intended to serve at the same time as a native implementation of enumerations as found in other languages and as a more concise notation for ADTs and GADTs. The proposal affects the Scala definition and its compiler in the following ways:enum
.scala.Enum
and a predefined runtime classscala.runtime.EnumValues
.This is all that's needed. After desugaring, the resulting programs are expressible as normal Scala code.
Motivation
enum
s are essentially syntactic sugar. So one should ask whether they are necessary at all. Here are some issues that the proposal addresses:Enumerations as a lightweight type with a finite number of user-defined elements are not very well supported in Scala. Using integers for this task is tedious and loses type safety. Using case objects is less efficient and gets verbose as the number of values grows. The existing library-based approach in the form of Scala's
Eumeration
object has been criticized for being hard to use and for lack of interoperability with host-language enumerations. Alternative approaches, such as Enumeratum fix some of these issues, but have their own tradeoffs.The standard approach to model an ADT uses a
sealed
base class withfinal
case classes and objects as children. This works well, but is more verbose than specialized syntactic constructs.The standard approach keeps the children of ADTs as separate types. For instance,
Some(x)
has typeSome[T]
, notOption[T]
. This gives finer type distinctions but can also confuse type inference. Obtaining the standard ADT behavior is possible, but very tricky. Essentially, one has to make the case classabstract
and implement theapply
method in the companion object by hand.Generic programming techniques need to know all the children types of an ADT or a GADT. Furthermore, this information has to be present during type-elaboration, when symbols are first completed. There is currently no robust way to do so. Even if the parent type is sealed, its compilation unit has to be analyzed completely to know its children. Such an analysis can potentially introduce cyclic references or it is not guaranteed to be exhaustive. It seems to be impossible to avoid both problems at the same time.
I think all of these are valid criticisms. In my personal opinion, when taken alone, neither of these criticisms is strong enough to warrant introducing a new language feature. But taking them together could shift the balance.
Objectives
Basic Idea
We define a new kind of
enum
class. This is essentially asealed
class whose instances are given by cases defined in its companion object. Cases can be simple or parameterized. Simple cases without any parameters map to values. Parameterized cases map to case classes. A shorthand formenum E { Cs }
defines both an enum classE
and a companion object with casesCs
.Examples
Here's a simple enumeration
or, even shorter:
Here's a simple ADT:
Here's
Option
again, but expressed as a covariant GADT, whereNone
is a value that extendsOption[Nothing]
.It is also possible to add fields or methods to an enum class or its companion object, but in this case we need to split the `enum' into a class and an object to make clear what goes where:
The canonical Java "Planet" example (https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html) can be expressed
as follows:
Syntax Extensions
Changes to the syntax fall in two categories: enum classes and cases inside enums.
The changes are specified below as deltas with respect to the Scala syntax given here
Enum definitions and enum classes are defined as follows:
Cases of enums are defined as follows:
Desugarings
Enum classes and cases expand via syntactic desugarings to code that can be expressed in existing Scala. First, some terminology and notational conventions:
We use
E
as a name of an enum class, andC
as a name of an enum case that appears in the companion object ofE
.We use
<...>
for syntactic constructs that in some circumstances might be empty. For instance<body>
represents either the body of a case between{...}
or nothing at all.Enum cases fall into three categories:
[...]
or with one or more (possibly empty) parameter sections(...)
.Simple cases and value cases are called collectively singleton cases.
The desugaring rules imply that class cases are mapped to case classes, and singleton cases are mapped to
val
definitions.There are seven desugaring rules. Rules (1) and (2) desugar enums and enum classes. Rules (3) and (4) define extends clauses for cases that are missing them. Rules (4 - 6) define how such expanded cases map into case classes, case objects or vals. Finally, rule (7) expands comma separated simple cases into a sequence of cases.
An
enum
definitionexpands to an enum class and a companion object
An enum class definition
expands to a
sealed
abstract
class that extends thescala.Enum
trait:If
E
is an enum class without type parameters, then a case in its companion object without an extends clauseexpands to
If
E
is an enum class with type parametersTs
, then a case in its companion object without an extends clauseexpands according to two alternatives, depending whether
C
has type parameters or not. IfC
has type parameters, they must have the same names and appear in the same order as the enum type parametersTs
(variances may be different, however). In this caseexpands to
For the case where
C
does not have type parameters, assumeE
's type parameters arewhere each of the variances
Vi
is either'+'
or'-'
. Then the case expands towhere
Bi
isLi
ifVi = '+'
andUi
ifVi = '-'
. It is an error ifBi
refers to some other type parameterTj (j = 0,..,n-1)
. It is also an error ifE
has type parameters that are non-variant.A class case
expands analogous to a case class:
However, unlike for a regular case class, the return type of the associated
apply
method is a fully parameterized type instance of the enum classE
itself instead ofC
. Also the enum case defines anenumTag
method of the formwhere
n
is the ordinal number of the case in the companion object, starting from 0.A value case
expands to a value definition
where
n
is the ordinal number of the case in the companion object, starting from 0.The statement
$values.register(this)
registers the value as one of theenumValues
of theenumeration (see below).
$values
is a compiler-defined private value inthe companion object.
A simple case
of an enum class
E
that does not take type parameters expands toHere,
$new
is a private method that creates an instance of ofE
(see below).A simple case consisting of a comma-separated list of enum names
expands to
Any modifiers or annotations on the original case extend to all expanded cases.
Enumerations
Non-generic enum classes
E
that define one or more singleton cases are called enumerations. Companion objects of enumerations define the following additional members.enumValue
of typescala.collection.immutable.Map[Int, E]
.enumValue(n)
returns the singleton case value with ordinal numbern
.enumValueNamed
of typescala.collection.immutable.Map[String, E]
.enumValueNamed(s)
returns the singleton case value whosetoString
representation iss
.enumValues
which returns anIterable[E]
of all singleton case values inE
, in the order of their definitions.Companion objects that contain at least one simple case define in addition:
A private method
$new
which defines a new simple case value with given ordinal number and name. This method can be thought as being defined as follows.Examples
The
Color
enumerationexpands to
The
Option
GADTexpands to
Note: We have added the
apply
method of the case class expansion becauseits return type differs from the one generated for normal case classes.
Implementation Status
An implementation of the proposal is in #1958.
Interoperability with Java Enums
On the Java platform, an enum class may extend
java.lang.Enum
. In that case, the enum as a whole is implemented as a Java enum. The compiler will enforce the necessary restrictions on the enum to make such an implementation possible. The precise mapping scheme and associated restrictions remain to be defined.Open Issue: Generic Programming
One advantage of the proposal is that it offers a reliable way to enumerate all cases of an enum class before any typechecking is done. This makes enums a good basis for generic programming. One could envisage compiler-generated hooks that map enums to their "shapes", i.e. typelevel sums of products. An example of what could be done is elaborated in a test in the dotty repo.
The text was updated successfully, but these errors were encountered: