什么是Scala中的证据参数?

17

我一直在寻找有关“证据参数”是什么的权威定义,但无果,因为要解决“找不到隐式值类型为...的证据参数”的问题。请问您能否提供一个非常好的解释,说明证据参数究竟是什么?


2
这是一篇不错的短文:http://www.cakesolutions.net/teamblogs/demystifying-implicits-and-typeclasses-in-scala - corn_dog
太棒了@corn_dog。那篇文章真的很到位。只是在想...我们不能使用结构类型来达到同样的目的吗? - matanster
1
不,完全不行,因为对于一个整数进行“Foo”的方式可能与对“java.sql.Timestamp”进行“Foo”的方式不同,因此您不能依赖于特定的方法形式。 - Sean Vieira
4
顺便提一下 - 原始评论提到了这篇文章,但该文章现已被删除,尽管几乎所有有关此主题的资源都指向它! - franklin
3个回答

17
我会尝试发布自己的答案,并在过程中逐步改进。让我们从一个激励场景开始,但您可以跳转到下面的TLDR,然后根据需要再回来这里。
在某种情况下,证据参数可以被视为一种丰富类的手段,以便从其原始定义之外添加一些行为(方法/ s)。
伟大帖子Cake Solutions的轻微重申:
如果您还没有直觉地编写过这样的代码,请参考以下代码演示证据参数的用法。
object EvidenceExample {

  // class with no methods
  case class Bar(value: String)

  // a trait the class Bar had not implemented
  trait WithFoo[A] {
    def foo(x: A): String
  }

  // object that attaches an implementation of the trait for Bar - for methods
  // willing to play along with this kind of trait attachment - see immediately below
  implicit object MakeItFoo extends WithFoo[Bar] {
    def foo(x: Bar) = x.value
  }

  // method willing to recognize anything as having trait WithFoo, 
  // as long as it has evidence that it does - the evidence being the previous object
  def callFoo[A](thing: A)(implicit evidence: WithFoo[A]) = evidence.foo(thing)
  
  callFoo(Bar("hi")) // and it works
}

你可能需要从下往上阅读代码,才能意识到一个名为Bar的类在其原始定义之外被丰富了。然而,只有与证据仪式相符的函数才能看到它被丰富了。
这种模式中没有太多的魔法 - 尽管这是一种独特的语言特性 - 包装对象将特性关联到Bar,而callFoo依赖于该关联。
我们甚至可以写出没有implicit的相同模式,但是最后一行需要一个额外的参数来调用方法 - 使用implicit还是不使用的经济学完全取决于您。
您可以根据需要进行升级或降级,例如这里是一个小语法改进:
(这里只修改了最后一个def,并删除了注释)
object EquivalentEvidenceExample {

  case class Bar(value: String)

  // a trait the class Bar had not implemented
  trait WithFoo[A] {
    def foo(x: A): String
  }
  
  implicit object MakeItFoo extends WithFoo[Bar] {
    def foo(x: Bar) = x.value
  }
  
  def callFoo[A:WithFoo](thing: A) = implicitly[WithFoo[A]].foo(thing) // lightly sugared syntax, frankly only more confusing
  
  callFoo(Bar("hi"))
}

并没有要求您使用字符串evidence来命名任何内容。编译器只是通过它在所有等效情况下的使用方式知道这是一个证据参数。

更一般地说,或者从词源上来说,借鉴自其他答案,证据参数是指“证明”类型特定属性的参数,并且在方法签名表现出这种要求的地方,编译器需要它(在其他答案中,没有提供类型Any<:< Foo的证据,因此存在缺失证据的情况)。

没有可用的隐式证据对象将导致著名的could not find implicit value for evidence parameter of type ...,因为编译器知道这是证据模式的一部分,而不仅仅是缺少隐式(无论这种区别对您有多重要)。

TLDR:

简而言之,对于某个类S,其证据参数是一种类型为T[S]的参数(即一个类),它定义了关于S的一个或多个内容——从而“证明”了关于S的一些东西——这使得调用方可以将S用于扩展使用,超出原始定义范围。这样的T[S]应该具有的确切形式,在我借用的上面的例子中,由implicit object MakeItFoo说明。

7
语言规范在§7.4 上下文边界和视图边界中使用“evidence”一词:

方法或非特质类的类型参数A也可以具有一个或多个上下文边界A:T。在这种情况下,类型参数可以实例化为任何类型S,只要在实例化点存在证据,表明S满足绑定T。这样的证据包括一个具有类型T[S]的隐式值。

通过这种语法糖,您可以获得规范称之为“evidence参数”的合成参数。(请注意,这也涵盖了现已弃用的视图边界<%)。

由于经常明确写出隐含参数也被称为"evidence",所以如果它证明了类型的特定属性,我认为任何隐含参数都可以称为"evidence"。 例如,<:< [A, B] 表明 AB 的子类型。

trait Foo

trait Bar[A] {
  def baz(implicit evidence: A <:< Foo): Unit
}

然后如果你尝试这样做:

trait Test {
  def bar: Bar[Any]

  bar.baz
}

这会导致编译错误:

<console>:58: error: Cannot prove that Any <:< Foo.
         bar.baz
             ^

精确的措辞可以通过implicitNotFound注释来指定。如果没有具体的代码示例,就不清楚是什么生成了“无法找到类型为...的证明参数的隐式值”。
这里是一个自定义消息的例子:
@annotation.implicitNotFound(msg = "Oh noes! No type class for ${A}")
trait MyTypeClass[A]

trait Bar[A] {
  def baz(implicit evidence: MyTypeClass[A]): Unit
}

然后:

trait Test {
  def bar: Bar[Any]

  bar.baz
}

失败并返回自定义消息:
<console>:58: error: Oh noes! No type class for Any
         bar.baz
             ^

1
这只是一种约定,而不是编程语言的内置功能吗?如果使用该术语而不是镜像参数名称会导致编译器错误,并且在语言规范中提到它,我可以进一步说服您支持这种做法。 - matanster
1
谢谢@matt - 我之前不知道这个词在规范中被使用。我现在已经做了详细的解释。 - 0__
诚然,我有时会觉得这与常规的隐式转换和丰富模式有些混淆。一篇好的博客文章可能会更加凝聚地将它们联系在一起(我认为,在这些语言特性中重复使用现有的语言标记在某种程度上可以成为辩论的话题,至少与下划线哲学保持一致)。 - matanster
一些不同之处在于,即使最终使用某种转换,您也可以将其用作远离实际转换应用的约束。另外,您实际上不需要进行一些实际的转换。请参阅 http://dcsobral.blogspot.com/2010/06/implicit-tricks-type-class-pattern.html。 - corn_dog

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接