Possibility to spread sealed trait to different files

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

Would it be technically feasible for sealed[packagename] to mean the sealing was scoped to packagename within the current compilation context?

So you could still do something like this:

src/main/scala/com/foo/models/SealedTrait.scala
src/main/scala/com/foo/models/SealedTraitCommon.scala
src/main/scala-2/com/foo/models/SealedTraitImpl.scala
src/main/scala-3/com/foo/models/SealedTraitImpl.scala

But it would still reject extending it from outside the library or in a different project in a multi-project build?

1 Like

Given that we have incremental compilation, it is not technically feasible. When incrementally recompiling, the compiler only sees a subset of the files comprising a project.

1 Like

I don’t understand why, but I trust your judgment that it’s harder than for private and protected.

“Technically infeasible” seems a bit hard to believe, however. In fact, it is almost possible to simulate package-level sealing within Scala 3, which seems to imply that it is not fundamentally incompatible with incremental compilation.

Sealing itself (preventing extension) is not the issue. But we can’t list the children, which we need for exhaustivity checking.

(If you don’t need/want exhaustivity checking, making the constructor private[pkg] is enough).

2 Likes

Oh, so it’s about the exhaustivity checking that sealed is supposed to provide. Sorry, I should have read the previous comments more thoroughly.

So it wouldn’t be an issue if sealed[packageName] simply did not provide that added benefit in those cases.

When a match on such a sealed type could not be checked to be exhaustive, a warning could be issued at the site of the match expression, explaining that, unfortunately, that sealed type cannot be checked for exhaustivity due to its scope.

But I see how providing weaker semantics for sealed in these cases might be controversial, even though such a weaker sealed would still be very useful to avoid unwanted extensions (complementarily to private and protected).

Something to consider: the Java implementation of sealed has a corresponding permits clause that lists the permissible extensions.

IIUC Java still doesn’t allow multiple top-level classes/interfaces in the same file, so this was an understandable choice on their part, but if we chose to follow suit, we could relax the same-file restriction as well.

2 Likes

It sort of would be an issue, because exhaustivity checking is one of the major benefits of sealed hierarchies. Otherwise, as @sjrd pointed out, it’d would be enough to just make the constructor package private.

1 Like