Cross-File Sealing for Sealed Traits with Explicit Subtype Declaration

As kindly advised by @soronpo, i am posting here a new thread related to Enabling sealed traits across different files with union type desugaring - #5 by morgen-peschke

As discussed with @som-snytt in Multi-file ADTs · scala/scala3 · Discussion #21907 · GitHub, this proposal might be helpfull in another context that involves the compilation flag -source:future, cf Modularity Improvements

Proposal: Cross-File Sealing for Sealed Traits with Explicit Subtype Declaration

Motivation
Currently, Scala’s sealed traits require all subtypes to be defined within the same file. This limitation restricts the flexibility of sealed traits, especially in larger projects with modular codebases. This proposal aims to introduce a mechanism for cross-file sealing, allowing subtypes to be defined in separate files while maintaining the benefits of compiler optimization and exhaustive pattern matching.

Proposed Solution
Introduce a new syntax for sealed traits that allows explicit declaration of subtypes using an applyTo clause. This clause would list the fully qualified names of the classes or objects that are allowed to extend the sealed trait.

Example

import com.example.MyClass1
import com.example.MyClass2
import com.example.MyClass3

sealed trait MyTrait {
  // ... trait definition ...
} applyTo(com.example.MyClass1, com.example.MyClass2, com.example.MyClass3)

To improve conciseness, the following syntactic sugar could be used, which is equivalent to the applyTo() version:

import com.example.MyClass1
import com.example.MyClass2
import com.example.MyClass3

sealed trait MyTrait {
  // ... trait definition ...
} (com.example.MyClass1, com.example.MyClass2, com.example.MyClass3)

Explanation
The import statements ensure that the specified subtypes are visible within the scope of the MyTrait definition.
The applyTo clause explicitly declares the allowed subtypes, enabling the compiler to:
Verify that the listed subtypes exist and extend MyTrait.
Perform exhaustive pattern matching checks.
Apply optimizations based on the closed set of subtypes.

Benefits
Cross-File Sealing:
Allows subtypes to be defined in separate files.

Compiler Optimization:
Maintains the benefits of compiler optimization and exhaustive pattern matching.

Clarity and Maintainability:
Provides a clear and explicit declaration of allowed subtypes.

Semantic Consistency: The use of applyTo aligns with the semantic of the apply method, making the syntax more intuitive.

Enforcement of Subtype Restrictions
A critical aspect of this proposal is ensuring that the compiler enforces the subtype restrictions defined in the applyTo clause. If a class or object defined outside of the listed subtypes attempts to extend the sealed trait, the compiler should generate a compilation error. This enforcement is essential for maintaining the guarantees of exhaustive pattern matching and compiler optimization.

Example

// In a separate file:
class UnauthorizedClass extends MyTrait // Should result in a compilation error

Potential Considerations
Requires changes to the Scala compiler to recognize and process the new syntax.
Modifying the compiler for this feature should not introduce significant risks. Sealed traits have a relatively simple AST (Abstract Syntax Tree) representation. The compiler can leverage existing mechanisms for type checking and analysis, with minor adaptations to handle the explicit subtype declarations. This localized nature of the change minimizes the potential for unintended side effects on other parts of the compiler.

Use Cases
Modeling complex domain entities with variations spread across multiple files.
Creating extensible frameworks with a closed set of extension points.
Enhancing the safety and reliability of pattern matching in large codebases.

Next Steps
I would appreciate feedback from the Scala community and the Scala Team on this proposal. If there is sufficient interest, I am willing to contribute to the implementation of this feature.

Thank you for your consideration.

On a purely syntactical matter, I think we should move closer to that of extend clauses, as this is basically the reverse:

  1. Use a keyword
  2. Do no use parentheses
  3. Move the clause before the body of the sealed trait
  4. Do not require fully qualified names, instead only “unambiguous names”. i.e. ones such that only one class-like with this name is in scope
    • This means adding any class to a library might be source incompatible with wildcard imports, is this a problem ?

Example:

import com.example.MyClass1
import com.example.MyClass2
import com.example.MyClass3
// import com.other.MyClass3 // Makes compilation fail: MyClass3 is no longer unambiguous

sealed trait MyTrait extendedBy MyClass1, MyClass2, MyClass3 {
  ...
}
// also allows for braceless:
sealed trait MyTrait extendedBy MyClass1, MyClass2, MyClass3:
  ...
5 Likes

Certainly seems useful (I’m sure I would use it if it was available), but we should think about the implications of the circular import dependencies that this will require.

(I have no idea how much those impact the Scala compiler, but I’ve lived through the horrors of how badly circular references can trash compile performance in some languages.)

2 Likes