Scala 中的类型类有什么用?

发布于 2024-10-25 17:26:16 字数 328 浏览 4 评论 0原文

据我了解,这篇博文“类型类” Scala 只是一个用特征和隐式适配器实现的“模式”。

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

我发现它很好但不是特别有用。您能否提供一个用例/示例来说明此功能的用途?

As I understand from this blog post "type classes" in Scala is just a "pattern" implemented with traits and implicit adapters.

As the blog says if I have trait A and an adapter B -> A then I can invoke a function, which requires argument of type A, with an argument of type B without invoking this adapter explicitly.

I found it nice but not particularly useful. Could you give a use case/example, which shows what this feature is useful for ?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

清晨说晚安 2024-11-01 17:26:16

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

假设您有一个事物列表,可以是整数、浮点数、矩阵、字符串、波形等。给定此列表,您想要添加内容。

实现此目的的一种方法是拥有一些 Addable 特征,该特征必须由可以添加在一起的每个单个类型继承,或者在处理时隐式转换为 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!

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

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 (它也包含更多操作,而不仅仅是 zeroappend

One use case, as requested...

Imagine you have a list of things, could be integers, floating point numbers, matrices, strings, waveforms, etc. Given this list, you want to add the contents.

One way to do this would be to have some Addable trait that must be inherited by every single type that can be added together, or an implicit conversion to an Addable if dealing with objects from a third party library that you can't retrofit interfaces to.

This approach becomes quickly overwhelming when you also want to begin adding other such operations that can be done to a list of objects. It also doesn't work well if you need alternatives (for example; does adding two waveforms concatenate them, or overlay them?) The solution is ad-hoc polymorphism, where you can pick and chose behaviour to be retrofitted to existing types.

For the original problem then, you could implement an Addable type class:

trait Addable[T] {
  def zero: T
  def append(a: T, b: T): T
}
//yup, it's our friend the monoid, with a different name!

You can then create implicit subclassed instances of this, corresponding to each type that you wish to make addable:

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...

The method to sum a list then becomes trivial to write...

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

The beauty of this approach is that you can supply an alternative definition of some typeclass, either controlling the implicit you want in scope via imports, or by explicitly providing the otherwise implicit argument. So it becomes possible to provide different ways of adding waveforms, or to specify modulo arithmetic for integer addition. It's also fairly painless to add a type from some 3rd-party library to your typeclass.

Incidentally, this is exactly the approach taken by the 2.8 collections API. Though the sum method is defined on TraversableLike instead of on List, and the type class is Numeric (it also contains a few more operations than just zero and append)

晨曦慕雪 2024-11-01 17:26:16

重读那里的第一条评论:

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

  ...
}

Reread the first comment there:

A crucial distinction between type classes and interfaces is that for class A to be a "member" of an interface it must declare so at the site of its own definition. By contrast, any type can be added to a type class at any time, provided you can provide the required definitions, and so the members of a type class at any given time are dependent on the current scope. Therefore we don't care if the creator of A anticipated the type class we want it to belong to; if not we can simply create our own definition showing that it does indeed belong, and then use it accordingly. So this not only provides a better solution than adapters, in some sense it obviates the whole problem adapters were meant to address.

I think this is the most important advantage of type classes.

Also, they handle properly the cases where the operations don't have the argument of the type we are dispatching on, or have more than one. E.g. consider this type class:

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)

  ...
}
唠甜嗑 2024-11-01 17:26:16

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

因此,您首先定义一个类来对问题域进行建模,然后考虑要添加到其中的元数据。像 Equals、Hashable、Viewable 等。这创建了问题域和使用类的机制的分离,并开放了子类化,因为类更精简。

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

[更新示例]
作为一个例子,这是我上周的一个用例。在我们的产品中,有几种 Maps 包含容器作为值的情况。例如,Map[Int, List[String]]Map[String, Set[Int]]。添加到这些集合可能会很冗长:

map += key -> (value :: map.getOrElse(key, List()))

所以我想要一个函数来包装它,这样我就可以编写

map +++= key -> value

主要问题是集合并不都具有相同的添加元素的方法。有些有“+”,而另一些则有“:+”。我还想保留向列表添加元素的效率,所以我不想使用创建新集合的折叠/映射。

解决方案是使用类型类:

  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) 这样的东西,简直丑陋

I think of type classes as the ability to add type safe metadata to a class.

So you first define a class to model the problem domain and then think of metadata to add to it. Things like Equals, Hashable, Viewable, etc. This creates a separation of the problem domain and the mechanics to use the class and opens up subclassing because the class is leaner.

Except for that, you can add type classes anywhere in the scope, not just where the class is defined and you can change implementations. For example, if I calculate a hash code for a Point class by using Point#hashCode, then I'm limited to that specific implementation which may not create a good distribution of values for the specific set of Points I have. But if I use Hashable[Point], then I may provide my own implementation.

[Updated with example]
As an example, here's a use case I had last week. In our product there are several cases of Maps containing containers as values. E.g., Map[Int, List[String]] or Map[String, Set[Int]]. Adding to these collections can be verbose:

map += key -> (value :: map.getOrElse(key, List()))

So I wanted to have a function that wraps this so I could write

map +++= key -> value

The main issue is that the collections don't all have the same methods for adding elements. Some have '+' while others ':+'. I also wanted to retain the efficiency of adding elements to a list, so I didn't want to use fold/map which create new collections.

The solution is to use type classes:

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

Here I defined a type class Addable that can add an element C to a collection CC. I have 2 default implementations: For Lists using :: and for other collections, using the builder framework.

Then using this type class is:

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

