据我所知,this blog post“Scala中的类型类”只是一个用特征和隐式适配器实现的“模式”。
正如博客所说,如果我有特征A
和适配器B -> A
,那么我可以调用一个函数,该函数需要类型为A
的参数,参数类型为{{1}无需显式调用此适配器。
我发现它很好但不是特别有用。您能给出一个用例/示例,它显示了此功能的用途吗?
答案 0 :(得分:84)
一个用例,根据要求......
想象一下,你有一个事物列表,可能是整数,浮点数,矩阵,字符串,波形等。鉴于此列表,你想要添加内容。
执行此操作的一种方法是使某些Addable
特征必须由可以一起添加的每种类型继承,或者如果处理来自的对象,则隐式转换为Addable
您无法改进接口的第三方库。
当您还想开始添加可以对对象列表执行的其他此类操作时,此方法会变得非常快。如果你需要替代方案,它也不能很好地工作(例如;添加两个波形会连接它们,还是覆盖它们?)解决方案是ad-hoc多态,你可以选择和选择行为对现有类型进行改造。
对于原始问题,您可以实现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
(它还包含一些操作,而不仅仅是zero
和append
)
答案 1 :(得分: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)
...
}
答案 2 :(得分:9)
我认为类型类是向类中添加类型安全元数据的能力。
因此,您首先定义一个类来为问题域建模,然后考虑要添加到其中的元数据。像Equals,Hashable,Viewable等等。这会创建问题域和机制的分离,以使用类并打开子类,因为类更精简。
除此之外,您可以在范围内的任何位置添加类型类,而不仅仅是定义类的位置,还可以更改实现。例如,如果我使用Point#hashCode计算Point类的哈希码,那么我仅限于那个特定的实现,它可能无法为我拥有的特定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也在这里帮助清单)
3.通过要求用户提供加法器来获得穷人的类型。所以像addToMap(map, key, value, adder)
这样的东西很简单
答案 3 :(得分:6)
我发现这篇博文有用的另一种方式是描述类型类:Monads Are Not Metaphors
在文章中搜索类型类。这应该是第一场比赛。在本文中,作者提供了一个Monad类型类的示例。
答案 4 :(得分:5)
论坛帖子&#34; What makes type classes better than traits?&#34;提出了一些有趣的观点:
- 类型类可以非常容易地表示在存在子类型时很难表示的概念,例如相等和排序。 练习:创建一个小的类/特征层次结构,并尝试在每个类/特征上实现
.equals
,使得对层次结构中任意实例的操作具有正确的反身性,对称性和传递性。- 类型类允许您提供证据,证明您的&#34;控制之外的类型&#34;符合某些行为。
其他人的类型可以是你的类型类的成员。- 你不能表达&#34;这个方法接受/返回与方法接收器相同类型的值&#34;在子类型方面,但这种(非常有用的)约束使用类型类是直截了当的。这是 f-bounded types problem (其中F-bounded类型通过其自己的子类型进行参数化)。
- 在特征上定义的所有操作都需要一个实例;始终存在
this
参数。因此,您无法在fromString(s:String): Foo
上定义trait Foo
方法,以便您可以在没有Foo
实例的情况下调用它。 在Scala中,这表现为人们拼命想要在同伴对象上进行抽象 但是对于类型类,它是直截了当的,如this monoid example中的零元素所示。- 类型可以归纳定义;例如,如果您有
JsonCodec[Woozle]
,则可以免费获得JsonCodec[List[Woozle]]
上面的例子说明了这些可以加在一起的东西&#34;。
答案 5 :(得分:4)
查看类型类的一种方法是启用追溯扩展或追溯多态。 Casual Miracles和Daniel Westheide上有一些很棒的帖子显示了在Scala中使用Type Classes来实现此目的的示例。
这是一个post on my blog 探讨了追溯超类型的scala中的各种方法,这是一种追溯扩展,包括类型类示例。
答案 6 :(得分:1)
除了 Ad-hoc多态之外,我不知道任何其他用例,这是最好的解释here。
答案 7 :(得分:1)
implicits 和类型都用于类型转换。它们的主要用例是在您无法修改但期望继承类型的多态的类上提供 ad-hoc多态(即)。如果出现implicits,您可以同时使用隐式def或隐式类(这是您的包装类,但对客户端隐藏)。类型类更强大,因为它们可以为已经存在的继承链添加功能(例如:在scala的排序函数中排序[T])。 有关详细信息,请参阅https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/
答案 8 :(得分:1)
在scala类型中
可以延长行为 - 在编译时 - 事后 - 无需更改/重新编译现有代码
Scala Implicits
方法的最后一个参数列表可以标记为隐式
隐式参数由编译器填写
实际上,您需要编译器的证据
...例如在范围
您还可以根据需要明确指定参数
下面带有类型类实现的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
}
答案 9 :(得分:0)
答案 10 :(得分:0)
我喜欢使用类型类作为依赖注入的轻量级Scala惯用形式,它仍然可以使用循环依赖,但不会增加很多代码复杂性。我最近改写了Scala project from using the Cake Pattern to type classes for DI并将代码大小减少了59%。