Polymorphism with Scala type classes

2019-06-17 03:50发布

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)
}

3条回答
The star\"
2楼-- · 2019-06-17 03:52

@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.

查看更多
狗以群分
3楼-- · 2019-06-17 04:02

Simply put: If you want to have your implementation in one place, you should use case classes for your hierarchy:

abstract class SuperClass;

case class Clazz(...) extends SuperClass;

def method(o : SuperClass) : Map[String, Any] = o match {
   case Clazz( ... ) => defaultMethod ++ ...
   case ...
} 

(Note that method may of course be recursive) Since you can have an open Sum Type in scala (compiler won't warn about missing patterns, though), that should tackle your problem without having to abuse typeclasses.

查看更多
Animai°情兽
4楼-- · 2019-06-17 04:12

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).

查看更多
登录 后发表回答