【问题标题】:Scala lazy evaluation and apply functionScala 惰性求值和应用函数
【发布时间】:2017-07-22 19:26:21
【问题描述】:

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

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$7/1170794006@e580929,fpinscala.datastructures.Stream$$$Lambda$8/1289479439@4c203ea1)

然后我使用cons构造了Stream:

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

输出是:

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

所以这里有两个问题:

  1. 当使用 apply 函数构造 Stream 时,所有 printAndReturn 都会被计算。这是因为对 apply(as.head, ...) 的递归调用会评估每个头吗?
  2. 如果第一个问题的答案是正确的,那么如何更改apply 使其不强制评估?

【问题讨论】:

    标签: scala lazy-evaluation


    【解决方案1】:
    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)
      

      这将延迟评估。

    【讨论】:

    • 我现在明白了。因此,您发布的解决方法是传递一个返回待评估函数的函数,这样即使 () => func 也会被评估,而 func 本身不会。对吗?
    【解决方案2】:

    当你打电话时

    Stream(
                printAndReturn,
                printAndReturn,
                printAndReturn,
                printAndReturn
            )
    

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

    对于 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
      )}")
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-06-24
      • 1970-01-01
      • 2017-05-12
      • 1970-01-01
      • 1970-01-01
      • 2023-04-05
      • 2021-01-13
      • 2022-07-22
      相关资源
      最近更新 更多