正如我从这篇博文中了解到的 那样,Scala 中的“类型类”只是用特征和隐式适配器实现的“模式”。
正如博客所说,如果我有 traitA
和适配器,B -> A
那么我可以调用一个函数,该函数需要 typeA
的参数,并带有 type 的参数,B
而无需显式调用此适配器。
我发现它很好,但不是特别有用。您能否提供一个用例/示例,说明此功能的用途?
一个用例,根据要求...
想象一下,你有一个东西列表,可以是整数、浮点数、矩阵、字符串、波形等。给定这个列表,你想添加内容。
做到这一点的一种方法是拥有一些Addable
必须由可以添加在一起的每个单一类型继承的特征,或者隐式转换为Addable
if 处理来自无法改装接口的第三方库的对象。
当您还想开始向对象列表添加其他此类操作时,这种方法很快就会变得难以应付。如果您需要替代方案(例如;添加两个波形是连接它们还是覆盖它们?),它也不能很好地工作。解决方案是临时多态性,您可以在其中挑选和选择要改进为现有类型的行为。
那么对于原始问题,您可以实现一个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
方法是定义 onTraversableLike
而不是 on List
,并且类型类是Numeric
(它还包含一些比zero
and更多的操作append
)
重读那里的第一条评论:
类型类和接口之间的一个关键区别是,类 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)
...
}
我认为类型类是向类添加类型安全元数据的能力。
因此,您首先定义一个类来对问题域进行建模,然后考虑添加元数据。诸如 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. 为每个集合类型编写一个方法。例如,addElementToSubList
等等addElementToSet
。这在实现中创建了很多样板并污染了命名空间 2. 使用反射来确定子集合是否是 List / Set。这很棘手,因为地图一开始是空的(当然 scala 在这里也有助于 Manifests) 3. 通过要求用户提供加法器来获得穷人的类型类。像这样的东西addToMap(map, key, value, adder)
,简直丑陋
我觉得这篇博文有帮助的另一种方式是它描述了类型类: Monads Are Not Metaphors
在文章中搜索 typeclass。这应该是第一场比赛。在本文中,作者提供了一个 Monad 类型类的示例。
论坛主题“是什么让类型类比特征更好? ”提出了一些有趣的观点:
- 类型类可以很容易地表示在存在子类型的情况下很难表示的概念,例如相等和排序。
练习:创建一个小的类/特征层次结构,并尝试.equals
在每个类/特征上实现,使得对层次结构中任意实例的操作具有适当的自反性、对称性和传递性。- 类型类允许您提供证据,证明您“控制”之外的类型符合某些行为。
其他人的类型可以是您的类型类的成员。- 你不能用子类型来表达“这个方法接受/返回与方法接收器相同类型的值”,但是这个(非常有用的)约束使用类型类很简单。这是f 有界类型问题(其中 F 有界类型在其自己的子类型上参数化)。
- 在 trait 上定义的所有操作都需要一个实例;总是有
this
争论。因此,您不能以这样的方式定义例如一个fromString(s:String): Foo
方法,trait Foo
即您可以在没有Foo
.
在 Scala 中,这表现为人们拼命地试图抽象出伴随对象。
但是使用类型类很简单,如这个幺半群示例中的零元素所示。- 类型类可以归纳定义;例如,如果您有一个,
JsonCodec[Woozle]
您可以免费获得一个JsonCodec[List[Woozle]]
。
上面的例子说明了“你可以加在一起的东西”。
查看类型类的一种方法是它们启用追溯扩展或追溯多态性。Casual Miracles和Daniel Westheide发表了几篇很棒的文章,展示了在 Scala 中使用类型类来实现这一目标的示例。
这是我博客上的一篇文章 ,探讨了追溯超类型scala 中的各种方法,一种追溯扩展,包括一个类型类示例。
除了Ad-hoc 多态性之外,我不知道任何其他用例,这里以最好的方式进行了解释。
隐式和类型类都用于类型转换。它们的主要用例是在您无法修改但期望继承类型的多态性的类上提供临时多态性(即)。在隐式的情况下,您可以同时使用隐式 def 或隐式类(这是您的包装类,但对客户端隐藏)。类型类更强大,因为它们可以向已经存在的继承链添加功能(例如:Scala 的排序函数中的 Ordering[T])。有关更多详细信息,您可以查看https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/
在 scala 类型类中
行为可以扩展 - 在编译时 - 事后 - 无需更改/重新编译现有代码
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
}
我喜欢使用类型类作为依赖注入的轻量级 Scala 惯用形式,它仍然适用于循环依赖,但不会增加很多代码复杂性。我最近重写了一个Scala 项目,使用 Cake Pattern 为 DI 键入类,代码大小减少了 59%。