【问题标题】:Implicit conversion vs. type class隐式转换与类型类
【发布时间】:2012-01-21 10:21:26
【问题描述】:

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

隐式转换

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

我对答案的期望

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

【问题讨论】:

  • 尽管类型类使用上下文边界,但在类型类点中您提到“视图边界”时存在一些混淆。
  • +1 个很好的问题;我对这个问题的彻底回答非常感兴趣。
  • @Daniel 谢谢。我总是搞错。
  • 您在一个地方弄错了:在第二个隐式转换示例中,您将列表的size 存储在一个值中,并说它避免了在后续调用 quantify 时对列表进行昂贵的遍历,但是在您每次调用quantify 时,list2quantifiable 都会再次被触发,从而重新实例化Quantifiable 并重新计算quantify 属性。我要说的是,实际上没有办法通过隐式转换来缓存结果。
  • @NikitaVolkov 您的观察是正确的。我在倒数第二段的问题中解决了这个问题。当转换后的对象在一个转换方法调用后使用更长时间(并且可能以其转换后的形式传递)时,缓存会起作用。虽然类型类在深入时可能会沿着未转换的对象链接。

标签: scala coding-style implicit-conversion


【解决方案1】:

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

"my string".newFeature

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

newFeature("my string")

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

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 的定义。
【解决方案2】:

您可以通过类比函数应用来思考这两种技术之间的区别,只需使用命名包装器即可。例如:

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 写的 "Functional Dependencies in Scala"

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

  • “拉皮条”一个类(通过隐式转换)是“零阶”情况...
  • 声明类型类是“一阶”案例...
  • 具有fundeps(或类似fundeps)的多参数类型类是一般情况。

【讨论】:

    【解决方案3】:

    虽然我不想从 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 层时,如果你不小心,即使这也可能是“危险的”。

    【讨论】:

    • 嗯,关于“无限灵活”:如果使用隐式定义,我们还可以导入我们需要的精确转换,或者只是显式调用所需的转换
    猜你喜欢
    • 2015-11-11
    • 2013-07-09
    • 1970-01-01
    • 2011-05-08
    • 1970-01-01
    • 2018-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多