Improved (?) translation of enums and ADTs

In the current implementation, we have useful translation rules for ADTs such as Option:

enum Option[+T]:
  case Some(x: T)
  case None

is equivalent more or less, to:

enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[Nothing]

From experiments, however, it’s not so useful for ADTs such as Result:

enum Result[+A]:
  case Success(a: A)
  case Failure(err: String)

Which is equivalent, more or less, to:

enum Result[+A]:
  case Success(a: A) extends Result[A]
  case Failure(err: String) extends Result[A]

It would probably be possible to infer that Failure is a Result[Nothing] since none of its fields is of type A, rather than limit the heuristic to does it have fields?.

It would probably be possible to extend the heuristic further, for types like Either:

enum Either[+A, +B]:
  case Left(a: A)
  case Right(b: B)

This could be equivalent to:

enum Either[+A, +B]:
  case Left(a: A) extends Either[A, Nothing]
  case Right(b: B) extends Either[Nothing, B]

I realise that this might be a breaking source and TASTY change (mostly because @smarter straight up told me), so this might be a hard sell.

4 Likes

The reason this is done for parameterless object cases in enums is that objects cannot have type parameters.

So if you want to generalize this behavior, the question is – for class cases, do you keep all the parent’s type parameters or not?

  • If you keep them, you have a weird and confusing situation where some parameters may become superfluous, so that Left[A, B] does not actually have type Either[A, B] but Either[A, Nothing], which could lead to surprises with type inference.

  • If you discard those parameters which do not appear in the signature of the case, the problem is that you now have synthetic parameter lists which do not correspond to anything in the source (currently, the parameter list is exactly the same as that of the parent). Then, changing the type of something may suddenly result in some parameters being implicitly added or removed from the case types, possibly breaking code in hard-to-understand ways.

What I would be in favor of: allow specifying the case type parameters without having to specify the extends clause too (which is super verbose). Use the type parameter names to mix and match appropriately. As in:

enum Either[+A, +B]:
  case Left[+A](a: A)
  case Right[+B](b: B)

  case Oops[C] // okay? or error: C is not a parameter in the parent
6 Likes

this one would be very nice

1 Like

I really like that, yes. The author keeps complete control, it’s fully compatible with what we have, but it’s also a lot less verbose.

This also seems vaguely related to another gripe I have: GADT syntax. Maybe there’s a way to generalise your idea so you can specify concrete type parameters?

enum Expr[A]:
  case Number(i: Int) extends Expr[Int]
  case Bool(b: Boolean) extends Expr[Boolean] 

to:

enum Expr[A]:
  case Number[Int](i: Int)
  case Bool[Boolean](b: Boolean)

(This specific one obviously does not work, it’s jus tto have something written down)

I don’t think there is much we can do about GADTs.

We could imagine something like:

enum Expr[A]:
  case Number(i: Int)   with A = Int
  case Bool(b: Boolean) with A = Boolean

or:

enum Expr[A]:
  case Number(i: Int)   extends[Int]
  case Bool(b: Boolean) extends[Boolean]

But I think these will never be accepted into Scala, as they do not constitute enough of an improvement :slight_smile:

3 Likes

I think I would have liked this approach a lot. Do you @LPTK think you could give a specific example where this could go bad?

I was thinking of something like this:

// In file Foo.scala

enum Foo[A]:
  case Foo1(x: Int)
  case ...
// In file Bar.scala

def mkFoo1(...): Foo1[Int] = ...

Now, if someone modifies Foo.scala (which could be defined in another project) in a way that makes the variance of Foo change, then Foo1 may silently stop taking any type parameter.

-enum Foo[A]:
+enum Foo[+A]:

Now the following line may yields an error after updating the library where Foo is defined, and someone who is not clear on the subtleties of enum desugaring will likely be thoroughly confused:

def mkFoo1(...): Foo1[Int] = ...

So while this approach technically works, I’m not sure it’s a very user-friendly design.

2 Likes