Groovy之Closure闭包进阶操作

这里对Groovy闭包的进阶操作进行介绍

abstract.png

Composition组合

Groovy支持对多个闭包进行组合。特别地, 闭包还重载了左移<<、右移>>运算符,。使得非常方便实现将一个闭包的输出作为另一个闭包的输入,示例如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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支持左柯里化、右柯里化、指定参数位置的柯里化等多种形式。示例如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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记忆化这一特性,缓存已经计算过的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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方法实现对缓存结果数量的限制。包括最多、最少以及指定范围数的缓存数。示例代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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中不论是普通递归还是尾递归,均存在上述风险。示例代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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。示例如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
}
}

参考文献

  1. Groovy In Action · 2nd Edition Dierk König、Guillaume Laforge著
0%