Enabling sealed traits across different files with union type desugaring

I love sealed traits and utilizing the compiler exhaustive check in pattern matching.
What I don’t like is that I’m limited to extending the sealed traits in the same source file.

I propose a compromise that we can optionally annotate the extending classes within the extended trait call site and then the extending classes can exist in other source files. We can desugar this concept into regular sealed traits with an auxiliary union type.
Here is an example:

Before desugaring

Foo.scala

sealed[Bar, Baz.type] trait Foo
case class OtherFoo() extends Foo

Bar.scala

case class Bar() extends Foo

Baz.scala

case object Baz extends Foo

Test.scala

def check(f: Foo): Unit =
  f match //match may not be exhaustive.
    case OtherFoo() =>
    case Baz =>

After desugaring

Foo.scala

type Foo$Sealed = OtherFoo | Bar | Baz.type 
trait Foo
case class OtherFoo() extends Foo

Bar.scala

case class Bar() extends Foo

Baz.scala

case object Baz extends Foo

Test.scala

def check(f: Foo$Sealed): Unit =
  f match //match may not be exhaustive.
    case OtherFoo() =>
    case Baz =>

Desugraing concept

  • The sealed trait/classed with annotated extenders is changed to the regular trait/class without the sealed modifier:
`sealed[A,B,C...] trait Foo`

is changed to

trait Foo
  • A new auxiliary type is added that is a union of all extenders:
type Foo$Sealed = A | B | C | ... | <Additional extenders that are in the same source file as Foo>
  • All extending classes and pattern matching references continue to reference the original Foo trait
  • All other references to Foo are changed to Foo$Sealed

What do you think?

5 Likes

I can’t say anything about whether the suggested implementation makes sense. But I would love to have sealed traits that can span multiple files. I am not entirely sure about the syntax though since it is yet another way of using the square brackets (on top types, scope).

I actually think the Java sealed interface syntax with permits is really clear.

2 Likes

AFAIK, it is not realistic to have sealed types that implicitly span across multiple source files, hence my suggestion to add explicit annotation of what is extending. The desugaring concept is to allow the majority of the compiler to remain unchanged. It may be possible to forfeit the desugaring and modify the wait sealed is supported throughout the compiler. But the annotation must remain, although we can think of a different syntax for it.

After some checking, there is a flaw in this desugaring concept:

def useFoo(foo: Foo$Sealed) = ???
def check(v : Any): Unit =
  v match
    case foo: Foo => useFoo(foo) //error

If we pattern match against Foo$Sealed in this case, then the compiler thinks it’s not exhaustive for _ : Foo.

Of the two, I think permits would be much less cryptic:

sealed[Bar, Baz.type] trait Foo
case class OtherFoo() extends Foo

sealed trait Foo permits Bar, Baz.type
case class OtherFoo() extends Foo

If something more analogous to extends x with y is desirable, permits x and y seems like it would also be reasonable:

sealed trait Foo 
  permits Bar and Baz.type
  extends Product with Serializable

case class OtherFoo() extends Foo
1 Like

I don’t see why we would need this $Sealed union type. It should be perfectly possible to directly implement multi file sealed traits and classes just like the single file ones. The single file restriction is necessary to identify what are the possible subclasses. If they’re listed in the class header, that works too.

6 Likes

Never thought I’d write this in a Scala forum, but: Take a look at Java :face_with_peeking_eye:
https://openjdk.org/jeps/409

1 Like