隐式转换与类型类


93

在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]

隐式转换

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

我对答案的期望

提出一个(或多个)用例,其中两个概念之间的差异很重要,并解释为什么我更喜欢一个用例。即使没有示例,也可以很好地解释这两个概念的本质以及它们之间的关系。


尽管类型类使用上下文边界,但是在您提到“视图范围”的类型类点中存在一些混淆。
Daniel C. Sobral

1
+1个好问题;我对对此的详尽答案非常感兴趣。
丹·伯顿

@Daniel谢谢。我总是弄错那些。
ziggystar 2011年

2
你在一个地方弄错了:在你的第二个隐式转换比如你存储size列表中的数值,并说,它避免了在后续调用量化名单昂贵的遍历,但是你每次调用上quantifylist2quantifiable被触发从而重新实例化Quantifiable并重新计算该quantify属性。我的意思是实际上没有办法用隐式转换来缓存结果。
Nikita Volkov

@NikitaVolkov您的观察是正确的。我在倒数第二段的问题中解决了这个问题。当一个转换方法调用之后转换的对象使用了更长的时间(并且可能以转换后的形式传递)时,缓存就起作用了。当深入时,类型类可能会沿着未转换的对象链接在一起。
ziggystar,2012年

Answers:


42

尽管我不想从Scala In Depth复制我的资料,但我认为值得注意的是类型类/类型特征无限灵活。

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

能够在其本地环境中搜索默认类型类。但是,我可以通过以下两种方式之一随时覆盖默认行为:

  1. 在Scope中创建/导入隐式类型类实例以短路隐式查找
  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​​层时,如果您不小心的话,这甚至可能是“危险的”。


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

这个例子也说明了这些概念是如何紧密联系的:如果没有机制来产生无限多个实例,类型类将几乎没有用处。如果没有implicit方法(当然不能进行转换),那么我只能有限地具有该Default属性的许多类型。


@Phillippe-我对您编写的技术非常感兴趣...但是在Scala 2.11.6上似乎不起作用。我发布了一个问题,要求您提供最新答案。在此先感谢您是否可以提供帮助:请参阅: stackoverflow.com/questions/31910923/…–
克里斯·贝德福德

@ChrisBedford我default为将来的读者添加了的定义。
Philippe 2015年

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]某种类似于Foo2[A, B]某些A实例的部分应用程序。Miles Sabin将其描述为“ Scala中的功能依赖性”就是一个很好的例子。

所以我的意思是,原则上:

  • (通过隐式转换)“拉皮条”一个类是“零阶”情况...
  • 声明类型类是“一阶”情况...
  • 一般情况下,会出现带有多头肌(或类似多头肌)的多参数类型。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.