在上一期中 ,我展示了Groovy的一些即用型功能,以及如何使用Groovy的原语来构建无限列表。 在本期中,我将继续探索函数式编程与Groovy的交集。
Groovy是一种多范式语言:它支持面向对象,元编程和函数式编程样式,它们大多彼此正交 (请参见正交性侧栏)。 元编程允许您向语言及其核心库添加功能。 通过将元编程与函数式编程相结合,可以使自己的代码更具函数性,或者扩展第三方函数库,以使其在Groovy中更好地工作。 我将首先展示Groovy的ExpandoMetaClass
如何工作以扩充类,然后如何使用此机制将Functional Java库(请参阅参考资料 )编织到Groovy中。
通过ExpandoMetaClass
打开类
Groovy的更强大功能之一是open class ,它具有重新打开现有类以增强或删除其功能的能力。 这与子类化不同,子类化是从现有类型派生出一种新类型。 打开类使您可以重新打开诸如String
类的类,并向其添加新方法。 测试库大量使用此功能来通过验证方法扩展Object
,以便应用程序中的所有类现在都具有验证方法。
Groovy中有两个开放类技术:类和ExpandoMetaClass
(参见相关主题 )。 在这个例子中,任何一个都可以工作。 我选择ExpandoMetaClass
是因为它在语法上更加简单。
如果您一直在关注本系列,那么您将熟悉我长期运行的数字分类示例。 清单1中所示的Groovy中完整的Classifier
使用了Groovy自己的功能构造:
清单1. Groovy中的完整Classifier
class Classifier {
def static isFactor(number, potential) {
number % potential == 0;
}
def static factorsOf(number) {
(1..number).findAll { i -> isFactor(number, i) }
}
def static sumOfFactors(number) {
factorsOf(number).inject(0, {i, j -> i + j})
}
def static isPerfect(number) {
sumOfFactors(number) == 2 * number
}
def static isAbundant(number) {
sumOfFactors(number) > 2 * number
}
def static isDeficient(number) {
sumOfFactors(number) < 2 * number
}
static def nextPerfectNumberFrom(n) {
while (!isPerfect(++n));
n
}
}
如果对在此版本中如何实现这些方法有任何疑问,可以参考以前的部分(尤其是“ 耦合和组成,第2部分 ”和“ Groovy中的功能部件,第1部分 ”)。 要使用此类的方法,我可以以“常规”功能方式调用这些方法: Classifier.isPerfect(7)
。 但是,通过使用元编程,我可以将这些方法直接“连接”到Integer
类中,从而允许我“询问”它属于哪个类别。
要将这些方法添加到Integer
类中,我访问该类的metaClass
属性(由Groovy为每个类预定义),如清单2所示:
清单2.将分类添加到Integer
Integer.metaClass.isPerfect = {->
Classifier.isPerfect(delegate)
}
Integer.metaClass.isAbundant = {->
Classifier.isAbundant(delegate)
}
Integer.metaClass.isDeficient = {->
Classifier.isDeficient(delegate)
}
在清单2中 ,我将三个Classifier
方法添加到Integer
。 现在,Groovy中的所有整数都具有这些方法。 (Groovy没有原始数据类型的概念;即使Groovy中的常量也使用Integer
作为基础数据类型。)在定义每种方法的代码块中,我可以访问预定义的delegate
参数,该参数代表正在调用的对象的值类上的方法。
初始化元编程方法后(请参见初始化元编程方法侧栏),可以“询问”有关类别的数字,如清单3所示:
清单3.使用元编程对数字进行分类
@Test
void metaclass_classifiers() {
def num = 28
assertTrue num.isPerfect()
assertTrue 7.isDeficient()
assertTrue 6.isPerfect()
assertTrue 12.isAbundant()
}
清单3展示了新添加的同时作用于变量和常量的方法。 现在,将方法添加到Integer
可能很简单,该方法返回特定数字的分类(可能作为枚举)。
向现有类添加新方法本身并不是特别的“功能性”,即使它们调用的代码具有较强的功能性也是如此。 但是,无缝添加方法的功能使合并包含重要功能功能的第三方库(例如Functional Java库)变得容易。 在第二部分中 ,我使用Functional Java库实现了数字分类器,在这里我将使用它来创建无限的完美数字流。
使用元编程映射数据类型
Groovy本质上是Java的方言,因此引入第三方库(例如Functional Java)是微不足道的。 但是,我可以通过在数据类型之间执行一些元编程映射来使接缝不太明显,从而将这些库进一步编织到Groovy中。 Groovy具有本机关闭类型(使用Closure
类)。 功能性Java还没有闭包的奢侈性(它依赖Java 5语法),从而迫使作者使用泛型和包含f()
方法的常规F
类。 使用Groovy ExpandoMetaClass
,我可以通过在两者之间创建映射方法来解决方法/关闭类型的差异。
我要扩充的类是Functional Java的Stream
类,它为无限列表提供了抽象。 我希望能够传递Groovy闭包代替Functional Java F
实例,因此我向Stream
类添加了重载方法,以将闭包映射到F
的f()
方法,如清单4所示:
清单4.使用ExpandoMetaClass
映射数据类型
Stream.metaClass.filter = { c -> delegate.filter(c as fj.F) }
// Stream.metaClass.filter = { Closure c -> delegate.filter(c as fj.F) }
Stream.metaClass.getAt = { n -> delegate.index(n) }
Stream.metaClass.getAt = { Range r -> r.collect { delegate.index(it) } }
第一行在Stream
上创建一个filter()
方法,该方法接受闭包(代码块的c
参数)。 第二行(带注释)与第一行相同,但为Closure
添加了类型声明; 它不会影响Groovy执行代码的方式,但可能更适合作为文档。 代码块的主体调用Stream
的预先存在的filter()
方法,将Groovy闭包映射到Functional Java fj.F
类。 我使用Groovy的半魔术符as
运算符来执行映射。
Groovy的as
运算符将闭包强制为接口定义,从而允许闭包方法映射到接口所需的方法。 考虑清单5中的代码:
清单5.使用as
创建轻量级迭代器
def h = [hasNext : { println "hasNext called"; return true},
next : {println "next called"}] as Iterator
h.hasNext()
h.next()
println "h instanceof Iterator? " + (h instanceof Iterator)
在清单5的示例中,我创建了具有两个名称-值对的哈希。 每个名称都是一个字符串(Groovy不需要使用双引号来分隔哈希键,因为默认情况下它们是字符串),并且值是代码块。 as
运算符将此哈希映射到Iterator
接口,该接口同时需要hasNext()
和next()
方法。 一旦执行了映射,就可以将哈希视为迭代器。 清单的最后一行显示true
。 如果我有一个单一方法的接口,或者当我希望接口中的所有方法都映射到单个闭包时,我可以省去哈希值,并直接将as
将闭包映射到函数上。 回到清单4的第一行,我将传递的闭包映射到单方法F
类。 在清单4中 ,我必须映射两个getAt
方法(一个接受一个数字,另一个接受一个Range
),因为filter
需要这些方法进行操作。
使用这个新增强的Stream
,我可以处理一个无限序列,如清单6所示:
清单6.在Groovy中使用无限功能Java流
@Test
void adding_methods_to_fj_classes() {
def evens = Stream.range(0).filter { it % 2 == 0 }
assertTrue(evens.take(5).asList() == [0, 2, 4, 6, 8])
assertTrue(evens[3..6] == [6, 8, 10, 12])
}
在清单6中 ,我创建了一个从0开始的偶数整数的无限列表,方法是使用闭合块对其进行过滤。 您不能一次获得所有无限序列,因此必须take()
获取任意数量的元素。 清单6的其余部分显示了测试断言,这些断言演示了流的工作方式。
Groovy中的无限流
在上一期中 ,我展示了如何在Groovy中实现一个惰性无限列表。 而不是手工创建它,为什么不依赖于Functional Java中的无限序列呢?
要创建一个无限的完美数Stream
,我需要两个附加的Stream
方法映射来理解Groovy闭包,如清单7所示:
清单7.完美数流的两个附加方法映射
Stream.metaClass.asList = { delegate.toCollection().asList() }
Stream.metaClass.static.cons = { head, closure -> delegate.cons(head, closure as fj.P1) }
// Stream.metaClass.static.cons =
// { head, Closure c -> delegate.cons(head, ['_1':c] as fj.P1)}
在清单7中 ,我创建了一个asList()
转换方法,可以轻松地将Functional Java流转换为列表。 我实现的另一个方法是重载cons()
,它是Stream
上构造新列表的方法。 创建无限列表时,数据结构通常包含第一个元素和一个闭包块,作为列表的尾部,在调用时会生成下一个元素。 对于我的Groovy完美数字流,我需要Functional Java来理解cons()
可以接受Groovy闭包。
如果使用as
将单个闭包映射到具有多个方法的接口,则对我在该接口上调用的任何方法都执行该闭包。 在大多数情况下,这种简单映射样式适用于Functional Java类。 但是,有些方法需要fj.P1
方法而不是fj.F
方法。 在某些情况下,我仍然可以避免使用简单的映射,因为下游方法不依赖于P1
任何其他方法。 如果需要更高的精度,我可能不得不使用清单7中注释行中所示的更复杂的映射,该映射必须使用映射到闭包的_1()
方法创建一个哈希。 尽管该方法看起来很奇怪,但这是fj.P1
类的标准方法,该方法返回第一个元素。
一旦将我的元编程映射的方法放在Stream
,就可以使用清单1中的Classifier
创建无限个完美数字流,如清单8所示:
清单8.使用Functional Java和Groovy的无限个完美数字流
import static fj.data.Stream.cons
import static com.nealford.ft.metafunctionaljava.Classifier.nextPerfectNumberFrom
def perfectNumbers(num) {
cons(nextPerfectNumberFrom(num), { perfectNumbers(nextPerfectNumberFrom(num))})
}
@Test
void infinite_stream_of_perfect_nums_using_functional_java() {
assertEquals([6, 28, 496], perfectNumbers(1).take(3).asList())
}
我使用静态导入既为cons()
从Java功能,并为自己的nextPerfectNumberFrom()
从方法Classifier
,以使代码更简洁。 perfectNumbers()
方法通过将种子号之后的第一个完美数作为第一个元素(第一个元素)并添加一个闭合块作为第二个元素,来返回无穷个完美数字序列(是, cons是一个动词)。 闭包块返回无限序列,其中下一个数字作为头,闭包计算另一个序列作为尾巴。 在测试中,我生成了一个从1开始的完美数字流,并获取接下来的三个完美数字并断言它们与列表匹配。
结论
当开发人员想到元编程时,他们通常只考虑自己的代码,而不考虑扩充别人的代码。 Groovy允许我不仅向诸如Integer
类的内置类中而且向诸如Functional Java之类的第三方库中添加新方法。 将元编程和函数式编程相结合,可以用很少的代码获得强大的功能,从而创建无缝链接。
尽管我可以直接从Groovy调用Functional Java类,但是与真正的闭包相比,该库的许多构建块都比较笨拙。 通过使用元编程,我可以映射Functional Java方法,以使它们了解便捷的Groovy数据结构,从而实现两全其美。 在Java定义本机闭包类型之前,开发人员经常需要在语言类型之间执行这些多语言映射:Groovy闭包和Scala闭包在字节码级别上不是一回事。 拥有Java标准将把这些对话推到运行时,并消除对映射的需求,就像我在这里展示的那样。 但是,在此之前,此功能可生成干净但功能强大的代码。
在下一部分中,我将讨论一些优化,函数式编程使您的运行时可以使用备忘录的Groovy制作并显示示例。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft8/index.html