【问题标题】:Understand Stream scala interleaved transformations behavior了解 Stream Scala 交错转换行为
【发布时间】:2018-10-23 08:15:46
【问题描述】:

我正在阅读Scala 中的函数式编程一书中包含的示例和练习,并从中获得乐趣。我正在研究严格和懒惰一章,讨论Stream

我无法理解以下代码摘录产生的输出:

sealed trait Stream[+A]{

  def foldRight[B](z: => B)(f: (A, => B) => B): B =   
  this match {
    case Cons(h,t) => f(h(), t().foldRight(z)(f))   
    case _ => z
  }

  def map[B](f: A => B): Stream[B] = foldRight(Stream.empty[B])((h,t) => {println(s"map h:$h"); Stream.cons(f(h), t)})

  def filter(f:A=>Boolean):Stream[A] = foldRight(Stream.empty[A])((h,t) => {println(s"filter h:$h"); if(f(h)) Stream.cons(h,t) else t})
}

case object Empty extends Stream[Nothing]

case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A]   

object Stream {
  def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {   
    lazy val head = hd   
    lazy val tail = tl
    Cons(() => head, () => tail)
  }
  def empty[A]: Stream[A] = Empty   

  def apply[A](as: A*): Stream[A] =   
    if (as.isEmpty) empty else cons(as.head, apply(as.tail: _*))
}

Stream(1,2,3,4,5,6).map(_+10).filter(_%2==0)

当我执行此代码时,我会收到以下输出:

map h:1
filter h:11
map h:2
filter h:12

我的问题是:

  1. 为什么地图和过滤器输出是交错的
  2. 您能否解释一下涉及的所有步骤,从创建流到获得此行为的最后一步?
  3. 列表中也通过过滤器转换的其他元素在哪里,所以 4 和 6?

【问题讨论】:

  • 我认为最后一个问题是不正确的。就像流一样,我希望根本没有打印。你没有消费流;你只是在改造它。

标签: scala functional-programming stream lazy-evaluation scala-collections


【解决方案1】:

我认为,理解这种行为的关键在于foldRight 的签名。

def foldRight[B](z: => B)(f: (A, => B) => B): B = ...

请注意,第二个参数f 是一个带有两个参数的函数,一个A 和一个别名(惰性)B。去掉那个懒惰,f: (A, B) => B,你不仅得到了预期的方法分组(所有map() 步骤在所有filter() 步骤之前),它们还以相反的顺序排列,6 首先处理,1最后处理,正如您对 foldRight 所期望的那样。

小小的=> 是如何施展魔法的?它基本上表示f() 的第二个参数将被保留,直到需要它为止。

所以,尝试回答您的问题。

  1. 为什么地图和过滤器输出是交错的?

因为对map()filter() 的每次调用都会延迟到请求值的时间点。

  1. 能否解释一下从 Stream 创建到获取此行为的最后一步所涉及的所有步骤?

不是真的。这将花费比我愿意贡献的更多时间和答案空间,但让我们在泥潭中迈出几步。

我们从Stream 开始,它看起来像一系列Cons,每个都包含一个Int 和对下一个Cons 的引用,但这并不完全准确。每个Cons 确实包含两个函数,当被调用时,第一个产生一个Int,第二个产生下一个Cons

调用map() 并将“+10”函数传递给它。 map() 创建一个新函数:“给定ht(两个值),创建一个 Cons。新Cons 的头函数在调用时将是应用于当前头部值的“+10”函数。新的尾部函数将产生t 接收到的值。这个新函数被传递给foldRight

foldRight 接收新函数,但函数的第二个参数的评估将延迟到需要时。 h() 被调用来检索当前的头部值,t()将被调用来检索当前的尾部值,并且对 foldRight 的递归调用将被调用。

调用filter() 并将“isEven”函数传递给它。 filter() 创建一个新函数:“给定 ht,创建一个 new Cons if h 通过 isEven 测试。如果没有,那么返回t。”这才是真正的t。不是以后评估其价值的承诺。

  1. 列表中也通过过滤器转换的其他元素(即 4 和 6)在哪里?

他们仍然在那里等待评估。我们可以通过使用模式匹配来强制进行评估,以逐个提取各种Cons

val c0@Cons(_,_) = Stream(1,2,3,4,5,6).map(_+10).filter(_%2==0)
//  **STDOUT**
//map h:1
//filter h:11
//map h:2
//filter h:12

c0.h()  //res0: Int = 12

val c1@Cons(_,_) = c0.t()
//  **STDOUT**
//map h:3
//filter h:13
//map h:4
//filter h:14

c1.h()  //res1: Int = 14

val c2@Cons(_,_) = c1.t()
//  **STDOUT**
//map h:5
//filter h:15
//map h:6
//filter h:16

c2.h()  //res2: Int = 16
c2.t()  //res3: Stream[Int] = Empty

【讨论】:

  • 感谢您的回复,据我所知,Stream(1,2,3,4,5,6).map(_+10).filter(_%2==0) 产生的输出是正确的,因为链条在找到头部时停止(12),然后是尾部的所有其他元素将被懒惰地评估。 但我仍然不明白交错的行为,您能否尝试更详尽地说明这一点? 我看不清楚惰性求值和交错行为之间的关系
  • 接受这个提示:如果不是最后的 .toList,foldRight 中的尾部将永远不会被强制评估。
  • @ jwvh 从你的回答中,当你提到函数 f 时,第二个参数是“...a by-name (lazy) B...”但是从下面的主题来看,我认为,那个“...lazy B...”是需要的,而不是按名称的请参考:stackoverflow.com/questions/50317282/…
  • @datnt;感谢您的链接,但“按需要”不是这种评估类型的常见、公认的术语。我会向您推荐this answer,以及它提供的链接,以支持我的断言。
  • @datnt;所以 cmets 不是详细讨论值得讨论的话题的地方。但要解决您的一些观点:传递Stream.empty[_] 不是技巧,也不会影响任何延迟评估。所有折叠(左/右)操作都需要一个与结果相同类型的“零”元素。在大多数情况下,它在开始时用于开始折叠迭代。在这种情况下,它会保存到结束以结束折叠。您会认为可以使用 object Empty 代替,因为每个 Stream.empty 实际上都是这样,但这不会进行类型检查。
猜你喜欢
  • 2016-11-24
  • 2018-08-25
  • 2019-11-08
  • 2022-11-28
  • 2021-02-11
  • 2023-03-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多