问题
Suppose, I have a sequence of operations, some of which depend on some of the results of previous ones. Something like this:
type Results = List[(Operation[_], Any)] // ???
trait Operation[Out] {
type Result = Out
def apply(results: Results): Out
}
class SomeOp extends Operation[String] {
def apply(results: Results) = "foo"
}
class OtherOp extends Operation[String] {
def apply(results: Results) = results
.collectFirst { case (_: SomeOp, x: String) => x }
.getOrElse("") + "bar"
}
def applyAll(
ops: List[Operation[_]],
results: Results = Nil
): Results = ops match {
case Nil => results.reverse
case head :: tail => applyAll(tail, (head -> head(results)) :: results)
}
applyAll(List(new SomeOp, new OtherOp)).last._2 // foobar
This works, but Any
in the result list looks ugly :(
Is there a way around it? Can I declare it somehow to guarantee that second element of the tuple is the of the #Result
type declared by the first element?
回答1:
There are several ways to get rid of Any
. Here is the list of options I could come up with so far:
- Use
forSome
to "correlate" result with the operation - Define a custom class that holds both operations and results
- Convert the whole design from lists to monad
The forSome
solution
The question title seems to ask exactly about the forSome
:
(Operation[X], X) forSome { type X }
Here, the type variable X
is bound by the forSome
quantifier, and it guarantees that the tuples in your list can store only operations and outputs of matching types.
While it prohibits the occurrence of tuples like (SomeOperation[String], Int)
, the instantiation becomes a bit cumbersome:
val newResult: (Operation[Y], Y) forSome { type Y } = head match {
case op: Operation[t] => (op -> op(results))
}
The t
is a type pattern on the left hand side of the match-case there. This is sometimes helpful for working with existentials, because it allows us to give the existential type a name, in this case t
.
Here is a demo of how this can be used:
type Results = List[(Operation[X], X) forSome { type X }]
trait Operation[Out] {
type Result = Out
def apply(results: Results): Out
}
class SomeOp extends Operation[String] {
def apply(results: Results) = "foo"
}
class OtherOp extends Operation[String] {
def apply(results: Results) = results
.collectFirst { case (_: SomeOp, x: String) => x }
.getOrElse("") + "bar"
}
def applyAll(
ops: List[Operation[_]],
results: Results = Nil
): Results = ops match {
case Nil => results.reverse
case head :: tail => {
val newResult: (Operation[Y], Y) forSome { type Y } = head match {
case op: Operation[t] => (op -> op(results))
}
applyAll(tail, newResult :: results)
}
}
println(applyAll(List(new SomeOp, new OtherOp)).last._2)
It simply outputs foobar
, as before.
Custom class for operations + results
Instead of using tuples with complex existentials, it might be easier to define a custom type to hold operations together with results:
case class OpRes[X](op: Operation[X], result: X)
With a corresponding method returning OpRes
added to Operation
,
everything becomes rather straightforward:
def opWithResult(results: Results): OpRes[Out] = OpRes(this, apply(results))
Here is a full compilable example:
case class OpRes[X](op: Operation[X], result: X)
type Results = List[OpRes[_]]
trait Operation[Out] {
type Result = Out
def apply(results: Results): Out
def opWithResult(results: Results): OpRes[Out] = OpRes(this, apply(results))
}
class SomeOp extends Operation[String] {
def apply(results: Results) = "foo"
}
class OtherOp extends Operation[String] {
def apply(results: Results) = results
.collectFirst { case OpRes(_: SomeOp, x: String) => x }
.getOrElse("") + "bar"
}
def applyAll(
ops: List[Operation[_]],
results: Results = Nil
): Results = ops match {
case Nil => results.reverse
case head :: tail => applyAll(tail, head.opWithResult(results) :: results)
}
println(applyAll(List(new SomeOp, new OtherOp)).last.result)
Again, it outputs foobar
, as before.
Maybe it should be just a monad?
Finally, the first sentence of your question contains the phrase
sequence of operations, some of which depend on some of the results of previous ones
This seems to me almost like the perfect practical definition of what a monad is, so maybe you want to represent sequences of computations by for
-comprehensions instead of existentially typed lists. Here is a rough sketch:
trait Operation[Out] { outer =>
def result: Out
def flatMap[Y](f: Out => Operation[Y]): Operation[Y] = new Operation[Y] {
def result: Y = f(outer.result).result
}
def map[Y](f: Out => Y) = new Operation[Y] {
def result: Y = f(outer.result)
}
}
object SomeOp extends Operation[String] {
def result = "foo"
}
case class OtherOp(foo: String) extends Operation[String] {
def result = foo + "bar"
}
case class YetAnotherOp(foo: String, bar: String) extends Operation[String] {
def result = s"previous: $bar, pre-previous: $foo"
}
def applyAll: Operation[String] = for {
foo <- SomeOp
fbr <- OtherOp(foo)
fbz <- YetAnotherOp(foo, fbr)
} yield fbz
println(applyAll.result)
It prints
previous: foobar, pre-previous: foo
I've made the chain of operations one operation longer to demonstrate that an operation in a monadic for-comprehension of course has access to all previously defined intermediate results (in this case, foo
and fbr
), not only to the previous one.
来源:https://stackoverflow.com/questions/49476578/correlate-two-type-parameters