首页 文章

Scala中的哪些类型类有用?

提问于
浏览
63

据我所知,Scala中的this blog post "type classes"只是一个用特征和隐式适配器实现的"pattern" .

正如博客所说,如果我有特性 A 和适配器 B -> A 那么我可以调用一个函数,它需要类型 A 的参数,参数类型为 B ,而不显式调用此适配器 .

我发现它很好但不是特别有用 . 您能给出一个用例/示例,它显示了此功能的用途吗?

11 回答

  • 0

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

    想象一下,你有一个列表,可以是整数,浮点数,矩阵,字符串,波形等 . 鉴于此列表,您想要添加内容 .

    一种方法是使一些 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 (它还包含一些操作,而不仅仅是 zeroappend

  • 9

    重读那里的第一条评论:

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

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

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

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

    [更新示例]作为示例,这是我上周使用的一个用例 . 在我们的产品中,有几种包含容器作为值的 Map . 例如, 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.使用反射来确定子集合是否是List / Set . 这是很棘手的,因为 Map 是空的开始(当然scala也帮助这里也有Manifests)3 . 通过要求用户提供加法器来让穷人的类型类 . 像 addToMap(map, key, value, adder) 这样的东西很简单

  • 0

    我发现这篇博文有用的另一种方式是描述类型类的地方:Monads Are Not Metaphors

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

  • 4

    查看类型类的一种方法是它们启用 retroactive extensionretroactive polymorphism . Casual MiraclesDaniel Westheide有一些很棒的帖子显示了在Scala中使用Type Classes来实现这一目的的示例 .

    这是一个post on my blog,它探索了 retroactive supertyping 的scala中的各种方法,这是一种追溯扩展,包括类型类示例 .

  • 1

    论坛帖子“What makes type classes better than traits?”提出了一些有趣的观点:

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

  • 1

    我不知道除了 Ad-hoc polymorhism 以外的任何其他用例,这是最好的解释方法 .

  • 6

    implicits和类型类都用于 Type-conversion . 两者的主要用例是提供 ad-hoc polymorphism (即)类,你可以_992191的排序函数) . 有关详细信息,请参阅https://lakshmirajagopalan.github.io/diving-into-scala-typeclasses/

  • 4

    在scala类型中

    • 启用ad-hoc多态

    • 静态类型(即类型安全)

    • 借用哈斯克尔

    • 解决表达式问题

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

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

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

    enter image description here

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

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

  • 31

    我喜欢使用类型类作为依赖注入的轻量级Scala惯用形式,它仍然可以使用循环依赖,但不会增加很多代码复杂性 . 我最近改写了Scala project from using the Cake Pattern to type classes for DI并将代码大小减少了59% .

相关问题