0
votes

I'm working on a little generic tool, in which I need to have something like this:

  • An Operator trait, which will provide tools for operating elements

  • A Publisher trait, responsible for publishing a result represented by a Set() in the following example

  • A class which will implement operation traits

  • A companion object for this class, who will implement the publisher operations. It is mandatory for my design to keep the published results in the same trait as the publish operations

In a nutshell, I've got the following structure:

trait Publisher[A]{
  var storage: Set[A] = Set[A]()
  def publishOper(elem: A) = storage += elem
}

trait Operator[A, B]{
  def operate(elem: A): B = ???
}


object Oper extends Publisher {

}

class Oper[A, B] extends Operator[A, B]{

  def publishOper(elem: A): B = {
    val res = operate(elem)
    publishOper(res)
  }

}

But, as you can imagine, I get the following error:

publishOper(res): Type mismatch, expected: A, actual: B

This raises several questions for me:

  1. How does type inference work when extending a companion object? (aka: Why does this happen?)

  2. How can I solve this, while trying to keep the same structure?

2
class Oper[A, B] extends Operator[A, B] with Publisher[B] { ...Rich Henry
That wouldn't work, as any class instance will have it's own storage, and publishes will not be reflected in the companion object. That's why I mentioned that it was mandatory for the design to keep them both together in the same trait.Maximiliano Felice
They are not the same type. I think you should consider making a separate object to implement Publisher, perhaps an implicit, and pass it to each Oper instance.Rich Henry
I.e. class Oper[A,B](implicit publisher: OperPub[B]) extends Operator[A,B]Rich Henry
This pretty much limits the extensibility of the solution at a point I can't afford. Case classes (as you show in your response) are very restrictive. I need to keep class implementations. And, as seen, I need to access the storage unit from the companion object.Maximiliano Felice

2 Answers

0
votes

Use a shared publisher object, perhaps via an implicit. This is actually a bit more versatile as you can control scope now. Also this is testable, using the companion object as a singleton is likely not.

implicit operPub = new Publisher[B] {
   // implementation...
}

class Oper[A, B](implicit publisher: Publisher[B]) extends Operator[A, B]{
  def publishOper(elem: A): B = {
    val res = operate(elem)
    publishOper(res)
  }
}

val a = new Oper[Int, String]
val b = new Oper[Int, String] // they both should get operPub
0
votes

Global publisher is a bad idea as it will be non-typed then. But if you really need it - just don't bind your publisher to the concrete type:

object Oper extends Publisher[Any]

class Oper[A, B] extends Operator[A, B]{

  def publishOper(elem: A): B = {
    val res = operate(elem)
    Oper.publishOper(res)
    res
  }
}

But this is a bad design. I'd recommend to define Oper as trait with external dependency:

trait Publisher[-A]{ //"-" - if can store Any then can store Int
   type T >: A //to compensate covariant position for set or any other your internal providers; implementations without explicit T will produce existential type `_ >: A` for T to guarantee that storage will have a biggest A type
   var storage: Set[T] = Set[T]()
   def publishOper(elem: A) = storage += elem
}

trait Operator[A, B]{
    def operate(elem: A): B = elem.asInstanceOf[B] //just mock
}

trait Oper[A, B] extends Operator[A, B]{
    def publisher: Publisher[B]

    def publishOper(elem: A): B = {
       val res = operate(elem)
       publisher.publishOper(res)
       res
    }
 }

Example:

scala> val pbl = new Publisher[Any]{}
pbl: Publisher[Any] = $anon$1@49dbe5f0

scala> class Oper1 extends Oper[Int, Int] { val publisher: Publisher[Int] = pbl }
defined class Oper1

scala> new Oper1
res10: Oper1 = Oper1@4bd282fd

scala> res10.publishOper(3)
res11: Int = 3

scala> res10.publishOper(4)
res12: Int = 4

scala> res10.publisher.storage
res13: Set[res10.publisher.T] = Set(3, 4)

scala> class Oper2 extends Oper[Double, Double] { val publisher: Publisher[Double] = pbl }
defined class Oper2

scala> new Oper2
res14: Oper2 = Oper2@271f68d2

scala> res14.publishOper(2.0)
res15: Double = 2.0

scala> res14.publishOper(3.0)
res16: Double = 3.0

scala> res14.publisher.storage
res17: Set[res14.publisher.T] = Set(3, 4, 2.0)

So now, depending on your context - you can choose the biggest type, that your publisher may work with (in my example it was Any). The compiler will automatically check if it's fine with your Oper - that's why we need contravariant Publisher here.

P.S. Interesting note: 3.0 was casted to 3 automatically as expected existential type of collection was Int, so for my example there is no duplication Set(3,4, 2.0, 3.0) but strings of course will be different: Set(3, 4, 2.0, "3"):

scala> class Oper3 extends Oper[String, String] { val publisher = pbl }
defined class Oper3

scala> new Oper3
res23: Oper3 = Oper3@f93893c

scala> res23.publishOper("3")
res38: String = 3

scala> res23.publisher.storage
res39: Set[res23.publisher.T] = Set(3.0, 4.0, 2.0, 3)