The special bit is using adder.add to add the elements and adder.empty to create new collections for new keys.

To compare, without type classes I would have had 3 options:
1. to write a method per collection type. E.g., addElementToSubList and addElementToSet etc. This creates a lot of boilerplate in the implementation and pollutes the namespace
2. to use reflection to determine if the sub collection is a List / Set. This is tricky as the map is empty to begin with (of course scala helps here also with Manifests)
3. to have poor-man's type class by requiring the user to supply the adder. So something like addToMap(map, key, value, adder), which is plain ugly

可是我不能没有你 2024-11-01 17:26:16

我发现这篇博文有用的另一种方式是它描述了类型类: Monads 是不是隐喻

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

Yet another way I find this blog post helpful is where it describes typeclasses: Monads Are Not Metaphors

Search the article for typeclass. It should be the first match. In this article, the author provides an example of a Monad typeclass.

强者自强 2024-11-01 17:26:16

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

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


The forum thread "What makes type classes better than traits?" makes some interesting points:

  • Typeclasses can very easily represent notions that are quite difficult to represent in the presence of subtyping, such as equality and ordering.
    Exercise: create a small class/trait hierarchy and try to implement .equals on each class/trait in such a way that the operation over arbitrary instances from the hierarchy is properly reflexive, symmetric, and transitive.
  • Typeclasses allow you to provide evidence that a type outside of your "control" conforms with some behavior.
    Someone else's type can be a member of your typeclass.
  • You cannot express "this method takes/returns a value of the same type as the method receiver" in terms of subtyping, but this (very useful) constraint is straightforward using typeclasses. This is the f-bounded types problem (where an F-bounded type is parameterized over its own subtypes).
  • All operations defined on a trait require an instance; there is always a this argument. So you cannot define for example a fromString(s:String): Foo method on trait Foo in such a way that you can call it without an instance of Foo.
    In Scala this manifests as people desperately trying to abstract over companion objects.
    But it is straightforward with a typeclass, as illustrated by the zero element in this monoid example.
  • Typeclasses can be defined inductively; for example, if you have a JsonCodec[Woozle] you can get a JsonCodec[List[Woozle]] for free.
    The example above illustrates this for "things you can add together".
忆伤 2024-11-01 17:26:16

查看类型类的一种方法是它们启用追溯扩展追溯多态性休闲奇迹丹尼尔·韦斯特海德 显示了在 Scala 中使用类型类来实现此目的的示例。

这是 在我的博客上发帖
探索了 scala 中的各种方法追溯超类型,一种追溯扩展,包括类型类示例。

One way to look at type classes is that they enable retroactive extension or retroactive polymorphism. There are a couple of great posts by Casual Miracles and Daniel Westheide that show examples of using Type Classes in Scala to achieve this.

Here's a post on my blog
that explores various methods in scala of retroactive supertyping, a kind of retroactive extension, including a typeclass example.

等风来 2024-11-01 17:26:16

除了临时多态性之外,我不知道还有任何其他用例,它的解释是这里可能是最好的方法。

I don't know of any other use case than Ad-hoc polymorhism which is explained here the best way possible.

吃兔兔 2024-11-01 17:26:16

隐式类型类都用于类型转换。它们的主要用例是为您无法修改但期望继承类型的多态性的类提供临时多态性(即)。如果是隐式,您可以使用隐式 def 或隐式类(这是您的包装类,但对客户端隐藏)。类型类更强大,因为它们可以向现有的继承链添加功能(例如:scala 排序函数中的 Ordering[T])。
有关更多详细信息,您可以参阅 https://lakshmirajagopalan.github.io/diving-into -scala-类型类/

Both implicits and typeclasses are used for Type-conversion. The major use-case for both of them is to provide ad-hoc polymorphism(i.e) on classes that you can't modify but expect inheritance kind of polymorphism. In case of implicits you could use both an implicit def or an implicit class (which is your wrapper class but hidden from the client). Typeclasses are more powerful as they can add functionality to an already existing inheritance chain(eg: Ordering[T] in scala's sort function).
For more detail you can see https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/

乙白 2024-11-01 17:26:16

在 scala 类型类中

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

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

Scala 隐式 方法

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

  • 隐式参数由编译器填充

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

  • ...例如作用域中存在类型类

  • 指定参数。

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

In scala type classes

  • Enables ad-hoc polymorphism
  • Statically typed (i.e. type-safe)
  • Borrowed from Haskell
  • Solves the expression problem

Behavior can be extended
- at compile-time
- after the fact
- without changing/recompiling existing code

Scala Implicits

The last parameter list of a method can be marked implicit

  • Implicit parameters are filled in by the compiler

  • In effect, you require evidence of the compiler

  • … such as the existence of a type class in scope

  • You can also specify parameters explicitly, if needed

Below Example extension on String class with type class implementation extends the class with a new methods even though string is 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
}
踏雪无痕 2024-11-01 17:26:16

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

在此处输入图像描述

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

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

This is an important difference (needed for functional programming):

enter image description here

consider inc:Num a=> a -> a:

a received is the same that is returned, this cannot be done with subtyping

北城孤痞 2024-11-01 17:26:16

我喜欢使用类型类作为依赖注入的轻量级 Scala 惯用形式,它仍然可以处理循环依赖,但不会增加很多代码复杂性。我最近重写了一个 Scala 项目,使用 Cake 模式为 DI 键入类 并实现了代码大小减少 59%。

I like to use type classes as a lightweight Scala idiomatic form of Dependency Injection that still works with circular dependencies yet doesn't add a lot of code complexity. I recently rewrote a Scala project from using the Cake Pattern to type classes for DI and achieved a 59% reduction in code size.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文