隐式转换与类型类

zig*_*tar 91 coding-style scala implicit-conversion

在Scala中,我们可以使用至少两种方法来改进现有或新类型.假设我们想要表达某些东西可以使用a来量化Int.我们可以定义以下特征.

隐式转换

trait Quantifiable{ def quantify: Int }
Run Code Online (Sandbox Code Playgroud)

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

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

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

输入类

另一种方法是定义一个"证据" Quantified[A],表明某种类型A可以量化.

trait Quantified[A] { def quantify(a: A): Int }
Run Code Online (Sandbox Code Playgroud)

然后,我们提供这种类型的类的实例为StringList地方.

implicit val stringQuantifiable = new Quantified[String] {
  def quantify(s: String) = s.size 
}
Run Code Online (Sandbox Code Playgroud)

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

def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = 
  as.map(ev.quantify).sum
Run Code Online (Sandbox Code Playgroud)

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

def sumQuantities[A: Quantified](as: List[A]) = 
  as.map(implicitly[Quantified[A]].quantify).sum
Run Code Online (Sandbox Code Playgroud)

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

现在问题来了.我如何决定这两个概念?

到目前为止我注意到了什么.

类型类

  • 类类允许良好的上下文绑定语法
  • 使用类型类我不会在每次使用时创建一个新的包装器对象
  • 如果类型类有多个类型参数,则上下文绑定语法不再起作用; 想象一下,我想要用整数来量化事物,而不是用一些普通类型的值来量化T.我想创建一个类型类Quantified[A,T]

隐式转换

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

我对答案的期望

提出一个(或多个)用例,其中两个概念之间的差异很重要,并解释为什么我更喜欢一个概念.同样解释这两个概念的本质及其相互之间的关系也很好.

jsu*_*eth 40

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

def foo[T: TypeClass](t: T) = ...
Run Code Online (Sandbox Code Playgroud)

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

  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
}
Run Code Online (Sandbox Code Playgroud)

这使得类类无限灵活.另一件事是类型类/特性更好地支持隐式查找.

在第一个示例中,如果使用隐式视图,编译器将对以下内容执行隐式查找:

Function1[Int, ?]
Run Code Online (Sandbox Code Playgroud)

这将看到Function1的伴侣对象和Int伴侣对象.

请注意,Quantifiable无处在隐查找.这意味着您必须将隐式视图放在包对象中将其导入范围.要记住正在发生的事情还需要做更多的工作.

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

Quantifiable[Int]
Run Code Online (Sandbox Code Playgroud)

它会查看Quantifiable伴侣对象 Int伴侣对象.这意味着您可以提供默认值新类型(如MyString类)可以在其伴随对象中提供默认值,并且将隐式搜索它.

通常,我使用类型类.对于最初的例子,它们更加灵活.我使用隐式转换的唯一地方是在Scala包装器和Java库之间使用API​​层时,如果你不小心,这甚至可能是"危险的".


Phi*_*ppe 20

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

"my string".newFeature
Run Code Online (Sandbox Code Playgroud)

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

newFeature("my string")
Run Code Online (Sandbox Code Playgroud)

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

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))
Run Code Online (Sandbox Code Playgroud)

这个例子还说明了概念是如何紧密相关的:如果没有机制来生成无限多个实例,那么类型类就不会那么有用了.没有implicit方法(不是转换,不可否认),我只能拥有有限多种类型的Default属性.


mer*_*ict 13

您可以通过类比功能应用程序来考虑两种技术之间的区别,只需使用命名包装器即可.例如:

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

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

trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int
Run Code Online (Sandbox Code Playgroud)

因此,你能想到的Foo1[B]有点像的部分应用程序Foo2[A, B]的一些A实例.Miles Sabin写的一个很好的例子就是"Scala中的功能依赖".

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

  • "拉皮条"一类(通过隐式转换)是"零级"案例......
  • 声明类型类是"第一顺序"的情况......
  • 具有fundeps(或类似fundeps)的多参数类型类是一般情况.