简体   繁体   中英

Scala: type classes, default and custom implicit conversions in the same scope

I'm wondering whether there is an elegant way to have both the default implicit conversions and some user-defined custom conversion in the same scope. I have the following use-case:

  1. Suppose we have a trait which defines some binary operation (eg PlusSupport , which defines plus(x, y) ) for generic type E
  2. We can add "+" syntax on objects that can be converted to PlusSupport and for that we need to provide implicit conversions
  3. There are a lot of default implicit conversions to PlusSupport for many different types and user of our library always import them as eg import defaultConversions._ (import all and don't think a lot )
  4. Now the user adds some custom implicit conversion ( implicit val customConversion = ... ) for some type which already has the default conversion from import defaultConversions._ (this custom conversion may be either user-written or provided by the third-party library com.3dparty.veryAdvancedConversions.AwesomePlus ); the user expects that his custom conversion will be used

Here is the code example:

trait A // some type A
trait B // some type B
// ... many other types goes here

// some binary operation
trait PlusSupport[E] {
  def plus(a: E, b: E): E
}

object defaultConversions {
  // default conversion of A to PlusSupport[A]
  implicit def mkPlusSupportForA: A => PlusSupport[A] = _ => (a1: A, a2: A) => a1

  // default conversion of B to PlusSupport[B]
  implicit def mkPlusSupportForB: B => PlusSupport[B] = _ => (b1: B, b2: B) => b1

  // ... many other conversions goes here
}

// + operator for elements with PlusSupport 
class PlusOps[E](lhs: E)(plus: PlusSupport[E]) {
  def +(rhs: E): E = plus.plus(lhs, rhs)
}

// adds "+" syntax
trait PlusSyntax {
  implicit def plusOps[E](lhs: E)(implicit mkPlusSupport: E => PlusSupport[E]): PlusOps[E]
  = new PlusOps[E](lhs)(mkPlusSupport(lhs))
}

object syntax extends PlusSyntax


def main(args: Array[String]): Unit = {
  // import all default conversions for A, B, C, D etc. etc.
  import defaultConversions._
  import syntax._

  // setup my custom conversion for A
  implicit val myCustomPlusForA: A => PlusSupport[A] = _ => (a1: A, a2: A) => a2

  val a1: A = new A {}
  val a2: A = new A {}
  val b1: B = new B {}
  val b2: B = new B {}

  // myCustomPlusForA should be used
  println((a1 + a2) == a1)
  println((a1 + a2) == a2)

  // default conversion for B should be used
  println((b1 + b2) == b1)
  println((b1 + b2) == b2)

}

It doesn't compile with the following error:

Error:(52, 19) type mismatch;
found   : A
required: String
   println((a1 + a2) == a1)

The code can be corrected in 2 ways:

  1. We can remove implicit val myCustomPlusForA -- everything will work fine and the default implicit conversion from defaultConversions will be used ; but we need to use exactly my custom conversion, so this is not an option

  2. We can change import defaultConversions._ to import defaultConversions.{everything except conversion for A} and then myCustomPlusForA will be used ; but this is also a bad option since the user of the library would not take care about it (the user just want to import all "defaults" and add some "customization", eg he can use implicit val myCustomPlusForA without implicit keyword (all compiles fine) and than to add implicit just to test how things are changed with full customization)

So the question is how to fix the code so that both import defaultConversions._ and implicit val myCustomPlusForA will be in the same scope and exactly myCustomPlusForA will be used by the compiler? Which code pattern should be used to achieved the desired behaviour?

Update: the workaround that I have found so far is to use default value for implicit parameter and completely remove import defaultConversions._ (even make defaultConversions private to avoid its usage by the users):

private def defaultMk[E](ev: E): E => PlusSupport[E] = ev match {
  case _: A => mkPlusSupportForA.asInstanceOf[E => PlusSupport[E]]
  case _: B => mkPlusSupportForB.asInstanceOf[E => PlusSupport[E]]
  case _ => ???
}

trait PlusSyntax {
  implicit def plusOps[E](lhs: E)(implicit mkPlusSupport: E => PlusSupport[E]
    = defaultConversions.defaultMk(lhs)): PlusOps[E] = new PlusOps[E](lhs)(mkPlusSupport(lhs))
}

but it really looks strange to do the check at runtime while all information is available at the compile time and the compiler should just "substitute" the correct conversion.

As @JesperNordenberg suggested in comment, one can use implicit prioritization. So, in order to make things work one just need to move methods from defaultConversions to a companion object of PlusSupport and delete defaultConversions at all:

trait PlusSupport[E] {
  def plus(a: E, b: E): E
}

// place default implicit conversions into companion object
private object PlusSupport {
  // default conversion of A to PlusSupport[A]
  implicit def mkPlusSupportForA: A => PlusSupport[A] = _ => (a1: A, a2: A) => a1

  // default conversion of B to PlusSupport[B]
  implicit def mkPlusSupportForB: B => PlusSupport[B] = _ => (b1: B, b2: B) => b1

  // ... many other conversions goes here
}


def main(args: Array[String]): Unit = {
  // no need to import from object PlusSupport

  import syntax._

  // setup my custom conversion for A
  implicit val myCustomPlusForA: A => PlusSupport[A] = _ => (a1: A, a2: A) => a2

  val a1: A = new A {}
  val a2: A = new A {}
  val b1: B = new B {}
  val b2: B = new B {}

  // myCustomPlusForA will be used
  println((a1 + a2) == a1)
  println((a1 + a2) == a2)

  // default conversion for B will be used
  println((b1 + b2) == b1)
  println((b1 + b2) == b2)

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM