如何关联两个类型参数?依赖前置结果的操作序列实现问询
First, let's address your core question: how to properly relate the type parameter of an Operation to its result, and build a safe system for operations that depend on prior results. Your initial code uses Any for results, which loses type safety—we can fix that while handling dependencies cleanly.
Step 1: Define a Type-Safe Results Container
Instead of using a list of (Operation[_], Any), create a trait that guarantees we retrieve the correct type when accessing an operation's result:
trait Results { def get[Out](op: Operation[Out]): Out }
This trait enforces that when you ask for the result of an Operation[Out], you get an Out—no casting required in user code.
Step 2: Refine the Operation Trait
Update the Operation trait to include a way to declare dependencies, and keep the type parameter for the result:
trait Operation[Out] { // Declare which operations this one depends on def dependencies: List[Operation[_]] // Execute the operation using previously computed results def apply(results: Results): Out }
Step 3: Implement Concrete Operations
Now, your operations can explicitly declare dependencies and safely access their results:
class SomeOp extends Operation[String] { // No dependencies override def dependencies: List[Operation[_]] = Nil override def apply(results: Results): String = "foo" } class OtherOp(val dependency: SomeOp) extends Operation[String] { // Depends on SomeOp override def dependencies: List[Operation[_]] = List(dependency) override def apply(results: Results): String = { // Safely get the result of SomeOp with the correct type results.get(dependency) + "bar" } }
Notice how OtherOp takes its dependency as a constructor parameter—this makes dependencies explicit and compile-time checked, avoiding the runtime pattern matching and casting from your original code.
Step 4: Implement the Results Container
We'll use a map under the hood, but wrap it to maintain type safety:
class MapResults(private val underlying: Map[Operation[_], Any]) extends Results { override def get[Out](op: Operation[Out]): Out = underlying(op).asInstanceOf[Out] } class ResultsBuilder(private val map: Map[Operation[_], Any] = Map.empty) { def add[Out](op: Operation[Out], result: Out): ResultsBuilder = { new ResultsBuilder(map + (op -> result)) } def build(): Results = new MapResults(map) } object ResultsBuilder { def empty: ResultsBuilder = new ResultsBuilder() }
The asInstanceOf here is safe because we only add results of the correct type via ResultsBuilder.add, which pairs an Operation[Out] with an Out.
Step 5: Execute All Operations with Dependency Ordering
To ensure operations run after their dependencies, we need a topological sort. Then we can execute operations in that order:
def topologicalSort(operations: List[Operation[_]]): List[Operation[_]] = { import scala.collection.mutable val visited = mutable.Set[Operation[_]]() val visiting = mutable.Set[Operation[_]]() val result = mutable.ListBuffer[Operation[_]]() def dfs(op: Operation[_]): Unit = { if (visiting.contains(op)) throw new IllegalArgumentException("Cycle detected in dependencies") if (!visited.contains(op)) { visiting.add(op) op.dependencies.foreach(dfs) visiting.remove(op) visited.add(op) result.append(op) } } operations.foreach(dfs) result.toList } def getAllOperations(operations: List[Operation[_]]): Set[Operation[_]] = { operations.flatMap(op => op +: getAllOperations(op.dependencies)).toSet } def applyAll(operations: List[Operation[_]]): Results = { // Include all dependencies even if not explicitly passed val allOperations = getAllOperations(operations).toList // Sort operations to respect dependencies val sortedOperations = topologicalSort(allOperations) // Execute each operation and collect results sortedOperations.foldLeft(ResultsBuilder.empty) { (builder, op) => val result = op.apply(builder.build()) builder.add(op, result) }.build() }
Testing the Implementation
Let's see it in action:
val someOp = new SomeOp() val otherOp = new OtherOp(someOp) val results = applyAll(List(otherOp)) println(results.get(someOp)) // Output: "foo" println(results.get(otherOp)) // Output: "foobar"
This code is fully type-safe: if you try to get a result of the wrong type, the compiler will throw an error. Dependencies are enforced at compile time, and cycles are detected at runtime.
内容的提问来源于stack exchange,提问作者Dima

