Scala 惰性求值和应用函数

Scala lazy evaluation and apply function

我正在按照一本书的示例在 Scala 中使用惰性求值实现 Steam class。

sealed trait Stream[+A]
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: _*))
    }
}

然后我使用了一个简单的函数来测试它是否有效

def printAndReturn: Int = {
    println("called")
    1
}

然后我像下面这样构造 Stream:

    println(s"apply: ${
        Stream(
            printAndReturn,
            printAndReturn,
            printAndReturn,
            printAndReturn
        )
    }")

输出是这样的:

called
called
called
called
apply: Cons(fpinscala.datastructures.Stream$$$Lambda/1170794006@e580929,fpinscala.datastructures.Stream$$$Lambda/1289479439@4c203ea1)

然后我用cons构造了Stream:

println(s"cons: ${
    cons(
        printAndReturn,
        cons(
            printAndReturn,
            cons(printAndReturn, Empty)
        )
    )
}")

输出为:

cons: Cons(fpinscala.datastructures.Stream$$$Lambda/1170794006@2133c8f8,fpinscala.datastructures.Stream$$$Lambda/1289479439@43a25848)

所以这里有两个问题:

  1. 当使用 apply 函数构造 Stream 时,所有 printAndReturn 都会被评估。这是因为对 apply(as.head, ...) 的递归调用会评估每个头部吗?
  2. 如果第一个问题的答案为真,那么我该如何更改apply使其不强制求值?
  1. 没有。如果您在 println 上放置一个断点,您会发现在您首次创建 Stream 时实际上正在调用该方法。 Stream(printAndReturn, ... 行实际上调用了您的方法,无论您将其放在那里多少次。为什么?考虑 consapply:

    的类型签名
    def cons[A](hd: => A, tl: => Stream[A]): Stream[A]
    

    对比:

    def apply[A](as: A*): Stream[A]
    

    请注意,cons 的定义的参数标记为 => A。这是一个按名称的参数。像这样声明一个输入会使它变得懒惰,延迟它的评估直到它被实际使用。因此,您的 println 将永远不会被使用 cons 调用。将其与 apply 进行比较。您没有使用名称参数,因此传递给该方法的任何内容都将自动进行评估。

  2. 遗憾的是,目前还没有超级简单的方法。您真正想要的是 def apply[A](as: (=>A)*): Stream[A] 之类的东西,但不幸的是,Scala 不支持名称参数的可变参数。有关如何解决此问题的一些想法,请参阅 this answer。一种方法是在创建 Stream 时包装函数调用:

    Stream(
      () => printAndReturn,
      () => printAndReturn,
      () => printAndReturn,
      () => printAndReturn)
    

    这将延迟评估。

当你打电话时

Stream(
            printAndReturn,
            printAndReturn,
            printAndReturn,
            printAndReturn
        )

伴随对象中的应用已被调用。查看 apply 的参数类型,您会注意到它是严格的。因此,在将参数分配给 as 之前,将首先评估参数。 as 变成了一个整数数组

对于2,你可以定义apply为

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

正如上面所建议的,您需要像

中那样将参数作为 thunk 本身传递
println(s"apply: ${Stream(
    () => printAndReturn,
    () => printAndReturn,
    () => printAndReturn,
    () => printAndReturn
  )}")