Scala中的类型类有什么用处?

66

根据这篇博客文章,在Scala中,“type classes”只是一个用特质和隐式适配器实现的“模式”。

正如该博客所说,如果我有特质A和一个适配器B -> A,那么我可以使用类型为B的参数调用需要参数类型为A的函数,而无需显式地调用此适配器。

我觉得这很好,但并不特别有用。你能否给出一个使用案例/示例,展示这个功能有什么用处?


3
我在我的博客文章中有一个具体的例子:http://www.scala-notes.org/2010/08/a-generic-interpolate-method-using-type-classes/ - Jesper
26
你真的应该接受对你提出的问题的答案,你知道的。 - Kevin Wright
在这里,您可以看到一个使用类型类实现适配器模式的实际示例 - https://maxondev.com/adapter-design-pattern-scala-implicits/ - Maxim
11个回答

86

一个使用案例,如要求...

想象一下你有一个东西列表,可能是整数、浮点数、矩阵、字符串、波形等。鉴于这个列表,你想要添加它们的内容。

其中一种方法是拥有一些必须被每个可以相加的类型继承的 Addable 特征,或者如果处理来自第三方库的对象,无法修改接口,则隐式转换为 Addable

当您还想开始添加其他可对对象列表执行的操作时,此方法很快变得不堪重负。如果您需要替代方案(例如:将两个波形相加会将它们串联起来还是覆盖在一起?)那么这也不起作用。解决方案是使用临时多态性,其中您可以选择和选择要适用于现有类型的行为。

对于原始问题,您可以实施一个 Addable 类型类:

trait Addable[T] {
  def zero: T
  def append(a: T, b: T): T
}
//yup, it's our friend the monoid, with a different name!

您可以创建隐式子类实例,对应于每个您希望可添加的类型:
implicit object IntIsAddable extends Addable[Int] {
  def zero = 0
  def append(a: Int, b: Int) = a + b
}

implicit object StringIsAddable extends Addable[String] {
  def zero = ""
  def append(a: String, b: String) = a + b
}

//etc...

对列表求和的方法现在变得非常简单,只需按照以下步骤进行即可...
def sum[T](xs: List[T])(implicit addable: Addable[T]) =
  xs.FoldLeft(addable.zero)(addable.append)

//or the same thing, using context bounds:

def sum[T : Addable](xs: List[T]) = {
  val addable = implicitly[Addable[T]]
  xs.FoldLeft(addable.zero)(addable.append)
}

这种方法的美妙之处在于您可以提供某个类型类的另一种定义,通过导入控制想要在作用域中使用的隐式参数,或者通过显式提供否则的隐式参数来实现。 因此,可以提供不同的添加波形方式,或者指定整数加法的模算术。将第三方库的类型添加到您的类型类也相当轻松。

顺便说一下,这正是2.8集合API采用的方法。 虽然sum方法是在TraversableLike上定义的,而不是在List上定义的,类型类是Numeric(它还包含一些不仅仅是zeroappend的操作)。


3
很好。它非常接近 Haskell 的类型类,唯一的区别是这里的语法有点繁琐。 - Ratn Deo--Dev
19
略微有些繁琐,但也更加灵活。在Scala中,由于类型类有名称,您可以定义多个类型类的变体,并通过将其引入作用域来控制要使用哪一个。 - Kevin Wright
非常好的解释。您为什么将该方法称为“append”而不是“add”? - mitchus
1
@mitchus 这是范畴论在Haskell和Scalaz中采用的约定。整数加法和字符串连接是幺半群的例子,它由一个被称为"append"的可结合二元运算符和一个零元素来描述。请参阅:https://en.wikibooks.org/wiki/Haskell/Monoids - mseddon

