返回介绍

数学基础

统计学习

深度学习

工具

Scala

一、视图

发布于 2023-07-17 23:38:22 字数 7343 浏览 0 评论 0 收藏 0

  1. 集合有相当多的方法来构造新的集合,如 map/filter/++ 等。我们称这些方法为变换器transformer ,因为它们至少接受一个集合作为参数,并产生另一个集合作为结果。

    变换器有两种主要的实现方式:

    • 严格的transformer:构造出带有所有元素的新集合。
    • 非严格的transformer(或者惰性的):只是构造出结果集合的一个代理,结果集合的元素会根据需要来构造。

    考虑如下的一个惰性map

    
    def lazyMap[T, U](iter : Iterable[T],  f : T => U) = new Iterable[U]{
      def iterator = iter.iterator map f
    }

    该方法将给定的集合 iter 通过 f 函数映射到一个新集合。

    lazyMap 在构造新的 Iterable 时并没有遍历给定集合 iter 的所有元素,而是返回一个 iterator。只有在需要 iterator 元素的时候才去迭代。

  2. 有一种系统化的方式可以将每个集合转换成惰性的版本,或者将惰性版本转换成非惰性版本。这个方式的基础是视图view

    视图是一种特殊的集合,它代表了某个基础集合,但是采用惰性的方式实现了所有的变换器。

  3. 要想从集合得到它的视图,可以对集合采用 view 方法。如果 xs 是一个集合,则 xs.view 就是同一个集合、但是所有变换器都是按惰性的方式来实现的。

    
    
    xxxxxxxxxx
    val v = Vector(1 to 10 : _*) v.map(_ +1).map( _ * 2)

    这里有两个连续的 map 操作,第一个 map 会构造出一个新的向量,然后第二个map 会构造出第三个向量。因此,第一个 map 构造出来的中间向量有些浪费。

    可以通过将两个函数 _ + 1_ * 2 整成一个函数,从而只需要一个 map 就可以。但是通常情况下,对一个集合的连续变化发生在不同的函数或者模块中,将这些变换整合在一起会破坏模块化设计。

    此时,使用视图就是最好的方法:

    
    
    xxxxxxxxxx
    v.view.map( _ + 1).map(_ * 2).force

    首先通过集合的 .view 方法将集合转换为视图,然后进行一系列的惰性变换,最后通过视图的 .force 方法将视图转换回集合。

    进一步拆解:

    • v.view :该调用得到一个 SeqView 对象,即一个惰性求值的 Seq 。该类型有两个类型参数:第一个类型参数Int 给出了该视图的元素类型;第二个类型参数 Vector[Int] 给出了当你 force 该视图时将取回的类型构造器。

      
      
      xxxxxxxxxx
      val v = Vector(1 to 10 : _*) val vv = v.view // SeqView[Int, Vector[Int]]
    • vv.map( _ + 1) 返回一个 SeqViewM(...) 的值。这本质上是一个记录了一个带有函数 ( _ + 1)map 操作需要被应用到向量 v 的包装器。但是,它并不会在 force 操作之前应用这个 map 操作。

      SeqViewM 后面的 M 表示该视图封装了一个 map 操作,还有其它字母表示其它惰性操作。如 S 表示惰性的 slice 操作,R 表示惰性的 reverse 操作。

    • vv.map( _ + 1).map( _ * 2) 返回一个 SeqViewMM(...) 的值。这里 MM 表示封装了两个 map 操作。

    • 最后的 force 操作,两个惰性的 map 操作会被执行,同时构造了新的Vector 。通过这种方式,我们避免了创建中间数据结构。

      注意一个细节:最终结果的静态类型是 Seq,而不是 Vector。这是因为第一次惰性 map 应用时,结果的静态类型为 SeqViewM[Int, Seq[_]]。也就是类型系统丢失了视图背后的 Vector 类型的信息。对任何特定类的视图的实现都需要大量的代码开发,因此 Scala 集合类库几乎只对一般化的集合类型而不是具体的实现提供了视图支持(Array 是个例外,对 Array 操作的惰性操作会得到 Array 本身)。

      
      
      xxxxxxxxxx
      val v = Vector(1 to 10 : _*) val vv = v.view // 结果类型 SeqView[Int, Vector[Int]] val m1 = vv.map( _ + 1) // 结果类型 SeqViewM[Int, Seq[_]] val m2 = m1.map( _ * 2) // 结果类型 SeqViewMM[Int, Seq[_]] val v2 = m2.force // 结果类型 Seq[Int]
  4. 采用视图有两个原因:

    • 首先是性能。通过将集合切换成视图,可以避免中间结果的产生。这些节约下来的开销可能非常重要。

    • 其次是针对可变序列。这类视图的很多变换器提供了对原始序列的一个窗口,可以用于有选择的对序列中的某些原始进行更新。

      如:

      
      
      xxxxxxxxxx
      val arr = (0 to 9).toArray

      可以通过创建该数组的一个切片的视图来创建到该数组的子窗口,它返回一个叫做 IndexedSeqViewS(...) 的对象:

      
      
      xxxxxxxxxx
      val subArr = arr.view.slice(3, 6) // 返回一个 IndexedSeqView[Int, Array[Int]] 类型对象

      现在视图 subArr 指向 arr 数组位置中 35 的元素。该视图并不会复制这些元素,它只是提供了对它们的引用。注意,这是一个 mutable.IndexedSeqView

      最后我们可以原地修改序列中的某些元素:

      
      
      xxxxxxxxxx
      def negate(xs: scala.collection.mutable.Seq[Int]) = { for (i <- 0 until xs.length) xs(i) = - xs(i) } negate(subArr) arr// 现在为 Array(0, 1, 2, -3, -4, -5, 6, 7, 8, 9)
  5. 既然视图具有如此多的优点,为什么还需要严格求值的集合?有两个原因:

    • 惰性求值的视图并不是始终性能最优。对于小型集合而言,组织视图和应用闭包的额外开销通常会大于省略中间数据结构的收益。

    • 惰性求值可能带来副作用。比如:

      
      
      xxxxxxxxxx
      val arr = (0 to 9).toArray val v = arr.view .... //经过了很多步之后 v.map( x => do_with(x))

      我们发现 do_with(x) 并没有真正的执行,这是因为 map 的结果是视图,元素并未真正创建。如果我们对 map 的结果调用 .forcedo_with 就能执行。也就是 do_with 执行的时刻由 force 来决定,这看起来比较奇怪。

  6. Scala 类库从 2.8 开始规定:除了流之外的所有集合都是严格求值的;从严格求值的集合到惰性求值的集合的唯一方式是通过 view 方法;从惰性求值集合到严格求值集合的唯一方式是通过 force 方法。

  7. 为了避免被延迟求值的各种细节困扰,应该将视图的使用局限在两种场景:

    • 要么在集合变换没有副作用的纯函数式的代码中应用视图。
    • 要么对所有修改都应用显式执行的可变集合应用视图。

    最好避免在既创建新集合、又有副作用的场景混合视图和各种集合操作。

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

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

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文