Managing trait instance consumers in a generic way

2019-08-30 05:45发布

问题:

Let's imagine I have the following base trait and case classes

sealed trait BaseTrait
case class Foo(x: Integer = 1) extends BaseTrait
case class Bar(x: String = "abc") extends BaseTrait

I would like to create a generic interface for classes which can process BaseTrait instances, something like the following

class FooProcessor(val model: FooModel) extends BaseProcessor[Foo] {
    val v: Option[Foo] = model.baseVar
}
class BarProcessor(val model: BarModel) extends BaseProcessor[Bar] {
    val v: Option[Bar] = model.baseVar
}

For this I have the following traits

trait BaseModel[T <: BaseTrait] {
    var baseVar: Option[T] = None
}
trait BaseProcessor[T <: BaseTrait] {
    def model: BaseModel[T]
    def process(x: T): Unit = model.baseVar = Option(x)
}

The model definitions are the following

class FooModel extends BaseModel[Foo]
class BarModel extends BaseModel[Bar]

Now lets imagine I have the following processors somewhere in my app

val fooProcessor = new FooProcessor(new FooModel)
val barProcessor = new BarProcessor(new BarModel)

I would like to handle them in a somewhat generic way, like this

def func[T <: BaseTrait](p: T) {
    val c/*: BaseProcessor[_ >: Foo with Bar <: BaseTrait with Product with Serializable]*/ = p match {
        case _: Foo => fooProcessor
        case _: Bar => barProcessor
    c.process(p)
}

The compiler is not really happy about the last line, it says

type mismatch;
found : T
required: _1

If I understand correctly this is basically the compiler trying to prevent barProcessor.process(Foo()) from happening. I've tried a couple of solutions to get around this and achieve the desired behavior:

  • the simplest way around this is calling the proper *Processor.process with the proper BaseTrait instance inside the match case, which seems to defy the whole point of handling them in a somewhat generic way
  • use an abstract type in the BaseModel and BaseProcessor, which one hand got rid of the somewhat unneeded type parameter in BaseModel but the compilers complaint is still valid and I was not able to figure out if it's possible to get that to work
  • get rid of the type parameter and contraint from the BaseModel, and just do a type cast in the processor to get the proper type, but the explicit type cast also isn't really what I was hoping for

Like so:

trait BaseModel {
    var baseVar: Option[BaseTrait] = None
}
trait BaseProcessor[T <: BaseTrait] {
    def model: BaseModel
    def process(x: T): Unit = model.baseVar = Some(x)
    def getBaseValue: T = model.baseVar.map(_.asInstanceOf[T])
}

I guess one could also somehow convince the compiler that the two types (T of the Processor and T of the func parameter p) are equivalent, but that also seems like an overkill (and I'm also not really sure how it can be done).

So my question is the following: is it possible to do what I'm trying to achieve here (managing processors in a uniform way + each processor knows their specific type of BaseTrait) in a somewhat easy fashion? Is there a better model for this which I'm missing?

Update

As per Tim's answer making the controllers implicit solves the problem, however if you want to have a class where you define your controllers + have 'func' on it's interface the compiler no longer seems to properly resolve the implicits. So if I try to do something like this

class ProcessorContainer {
    implicit val fooProcessor = new FooProcessor(new FooModel)
    implicit val barProcessor = new BarProcessor(new BarModel)
    def func[T <: BaseTrait](p: T) = typedFunc(p)
    private def typedFunc[T <: BaseTrait](p: T)(implicit processor: BaseProcessor[T]) =
        processor.process(p)
}

class Test {
    val processorContainer = new ProcessorContainer
    processorContainer.func(Foo())
    processorContainer.func(Bar())
}

I get the following compile error (one for Foo and one for Bar):

could not find implicit value for parameter processor: BaseProcessor[Foo]
not enough arguments for method

Is there a way around this? I could of course expose the controllers so they can be passed in implicitly, however I'd prefer not doing that.

回答1:

You can create a simple typeclass by making the processors implicit and passing them as an extra argument to func:

implicit val fooProcessor = new FooProcessor(new FooModel)
implicit val barProcessor = new BarProcessor(new BarModel)

def func[T <: BaseTrait](p: T)(implicit processor: BaseProcessor[T]) =
  processor.process(p)

If you pass a Foo to func it will call FooProcessor.process on it, and if you pass a Bar to func it will call BarProcessor on it.