32
重新阅读第一条评论:
关于类型类和接口的一个重要区别是,对于类A成为接口的“成员”,它必须在自己定义的地方声明。相比之下,任何类型都可以在任何时候添加到类型类中,只要您可以提供所需的定义,因此在任何给定时间,类型类的成员取决于当前范围。因此,我们不关心A的创建者是否预期了我们希望它属于的类型类;如果没有,我们可以简单地创建自己的定义,以表明它确实属于,并相应地使用它。因此,这不仅提供了比适配器更好的解决方案,在某种程度上还消除了适配器旨在解决的整个问题。
我认为这是类型类最重要的优点之一。
此外,它们可以正确处理操作没有我们正在调度的类型的参数或有多个参数的情况。例如,请考虑以下类型类:
case class Default[T](val default: T)

object Default {
  implicit def IntDefault: Default[Int] = Default(0)

  implicit def OptionDefault[T]: Default[Option[T]] = Default(None)

  ...
}

7
这确实是一个重要的区别:类型 A 不必知道自己是某个类型类的成员,而且它可以被添加到新的类型类中,而不需要修改 A 本身。这与在 Java 中使用常规接口不同,你必须让 A 实现该接口。 - Jesper

9
我认为类型类是向类添加类型安全元数据的能力。
因此,您首先定义一个类来模拟问题域,然后考虑要添加的元数据。例如Equals、Hashable、Viewable等内容。这样可以将问题域和使用该类的机制分离,并打开子类化,因为该类更加精简。
除此之外,您可以在任何范围内添加类型类,而不仅仅是在定义类的位置,并且可以更改实现。例如,如果我通过使用Point#hashCode为Point类计算哈希码,则我受限于特定实现,可能无法为我拥有的特定点集创建良好的值分布。但是,如果我使用Hashable[Point],则可以提供自己的实现。
[更新示例] 以下是上周我遇到的一个用例。在我们的产品中,有几种包含容器作为值的Map情况。例如,Map[Int, List[String]]Map[String, Set[Int]]。向这些集合添加内容可能会很冗长:
map += key -> (value :: map.getOrElse(key, List()))

所以我想要一个函数来包装这个,这样我就可以写:
map +++= key -> value

主要问题在于集合并不都具有相同的添加元素方法。一些使用 '+',另一些则使用 ':+'。我还希望保留向列表添加元素的效率,所以不想使用 fold/map 等会创建新集合的方法。
解决方案是使用类型类:
  trait Addable[C, CC] {
    def add(c: C, cc: CC) : CC
    def empty: CC
  }

  object Addable {
    implicit def listAddable[A] = new Addable[A, List[A]] {
      def empty = Nil

      def add(c: A, cc: List[A]) = c :: cc
    }

    implicit def addableAddable[A, Add](implicit cbf: CanBuildFrom[Add, A, Add]) = new Addable[A, Add] {
      def empty = cbf().result

      def add(c: A, cc: Add) = (cbf(cc) += c).result
    }
  }

我定义了一个类型类Addable,可以将元素C添加到集合CC中。我有两个默认实现:对于使用::的列表和其他集合,使用构建器框架。

然后使用这个类型类是:

