懒惰删除

    技术2024-02-23  110

    关于本系列

    本系列旨在将您的观点重新定位为实用的心态,帮助您以新的方式看待常见问题并找到改善日常编码的方式。 它探讨了函数式编程的概念,允许在Java语言中进行函数式编程的框架,在JVM上运行的函数式编程语言以及语言设计的一些未来方向。 该系列面向那些了解Java及其抽象如何工作,但很少或没有使用功能语言经验的开发人员。

    惰性评估 (尽可能长的延迟表达式评估)是许多功能编程语言的功能。 惰性集合按需提供其元素,而不是对其进行预先计算,从而提供了许多好处。 首先,您可以推迟昂贵的计算,直到绝对需要它们为止。 其次,您可以创建无限集合,只要它们不断接收请求,它们就可以继续传递元素。 第三,懒惰地使用诸如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

    Processed: 0.015, SQL: 9