Finalising Enumerations for Scala 3

You can still use scala.Enumeration in Scala 3, though, right? Or has it been deprecated because of the new enum design?

Oh, and because you asked.

3 Likes

I don’t see that it has even been deprecated.

PSA: Deprecation does not imply elimination.

I am pleased and embarrassed to learn that my newbie frustration is preserved on the internet. That must mean my suffering was not in vain.

The thread includes the classic reply:

You can use Java enums. But Scala will never have them.

That was a long time ago. Fortunately, I have forgotten all my pain points with Scala 2 and eagerly look forward to Scala 3.

3 Likes

sure, but practically no one uses it in Scala 2 anyway

1 Like

When an enum includes parameterized cases the values companion member is not generated (correctly, as it would need to include null elements). However the fromOrdinal member is generated, and is undefined at ordinals corresponding to parameterized cases. I find this surprising because fromOrdinal is not an inverse of ordinal in these cases (a roundabout way of saying it’s kind of weird to have fromOrdinal at all for such data types)

My initial suggestion was to generate fromOrdinal only in the same circumstances where values is generated; but it turns out readResolve for non-parameterized cases relies on fromOrdinal, so maybe we should make it private in this case?

4 Likes

The reasoning for fromOrdinal always being public is for a third party framework to always have a way to efficiently recover a singleton value from some canonical representation, which is needed for example maybe in a third party serialisation library, which would need to serialise the singleton values of an enum that also has parameterized cases. Perhaps ordinal is not the most obvious representation, or a fromOrdinal alternative should be left exclusively to frameworks to implement themselves.

1 Like

First, apologies that I’m not acquainted enough with internals to speak about the serialisation aspects.|

This reply is w.r.t. fromOrdinal / toOrdinal / values being generated at all or not, as opposed to whether fromOrdinal is changed to be a total function.

@tpolecat Yes, and since we are talking about inverse here, I think it’s also weird to have ordinal defined. But as it stands that one exists on the concrete value, or not, depending on whether it’s a nullary constructed value.

I do believe that fromOrdinal, toOrdinal and values should uniformly exist (or not) across the whole data type iff it is a coproduct of nullary data constructors.

package enumz

object Test {
  def main(args: Array[String]): Unit = {

    println(Nope.A.ordinal) // This should not compile
    //println(Nope.B.ordinal) // ok, value ordinal is not a member of object enumz.Nope
    //println(Nope.values) // ok, value values is not a member of object enumz.Nope.
                         // Although class Nope is an enum, it has non-singleton cases,
                         // meaning a values array is not definedbloop

    println(Nope.fromOrdinal(0)) // this should not compile
    println(Nope.fromOrdinal(1)) // this should not compile, blows up at runtime
  }
}


enum Nope {
  case A
  case B(x: Int)
  case C
}

Internals aside, any alternative behaviour is at the very least very surprising. Hopefully this is not a controversial opinion at all?

How about we make fromOrdinal return null instead of throwing an exception, that way those who want fromOrdinal to return an Option, need only wrap the call in Option(_).

So instead of Try(E.fromOrdinal(i)).toOption it’s simply Option(E.fromOrdinal(i)).

2 Likes

Does fromOrdinal really need to exist? If it’s added it will be in the autocomplete namespace of every enum, which is one of the headline features of Scala 3 and is so convenient that I expect sealed hierarchies will become extinct.
I don’t think it is a good idea to add another partial function to that autocomplete list. The proposed usage (for serialization) seems extremely sketchy to me as it is not stable if somebody decides to reorder the enum cases to be alphabetical or something. Java explicitly avoids this trap:

Enum constants are deserialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not transmitted. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the static method Enum.valueOf(Class, String) with the enum constant’s base type and the received constant name as arguments.

I guess there is an argument that this is much less efficient than using an Int but I see people troll themselves with List#head, Option#get with such regularity that it’s not funny anymore. People will use it if it is there and it won’t be able to be removed for a really long time.

EDIT: Or at least, could it be called unsafeFromOrdinal?

