简体   繁体   中英

Generic method arguments in scala

I'm not sure the title is describing my question the best but let's give it a shot.

I have a background job execution application that resembles a simple pipeline processing. There are Command objects that do some calculation and return an OUTPUT , and Worker that receive OUTPUT as input and can return Result The object model looks something like this:

type OUTPUT <: AnyRef
trait Command[OUTPUT] {
  def doSomething(): OUTPUT
}
sealed trait Worker[IN <: AnyRef, OUT <: Result] {
  def work(input: IN): OUT
}

case class WorkA() extends Worker[String, LongResult] {
  override def work(input: String): LongResult = LongResult(Long.MaxValue)
}

case class WorkB() extends Worker[Long, StringResult] {
  override def work(input: Long): StringResult = StringResult(input.toString)
}

There are few problems with this approach:

When mapping on a collection of Worker I can't make sure the worker accepts the same OUTPUT as input.

Unless I'm mapping a List of Command , the code does not compile because of type erasure - it expects a _$1 but receives a Long or a String (everything that was previously accepted as OUTPUT )

val workers = List(
  new WorkA(),
  new WorkB()
)

val aSimpleCommand = new Command[Long] {
  override def doSomething() = 123123123L
}
// Obviously this doesn't compile.
workers.map(worker => worker.work(aSimpleCommand.doSomething()))

I'm looking for the right Scala mechanism to disallow this at compile time. How can I map ONLY on the Worker that DO actually support OUTPUT - and in this case, only WorkB

If you want to do this at compile time you can use shapeless HLists, preserving the type of your list all the way through, and then using a Poly to handle the cases:

val myWorkers: WorkA :: WorkB :: WorkB :: HNil =
  WorkA() :: WorkB() :: WorkB() :: HNil

object doWork extends Poly1 {
  implicit def caseLong[A] = at[Worker[Long, A]] {
    w => w.work(aSimpleCommand.doSomething())}
  implicit def caseString = //whatever you want to do with a String worker
}

myWorkers map doWork

For a less safe example that doesn't need shapeless, you can match cases as long as you have concrete types:

val myWorkers: List[Worker[_, _]] = ...
myWorkers collect {
  case wa: WorkA => //works
  case lw: Worker[Long, _] => //doesn't work
}

If it is feasible to explicitly state all classes that extend Worker[Long, _] , then you could map the workers over a partial function like so:

val res = workers map {
  case b: WorkB => Some(b.work(aSimpleCommand.doSomething()))
  case _ => None
}

In your example, this will return List(None, Some(StringResult(123123123)) . You can also collect only the existing values:

res collect {case Some(r) => r} // List(StringResult(123123123))

Now, this isn't a very practical solution. Maybe the following thoughts help you come up with something better:

As you have already stated, because of type erasure, we cannot create our partial function to accept values of type Worker[Long, _] at runtime. ClassTag (and TypeTag ) provide a solution to this problem by making the compiler create evidence accessible at runtime for the erased types. For example, the following function extracts the runtime class of a worker's input:

import scala.reflect.ClassTag
def getRuntimeClassOfInput[T: ClassTag](worker: Worker[T, _]) = implicitly[ClassTag[T]].runtimeClass

Example usage:

println(getRuntimeClassOfInput(new WorkB)) // long

The problem is that this doesn't seem to work once the workers are inside a list. I suppose that it's because once you have multiple different workers in the list, the list becomes a List[Worker[Any, Result] and you lose all type information. What might solve this problem are Heterogenous Lists because unlike the standard lists they keep the static type information of all elements.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM