【问题标题】:How to overload generic method with different evidence without ambiguity?如何用不同的证据重载泛型方法而没有歧义?
【发布时间】:2019-08-06 22:45:52
【问题描述】:

我有一个自定义的compare 方法,它接受两个参数。其中一个应该可以隐式转换为另一个:

object Test extends App {
  def compare[T1, T2](a: T1, b: T2)(implicit ev: T1 => T2) = compareImpl[T2](ev(a), b)
  def compare[T1, T2](a: T1, b: T2)(implicit ev: T2 => T1) = compareImpl[T1](a, ev(b))

  def compareImpl[T](a: T, b: T) = a == b

  case class Foo(s: String)
  case class Bar(s: String)

  implicit def foo2bar(f: Foo): Bar = Bar(f.s)

  println(compare(Foo("hello"), Bar("hello")))

}

但是这个 sn-p 给了我错误:

error: ambiguous reference to overloaded definition,
       both method compare in object Test of type [T1, T2](a: T1, b: T2)(implicit ev: T2 => T1)Boolean
       and  method compare in object Test of type [T1, T2](a: T1, b: T2)(implicit ev: T1 => T2)Boolean
       match argument types (Test.Foo,Test.Bar) and expected result type Any
         implicit def foo2bar(f: Foo): Bar = Bar(f.s)

如果我删除第二个compare,它可以工作,但如果我删除compare(Bar("hello), Foo("hello")),它将无法编译。

我怎样才能让这两个版本没有歧义?

【问题讨论】:

  • 问题是,在类型擦除之后,两种方法是完全一样的。顺便说一句,即使没有擦除,呼叫站点也可能是模棱两可的,如果我 compare(1, "1") 并且同时存在 Int => StringString => Int。恕我直言,太多的魔法在这里是有问题的。

标签: scala implicit scala-macros


【解决方案1】:

我最终使用了宏,因为目前 Scala 没有 lambda 类型并且它会进行泛型类型擦除,所以不会开箱即用地支持这样的东西。

宏定义:

import scala.reflect.runtime.universe._
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros
import scala.language.implicitConversions

def compare[T1, T2](a: T1, b: T2): Boolean = macro compareImpl[T1,T2]

def compareImpl[T1: c.WeakTypeTag, T2: c.WeakTypeTag](c: Context)(a: c.Expr[T1], b: c.Expr[T2]): c.Expr[Boolean] = {
  import c.universe._
  // Search for T1=>T2 first. If not found, search T2=>T1
  val f1 = c.inferImplicitValue(c.weakTypeOf[T1 => T2])
  if (f1.isEmpty) {
      val f2 = c.inferImplicitValue(c.weakTypeOf[T2 => T1])
      if(f2.isEmpty) {
          c.abort(c.enclosingPosition, s"Cannot find ${weakTypeOf[T1]}=> ${weakTypeOf[T2]}")
      }
      else {
          c.Expr(q"$f2.apply($b) == $a")
      }
  }
  else {
      c.Expr(q"$f1.apply($a) == $b")
  }
}

测试:

case class Foo(s: String)
case class Bar(s: String)

implicit def foo2bar(f: Foo): Bar = Bar(f.s)

println(compare(Foo("hello"), Bar("hello")))
println(compare(Bar("hello"), Foo("hello")))

【讨论】:

    【解决方案2】:

    没有宏的解决方案(它基于类型类)

      def compare[T1, T2](a: T1, b: T2)(implicit cmp: Compare[T1, T2]) = (compareImpl[cmp.T] _).tupled(cmp(a, b))
      def compareImpl[T](a: T, b: T) = a == b
    
      trait Compare[T1, T2] {
        type T
        type Out = (T, T)
        def apply(a: T1, b: T2): Out
      }
    
      object Compare {
        type Aux[T1, T2, T0] = Compare[T1, T2] { type T = T0 }
        def instance[T1, T2, T0](f: (T1, T2) => (T0, T0)): Aux[T1, T2, T0] = new Compare[T1, T2] {
          override type T = T0
          override def apply(a: T1, b: T2): Out = f(a, b)
        }
    
        implicit def directCompare[T1, T2](implicit ev: T1 => T2): Aux[T1, T2, T2] = instance((a, b) => (ev(a), b))
        implicit def reverseCompare[T1, T2](implicit ev: T2 => T1): Aux[T1, T2, T1] = instance((a, b) => (a, ev(b)))
      }
    
      case class Foo(s: String)
      case class Bar(s: String)
    
      implicit def foo2bar(f: Foo): Bar = Bar(f.s)
    
      println(compare(Foo("hello"), Bar("hello"))) // true
    

    或者,如果您愿意,您甚至可以优先考虑直接和反向方向

      def compare[T1, T2](a: T1, b: T2)(implicit cmp: Compare[T1, T2]) = (compareImpl[cmp.T] _).tupled(cmp(a, b))
      def compareImpl[T](a: T, b: T) = a == b
    
      trait Compare[T1, T2] {
        type T
        type Out = (T, T)
        def apply(a: T1, b: T2): Out
      }
    
      trait LowPriorityCompare {
        type Aux[T1, T2, T0] = Compare[T1, T2] { type T = T0 }
        def instance[T1, T2, T0](f: (T1, T2) => (T0, T0)): Aux[T1, T2, T0] = new Compare[T1, T2] {
          override type T = T0
          override def apply(a: T1, b: T2): Out = f(a, b)
        }
    
        implicit def reverseCompare[T1, T2](implicit ev: T2 => T1): Aux[T1, T2, T1] = instance((a, b) => (a, ev(b)))
      }
    
      object Compare extends LowPriorityCompare {
        implicit def directCompare[T1, T2](implicit ev: T1 => T2): Aux[T1, T2, T2] = instance((a, b) => (ev(a), b))
      }
    
      case class Foo(s: String)
      case class Bar(s: String)
    
      implicit def foo2bar(f: Foo): Bar = Bar(f.s)
      implicit def bar2foo(f: Bar): Foo = Foo(f.s)
    
      println(compare(Foo("hello"), Bar("hello"))) // true
    

    【讨论】:

    • 不错的解决方案!一个问题 - 当编译器查找隐式 Aux 时,它可以匹配 directCompare 或 reverseCompare,因为它们具有相同的类型擦除。它如何不产生歧义?
    • @texasbruce 隐式在typer 阶段被解析,类型擦除发生在erasure 阶段typelevel.org/scala/docs/phases.html 所以在隐式解析期间还没有类型被擦除。类型擦除与隐式解析无关。
    【解决方案3】:

    这里的问题是因为您的两个 compare 函数都具有完全相同的类型参数,这对于 Scala 编译器来说是模棱两可的,无法确定使用哪一个。

    例如,当您对 compare[Foo, Bar] 进行比较时,Scala 编译器不清楚它是否应该将 compare 函数与 (implicit ev: T1 => T2) 或第二个函数与 (implicit ev: T2 => T1) 一起使用,因为 FooBar 可以放置为 T1T2

    事实上,这就是当您删除比较函数之一时,它起作用的原因。因为没有compare 函数的重载版本,所以FooBar 可以作为T1T2 放在你的唯一compare 函数中。

    这是另一个 Stackoverflow 问题的答案,它与您的问题有某种关系,并详细描述了问题:

    https://stackoverflow.com/a/16865745/2508492

    【讨论】:

    • 其实很清楚,因为只有Foo=>Bar,没有Bar=>Foo。编译器无法正确区分它们。只有当转换证据是双向的时,它才应该是模棱两可的。
    • 是的,这对我们来说很清楚,但对 Scala 编译器来说不是。这就是当您删除 compare 函数之一时,它实际上会起作用的原因。让 Scala 编译器模棱两可的事实是 (implicit ev: T1 => T2)(implicit ev: T2 => T1) 都可以指向同一个 foo2bar 函数,这对于 Scala 编译器来说是模棱两可的。
    猜你喜欢
    • 2013-08-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多