class RichCollectionMap[A, C, B[_], M[X, Y] <: collection.Map[X, Y]](map: M[A, B[C]])(implicit adder: Addable[C, B[C]]) {
    def updateSeq[That](a: A, c: C)(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That  = {
      val pair = (a -> adder.add(c, map.getOrElse(a, adder.empty) ))
      (map + pair).asInstanceOf[That]
    }

    def +++[That](t: (A, C))(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That  = updateSeq(t._1, t._2)(cbf)
  }

  implicit def toRichCollectionMap[A, C, B[_], M[X, Y] <: col

特别之处在于使用 adder.add 添加元素和 adder.empty 为新的键创建新集合。
相比之下,如果没有类型类,我将有三个选项: 1. 为每种集合类型编写一个方法。例如,addElementToSubListaddElementToSet 等。这会在实现中创建大量样板,并污染命名空间。 2. 使用反射确定子集合是否为 List / Set。这很棘手,因为映射一开始是空的(当然,Scala 在这里也有帮助,使用 Manifests)。 3. 通过要求用户提供 adder 来拥有穷人版的类型类。因此,像 addToMap(map, key, value, adder) 这样的东西,非常丑陋。

1
谢谢。在商业应用程序的背景下,类型类有助于建模与业务域正交的关注点。Equals和Hashable是很好的例子,但Java类已经有了“equals”和“hashCode”方法,不幸的是。我正在考虑诸如网络和持久性之类的问题,并可能很快发布一个相关问题。 - Michael
我不确定问题是getOrElse/mapwithDefaultValue的冗长,还是向map/collection添加项和默认值不是问题? - Val
1
问题是在我无法控制的几种类型上创建一个公共接口(+++=),因此我无法直接将方法添加到它们的接口中。 - IttayD

6

我发现这篇博客文章有帮助的另一种方式是它描述了类型类: Monads Are Not Metaphors

在文章中搜索“typeclass”,应该是第一个匹配项。在本文中,作者提供了一个Monad类型类的示例。


5

论坛帖子 "什么使类型类比特质更好?" 提出了一些有趣的观点:

  • 类型类可以非常容易地表示在子类型存在时很难表示的概念,例如相等性和排序。
    练习:创建一个小的类/特质层次结构,并尝试以这样一种方式实现 .equals,即对来自层次结构中任意实例的操作都是适当的反射性、对称性和传递性。
  • 类型类允许您提供证据表明类型在您“控制”之外符合某些行为。
    别人的类型可以成为您的类型类成员。
  • 您无法用子类型来表达“此方法接受/返回与方法接收器相同类型的值”,但是使用类型类可以轻松实现此(非常有用的)约束。这是f-bounded types问题(其中F-bounded类型是参数化其自身的子类型)。
  • 在特质上定义的所有操作都需要一个实例;始终存在一个 this 参数。因此,您无法以这样的方式在 trait Foo 上定义一个 fromString(s:String): Foo 方法,以便可以在没有 Foo 实例的情况下调用它。
    在Scala中,这表现为人们拼命尝试抽象伴生对象。
    但是使用类型类很容易,正如此单子群示例中的零元素所示。
  • 类型类可以归纳定义;例如,如果您有一个 JsonCodec[Woozle],则可以免费获取 JsonCodec[List[Woozle]]
    上面的示例说明了“您可以相加的东西”。

4

1

无论是隐式转换还是类型类都用于类型转换。两者的主要用例是为您无法修改但需要继承类型多态的类提供特设多态。在隐式转换中,您既可以使用隐式def也可以使用隐式类(这是您的包装类,但对客户端隐藏)。类型类更强大,因为它们可以向已存在的继承链添加功能(例如:scala的sort函数中的Ordering[T])。 有关详细信息,请参见https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/


1

除了这里最好的解释之外,我不知道还有哪些其他用例可以使用临时多态


1
在Scala中,类型类:
  • 实现了自适应多态性
  • 是静态类型的(即类型安全)
  • 借鉴了Haskell语言
  • 解决了表达式问题

行为可以在以下情况下扩展:

  • 在编译时
  • 事后
  • 不改变/重新编译现有代码

Scala隐式参数:

方法的最后一个参数列表可以标记为implicit

  • 编译器会填充隐式参数

  • 实际上,你需要编译器提供的证据

  • 例如,范围内存在类型类的存在

  • 如果需要,您还可以显式指定参数

下面的示例对String类进行扩展,并使用类型类实现扩展该类的新方法,即使string是final也可以扩展 :)

/**
* Created by nihat.hosgur on 2/19/17.
*/
case class PrintTwiceString(val original: String) {
   def printTwice = original + original
}

object TypeClassString extends App {
  implicit def stringToString(s: String) = PrintTwiceString(s)
  val name: String = "Nihat"
  name.printTwice
}

1
以上示例只是将某种类型A隐式转换为B的示例。这并不是Scala类型类模式的典型示例。 - Alexey Novakov

0

是一个重要的区别(函数式编程所需):

enter image description here

考虑 inc:Num a=> a -> a
传入的a与返回的相同,这不能通过子类型实现。

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