【问题标题】:What are type classes in Scala useful for?Scala 中的类型类有什么用?
【发布时间】:2011-07-21 11:59:59
【问题描述】:

据我从this blog post 了解到,Scala 中的“类型类”只是用特征和隐式适配器实现的“模式”。

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

我觉得它不错,但不是特别有用。您能否提供一个用例/示例,说明此功能的用途?

【问题讨论】:

标签: scala implicit


【解决方案1】:

一个用例,根据要求...

假设你有一个东西列表,可以是整数、浮点数、矩阵、字符串、波形等。给定这个列表,你想添加内容。

做到这一点的一种方法是拥有一些Addable trait,它必须由可以添加在一起的每个单一类型继承,或者如果处理来自第三方库的对象,则隐式转换为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!

然后,您可以创建 this 的隐式子类实例,对应于您希望使其可添加的每种类型:

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

这种方法的美妙之处在于您可以提供某个类型类的替代定义,或者通过导入控制您想要的范围内的隐式,或者通过显式提供其他隐式参数。因此,可以提供不同的波形相加方式,或为整数相加指定模运算。将某个 3rd-party 库中的类型添加到您的类型类中也相当轻松。

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

【讨论】:

  • 太棒了。它非常接近 Haskell 类型类,只是这里的语法有点麻烦。
  • 繁琐一点,灵活一点。因为它们是命名的,所以您可以在 Scala 中定义类型类的多个变体,并通过将其拉入作用域来控制要使用的变体。
  • 很好的解释。您将方法称为 append 而不是 add 的任何原因?
  • @mitchus 这是 Haskell 和 scalaz 采用的范畴论的约定。整数加法和字符串连接是 monoid 的示例,它由一个称为“append”的关联二元运算符和一个零元素来描述。见:en.wikibooks.org/wiki/Haskell/Monoids
【解决方案2】:

重读那里的第一条评论:

类型类和接口之间的一个关键区别是,类 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)

  ...
}

【讨论】:

  • 这确实是一个至关重要的区别:A 类型不必知道它是类型类的成员,它可以添加到新的类型类中而无需修改 A 本身.与使用普通接口(如在 Java 中)不同,您必须让 A 实现接口。
【解决方案3】:

我认为类型类是向类添加类型安全元数据的能力。

因此,您首先定义一个类来为问题域建模,然后考虑添加元数据。诸如 Equals、Hashable、Viewable 之类的东西。这创建了问题域和使用类的机制的分离,并打开了子类化,因为类更精简。

除此之外,您可以在范围内的任何位置添加类型类,而不仅仅是定义类的位置,并且您可以更改实现。例如,如果我使用 Point#hashCode 计算 Point 类的哈希码,那么我将仅限于特定的实现,这可能无法为我拥有的特定点集创建良好的值分布。但是如果我使用 Hashable[Point],那么我可以提供自己的实现。

[更新示例] 例如,这是我上周的一个用例。在我们的产品中,有几种地图包含容器作为值的情况。例如,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中。我有 2 个默认实现:对于使用 :: 的列表和对于其他集合,使用构建器框架。

那么使用这个类型类就是:

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 为新键创建新集合。

比较一下,如果没有类型类,我会有 3 个选项: 1.为每个集合类型编写一个方法。例如,addElementToSubListaddElementToSet 等。这会在实现中创建大量样板文件并污染命名空间 2.使用反射判断子集合是否为List/Set。这很棘手,因为地图一开始是空的(当然 scala 在这里也有助于 Manifests) 3. 要求用户提供加法器,具有穷人型等级。所以像addToMap(map, key, value, adder) 这样的东西很丑

【讨论】:

  • 谢谢。我想说的是,在业务应用程序的上下文中,类型类有助于对与业务领域正交的关注点进行建模。 Equals 和 Hashable 是很好的例子,但不幸的是,Java 类已经有了“equals”和“hashCode”方法。我正在考虑诸如网络和持久性之类的问题,并且可能很快就会发布一个关于它的问题。
  • 如果问题是 getOrElse/mapwithDefaultValue 的冗长或将项目附加到地图/集合和默认值不是问题,我没有得到?
  • 问题是在我无法控制的几种类型上创建一个通用接口 (+++=)。所以我不能直接将方法添加到他们的界面中。
