Scala:类型类,同一范围内的默认和自定义隐式转换

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

我想知道是否有一种优雅的方法可以在同一范围内同时进行默认隐式转换和一些用户定义的自定义转换。我有以下用例:

  1. 假设我们有一个特征,它为泛型 E
  2. 定义了一些二进制操作(例如 PlusSupport,它定义了 plus(x, y)
  3. 我们可以在可以转换为 PlusSupport 的对象上添加 "+" 语法,为此我们需要提供隐式转换
  4. 对于我们库的许多不同类型和用户,有很多到 PlusSupport 的默认隐式转换 总是 将它们导入为例如import defaultConversions._(全部导入,不要多想)
  5. 现在,用户为已经具有来自 import defaultConversions._ 的默认转换的某些类型添加了一些自定义隐式转换 (implicit val customConversion = ...)(此自定义转换可能是用户编写的,也可能是由第三方提供的-派对图书馆 com.3dparty.veryAdvancedConversions.AwesomePlus);用户希望使用他的自定义转换

代码示例如下:

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)

}

它没有编译并出现以下错误:

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

可以通过两种方式更正代码:

  1. 我们可以删除 implicit val myCustomPlusForA——一切都会正常工作,并且将使用 defaultConversions默认隐式转换;但我们需要完全使用我的自定义转换,所以这不是一个选项

  2. 我们可以把import defaultConversions._改成import defaultConversions.{everything except conversion for A},然后用myCustomPlusForA;但这也是一个糟糕的选择,因为图书馆的用户不会关心它(用户只想导入所有 "defaults" 并添加一些 "customization",例如他可以使用 implicit val myCustomPlusForA没有 implicit 关键字(所有编译都很好),而不是添加 implicit 只是为了测试完全自定义如何改变)

所以问题是如何修复代码,使 import defaultConversions._implicit val myCustomPlusForA 都在同一范围内,并且 myCustomPlusForA 将被编译器使用?应使用哪种代码模式来实现所需的行为?

更新: 到目前为止我找到的解决方法是 use default value 用于隐式参数并完全删除 import defaultConversions._ (甚至使 defaultConversions private 以避免用户使用它):

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

但是在运行时进行检查确实看起来很奇怪,而所有信息在编译时都是可用的,编译器应该 "substitute" 正确的转换。

正如@JesperNordenberg 在评论中建议的那样,可以使用隐式优先级排序。因此,为了使事情正常进行,只需将方法从 defaultConversions 移动到 PlusSupport 的伴随对象并完全删除 defaultConversions

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)

}