Correlate two type parameters

試著忘記壹切 提交于 2019-12-24 10:27:50

问题


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:

  1. Use forSome to "correlate" result with the operation
  2. Define a custom class that holds both operations and results
  3. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!