在“ 懒惰,第1部分 ”中,我探索了Java™中的惰性库。 在本期中,我将演示如何使用闭包作为构建块来构建简单的延迟列表,然后探索延迟评估的一些性能优势以及Groovy,Scala和Clojure的一些延迟方面。
建立一个懒惰列表
在本系列的前期文章中,我展示了Groovy中一个懒惰列表的简单实现。 但是,我没有显示其工作原理的派生,这是这里的主题。
从上一期中您知道,在绝对需要之前,可以将语言分为严格 (急切地评估所有表达式)或懒惰 (延后评估)两类。 Groovy本质上是一种严格的语言,但是我可以通过将严格列表递归地包装在闭包中来将非懒惰列表转换为懒惰列表。 这使我可以通过延迟闭合块的执行来推迟对后续值的评估。
Groovy中的严格空列表由数组表示,并使用空方括号: []
。 如果将其包装在一个闭包中,它将成为一个懒惰的空列表:
{-> [] }
如果需要在列表中添加a
元素,可以将其添加到最前面,然后再次使整个新列表变懒:
{-> [ a, {-> [] } ] }
传统上,将添加到列表顶部的方法称为prepend或cons 。 要添加更多元素,我对每个新项目重复此操作; 将三个元素( a
, b
和c
)添加到列表中将产生:
{-> [a, {-> [b, {-> [ c, {-> [] } ] } ] } ] }
这种语法很笨拙,但是一旦理解了原理,就可以在Groovy中创建一个类,该类为惰性集合实现一组传统方法,如清单1所示:
清单1.使用闭包在Groovy中构建一个惰性列表
class PLazyList {
private Closure list
private PLazyList(list) {
this.list = list
}
static PLazyList nil() {
new PLazyList({-> []})
}
PLazyList cons(head) {
new PLazyList({-> [head, list]})
}
def head() {
def lst = list.call()
lst ? lst[0] : null
}
def tail() {
def lst = list.call()
lst ? new PLazyList(lst.tail()[0]) : nil()
}
boolean isEmpty() {
list.call() == []
}
def fold(n, acc, f) {
n == 0 || isEmpty() ? acc : tail().fold(n - 1, f.call(acc, head()), f)
}
def foldAll(acc, f) {
isEmpty() ? acc : tail().foldAll(f.call(acc, head()), f)
}
def take(n) {
fold(n, []) {acc, item -> acc << item}
}
def takeAll() {
foldAll([]) {acc, item -> acc << item}
}
def toList() {
takeAll()
}
}
在清单1中 ,构造函数是私有的; 通过使用nil()
从一个空列表开始调用它,该方法构造一个空列表。 cons()
方法使我可以通过在传递的参数之前添加新元素,然后将结果包装在闭合块中。
接下来的三种方法启用列表遍历。 head()
方法返回列表的第一个元素,而tail()
返回除第一个元素之外的所有元素的子列表。 在这两种情况下,我都call()
闭包块-称为强制懒惰求值。 因为我正在获取值,所以在收获值时它不再变得懒惰。 毫不奇怪, isEmpty()
方法检查是否还有待解决的术语。
其余方法是用于对列表执行操作的高阶函数。 的fold()
和foldAll()
方法执行折叠抽象,也被称为减少 -或,在Groovy只, injectAll()
我已经在许多先前的文章(例如“从功能上思考,第3部分 ”)中展示了这种方法的用法,但这是我第一次展示纯粹是基于闭包块编写的递归定义。 foldAll()
方法检查列表是否为空,如果为空,则返回acc
(累加器,即折叠操作的种子值)。 否则,它将递归调用foldAll()
的tail()
foldAll()
上的foldAll()
,并传递累加器和列表的头部。 函数( f
参数)应接受两个参数并产生单个结果; 当您在相邻元素上折叠一个元素时,这是“折叠”操作。
清单2显示了构建列表并进行操作。
清单2.执行惰性列表
def lazylist = PLazyList.nil().cons(4).cons(3).cons(2).cons(1)
println(lazylist.takeAll()) //[1, 2, 3, 4]
println(lazylist.foldAll(0, {i, j -> i + j})) // 10
lazylist = PLazyList.nil().cons(1).cons(2).cons(4).cons(8)
println(lazylist.take(2)) //[8, 4]
在清单2中 ,我通过将值cons()
放入一个空列表来创建一个列表。 请注意,当我使用元素的takeAll()
时,它们以与添加到列表的相反顺序返回。 记住, cons()
确实是prepend的简写; 它将元素添加到列表的前面。 foldAll()
方法使我能够通过提供一个转换代码块{i, j -> i + j}
来foldAll()
列表,该代码块使用加法作为折叠操作。 最后,我使用take()
方法仅强制评估前两个元素。
实际的惰性列表实现与此不同,避免了递归并添加了更灵活的操作方法。 但是,从概念上了解实现内部发生的情况有助于理解和使用。
懒惰的好处
懒惰列表有几个好处。 首先,您可以使用它们创建无限序列。 由于只有在需要时才评估值,因此可以使用惰性集合对无限列表建模。 在“ Groovy的功能特性,第1部分 ”部分中,我将展示在Groovy中实现的示例。 第二个好处是减小了存储大小。 如果(而不是持有整个集合)可以推导出后续值,则可以以存储空间换取执行速度。 选择使用惰性集合将在存储值和计算新值之间进行权衡。
第三,延迟集合的主要优势之一是运行时可以生成更高效的代码。 考虑清单3中的代码:
清单3.在Groovy中查找回文
def isPalindrome(s) {
def sl = s.toLowerCase()
sl == sl.reverse()
}
def findFirstPalindrome(s) {
s.tokenize(' ').find {isPalindrome(it)}
}
s1 = "The quick brown fox jumped over anna the dog";
println(findFirstPalindrome(s1)) // anna
s2 = "Bob went to Harrah and gambled with Otto and Steve"
println(findFirstPalindrome(s2)) // Bob
清单3中的isPalindrome()
方法将isPalindrome()
的大小写规范化,然后确定该词是否具有相同的相反字符。 findFirstPalindrome()
方法尝试使用Groovy的find()
方法在传递的字符串中查找第一个回文,该方法接受代码块作为过滤机制。
假设我有大量字符需要在其中找到第一个回文。 在执行findFirstPalindrome()
方法期间, 清单3中的代码首先急切地标记整个序列,创建中间数据结构,然后发出find()
命令。 Groovy的tokenize()
方法不是很懒,因此在这种情况下,它可能会构建一个巨大的临时数据结构,而只是丢弃其中的大部分。
考虑清单4中出现的用Clojure编写的相同代码:
清单4. Clojure的回文
(defn palindrome? [s]
(let [sl (.toLowerCase s)]
(= sl (apply str (reverse sl)))))
(defn find-palindromes [s]
(filter palindrome? (clojure.string/split s #" ")))
(println (find-palindromes "The brown fox jumped over anna."))
; (anna)
(println (find-palindromes "Bob went to Harrah and gambled with Otto"))
; (Bob Harrah Otto)
(println (take 1 (find-palindromes "Bob went to Harrah and gambled with Otto")))
; (Bob)
清单3和清单4中的实现细节相同,但是使用不同的语言构造。 在Clojure palindrome?
函数,我将参数字符串转换为小写,然后与反向字符串检查是否相等。 apply
的额外调用将reverse
返回的字符序列转换回字符串以进行比较。 find-palindromes
函数使用Clojure的filter
函数,该函数接受一个函数来充当过滤器和要过滤的集合。 为了palindrome?
电话palindrome?
函数,Clojure提供了几种选择。 我可以创建一个匿名函数来调用它,例如#(palindrome? %)
,它是接受单个参数的匿名函数的语法糖。 长手版本将如下所示:
(fn [x]
(palindrome? x))
当我只有一个参数时,Clojure允许我避免声明匿名函数和命名参数,在#(palindrome? %)
函数调用中,我用%
代替了该参数。 在清单4中 ,我可以直接使用甚至更短的函数名称形式。 filter
期望一个方法,该方法接受单个参数并返回一个与palindrome?
匹配的布尔值palindrome?
。
从Groovy到Clojure的翻译不仅仅是语法。 Clojure的所有可能是惰性的数据结构都是惰性的,包括对filter
和split
等集合的操作。 因此,在Clojure版本中,一切都是自动惰性的,这在清单4的第二个示例中体现出来,当我用倍数调用集合上的find-palindromes
时。 从filter
返回的是一个惰性集合,在我打印它时会强制这样做。 如果我只想要第一个条目,则必须从列表中take
所需的懒惰条目数。
Scala以稍微不同的方式处理懒惰。 它不是在默认情况下使所有内容都变得懒惰,而是提供了集合的懒惰视图 。 考虑清单5中的回文问题的Scala实现:
清单5. Scala回文
def isPalindrome(x: String) = x == x.reverse
def findPalidrome(s: Seq[String]) = s find isPalindrome
findPalindrome(words take 1000000)
在清单5中 ,通过take
方法从集合中提取100万个单词将是非常低效的,特别是如果目标是找到第一个回文。 要将words
集合转换为惰性集合,请使用view
方法:
findPalindrome(words.view take 1000000)
view
方法允许对集合进行延迟遍历,从而使代码效率更高。
惰性字段初始化
在离开懒惰的话题之前,我会提到两种语言都有一个很好的工具可以使昂贵的初始化变得懒惰。 通过在val
声明前添加lazy
,可以将Scala中的字段从急切转换为需要的求值:
lazy val x = timeConsumingAndOrSizableComputation()
对于清单6中的代码,这基本上是语法糖:
清单6. Scala为惰性字段生成的语法糖
var _x = None
def x = if (_x.isDefined) _x.get else {
_x = Some(timeConsumingAndOrSizableComputation())
_x.get
}
Groovy使用称为抽象语法树(AST)转换的高级语言功能,具有类似的功能。 它们使您能够与编译器的基础抽象语法树的生成进行交互,从而允许在较低级别进行用户转换。 预定义的转换之一是@Lazy
属性,如清单7所示:
清单7. Groovy中的惰性字段
class Person {
@Lazy pets = ['Cat', 'Dog', 'Bird']
}
def p = new Person()
assert !(p.dump().contains('Cat'))
assert p.pets.size() == 3
assert p.dump().contains('Cat')
在清单7中 ,在第一次访问数据结构之前, Person
实例p
似乎没有Cat
值。 Groovy还允许您使用闭合块来初始化数据结构:
class Person {
@Lazy List pets = { /* complex computation here */ }()
}
最后,您还可以告诉Groovy使用软引用 (可以在需要时回收Java的指针引用版本)来保存您的延迟初始化字段:
class Person {
@Lazy(soft = true) List pets = ['Cat', 'Dog', 'Bird']
}
结论
在本期中,我更深入地研究了惰性,使用Groovy中的闭包从零开始构建了一个惰性集合。 我还讨论了为什么您可能要考虑一个惰性结构,并列出了一些好处。 特别是,运行时优化资源的能力是一个巨大的胜利。 最后,我在Scala和Groovy中展示了一些与惰性初始化字段相关的深奥但有用的惰性表现。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft19/index.html