Possibility to spread sealed trait to different files

Hi there,

I find myself often in the situation that I want to introduce a sealed trait for a number of types, which themselves are quite comprehensive with their individual implementations along with companion objects etc. of hundreds of lines. It’s very unfortunate that I would have to move all these files together in one, just to be able to “seal” them.

Is there any other way to do this? Would this be something worth considering? I imagine something like

// file: Foo.scala
@allowedImplementations(Bar, Baz)
sealed trait Foo
// file: Bar.scala
object Bar {
   ...
}
trait Bar extends Foo
// file: Baz.scala
object Baz {
   ...
}
trait Baz extends Foo
9 Likes

See here for previous discussion.

I second this. My use-case would mainly be:

For data models I typically follow the Java convention of having one class in each file, so that Person ends up in “Person.scala”, especially if I have in the order of 10-100 such classes.

However, sometimes some of these classes will have a common sealed super trait. Then these would have to go in the same file, and the convention is broken. So when someone goes into the codebase looking for the definition of Person, they don’t find it where they expect because it is in the file “Entity.scala”.

This isn’t a big issue, but it would certainly be nice to have.

5 Likes

I had this discussion some time ago in the scala users forum. In my opinion the single file limitation of sealed classes/traits is arbitrarily limiting. I don’t understand the advantage of this seemingly useless limitation.

I would love to spread the implementation of a sealed class and its subclasses across multiple files.

3 Likes

What do other languages do? It’s been way too long since I did C++, but…don’t they have some sort of “friend” mechanism that is entirely intended to create a limited visibility scope across a set of classes? Or am I remembering it wrong?

Don’t Java sealed classes just list which classes can extend, explicitly, without same-file restriction?

3 Likes

That appears to be correct:

1 Like

Common Lisp has no connection of file name nor directory name to code within the file. I may name my files as I like and sort the files within directories with liberty, and I may distribute the definitions within those files as I like, as long as I declare the dependencies (load order) to whatever system is trying to build the system.

1 Like

In some context, I proposed applying “compilation unit” to a directory of source files. (Edit: the context was the other prepandemic thread where this issue was raised.)

I suggested that a directory named foo.scala is a compilation unit.

“compilation unit” is sufficiently flexible to accommodate various book-keeping practices.

Worth adding that the concept is intended to support usual editing practices. “If I can see it, it’s part of my compilation unit.” The rules around name binding also pertain to “What can I see, what is my expectation if I’m editing this file or compilation unit?”

1 Like

Why do we need sealed classes – for exhaustivity check only?
Can restriction on this in base trait force the check?

trait Base {
   this:  Variant1 |  Variant2 | Variant3 =>
}

Yes, to be able to enumerate the cases, and for exhausitivity check in pattern matching. I don’t see how a union type self-type solves that. Apart from the fact that it is Scala 3 only and I won’t be writing that for the next five years, the user can clearly see in the API: sealed trait Flow, known subclasses: trait Ex, trait Trig, trait Act; for example. This clearness and visibility is not achieved by self-types which are only about safety of implementation, not user visibility and API documentation.

1 Like

Well, my try: Scastie - An interactive playground for Scala.

crashes with

java.util.NoSuchElementException: head of empty list while compiling /tmp/scastie6452550532840750651/src/main/scala/main.scala

compiler crash is because of

this: Ex |  Trig | Act =>

instead

this: Ex[?] |  Trig | Act =>

(looks like you found a bug in a compiler)

1 Like

Haha, ok. Well, the compiler doesn’t emit an exhaustivity warning if I omit a case, like

      flow match {
        case x: Ex[_] => ???
        case a: Act   => ???
      }

And even if it were to be refined to do that - it looks like an abuse of a feature that is now in competition with an established feature - sealed traits - very much like you could encode partial type application in Scala 2 using the type lambda trick. I think one should stick to the feature that was designed for this purpose.

1 Like

Are there any drawbacks to composing the subtypes using standard composition techniques?

sealed trait Foo
trait Bar extends Foo with BarImpl
trait Baz extends Foo with BazImpl

BarImpl and BazImpl can be defined in other files without any issue.

Not really, especially when done with care check the conversation shared by @jimka2001 in the users’ forum; in particular the final message from myself in which I link a GitHub repository that explains in detail how to do such technique without leaking details.

However, the idea of raising this proposal is not because there aren’t workarounds but because for many, it would be great if such functionality was part of the language. This is no different from many features of Scala, like patter matching; you can live without them but experience has shown that it is better when such things are part of the main language since that increases our expressibility.

6 Likes

First of all, forgive me for asking such a simple question! I wanted to understand the reason in which cases the single-file approach to sealed types can be limiting, and that repository indeed shows how difficult it is to get around it when dealing with more complex base types. Thanks for the clarification.

1 Like

No need for that :slight_smile: Asking questions is the very root of our existence and progress.

Now, I hope my reply didn’t feel rude, not my intention.
Just wanted to clarify that the idea has already been discussed and to clarify on what is the goal of the current thread.

BTW, while someone may argue that you may have looked at the links and found the answer; being honest it was like at three levels of indirection.

Anyways, this is getting a bit off-topic :wave:

3 Likes

I have always found it odd that private and protected can be scoped, but sealed cannot.

sealed[packagename] seems natural and convenient.

3 Likes

The limitation is that it’s necessary to enumerate child classes, for pattern match checks.

There is not currently a restriction that a package must be sourced from a single directory, for instance.

1 Like