We are refactoring an inherited method
to use a type class instead - we would like to concentrate all of the method
implementations in one place, because having them scattered among the implementing classes is making maintenance difficult. However, we're running into some trouble as we're fairly new to type classes. At present method
is defined as
trait MethodTrait {
def method: Map[String, Any] = // default implementation
}
abstract class SuperClass extends MethodTrait {
override def method = super.method ++ // SuperClass implementation
}
class Clazz extends SuperClass {
override def method = super.method ++ // Clazz implementation
}
and so on, where there are a total of 50+ concrete classes, the hierarchy is fairly shallow (abstract class SuperClass
-> abstract class SubSuperClass
-> abstract class SubSubSuperClass
-> class ConcreteClass
is as deep as it goes), and a concrete class never extends another concrete class. (In the actual implementation, method
returns a Play Framework JsObject
instead of a Map[String, Any]
.) We're trying to replace this with a type class:
trait MethodTrait[T] {
def method(target: T): Map[String, Any]
}
class MethodType {
type M[T] = MethodTrait[T]
}
implicit object Clazz1Method extends MethodTrait[Clazz1] {
def method(target: Clazz1): Map[String, Any] { ... }
}
implicit object Clazz2Method extends MethodTrait[Clazz2] {
def method(target: Clazz2): Map[String, Any] { ... }
}
// and so on
I'm running into two problems:
A. Mimicking the super.method ++
functionality from the previous implementation. At present I'm using
class Clazz1 extends SuperClass
class Clazz2 extends SubSuperClass
private def superClassMethod(s: SuperClass): Map[String, Any] = { ... }
private def subSuperClassMethod(s: SubSuperClass): Map[String, Any] = {
superClassMethod(s) ++ ...
}
implicit object Clazz1Method extends MethodTrait[Clazz1] {
def method(target: Clazz1): Map[String, Any] = {
superClassMethod(target) ++ ...
}
}
implicit object Clazz2Method extends MethodTrait[Clazz2] {
def method(target: Clazz2): Map[String, Any] = {
subSuperClassMethod(target) ++ ...
}
}
but this is ugly, and I won't get a warning or error if I accidentally call a method too far up the hierarchy e.g. if Clazz2
calls superClassMethod
instead of subSuperClassMethod
.
B. Calling method
on a superclass, e.g.
val s: SuperClass = new Clazz1()
s.method
Ideally I'd like to be able to tell the compiler that every subclass of SuperClass
has a corresponding implicit object for method
in the type class and so s.method
is type-safe (or I'll get a compile time error if I've neglected to implement a corresponding implicit object for a subclass of SuperClass
), but instead all I've been able to come up with is
implicit object SuperClassMethod extends MethodTrait[SuperClass] {
def method(target: SuperClass): Map[String, Any] = {
target match {
case c: Clazz1 => c.method
case c: Clazz2 => c.method
...
}
}
}
which is ugly and won't give me a compile-time warning or error if I've omitted a class since I can't define SuperClass
as a sealed trait.
We'd be open to alternatives to type classes that would allow us to concentrate the method
code in one place. method
is only being called from two places:
A. Other method
implementations, for example Clazz1
has a val clazz2: Option[Clazz2]
, in which case the method
implementation in Clazz1
would be something like
def method = super.method ++ /* Clazz1 method implementation */ ++
clazz2.map(_.method).getOrElse(Map())
B. The top level Play Framework controller (i.e. the abstract class from which all of the controllers inherit), where we've defined a three ActionBuilders
that call method
, e.g.
def MethodAction[T <: MethodTrait](block: Request[AnyContent] => T) = {
val f: Request[AnyContent] => SimpleResult =
(req: Request[AnyContent]) => Ok(block(req).method)
MethodActionBuilder.apply(f)
}
I think type classes are not compatible with your scenario. They are useful when the types are disjoint, but you actually require that the instances are reflecting a super-type/sub-type hierarchy and are not independent.
With this refactoring, you are just creating the danger of the wrong instance being picked:
trait Foo
case class Bar() extends Foo
trait HasBaz[A] { def baz: Set[Any] }
implicit object FooHasBaz extends HasBaz[Foo] { def baz = Set("foo") }
implicit object BarHasBaz extends HasBaz[Bar] { def baz = FooHasBaz.baz + "bar" }
def test[A <: Foo](x: A)(implicit hb: HasBaz[A]): Set[Any] = hb.baz
val bar: Foo = Bar()
test(bar) // boom!
So you ended up re-writing the polymorphic dispatch with your pattern matcher in SuperClassMethod
. You basically go OO -> FP -> OO, while rendering the idea of type classes unusable (to be open), ending up rather in a sum type (all sub types known).
@0__ is on to something-- implicit resolution occurs at compilation, so the type class instance that gets used for a given input will not depend on the runtime type of that input.
To get the behavior you want, you'd need to write some implicit definition that will reflect on the actual type of the object on which you want to call method
to pick the right typeclass instance.
I think this is more of a maintenance problem than what you've got right now.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With