You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Scala Stream交错转换行为疑问:map与filter执行解析

关于Scala Stream中map与filter交错执行及惰性求值的问题解答

先看你给出的代码:

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. 为何map与filter的输出呈现交错状态?

核心原因是Stream的惰性求值,以及foldRight的参数设计。

咱们看foldRight的定义:第二个参数f的第二个入参是=> B,这是一个惰性参数——只有在需要的时候才会计算。当你链式调用map和filter时,整个过程是按需逐个元素处理的:

  • 当需要处理第一个元素时,map会先计算原Stream的第一个元素(触发map里的println),生成映射后的元素,然后把这个元素传给filter;
  • filter处理这个映射后的元素(触发filter里的println),判断是否符合条件。如果不符合,就会请求map的下一个元素(因为要返回map的tail),这时候才会触发下一个元素的map操作。
  • 整个流程就是“处理一个元素的map → 处理该元素的filter → 再处理下一个元素的map”,所以输出是交错的。

2. 从Stream创建到产生该行为的全部执行步骤

咱们一步步拆解整个流程(假设你是在Scala REPL中执行这段代码,REPL会尝试求值并显示结果,这是触发计算的关键):

  1. Stream初始化:Stream(1,2,3,4,5,6)创建了一个由Cons节点链接的惰性Stream。每个Cons的head和tail都是lazy val,初始时没有计算任何元素,只是保存了计算逻辑。
  2. 调用map:map(_+10)返回的是foldRight的表达式,但此时foldRight并没有执行任何实际计算——因为Stream是惰性的,只有当需要结果时才会运行。
  3. 调用filter:同样,filter(_%2==0)也是返回foldRight的表达式,依旧没有实际执行。
  4. 触发求值:REPL尝试显示最终Stream的内容,这会触发对Stream头部元素的求值,启动整个计算链:
    • 首先执行filter的foldRight,它需要处理map返回的Stream的第一个元素;
    • 这会触发map的foldRight处理原Stream的第一个元素:调用h()获取1,执行map里的println("map h:1"),生成Stream.cons(11, t)(t是原Stream tail的map结果,此时还是惰性的);
    • 这个11被传给filter的f函数:执行println("filter h:11"),判断11%2==0不成立,所以返回t(也就是map的tail,对应原Stream的2及以后的元素);
    • 现在filter需要获取t的内容,触发map的foldRight处理原Stream的第二个元素:调用h()获取2,执行println("map h:2"),生成Stream.cons(12, t);
    • 12传给filter的f函数:执行println("filter h:12"),判断12%2==0成立,于是返回Stream.cons(12, t);
    • 此时REPL已经获取到第一个符合条件的元素12,默认停止继续求值(因为Stream是惰性的,REPL不会主动遍历所有元素),所以流程到此结束。

3. 符合filter条件的其他元素(如4、6)为何未出现在输出中?

还是因为惰性求值:Stream不会提前计算所有元素,只有当你明确请求后续元素时,才会触发对应的map和filter操作。

在你的场景中,REPL只需要获取到第一个符合条件的元素就停止了,没有继续请求后续元素,所以原Stream中3、4、5、6对应的map和filter都没有执行。如果你强制遍历所有元素(比如把结果转成List):

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

你就会看到所有符合条件的元素对应的输出,比如map h:3、filter h:13、map h:4、filter h:14等等。

内容的提问来源于stack exchange,提问作者Giorgio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:30:45