【解决方案4】:

我发现这篇博文的另一个有用之处是它描述了类型类:Monads Are Not Metaphors

在文章中搜索 typeclass。这应该是第一场比赛。在本文中,作者提供了一个 Monad 类型类的示例。

【讨论】:

    【解决方案5】:

    论坛帖子“What makes type classes better than traits?”提出了一些有趣的观点:

    • 类型类可以很容易地表示存在子类型时很难表示的概念,例如 equalityordering
      练习:创建一个小的类/特征层次结构,并尝试在每个类/特征上实现.equals,使得对层次结构中任意实例的操作具有适当的自反性、对称性和传递性。
    • 类型类允许您提供证据,证明您“控制”之外的类型符合某些行为。
      其他人的类型可以是您的类型类的成员。
    • 您不能用子类型来表达“此方法接受/返回与方法接收器相同类型的值”,但是这个(非常有用的)约束使用类型类很简单。这是 f-bounded types problem(其中 F 有界类型在其自己的子类型上参数化)。
    • 所有在 trait 上定义的操作都需要一个实例;总是有一个this 参数。因此,您不能在trait Foo 上定义例如fromString(s:String): Foo 方法,这样您就可以在没有Foo 实例的情况下调用它。
      在 Scala 中,这表现为人们拼命地尝试对伴随对象进行抽象。
      但使用类型类很简单,如 this monoid example 中的零元素所示。
    • 类型类可以归纳定义;例如,如果您有一个JsonCodec[Woozle],您可以免费获得一个JsonCodec[List[Woozle]]
      上面的示例说明了“可以添加在一起的事物”。

    【讨论】:

      【解决方案6】:

      查看类型类的一种方法是启用追溯扩展追溯多态性Casual MiraclesDaniel Westheide 有几篇很棒的帖子,展示了在 Scala 中使用类型类来实现这一目标的示例。

      这是post on my blog 探索了 retroactive supertyping scala 中的各种方法,这是一种追溯扩展,包括一个 typeclass 示例。

      【讨论】:

        【解决方案7】:

        除了 Ad-hoc polymorphism 之外,我不知道任何其他用例,here 以最好的方式解释。

        【讨论】:

          【解决方案8】:

          implicitstypeclasses 都用于类型转换。它们的主要用例是提供 ad-hoc polymorphism(即)在您无法修改但期望继承类型的多态性的类上。在隐式的情况下,您可以同时使用隐式 def 或隐式类(这是您的包装类,但对客户端隐藏)。类型类更强大,因为它们可以向已经存在的继承链添加功能(例如:Scala 的排序函数中的 Ordering[T])。 更多详情可以查看https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/

          【讨论】:

            【解决方案9】:

            在 scala 类型类中

            • 启用临时多态性
            • 静态类型(即类型安全)
            • 从 Haskell 借来的
            • 解决表达问题

            行为可以扩展 - 在编译时 - 事后 - 无需更改/重新编译现有代码

            Scala 隐式

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

            • 隐式参数由编译器填写

            • 实际上,您需要编译器的证据

            • ……比如作用域中存在类型类

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

            下面的示例扩展在具有类型类实现的 String 类上使用新方法扩展了该类,即使字符串是最终的:)

            /**
            * 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
            }
            

            【讨论】:

            • 上面的例子只是某种类型 A 到 B 的隐式转换。它不是 Scala 类型类模式的真正惯用例子。
            【解决方案10】:

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

            考虑inc:Num a=&gt; a -&gt; a

            收到的a 与返回的相同,这不能通过子类型来完成

            【讨论】:

              【解决方案11】:

              我喜欢将类型类用作依赖注入的轻量级 Scala 惯用形式,它仍然适用于循环依赖,但不会增加很多代码复杂性。我最近重写了Scala project from using the Cake Pattern to type classes for DI,代码大小减少了 59%。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2021-03-15
                • 1970-01-01
                • 2012-02-02
                • 2011-09-08
                • 1970-01-01
                相关资源
                最近更新 更多