clojure和scala
惰性序列 (也称为stream )是一种有趣的功能数据结构,您可能从未听说过。 基本上,延迟序列是一个列表,只有在您实际使用它之前,它才是完全未知的/无法计算的列表。 想象一下,创建一个列表非常昂贵,并且您不想计算太多,但是仍然允许客户端根据需要或需要消耗大量资源。 与迭代器类似,但是迭代器具有破坏性-一旦您阅读它们,它们就会消失。 另一方面,惰性序列会记住已计算的元素。
注意,这种抽象甚至允许我们构造和使用无限的流! 完全有可能创建一个质数或斐波那契数列的惰性序列。 由客户决定他们要消耗多少元素-仅此而已
许多将要产生。 将该表与渴望的列表进行比较,该列表必须在首次使用和迭代之前进行预先计算,而忘记了已经计算出的值。
但是请记住,总是从头开始遍历惰性序列,因此为了找到第N个元素,惰性序列将必须计算前面的N-1个元素。
我尝试避免纯粹的学术示例,因此不会有斐波那契数列示例。 您可以在有关该主题的每篇文章中找到它。 相反,我们将实现一些有用的功能-Cron表达式测试实用程序,返回下一次触发时间的序列。 我们已经使用递归和迭代器实现了对Cron表达式的测试 。 为了快速回顾一下,我们想确保我们的Cron表达式正确并在我们真正期望的时候触发。 Quartz Scheduler提供了方便的CronExpression.getNextValidTimeAfter(Date)
方法,该方法返回给定日期之后的下一个触发时间。 如果要计算例如接下来的十次触发时间,则需要调用此方法十次,但是! 第一次调用的结果应作为第二次调用的参数传递–毕竟,一旦我们知道作业何时将首次触发,我们就想知道下一次调用(在第一次调用之后)的触发时间是多少。 接下来,为了找到第三次调用时间,我们必须将第二次调用时间作为参数。 此描述使我们想到了简单的递归算法:
def findTriggerTimesRecursive(expr: CronExpression, after: Date): List[Date] =
expr getNextValidTimeAfter after match {
case null => Nil
case next => next :: findTriggerTimesRecursive(expr, next)
}
getNextValidTimeAfter()
可能返回null
以指示Cron表达式将不再触发(例如,它仅在2013年运行,并且我们已经到年底)。 但是,此解决方案有多个问题:
- 我们真的不知道客户端需要多少个未来日期,因此我们很可能会生成过多的不必要的CPU周期1
- 更糟糕的是,某些Cron表达式永无休止。
"0 0 17 * * ? *"
将每年每天下午5点运行到无穷大。 我们肯定没有那么多时间和记忆 - 我们的实现不是尾递归的。 容易修复
如果我们有一个“列表式”数据结构,可以像其他序列一样传递并使用它,却又不急于对其进行评估,该怎么办? 这是Scala of Stream[Date]
中的实现,仅在需要时才计算下一次触发时间:
def findTriggerTimes(expr: CronExpression, after: Date): Stream[Date] =
expr getNextValidTimeAfter after match {
case null => Stream.Empty
case next => next #:: findTriggerTimes(expr, next)
}
仔细看一下,因为几乎一样! 我们将List[Date]
替换为Stream[Date]
(均实现LinearSeq
),将Nil
替换为Stream.Empty
,将::
替换为#::
。 最后的变化至关重要。 #::
方法(是的,这是一个方法…)接受tl: => Stream[A]
– 按名称 。 这意味着在这里未真正调用findTriggerTimes(expr, next)
! 实际上,这是我们传递给#::
高阶函数的闭包。 仅在需要时评估此关闭。 让我们玩一下这段代码:
val triggerTimesStream = findTriggerTimes("0 0 17 L-3W 6-9 ? *")
println(triggerTimesStream)
//Stream(Thu Jun 27 17:00:00 CEST 2013, ?)
val firstThree = triggerTimesStream take 3
println(firstThree.toList)
//List(Thu Jun 27 17:00:00 CEST 2013, Mon Jul 29 17:00:00 CEST 2013, Wed Aug 28 17:00:00 CEST 2013)
println(triggerTimesStream)
//Stream(Thu Jun 27 17:00:00 CEST 2013, Mon Jul 29 17:00:00 CEST 2013, Wed Aug 28 17:00:00 CEST 2013, ?)
仔细地看。 最初打印流几乎不显示第一个元素。 Stream.toString
中的Stream.toString
表示流的未知剩余部分。 然后,我们考虑前三个要素。 有趣的是,我们必须将结果转换为List
。 仅调用take(3)
几乎不会返回另一个流,因此会尽可能长地进一步推迟评估。 但是再次打印原始流也显示了所有三个元素,但是第四个元素还未知。
让我们做一些更高级的事情。 假设我们想知道Cron表达式何时会第100次触发? 从今天起,一年内它将触发多少次?
val hundredth = triggerTimesStream.drop(99).head
val calendar = new GregorianCalendar()
calendar.add(Calendar.YEAR, 1)
val yearFromNow = calendar.getTime
val countWithinYear = triggerTimesStream.takeWhile(_ before yearFromNow).size
计算第100次点火时间非常简单-只需丢弃前99个日期,然后取剩下的第一个。 但是, discard这个词有点不幸-这些项目是在triggerTimesStream
中计算并缓存的,因此,下次我们尝试访问前100个元素中的任何一个时,它们将立即可用。 有趣的事实:Scala中的Stream[T]
是不可变的并且是线程安全的,但是当您对其进行迭代时,它会在内部不断变化。 但这是一个实现细节。
您可能想知道为什么我使用takeWhile(...).size
而不是简单的filter(...).size
甚至count(...)
? 好吧,由于定义流中的触发时间正在增长,因此,如果我们只想计算一年内的日期,那么当我们找到第一个不匹配的日期时就可以停止。 但这不仅是微观优化。 还记得流可以是无限的吗? 想一想。 同时,我们会将小型实用程序移植到Clojure。
Clojure
Clojure中的流( lazy-seq
):
(defn find-trigger-times [expr after]
(let [next (. expr getNextValidTimeAfter after)]
(case next
nil []
(cons next (lazy-seq (find-trigger-times expr next))))))
这几乎是Scala代码的精确翻译,除了它使用一个let
绑定来捕获getNextValidTimeAfter()
结果。 可以使用if-let
形式编写较少的文化素养,但翻译更紧凑:
(defn find-trigger-times [expr after]
(if-let [next (. expr getNextValidTimeAfter after)]
(cons next (lazy-seq (find-trigger-times expr next)))
[]))
if-let
将条件和绑定结合在一起。 如果与next
绑定的表达式为false(在我们的示例中为nil
),则根本不评估第三行。 而是返回第四行的结果(空序列)。 这两个实现是等效的。 为了完整起见,让我们看看如何获取第100个元素并计算一年内与Cron表达式匹配的日期数:
(def expr (new CronExpression "0 0 17 L-3W 6-9 ? *"))
(def trigger-times (find-trigger-times expr (new Date)))
(def hundredth (first (drop 99 trigger-times)))
(def year-from-now (let [calendar (new GregorianCalendar)]
(. calendar add Calendar/YEAR 1)
(. calendar getTime)))
(take-while #(.before % year-from-now) trigger-times)
注意,再次,我们使用了take-while
不是简单的filter
时空复杂度
想象一下使用filter()
而不是takeWhile()
来计算Cron触发器在明年内将触发多少次。 请记住,一般来说流(尤其是我们的Cron流)可以是无限的。 Stream
上的简单filter()
将一直运行到结束为止-无限流可能永远不会发生。 即使是size
类的简单方法也是如此, Stream
会不断评估,直到达到终点为止。 但是,您的程序会尽快填满整个堆空间。 为什么? 因为一旦评估了元素, Stream[T]
将对其进行缓存以备后用。 意外抓住大Stream
头是另一个危险:
val largeStream: Stream[Int] = //,..
//...
val smallerStream = largeStream drop 1000000
smallerStream
是对没有前一百万个元素的流的引用。 但是这些元素仍然缓存在原始largeStream
。 只要您保留对它的引用,它们就会保留在内存中。 当largeStream
引用超出范围时,前一百万个元素有资格进行垃圾回收,而流的其余部分仍被引用。
上面的讨论同样适用于Scala和Clojure。 如您所见,使用惰性序列时必须非常小心。 它们在功能语言中非常强大且无处不在-但是
“ 能力强大,责任重大 ” 。 开始使用可能无限的实体的那一刻,您必须要小心。
重复
如果您对Clojure或Scala更有经验,您可能想知道为什么我没有使用(iterate fx)
或Stream.iterate()
。 当您有无限的流并且每个元素都可以作为前一个元素的函数进行计算时,这些辅助方法非常有用。 显然,Cron stream无法利用此方便的工具,因为它可能是有限的,如先前所示。 但是为了完整起见,这是一个简短得多但使用iterate
错误实现:
def infiniteFindTriggerTimes(expr: CronExpression, after: Date) =
Stream.iterate(expr getNextValidTimeAfter after){last =>
expr getNextValidTimeAfter last
}
…和Clojure:
(defn find-trigger-times [expr after]
(iterate
#(. expr getNextValidTimeAfter %)
(. expr getNextValidTimeAfter after)))
两种情况下的想法都很简单:我们提供了初始元素x
(Scala中的第一个参数,Clojure中的第二个参数)和将前一个元素转换为当前元素的函数f
。 换句话说,我们产生以下流: [x, f(x), f(f(x)), f(f(f(x))), ...]
。
以上实现会一直工作到流结束(如果有)。 因此,以肯定的事物结束时,我们将使用天真的素数使用iterate
来产生无限量的素数流(为这样的理论问题道歉) prime?
谓词:
(defn- divisors [x]
(filter #(zero? (rem x %))
(range 2 (inc (Math/sqrt x)))))
(defn- prime? [x] (empty? (divisors x)))
(defn- next-prime [after]
(loop [x (inc after)]
(if (prime? x)
x
(recur (inc x)))))
(def primes (iterate next-prime 2))
我希望这个想法和实现都清楚。 如果数字没有除数,则将其视为质数。 next-prime
返回大于给定值的后续质数。 因此(next-prime 2)
产生3
, (next-prime 3)
产生5
,依此类推。 使用此函数,我们可以通过简单地提供第一个素数和next-prime
一个素数函数来构建primes
惰性序列。
结论
惰性序列(或流)是很好的抽象,用命令式语言表示不可能或很乏味。 它们看起来像普通列表,但仅在需要时才进行评估。 Scala和Clojure都对它们有很大的支持,它们的行为类似。 您可以在流上进行映射,过滤,剪切等操作,只要它们不是真正需要的,它们就不会真正计算其元素。 而且,它们缓存已经计算出的值,同时仍然是线程安全的。 但是,在处理无穷大时,必须小心。 如果您试图无辜地计算无限流中的元素或找到不存在的项(例如primes.find(_ == 10)
),那么没有人会救您。
1 –
getNextValidTimeAfter()
完整实现长度为400行 。
翻译自: https://www.javacodegeeks.com/2013/05/lazy-sequences-in-scala-and-clojure.html
clojure和scala