4 Likes

Yes, I think this needs to be supplied at least as one method out of several, or the feature is essentially broken for high-performance use. You can’t tolerate either exceptions (~1000x penalty for bad case) or Option (~3x slower for everything always). Given the high-performance version, you can get the others; but given the others, you’re always stuck.

So, yes, a null variant would be great. I don’t know if that should be the main variant. fromOrdinalOrNull, like applyOrNull, would be reasonable.

(I also agree that things that can’t possibly work shouldn’t compile.)

1 Like

I like sealed hierarchies because it prevents me making mistakes with refactoring, not just because I can’t remember the names the first time around.

Interesting, what do sealed hierarchies give you that Scala 3.x enums don’t? They both do exhaustivity checking right?

If you want it not to appear in autocomplete by default, the generated method could be private[scala] and there could be extension methods that could be imported.

This would also resolve the issue of having both lower level and safer versions without generating twice as much code. The extension methods are defined once and could include as many varieties as desired. Most people shouldn’t be working with ordinals anyway and therefore can just not add them import.

1 Like

Yes, as far as I know. I’m just saying that autocomplete is not a replacement for exhaustivity. Autocomplete saves you a little time while coding; exhaustivity checking lets you refactor with confidence that sneaky bugs are impossible.

Yes I agree completely, my point about autocomplete is just that if we add unsafe methods to the public API of every enum, people will use them without a lot of care because they will be suggested by their IDE. Especially if they have an innocuous name. I hope if we add such a method it will be unsafeFromOrdinal or accursedUnutterableNotConsistentUnderRefactoringFromOrdinal. I also like @nafg’s suggestion.

3 Likes

Re: fromOrdinal

My apologies if this has been broached already.

There is an implied type conversion from Int to [0..N)

The argument type of fromOrdinal should be [0..N) not Int

1 Like

hell no! Nulls should not appear in any APIs unless explicitly mentioned in the name (such as fromOrdinalOrNull), in my opinion.

I see a lot of value in having a fromOrdinal that throws (for working efficiently due to the lack of Option wrapping), as long as there’s a fromOrdinalOption that returns an Option. That way we still provide a way for users to specify how that “missing int” is to be handled (instead of relying on the exception being thrown).

About the argument type being [0..N)as suggested by @gabrieljones - this makes it impossible to call when all you have is an Int. Users would be forced to do a type check which could be built-in into the method we’re discussing.
Edit: By the above, I mean it shouldn’t be the only way to construct a value from some kind of Int. But I’d gladly welcome having both that (a strongly typed function that takes [0..N) and never throws or wraps into Option) and the more dynamic ones (Int => Option[MyEnum], Int => MyEnum).

4 Likes

Hey Jakub,

What’s your take on Finalising Enumerations for Scala 3 that Rob pointed out, and my expansion on this in Finalising Enumerations for Scala 3 ?

I feel it is the more central issue. There should be an agreement under what circumstance it is correct at all to generate fromOrdinal and toOrdinal, and after that discuss what their type signature should be.

I think neither of these methods (fromOrdinal, toOrdinal, values) should be generated when there are parameterized cases.

IMO: An enum is either an enumeration (equivalent to a sealed trait with just case objects implementing it) or something else.

I’ll skip over that I think “enum” is confusing when cases can be parameterized because I don’t see this syntax changing at this point :confused:

6 Likes

We agree in spirit.

  1. Ideally we wouldn’t even be having this discussion, we’d just have fromOrdinal (which throws) and fromOrdianlOption but the problem is Martin keeps rejecting all this stuff for some reason so now we need to think of… compromises.
  2. Don’t forget that we’re going to have safe null support in Scala 3 so the null returning method would be fromOrdinal(i: Int): E | Null with the null safely included in the type-system which IIUC would require some kind of null checking before one could get to the E.

@japgolly - It is absolutely not ideal to have fromOrdinal which throws and fromOrdinalOption which doesn’t. You still have no high-performance method that works in the presence of errors!