Groovy之Closure闭包进阶操作

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

Groovy之Closure闭包进阶操作

abstract.png

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)
    }
}

参考文献

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

原文始发于微信公众号(青灯抽丝):Groovy之Closure闭包进阶操作

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/21332.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!