Proposal for Enumerations in Scala

Or maybe just using the good old syntax?

sealed trait Json {
  def foo: Int
}

object Json {
  case class Bool(value: Boolean) extends Json {
    def foo = if (value) 1 else 0
  }
  case class Array(items: Seq[Json]) extends Json {
    def foo = items.size
    def bar = foo
  }
}

Seems a lot cleaner to me.

Clean/pollution is in the eye of the beholder, it seems. :smile:

1 Like

Perhaps, but then is it worth spending time on a new syntax-sugar feature that looks pretty much like before and does not reduce boilerplate?

But I think it must be reiterated. An enum construct with severe limitations and a very low ceiling on what it can do:

  1. can’t have subhierarchies
  2. can’t declare methods for branches (without a workaround)
  3. can’t inherit new traits in branches
  4. can’t declare implicits for branches
  5. all while types of the branches are reachable through pattern matching and must be for GADTs to work – making the argument that .apply widens irrelevant, meaning that all the above concerns are very relevant as a programmer will observe the subtypes of branches daily

Is just un-Scala! it’s a construct that does not scale with usage, that does not help contain complexity, but gives up at a certain point of complexity and forces a retreat to a low-level construct. This really goes against the principle of scaling with the codebase and how the other language constructs scale really well. You could argue that case class is also too limited and doesn’t scale, but I don’t think enums will have the success of case classes, not when e.g. nearly all the sealed hierarchies in my libraries are multi-level, it’s not worth it to use a different syntax for the minority of them that are simplistic.

EDIT: LPTK’s post clarifies some of the capabilities of enums, but still, making workarounds for new features before they’re even out is too much, telling newcomers “just make a private trait if you want to add methods to enum branch” and having to remember that yourself is hardly practical.

2 Likes

Nice workaround, but it’s a workaround for a feature that isn’t even released yet! I’d rather have a new release of the language make the ‘book of hacks & workarounds’ thinner and have widely used capabilities available in a straightforward mannger, not add even more weirdness to the language.

2 Likes

Wait until he has at least 500 lines of methods in each case. Then we’ll see if he still thinks it looks a lot cleaner :grin:

Separation of concerns - Wikipedia :slight_smile:

1 Like

For me personally the cleanest way to deal with this is pattern matching and not the typical inheritance polymorphism:

enum Json {
  case Bool(value: Boolean)
  case Array(items: Seq[Json])
 
  def foo: Int = this match {
     case Bool(value) => ???
     case Array(items) => ???
  }
}

I find this quite pleasant, and it saves quite a few keystrokes compared to the status quo. IIRC this is also the reason methods are dropped from the cases. Since pattern matching is quite natural in combination with ADTs. Though it remains subjective of course if this is really easier to read (IMO it is).

Most importantly (for me), constructors will return the type of the ADT and not the specialized branch, which prevents quirks and also saves on boilerplate on smart constructors:

// instead of having type Some[Int]
def some[A](value: A): Option[A] = Some(value) 
3 Likes

I’ve used this a couple times in my “try out Dotty” project, and it works really, really well for the simple case of an enumerable set of values.

For creating an ADT or GADT, my guess is this will quickly become a “cute trick” and be relegated obscurity, similar to how you can define a class such that every instance is an extractor, but almost nobody actually does (Regex notwithstanding, as most people seem to treat that as compiler magic, even though it’s not).

1 Like

That’s interesting, since so far everybody else I talked to is strongly in favor of dropping this feature! So it would be good to see arguments why you think it’s important to have.

AFAIK the canonical (or at least what seems to be the most common) example is what’s inferred if you do something like this:

(_: List[Foo]).foldLeft(Foo.Empty)(_ combine _)

Most times you’d want this to return Foo, but the compiler complains that combine returns a Foo and it expects a Foo.Empty.type.

Granted, if you ever actually need something to return Foo.Empty.type, then getting back down there from Foo is a royal pain, and upcasting from Foo.Empty.type is pretty easy (even if it does require some annoying boilerplate).

I’m in favor of returning the more precise type, as it’s easier to work around when the default is wrong, but I can certainly empathize with the annoyance of having to manually create a smart constructor. A better solution for me would be to return the precise type and synthesize smart constructors and an .upcast method to make it easier to get to the type the compiler needs.

1 Like

That particular type inference problem might be solvable: When faced with the problem of instantiating
a type variable X with constraint C <: X where C is a case of enum E, we could in some situations instantiate X to E instead of to C. Similar automatic widenings happen for singleton types and union types already.

5 Likes

The problem at hand was to add methods to individual cases, not methods on the whole enum. This cannot be done with pattern matching. I also added a whole-enum method foo in my example just to illustrate that you can also do it.

1 Like

It comes up all over the place with type inference! I much prefer the constructors to not reveal the branch of the ADT, if we’re going to have separate syntax for enums.

For instance var a = Some(x); while (p) { a = Option(foo) } doesn’t work in Scala 2.

So I’m in @bmeesters’s camp on this one. :+1: for constructors returning the type of the ADT!

(Except if the branches have their own type you do need a way to get a thing of that type. If you have both an autogenerated apply method and a constructor, the constructor can be exact and the apply return the ADT, for instance.)

3 Likes

Yes, fair enough, that seems to be a limitation. That said, I can’t remember if I ever did that. Normally I put all the methods in the sealed trait, so there are no method definitions in the specialized branches at all. And therefore it wouldn’t be a deal breaker for me. Though it might not be a style of programming everyone is comfortable with.

Yes as mentioned by @morgen-peschke and @Ichoran, problems often occur with type inference. Almost every fold I write in combination with ADTs need an explicit annotation to not go wrong.

If this can be solved by changing the type inference algorithm than I would not be against dropping this feature.

3 Likes

@bmeesters @Ichoran Good to know. While folds look potentially solvable for type inference, variable type inference does not. So there will still be corner cases.

The main argument for treating enum constructors like normal class constructors was typelevel programming. There you really would like to have the precise types. I believe that’s something we
need to discuss further.

3 Likes

Why isn’t using new Option.Some(2) there the answer?

I am not quite sure. It would be good if the people who proposed the change for better typelevel programming would weigh in here.

1 Like

I really like enums exactly the way they are designed: with constructors for the terms yielding the type of the enum, rather than the more specific type of the sum. I don’t really view enum as being a shorthand for defining a sealed trait, although of course it is that, but rather, a canonical / idiomatic way to define a sum type. It’s exactly what Scala is missing for sum types.

Like several others here, I’d view yielding the specific sum type as a step backward for enum, because as others have mentioned, this yields suboptimal type inference. The number of times you want Some(1) to type as Some[Int] rather than Option[Int] is insignificant in compared to the number of times you want the latter.

I also think defining methods in the enum class directly, pattern matching over the terms, is ideal, because it consolidates the logic for a given method. The more object-oriented style of scattering the logic for a single method across many different subtypes is less useful for a “pure data” entity like enum; and anyway, you have ordinary traits / sealed traits for that use case.

In summary, I’m quite excited for enums as they are designed today, and without any support for multi-level enums, which could be useful but in my opinion should wait, because the feature doesn’t exist in other programming languages and is definitely beyond MVP.

4 Likes