Scala能否实现添加元素时的编译期类型检查?
Problem Description
I'm trying to build a list of Thing instances where, after adding a subclass of ComplexThing, no more elements can be added. Right now I can enforce this with runtime checks, but I want to make this a compile-time constraint so my IDE highlights errors immediately. Here's my current code:
trait Thing trait SimpleThing extends Thing trait ComplexThing extends Thing sealed case class DummySimpleThing() extends SimpleThing sealed case class InstanceOfComplexThing() extends ComplexThing class ThingRepository { private val buffer = new ListBuffer[Thing]() def addThing(thing: Thing): ThingRepository = { buffer += thing this } } object TestThingRepository { def testAddThings() = { new ThingRepository() .addThing(DummySimpleThing()) .addThing(DummySimpleThing()) // it's ok to add SimpleThing after SimpleThing .addThing(InstanceOfComplexThing()) // it's ok to add ComplexThing after SimpleThing .addThing(DummySimpleThing()) // Not allowed to add anything after ComplexThing, how can I get compile time error here? .addThing(InstanceOfComplexThing()) // Not allowed anything after ComplexThing, how can I get compile time error here? } }
Solution: Type State Pattern
Absolutely! You can enforce this rule at compile time in Scala using the Type State Pattern, which leverages Scala's strong type system to track the repository's state (open for additions vs. closed after a ComplexThing is added). This lets the compiler block invalid addThing calls before runtime.
Here's the revised implementation:
import scala.collection.mutable.ListBuffer // Define state markers to track if the repository accepts new items trait RepositoryState sealed trait Open extends RepositoryState // Can add any Thing sealed trait Closed extends RepositoryState // Cannot add anything after ComplexThing trait Thing trait SimpleThing extends Thing trait ComplexThing extends Thing sealed case class DummySimpleThing() extends SimpleThing sealed case class InstanceOfComplexThing() extends ComplexThing // Repository with a generic state parameter class ThingRepository[S <: RepositoryState] private (private val buffer: ListBuffer[Thing]) { // Private constructor: use the companion object to create instances def this() = this(new ListBuffer[Thing]()) // Add a SimpleThing: keeps the repository in Open state def addThing(thing: SimpleThing)(implicit ev: S =:= Open): ThingRepository[Open] = { buffer += thing this.asInstanceOf[ThingRepository[Open]] } // Add a ComplexThing: transitions the repository to Closed state def addThing(thing: ComplexThing)(implicit ev: S =:= Open): ThingRepository[Closed] = { buffer += thing this.asInstanceOf[ThingRepository[Closed]] } } // Companion object to create an initial Open-state repository object ThingRepository { def apply(): ThingRepository[Open] = new ThingRepository[Open](new ListBuffer[Thing]()) }
Testing the Compile-Time Constraint
Now when you try to add items after a ComplexThing, the compiler (and your IDE) will throw an error immediately:
object TestThingRepository { def testAddThings() = { ThingRepository() .addThing(DummySimpleThing()) // ✅ Ok: stays Open .addThing(DummySimpleThing()) // ✅ Ok: stays Open .addThing(InstanceOfComplexThing()) // ✅ Ok: transitions to Closed // The lines below will cause COMPILE ERRORS (IDE will highlight them) // .addThing(DummySimpleThing()) // .addThing(InstanceOfComplexThing()) } }
How This Works
- State Tracking: The generic parameter
Stracks whether the repository isOpenorClosed. - Implicit Evidence: The
ev: S =:= Openparameter ensuresaddThingcan only be called on anOpenrepository—no valid evidence exists forClosedstate, so those calls are blocked. - State Transition: Adding a
ComplexThingreturns aThingRepository[Closed], which has no validaddThingmethods, preventing any further additions.
This approach turns your runtime check into a compile-time guarantee, catching mistakes early in development.
内容的提问来源于stack exchange,提问作者Capacytron

