从这篇博客文章中我了解到 ,Scala中的“类型类”只是使用特征和隐式适配器实现的“模式”。
如博客中所述,如果我具有traitA
和适配器,B -> A
那么我可以调用一个函数,该函数需要typeA
的参数和type类型的参数,B
而无需明确调用此适配器。
我发现它不错,但不是特别有用。您能否给出一个用例/示例,以说明此功能的用途?
从这篇博客文章中我了解到 ,Scala中的“类型类”只是使用特征和隐式适配器实现的“模式”。
如博客中所述,如果我具有traitA
和适配器,B -> A
那么我可以调用一个函数,该函数需要typeA
的参数和type类型的参数,B
而无需明确调用此适配器。
我发现它不错,但不是特别有用。您能否给出一个用例/示例,以说明此功能的用途?
Answers:
一个用例,根据要求...
假设您有一个东西列表,可能是整数,浮点数,矩阵,字符串,波形等。给定该列表,您想添加内容。
做到这一点的一种方法是拥有一些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!
然后,您可以创建此对象的隐式子类实例,这些实例与您希望使其可添加的每种类型相对应:
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
)
append
而不是方法add
?
重读那里的第一条评论:
类型类和接口之间的关键区别在于,要使类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
实现接口。
我认为类型类是将类型安全的元数据添加到类的能力。
因此,您首先定义一个类以对问题域建模,然后考虑要添加到其中的元数据。诸如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.为每个集合类型编写一个方法。例如,addElementToSubList
和addElementToSet
等,这创造了很多样板的实施和污染命名空间2.使用反射来确定该子集是一个列表/设置键。这很棘手,因为地图一开始是空的(当然,scala在这里也对Manifests有所帮助)3.通过要求用户提供加法器来具有穷人类型类。像这样addToMap(map, key, value, adder)
,这很丑陋
论坛线程“是什么使类型类比特征更好? ”提出了一些有趣的观点:
- 类型类可以很容易代表的概念是相当困难的代表亚型的存在,如平等和排序。
练习:创建一个小的类/特征层次结构并尝试实现.equals
对每个类/特征的方式,使得对层次结构中任意实例的操作都可以适当地自反,对称和传递。- 类型类使您可以提供证据,证明“控件”之外的类型符合某些行为。
他人的类型可以是您的typeclass的成员。- 您不能用子类型表示“此方法采用/返回与方法接收者相同类型的值”,但是使用类型类可以很简单地实现此约束(非常有用)。这是f界类型问题(其中F边界类型是通过其自身的子类型进行参数化的)。
- 在特征上定义的所有操作都需要一个实例; 总有
this
争论。所以你不能例如定义fromString(s:String): Foo
的方法,trait Foo
以这样的方式,你可以把它叫做没有的一个实例Foo
。
在Scala中,这表现为人们拼命尝试对伴侣对象进行抽象。
但这对于类型类很简单,如本monoid示例中的零元素所示。- 类型类可以归纳定义;例如,如果您有,则
JsonCodec[Woozle]
可以JsonCodec[List[Woozle]]
免费获得。
上面的示例针对“您可以添加在一起的东西”对此进行了说明。
查看类型类的一种方法是,它们启用追溯扩展或追溯多态性。Casual Miracles和Daniel Westheide撰写了几篇很棒的文章,展示了在Scala中使用类型类实现此目的的示例。
无论implicits和类型类用于类型转换。两者的主要用例是在您无法修改但希望继承为多态的类上提供临时多态(即)。在隐式的情况下,您可以使用隐式def或隐式类(这是您的包装器类,但对客户端隐藏)。类型类更强大,因为它们可以将功能添加到已经存在的继承链中(例如:scala的sort函数中的Ordering [T])。有关更多详细信息,请参见https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/
在scala类型类中
行为可以扩展-在编译时-事后-无需更改/重新编译现有代码
斯卡拉隐式
方法的最后一个参数列表可以标记为隐式
隐式参数由编译器填充
实际上,您需要编译器的证据
……例如范围内类型类的存在
如果需要,还可以显式指定参数
下面的示例扩展对具有类型类实现的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
}
我喜欢将类型类用作轻量级的Scala惯用形式的Dependency Injection,它仍然可以与循环依赖一起工作,但不会增加很多代码复杂性。我最近重写了一个Scala项目,该项目使用了Cake Pattern来为DI输入类,并且使代码大小减少了59%。