Reputation: 19478
I have an abstract class (this is an analogous example) Printer
which prints the supplied argument unless it fails to pass a filter. Printer
is contravariant w.r.t. its generic type T
.
class Printer[-T] {
val filters: Seq[Function2[T => Boolean]]
def print(t: T): Unit {
if (filters.forall(_(t))) doPrint(t)
}
def doPrint(t: T): Unit
}
I now have 20 subclasses of Printer
-- one for Strings, Ints, etc. Since Printer
is contravariant, filters
must be a val
. However if I want a method of Printer
to add a filter it needs to be immutable.
def addFilter[TT <: T](t: TT): Printer[TT]
Unfortunately I now need to implement this method in each of my 20 subclasses. Is there any way around this?
Update: Also, in addFilter
, I don't know how I can return the subclass instead of the superclass Printer
. For example, if I called addFilter
on a StringPrinter
I would ideally get the type StringPrinter
back.
Upvotes: 1
Views: 200
Reputation: 15074
THe following is a bit of a departure from the way you have coded your Printer
, but potentially could fulfil your intentions. Note that this way of coding your class enables a greater separation of concerns: you can define filters and print implementations (the doPrint
argument) independently of how they are being used:
case class Printer[T](val filters: List[Function1[T, Boolean]], val doPrint: T => Unit) {
def print(t: T): Unit = {
if (filters.forall(_(t))) doPrint(t)
}
def addFilter[TT <: T](f: TT => Boolean): Printer[TT] = copy(f :: filters)
}
Note that I haven't needed to specify contravariancy here, not sure if that will be an issue for you.
To use the class, you don't need to subclass, just pass suitable arguments into the constructor (actually, the companion apply
factory method provided for free for case classes) - eg:
case class Foo(x: Int)
val fooChk1: Foo => Boolean = (f: Foo) => f.x != 1
val fooPrinter1 = Printer(fooChk1 :: Nil, (f: Foo) => println(s"Foo: x = ${f.x}"))
val fooChk3: Foo => Boolean = (f: Foo) => f.x != 3
val fooPrinter2 = fooPrinter1.addFilter(fooChk3)
val foo3 = Foo(3)
fooPrinter1.print(foo3) // Prints 'Foo: x = 3'
fooPrinter3.print(foo3) // Prints nothing
There is also a fair bit of scope for using implicits here, too - both for parameters to Print
(eg. change the constructor to (val filters: List[Function1[T, Boolean]])(implicit val doPrint: T => Unit)
), and for specific Print[X]
variants.
Upvotes: 1