Scala中的类型类有什么用?


67

这篇博客文章中我了解到 ,Scala中的“类型类”只是使用特征和隐式适配器实现的“模式”。

如博客中所述,如果我具有traitA和适配器,B -> A那么我可以调用一个函数,该函数需要typeA的参数和type类型的参数,B而无需明确调用此适配器。

我发现它不错,但不是特别有用。您能否给出一个用例/示例,以说明此功能的用途?


3
我的博客文章中有一个具体的示例:scala-notes.org/2010/08/…–
Jesper

25
您知道,您确实应该接受问题的答案。
凯文·赖特

在这里,您可以看到使用类型类实现适配器模式的实际示例-maxondev.com/adapter-design-pattern-scala-implicits
Maxim,

Answers:


85

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

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

做到这一点的一种方法是拥有一些Addable必须由可以加在一起的每种类型继承的特征,或者隐式转换为Addableif,如果处理来自第三方库的对象,而这些对象无法改装接口。

当您还想开始添加可以对对象列表执行的其他此类操作时,这种方法很快就会变得不知所措。如果您需要替代方法(例如,添加两个波形是否将它们串联或叠加),它也不能很好地工作。解决方案是临时多态性,您可以在其中选择并选择要改型为现有类型的行为。

那么对于最初的问题,您可以实现一个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 collections API所采用的方法。尽管sum方法是在onTraversableLike而不是on上定义的List,并且类型类是Numeric(它比justzero和包含更多的操作append


3
太好了。它非常接近Haskell类型类,只有语法有点麻烦。
拉恩·迪欧(Ratn Deo)

19
多一点麻烦,多一点灵活性。因为它们是命名的,所以您可以在Scala中定义类型类的多个变体,并通过将其拉入作用域来控制要使用的变体。
凯文·赖特

非常好的解释。您是因为某种原因append而不是方法add
mitchus

1
@mitchus,这是Haskell和scalaz所采用的类别理论的惯例。整数加法和String串联是monoid的示例,其由一个称为“ append”的关联二进制运算符和一个零元素来描述。参见:en.wikibooks.org/wiki/Haskell/Monoids
mseddon 2015年

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类的哈希码,那么我将限于该特定实现,对于我所拥有的特定Points,它可能无法很好地分配值。但是,如果我使用Hashable [Point],则可以提供自己的实现。

[示例更新]作为示例,这是我上周使用的一个用例。在我们的产品中,有些情况下Maps包含容器作为值。例如,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.使用反射来确定该子集是一个列表/设置键。这很棘手,因为地图一开始是空的(当然,scala在这里也对Manifests有所帮助)3.通过要求用户提供加法器来具有穷人类型类。像这样addToMap(map, key, value, adder),这很丑陋


1
谢谢。我要说的是,在业务应用程序的上下文中,类型类有助于对与业务领域正交的问题进行建模。Equals和Hashable是很好的例子,但是不幸的是Java类已经具有“ equals”和“ hashCode”方法。我正在考虑诸如网络和持久性之类的问题,并可能很快就发布一个问题。
迈克尔

如果问题是getOrElse / mapwithDefaultValue的冗长性或将项附加到地图/集合中,则默认值不是问题,我没有得到?
2014年

1
问题是在我无法控制的几种类型上创建了一个通用接口(+++ =)。因此我无法将方法直接添加到他们的界面中。
IttayD 2014年

6

我发现此博客文章对您有所帮助的另一种方式是在其中描述类型类: Monad不是隐喻

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


5

论坛线程“是什么使类型类比特征更好? ”提出了一些有趣的观点:

  • 类型类可以很容易代表的概念是相当困难的代表亚型的存在,如平等排序
    练习:创建一个小的类/特征层次结构并尝试实现.equals对每个类/特征的方式,使得对层次结构中任意实例的操作都可以适当地自反,对称和传递。
  • 类型类使您可以提供证据,证明“控件”之外的类型符合某些行为。
    他人的类型可以是您的typeclass的成员。
  • 您不能用子类型表示“此方法采用/返回与方法接收者相同类型的值”,但是使用类型类可以很简单地实现此约束(非常有用)。这是f界类型问题(其中F边界类型是通过其自身的子类型进行参数化的)。
  • 在特征上定义的所有操作都需要一个实例; 总有this争论。所以你不能例如定义fromString(s:String): Foo的方法,trait Foo以这样的方式,你可以把它叫做没有的一个实例Foo
    在Scala中,这表现为人们拼命尝试对伴侣对象进行抽象。
    但这对于类型类很简单,如本monoid示例中的零元素所示
  • 类型类可以归纳定义;例如,如果您有,则JsonCodec[Woozle]可以JsonCodec[List[Woozle]]免费获得。
    上面的示例针对“您可以添加在一起的东西”对此进行了说明。


1

除了临时多态性之外,我不知道有任何其他用例,在这里将以最佳方式进行解释。


1

无论implicits类型类用于类型转换。两者的主要用例是在您无法修改但希望继承为多态的类上提供临时多态(即)。在隐式的情况下,您可以使用隐式def或隐式类(这是您的包装器类,但对客户端隐藏)。类型类更强大,因为它们可以将功能添加到已经存在的继承链中(例如:scala的sort函数中的Ordering [T])。有关更多详细信息,请参见https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/


1

在scala类型类中

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

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

斯卡拉隐式

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

  • 隐式参数由编译器填充

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

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

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

下面的示例扩展对具有类型类实现的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
}

上面的示例只是从A类型到B的隐式转换。这并不是Scala Type Class模式的惯用示例。
阿列克谢诺瓦科夫

0

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

在此处输入图片说明

考虑inc:Num a=> a -> a

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.