隐式转换 vs. 类型类

96

在Scala中,我们可以使用至少两种方法来适配现有或新类型。假设我们想要表达某个东西可以使用Int进行量化。我们可以定义以下特质。

隐式转换

trait Quantifiable{ def quantify: Int }

然后我们可以使用隐式转换来量化例如字符串和列表。

implicit def string2quant(s: String) = new Quantifiable{ 
  def quantify = s.size 
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{ 
  val quantify = l.size 
}

导入这些后,我们可以在字符串和列表上调用方法quantify。请注意,可量化列表存储其长度,因此避免了在后续对quantify的调用中对列表进行昂贵的遍历。

类型类

另一种方法是定义一个“证人”Quantified[A],它表明某些类型A可以被量化。

trait Quantified[A] { def quantify(a: A): Int }

然后我们在某个地方为StringList提供了这个类型类的实例。

implicit val stringQuantifiable = new Quantified[String] {
  def quantify(s: String) = s.size 
}

如果我们想要编写一个需要量化其参数的方法,我们会这样写:

def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = 
  as.map(ev.quantify).sum

或者使用上下文绑定语法:

def sumQuantities[A: Quantified](as: List[A]) = 
  as.map(implicitly[Quantified[A]].quantify).sum

但是什么时候使用哪种方法呢?

现在问题来了,我该如何决定这两个概念之间的区别?

我目前所注意到的是:

类型类

  • 类型类允许使用美好的上下文限定语法
  • 使用类型类时,我不会在每次使用时创建新的包装对象
  • 如果类型类具有多个类型参数,则上下文绑定语法将不再起作用;想象一下,我不仅想量化整数,还想量化某些通用类型 T 的值。我希望创建一个类型类 Quantified[A,T]

隐式转换

  • 由于我创建了一个新对象,因此我可以在其中缓存值或计算更好的表示形式;但是是否应该避免这样做,因为它可能会发生多次,并且显式转换可能只会被调用一次?

我期望得到的答案

提供一个(或多个)使用案例,说明两个概念之间的区别很重要,并解释为什么我更喜欢其中的一个。即使没有示例,也要解释这两个概念的本质及其相互关系。


在类型类中提到“视图界定(view bound)”时存在一些混淆,尽管类型类使用上下文界定(context bounds)。 - Daniel C. Sobral
1
+1 优秀的问题;我非常期待一个详尽的答案。 - Dan Burton
@Daniel 谢谢。我总是搞错这些。 - ziggystar
2
你在一个地方弄错了:在你的第二个隐式转换示例中,你将列表的size存储在一个值中,并说它避免了在后续调用quantify时昂贵的列表遍历,但是每次调用quantify时,list2quantifiable都会再次触发,从而重新实例化Quantifiable并重新计算quantify属性。我的意思是,实际上没有办法使用隐式转换缓存结果。 - Nikita Volkov
@NikitaVolkov,您的观察是正确的。我在倒数第二段中提出了这个问题并作出了回答。当转换后的对象在一个转换方法调用后使用更长时间(可能以其转换形式传递)时,缓存会起作用。而类型类可能会在深入时沿着未转换的对象链进行。 - ziggystar
3个回答

42

虽然我不想复制我在Scala In Depth中的材料,但我认为值得注意的是类型类/类型特征无限灵活。

def foo[T: TypeClass](t: T) = ...

该功能可以搜索其本地环境以查找默认类型类。然而,我可以通过以下两种方式之一随时覆盖默认行为:

  1. 在作用域中创建/导入一个隐式类型类实例,以绕过隐式查找
  2. 直接传递一个类型类

这里是一个示例:

def myMethod(): Unit = {
   // overrides default implicit for Int
   implicit object MyIntFoo extends Foo[Int] { ... }
   foo(5)
   foo(6) // These all use my overridden type class
   foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}

这使得类型类更加灵活。另一件事是,类型类/特质更好地支持隐式查找。
在您的第一个示例中,如果使用隐式视图,编译器将进行隐式查找:
Function1[Int, ?]

本文将讨论Function1的伴生对象和Int的伴生对象。

请注意,Quantifiable在隐式查找中不存在。这意味着您必须将隐式视图放置在包对象中或者将其导入到作用域中。这需要更多的工作来记住发生了什么。

另一方面,类型类是显式的。您可以在方法签名中看到它正在寻找什么。您还可以进行隐式查找。

Quantifiable[Int]

这将查找Quantifiable的伴生对象和Int的伴生对象。这意味着您可以提供默认值,而新类型(例如MyString类)可以在其伴生对象中提供默认值,并且将隐式搜索它。

通常,我使用类型类。对于最初的示例来说,它们具有无限的灵活性。仅在使用Scala包装器和Java库之间的API层时,我才使用隐式转换,即使这样也要小心谨慎,因为它可能会带来“危险”。


关于“无限灵活”的问题:对于隐式定义的情况,我们也可以导入所需的精确转换或显式调用所需的转换。 - plomovtsev

20

一个可能起作用的标准是您希望新功能“感觉”如何; 使用隐式转换,您可以使其看起来像另一种方法:

"my string".newFeature

使用类型类时,它看起来总像是在调用外部函数:

newFeature("my string")

通过类型类而不是隐式转换,你可以将属性添加到一个类型而不是类型的实例中。这样,即使您没有类型实例可用,也可以访问这些属性。一个典型的例子是:

trait Default[T] { def value : T }

implicit object DefaultInt extends Default[Int] {
  def value = 42
}

implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
  def value = implicitly[Default[T]].value :: Nil
}

def default[T : Default] = implicitly[Default[T]].value

scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))

这个例子也展示了概念之间的紧密关联:如果没有产生无限数量实例的机制,类型类将不会那么有用;如果没有隐式方法(诚然不是转换),我只能让有限个类型具有“默认”属性。

@Phillippe - 我对你写的技术非常感兴趣...但是它似乎在Scala 2.11.6上无法工作。我发布了一个问题,请求更新你的答案。如果你能帮忙,提前致谢:请参见:http://stackoverflow.com/questions/31910923/implicit-conversions-that-add-properties-to-a-type-rather-than-to-an-instance-o - Chris Bedford
@ChrisBedford 我为了未来的读者添加了default的定义。 - Philippe

13

你可以通过函数应用类比来理解这两种技术的区别,只是带有一个命名的包装器。例如:

trait Foo1[A] { def foo(a: A): Int }  // analogous to A => Int
trait Foo0    { def foo: Int }        // analogous to Int

前者的实例封装了一个类型为 A => Int 的函数,而后者的实例已经应用于一个 A。您可以继续这个模式...

trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int
因此,您可以将Foo1[B]视为对某个A实例应用了Foo2[A,B]的部分应用。Miles Sabin在"Functional Dependencies in Scala"中给出了一个很好的例子。所以,我的观点是,在原则上:
  • 通过隐式转换来“增强”类是“零阶”情况...
  • 声明类型类是“一阶”情况...
  • 带有基金会依赖项(或类似基金会依赖项)的多参数类型类是一般情况。

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