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)
所以这里有两个问题:
- 当使用 apply 函数构造 Stream 时,所有
printAndReturn
都会被评估。这是因为对 apply(as.head, ...)
的递归调用会评估每个头部吗?
- 如果第一个问题的答案为真,那么我该如何更改
apply
使其不强制求值?
没有。如果您在 println
上放置一个断点,您会发现在您首次创建 Stream
时实际上正在调用该方法。 Stream(printAndReturn, ...
行实际上调用了您的方法,无论您将其放在那里多少次。为什么?考虑 cons
和 apply
:
的类型签名
def cons[A](hd: => A, tl: => Stream[A]): Stream[A]
对比:
def apply[A](as: A*): Stream[A]
请注意,cons
的定义的参数标记为 => A
。这是一个按名称的参数。像这样声明一个输入会使它变得懒惰,延迟它的评估直到它被实际使用。因此,您的 println
将永远不会被使用 cons
调用。将其与 apply
进行比较。您没有使用名称参数,因此传递给该方法的任何内容都将自动进行评估。
遗憾的是,目前还没有超级简单的方法。您真正想要的是 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
)}")
我正在按照一本书的示例在 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)
所以这里有两个问题:
- 当使用 apply 函数构造 Stream 时,所有
printAndReturn
都会被评估。这是因为对apply(as.head, ...)
的递归调用会评估每个头部吗? - 如果第一个问题的答案为真,那么我该如何更改
apply
使其不强制求值?
没有。如果您在
的类型签名println
上放置一个断点,您会发现在您首次创建Stream
时实际上正在调用该方法。Stream(printAndReturn, ...
行实际上调用了您的方法,无论您将其放在那里多少次。为什么?考虑cons
和apply
:def cons[A](hd: => A, tl: => Stream[A]): Stream[A]
对比:
def apply[A](as: A*): Stream[A]
请注意,
cons
的定义的参数标记为=> A
。这是一个按名称的参数。像这样声明一个输入会使它变得懒惰,延迟它的评估直到它被实际使用。因此,您的println
将永远不会被使用cons
调用。将其与apply
进行比较。您没有使用名称参数,因此传递给该方法的任何内容都将自动进行评估。遗憾的是,目前还没有超级简单的方法。您真正想要的是
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
)}")