惰性评估 (尽可能长的延迟表达式评估)是许多功能编程语言的功能。 惰性集合按需提供其元素,而不是对其进行预先计算,从而提供了许多好处。 首先,您可以推迟昂贵的计算,直到绝对需要它们为止。 其次,您可以创建无限集合,只要它们不断接收请求,它们就可以继续传递元素。 第三,懒惰地使用诸如map
和filter
类的功能概念使您能够生成更有效的代码(请参阅参考资料 ,以获得Brian Goetz的相关讨论的链接)。 Java本身不支持懒惰,但是有几种框架和后继语言支持懒惰,我将在本期和下期中探讨这些内容。
考虑一下用于打印列表长度的伪代码片段:
print length([2+1, 3*2, 1/0, 5-4])
如果您尝试执行此代码,则结果将根据其编写的编程语言类型而有所不同: strict或nonstrict (也称为lazy )。 在严格的编程语言中,由于列表的第三个元素,执行(甚至编译)此代码会导致DivByZero
异常。 在非严格语言中,结果为4
,它可以准确报告列表中的项目数。 毕竟,我要调用的方法是length()
,而不是lengthAndThrowExceptionWhenDivByZero()
! Haskell是使用中的一些不严格的语言(见的一个相关主题 )。 Java,Java不支持非严格评估,但是您仍然可以利用Java中的惰性概念。
Java中的惰性迭代器
Java缺乏对惰性集合的本机支持并不意味着您不能使用Iterator
模拟一个集合。 在本系列的前几期中,我将使用一个简单的质数算法来说明功能概念。 我将在上一部分中介绍的优化类的基础上,对清单1进行增强:
清单1.确定质数的简单算法
import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.sqrt;
public class Prime {
public static boolean isFactor(int potential, int number) {
return number % potential == 0;
}
public static Set<Integer> getFactors(int number) {
Set<Integer> factors = new HashSet<Integer>();
factors.add(1);
factors.add(number);
for (int i = 2; i < sqrt(number) + 1; i++)
if (isFactor(i, number)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
public static int sumFactors(int number) {
int sum = 0;
for (int i : getFactors(number))
sum += i;
return sum;
}
public static boolean isPrime(int number) {
return number == 2 || sumFactors(number) == number + 1;
}
public static Integer nextPrimeFrom(int lastPrime) {
lastPrime++;
while (! isPrime(lastPrime)) lastPrime++;
return lastPrime;
}
}
上一部分将详细讨论此类如何确定整数是否为质数的内部细节。 在清单1中 ,我添加了nextPrimeFrom()
方法,以根据输入参数生成下一个素数。 该方法在本文的后续示例中起作用。
通常,开发人员将迭代器视为使用集合作为后备存储,但是任何支持Iterator
接口的条件都可以使用。 因此,我可以创建一个质数的无限迭代器,如清单2所示:
清单2.创建一个惰性迭代器
public class PrimeIterator implements Iterator<Integer> {
private int lastPrime = 1;
public boolean hasNext() {
return true;
}
public Integer next() {
return lastPrime = Prime.nextPrimeFrom(lastPrime);
}
public void remove() {
throw new RuntimeException("Can't change the fundamental nature of the universe!");
}
}
在清单2中 , hasNext()
方法始终返回true
,因为据我们所知,质数的数量是无限的。 remove()
方法不适用于此处,因此在意外调用的情况下会抛出异常。 主力方法是next()
方法,该方法仅用一行处理两个杂项。 首先,它通过调用清单1中添加的nextPrimeFrom()
方法,基于最后一个生成下一个质数。 其次,它利用Java在单个语句中赋值和返回的能力,从而更新了内部的lastPrime
字段。 我执行清单3中的惰性迭代器:
清单3.测试惰性迭代器
public class PrimeTest {
private ArrayList<Integer> PRIMES_BELOW_50 = new ArrayList<Integer>() {{
add(2); add(3); add(5); add(7); add(11); add(13);
add(17); add(19); add(23); add(29); add(31); add(37);
add(41); add(43); add(47);
}};
@Test
public void prime_iterator() {
Iterator<Integer> it = new PrimeIterator();
for (int i : PRIMES_BELOW_50) {
assertTrue(i == it.next());
}
}
}
在清单3中 ,我创建一个PrimeIterator
并验证它报告了前50个素数。 尽管不是迭代器的典型用法,但它确实模仿了惰性集合的一些有用行为。
使用LazyList
Jakarta通用包括LazyList
类(请参阅相关信息 ),它使用装饰设计图案和工厂的组合。 要使用Commons LazyList
,必须包装一个现有列表使其变得懒惰,并为新值创建一个工厂。 考虑清单4中LazyList
的用法:
清单4.测试Commons LazyList
public class PrimeTest {
private ArrayList<Integer> PRIMES_BELOW_50 = new ArrayList<Integer>() {{
add(2); add(3); add(5); add(7); add(11); add(13);
add(17); add(19); add(23); add(29); add(31); add(37);
add(41); add(43); add(47);
}};
@Test
public void prime_factory() {
List<Integer> primes = new ArrayList<Integer>();
List<Integer> lazyPrimes = LazyList.decorate(primes, new PrimeFactory());
for (int i = 0; i < PRIMES_BELOW_50.size(); i++)
assertEquals(PRIMES_BELOW_50.get(i), lazyPrimes.get(i));
}
}
在清单4中 ,我创建了一个新的空ArrayList
并将其包装在Commons LazyList.decorate()
方法中,以及用于生成新值的PrimeFactory
。 Commons LazyList
将使用列表中已经存在的任何值,但是当调用get()
方法以get()
尚无值的索引时, LazyList
将使用工厂(在这种情况下为PrimeFactory()
)来生成和填充值。 PrimeFactory
出现在清单5中:
清单5. LazyList
使用的PrimeFactory
public class PrimeFactory implements Factory {
private int index = 0;
@Override
public Object create() {
return Prime.indexedPrime(index++);
}
}
所有惰性列表都需要一种生成后续值的方法。 在清单2中 ,我结合使用了next()
方法和Prime
的nextPrimeFrom()
方法。 对于清单4中的 Commons LazyList
,我使用PrimeFactory
实例。
Commons LazyList
实现的一个怪癖是,当请求新值时, LazyList
传递给工厂方法的信息。 按照设计,它甚至不传递所请求元素的索引,从而迫使对PrimeFactory
类的当前状态进行维护。 这对后备列表产生了不希望的依赖关系(因为必须将其初始化为空才能使索引编号与PrimeFactory
的内部状态同步)。 Commons LazyList
只是一个基本的实现。 存在更好的开源替代方案,例如Totally Lazy。
完全懒惰
Totally Lazy是一个向Java添加一流的惰性的框架(请参阅参考资料 )。 在上一部分中 ,我介绍了Totally Lazy,但没有做到惯用司法。 该框架的目标之一是通过使用静态导入的组合来创建可读性强的Java代码。 编写清单6中的简单素数查找器是为了充分利用此Totally Lazy功能:
清单6.完全惰性的,完全利用静态导入
import com.googlecode.totallylazy.Predicate;
import com.googlecode.totallylazy.Sequence;
import static com.googlecode.totallylazy.Predicates.is;
import static com.googlecode.totallylazy.numbers.Numbers.equalTo;
import static com.googlecode.totallylazy.numbers.Numbers.increment;
import static com.googlecode.totallylazy.numbers.Numbers.range;
import static com.googlecode.totallylazy.numbers.Numbers.remainder;
import static com.googlecode.totallylazy.numbers.Numbers.sum;
import static com.googlecode.totallylazy.numbers.Numbers.zero;
import static com.googlecode.totallylazy.predicates.WherePredicate.where;
public class Prime {
public static Predicate<Number> isFactor(Number n) {
return where(remainder(n), is(zero));
}
public static Sequence<Number> factors(Number n){
return range(1, n).filter(isFactor(n));
}
public static Number sumFactors(Number n){
return factors(n).reduce(sum);
}
public static boolean isPrime(Number n){
return equalTo(increment(n), sumFactors(n));
}
}
在清单6中 ,完成了静态导入之后,该代码是Java的非典型代码,但可读性很强。 共懒惰的部分原因是对的JUnit的Hamcrest测试扩展流畅界面启发(见相关信息 ),并使用一些Hamcrest的类。 所述isFactor()
方法变得到一个呼叫where()
方法,使用共懒惰的remainder()
方法与Hamcrest一起is()
方法。 同样, factors()
方法成为对range()
对象的filter()
调用,我使用现在熟悉的reduce()
方法确定总和。 最后, equalTo()
isPrime()
方法使用Hamcrest的equalTo()
方法来确定因子之和是否等于递增的数量。
精明的读者会注意到, 清单6中的实现确实实现了我在前一部分中写的优化,它使用一种更有效的算法来确定因素。 优化的版本显示在清单7中:
清单7.优化的质数查找器的完全惰性实现
public class PrimeFast {
public static Predicate<Number> isFactor(Number n) {
return where(remainder(n), is(zero));
}
public static Sequence<Number> getFactors(final Number n){
Sequence<Number> lowerRange = range(1, squareRoot(n)).filter(isFactor(n));
return lowerRange.join(lowerRange.map(divide().apply(n)));
}
public static Sequence<Number> factors(final Number n) {
return getFactors(n).memorise();
}
public static Number sumFactors(Number n){
return factors(n).reduce(sum);
}
public static boolean isPrime(Number n){
return equalTo(increment(n), sumFactors(n));
}
}
清单7中显示了两个主要更改。 首先,我改进了getFactors()
算法,以获取平方根以下的因子,然后生成平方根上方的对称因子。 在Totally Lazy中,甚至可以使用其流畅的界面样式来表示诸如divide()
类的操作。 第二个更改涉及备忘录,该备忘录将自动缓存具有相同参数的函数调用; 我已经将sumFactors()
方法更改为使用factors()
方法,这是记忆化的getFactors()
方法。 完全懒惰将备忘录作为框架的一部分来实现,因此无需其他代码即可实现此优化。 但是,框架作者将其拼写为memorise()
而不是更传统的(如Groovy一样) memoize()
。
正如其名,Totally Lazy尝试在整个框架中尽可能多地使用惰性。 实际上,Totally Lazy框架本身包括primes()
生成器,该生成器使用框架的构造块实现无限数量的素数序列。 考虑清单8中显示的Numbers
类的摘录:
清单8.实现无限素数的完全懒惰摘录
public static Function1<Number, Number> nextPrime = new Function1<Number, Number>() {
@Override
public Number call(Number number) throws Exception {
return nextPrime(number);
}
};
public static Computation<Number> primes = computation(2, computation(3, nextPrime));
public static Sequence<Number> primes() {
return primes;
}
public static LogicalPredicate<Number> prime = new LogicalPredicate<Number>() {
public final boolean matches(final Number candidate) {
return isPrime(candidate);
}
};
public static Number nextPrime(Number number) {
return iterate(add(2), number).filter(prime).second();
}
nextPrime()
方法创建一个新的Function1
,它是Totally Lazy对伪高阶函数的实现,该函数旨在接受单个Number
参数并产生Number
结果。 在这种情况下,它从nextPrime()
方法返回结果。 创建primes
变量以保存素数的状态,以2
(第一个素数)作为种子值执行计算,并对下一个素数使用新的计算。 这是惰性实现中的典型模式:保留下一个元素以及用于生成后续值的方法。 prime()
方法仅仅是早期执行的prime
计算的包装。
为了确定清单8中的nextPrime()
,Totally Lazy创建一个新的LogicalPredicate
来封装素数的确定,然后创建nextPrime()
方法,该方法使用Totally Lazy中的流畅接口来确定下一个素数。
完全懒惰在使用Java中的低静态导入来促进可读性强的代码方面做得非常出色。 许多开发人员认为Java是内部特定于域的语言的不良宿主,但是Totally Lazy打破了这种态度。 而且它会积极使用惰性功能,从而延迟所有可能的操作。
结论
在本期中,我探索了惰性,首先使用迭代器在Java中创建一个模拟的惰性集合,然后使用Jakarta Commons Collections的基本LazyList
类。 最后,我使用Totally Lazy实现了示例代码,在内部使用了lazy集合来确定质数,并在质数的lazy无限集合中使用了lazy集合。 Totally Lazy还通过使用静态导入来提高代码的可读性来说明流利的界面样式的表现力。
在下一部分中,我将继续探索懒惰,转向Groovy,Scala和Clojure。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft18/index.html