Composition组合
Groovy支持对多个闭包进行组合。特别地, 闭包还重载了左移<<、右移>>运算符,。使得非常方便实现将一个闭包的输出作为另一个闭包的输入,示例如下所示
class CompositionDemo {
static void main(String[] args) {
// f(x) = x+2
def fx = {x -> x+2}
// g(x) = x*3
def gx = {x -> x*3}
// 闭合组合
// h1(x) = f( g(x) ) = (x*3) + 2 = (3*3) + 2 = 11
def h1x = {x -> fx( gx(x) )}
assert h1x(3) == 11
// h2(x) = g( f(x) ) = (x+2) * 3 = (3+2) * 3 = 15
def h2x = {x -> gx( fx(x) )}
assert h2x(3) == 15
// 闭包重载了左移<<, 右移>>运算符, 将一个闭包的输出作为另一个闭包的输入
// y1(x) = g( f(x) ) = (x+2) * 3
def y1x = gx << fx
assert y1x(3) == 15
// y2(x) = g( f(x) ) = (x+2) * 3
def y2x = fx >> gx
assert y2x(3) == 15
}
}
Currying柯里化
具体到Groovy当中,其闭包的柯里化可以实现对原来接收N个参数的闭包,仅确定其中的部分参数,并使之返回一个新的闭包。在新闭包中仅包含未确定的参数。典型地,Groovy支持左柯里化、右柯里化、指定参数位置的柯里化等多种形式。示例如下所示
class CurryingDemo {
static void main(String[] args) {
// 左柯里化
Closure closure5 = { x,y, z -> "$x--$y~~$z" }
Closure closure6 = closure5.curry("Aaron")
assert closure6.call("Bob", "Amy") == "Aaron--Bob~~Amy"
Closure closure7 = closure5.curry("China", "USA")
assert closure7.call("UK") == "China--USA~~UK"
// 右柯里化
Closure closure8 = closure5.rcurry("Z1")
assert closure8.call("X1", "Y1") == "X1--Y1~~Z1"
Closure closure9 = closure5.rcurry("Y2", "Z2")
assert closure9.call("X2") == "X2--Y2~~Z2"
// 基于索引的柯里化, 参数的位置索引从0开始
Closure c1 = { a,b,c,d -> "$a--$b~~$c>>$d" }
// 设置闭包第二个参数为"Hello"
Closure c2 = c1.ncurry(1,"Hello")
assert c2.call("0", "2", "3") == "0--Hello~~2>>3"
// 从指定索引处依次设置多个参数, 这里闭包第二、三个参数分别为"Hello"、"Aaron"
Closure c3 = c1.ncurry(1,"Hello", "Aaron")
assert c3.call("0", "3") == "0--Hello~~Aaron>>3"
}
}
Memoization记忆化
在递归过程中,如果记住某些重复计算的结果,则可以大大减少计算量。典型地,通过递归的形式计算斐波那契数列的第N项时,其会产生多次重复的计算调用过程。这个时候我们在Groovy闭包中就可以引入Memoization记忆化这一特性,缓存已经计算过的结果
class MemoizationDemo {
static void test1() {
def count = 0
// 递归调用闭包时, 必须先定义一个变量作为闭包的名字, 然后才能在闭包内部递归调用
def fib1
fib1 = { long n ->
count++ // 计数
if( n==0 ) {
return 0
} else if( n==1 ) {
return 1
} else {
return fib1(n-1) + fib1(n-2)
}
}
assert fib1(6) == 8
assert count == 25
count = 0 // 清空计数器
def fib3
fib3 = { long n ->
count++ // 计数
if( n==0 ) {
return 0
} else if( n==1 ) {
return 1
} else {
return fib3(n-1) + fib3(n-2)
}
}.memoize() // 通过 memoize() 方法实现记忆化
assert fib3(6) == 8
assert count == 7
}
}
进一步地,还可以通过有参版本的memoize方法实现对缓存结果数量的限制。包括最多、最少以及指定范围数的缓存数。示例代码如下所示
class MemoizationDemo {
static void test2() {
def f1
// 采用基于LRU的淘汰策略, 最多保留3个缓存数据
f1 = { n ->
if(n==1) {
return 1
}
return f1(n-1) + n
}.memoizeAtMost(3);
def f2
// 采用基于LRU的淘汰策略, 发生GC时至少保留3个缓存数据
f2 = { n ->
if(n==1) {
return 1
}
return f2(n-1) + n
}.memoizeAtLeast(3);
def f3
// 采用基于LRU的淘汰策略
// 最多保留5个缓存数据, 发生GC时至少保留3个缓存数据
f3 = { n ->
if(n==1) {
return 1
}
return f3(n-1) + n
}.memoizeBetween(3,5);
}
}
Trampoline蹦床
对于递归而言,其最大风险就在于由于递归过深导致的SOF栈溢出。在Groovy中不论是普通递归还是尾递归,均存在上述风险。示例代码如下所示
class TrampolineDemo {
/**
* 基于普通递归的阶乘
*/
static void test1() {
def fact1
fact1 = { n ->
if(n<2) {
return 1
}
return fact1(n-1) * n
}
// 递归过深导致栈溢出SOF
fact1(2000)
}
/**
* 基于尾递归的阶乘
*/
static void test2() {
def fact2
fact2 = { n, result=1 ->
if(n<2) {
return result
}
return fact2(n-1, result*n)
}
// 递归过深导致栈溢出SOF
fact2(2000)
}
}
通过test2的代码,我们实际上可以发现Groovy并未对尾递归形式作进一步优化,使得其依然存在SOF栈溢出的风险。但在Groovy中则可对尾递归应用trampoline,使得Groovy能够对尾递归的调用进行优化,避免发生SOF。这实际上也是我们使用尾递归写法的真正意义所在。具体地。一方面,在递归闭包时通过trampoline方法传参;另一方面,在定义闭包过程中,则通过无参版本的trampoline方法将闭包包装为TrampolineClosure。示例如下所示
class TrampolineDemo {
/**
* 基于尾递归的阶乘, 并使用蹦床函数trampoline进行包装
*/
static void test3() {
def fact3
fact3 = { n, result=1 ->
if(n<2) {
return result
}
//
// 其会返回一个TrampolineClosure实例, 用于对原闭包进行包装
return fact3.trampoline(n-1, result*n)
}.trampoline() // 通过trampoline方法将闭包包装为TrampolineClosure
fact3(2000)
}
}
参考文献
-
Groovy In Action · 2nd Edition Dierk König、Guillaume Laforge著
原文始发于微信公众号(青灯抽丝):Groovy之Closure闭包进阶操作
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/21332.html