隐式转换与类型类

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

隐式转换

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

我期望从答案中得到什么

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

8085 次浏览

One criterion that can come into play is how you want the new feature to "feel" like; using implicit conversions, you can make it look like it is just another method:

"my string".newFeature

...while using type classes it will always look like it you are calling an external function:

newFeature("my string")

One thing that you can achieve with type classes and not with implicit conversions is adding properties to a type, rather than to an instance of a type. You can then access these properties even when you do not have an instance of the type available. A canonical example would be:

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

This example also shows how the concepts are tightly related: type classes would not be nearly as useful if there were no mechanism to produce infinitely many of their instances; without the implicit method (not a conversion, admittedly), I could only have finitely many types have the Default property.

You can think of the difference between the two techniques by analogy to function application, just with a named wrapper. For example:

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

An instance of the former encapsulates a function of type A => Int, whereas an instance of the latter has already been applied to an A. You could continue the pattern...

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

thus you could think of Foo1[B] sort of like the partial application of Foo2[A, B] to some A instance. A great example of this was written up by Miles Sabin as "Functional Dependencies in Scala".

So really my point is that, in principle:

  • "pimping" a class (through implicit conversion) is the "zero'th order" case...
  • declaring a typeclass is the "first order" case...
  • multi-parameter typeclasses with fundeps (or something like fundeps) is the general case.

While I don't want to duplicate my material from Scala In Depth, I think it's worth noting that type classes / type traits are infinitely more flexible.

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

has the ability to search its local environment for a default type class. However, I can override default behavior at any time by one of two ways:

  1. Creating/importing an implicit type class instance in Scope to short-circuit implicit lookup
  2. Directly passing a type class

Here's an example:

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
}

This makes type classes infinitely more flexible. Another thing is that type classes / traits support implicit lookup better.

In your first example, if you use an implicit view, the compiler will do an implicit lookup for:

Function1[Int, ?]

Which will look at Function1's companion object and the Int companion object.

Notice that Quantifiable is nowhere in the implicit lookup. This means you have to place the implicit view in a package object or import it into scope. It's more work to remember what's going on.

On the other hand, a type class is explicit. You see what it's looking for in the method signature. You also have an implicit lookup of

Quantifiable[Int]

which will look in Quantifiable's companion object and Int's companion object. Meaning that you can provide defaults and new types (like a MyString class) can provide a default in their companion object and it will be implicitly searched.

In general, I use type classes. They are infinitely more flexible for the initial example. The only place I use implicit conversions is when using an API layer between a Scala wrapper and a Java library, and even this can be 'dangerous' if you're not careful.