[TOC]

闭包

闭包是一种可执行的代码块的方法,闭包也是对象,可以想方法一样传递参数。由于闭包是代码块,因此也可以在需要时执行。定义闭包的时候可以使用一个或者多个参数。闭包可以访问属性信息,意味着可以访问并修其作用于内的所有变量值。

闭包

def greeting = 'hello'
def clos = {
    param ->
        println "$greeting $param"
}

clos.call("world")

greeting = 'welcome'
clos.call('world')

def demo(clos){
    def greeting = 'Bonjour'//this will not affect closure
    clos.call('ken')
}

demo(clos)   //output welcome ken

以上代码说明,只有闭包被定义且存在,而不是在被调用时,可以访问其状态值。

闭包也常用在集合中。使用闭包可以更高效的遍历元素。

1.upto(10,{
    println it 
})
//或者你也可以这么写
1.upto(10){
    println it
}
//阶乘
def fac = 1
1.upto(5){
    num ->
        fac *= num
}

println "Factorical : $fac"

闭包与集合和字符串

常用each方法来遍历。

find()返回第一个满足条件的元素,findAll()返回所有满足条件的元素

def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]
list.findAll {
    it > 5
}.each {
    println it
}

any() and every()

def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

def anyEle = list.any {
    it > 5
}
println anyEle //true

def everyEle = list.every {
    it > 5
}

println everyEle  //false

collect() and inject

collect用来遍历集合并且返回一个集合


def list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

//collect 返回一个由closure转换后的集合
def newlist = list.collect {
    it * it
}

println newlist

//collect 的高级范例
def doubles = {
    item ->
        2 * item
}

def triples = {
    item ->
        3 * item
}

def isEven = {
    item -> 
        item%2 == 0
}
def map(clos,list){
    return list.collect(clos)
}

inject用来遍历集合,首先将需要传递的值和集合项目传递给必报,此时其传递的值将作为处理结果,然后再和下一个集合项目一起传递给闭包,以此类推

def factorial = [2, 3, 4, 5]
        .inject(1) {
    previous, element ->
        previous * element
}

println factorial
//=====
def list = [2, 3, 4, 5]
factorial = list.inject(1) {
    prviouse, element ->
        prviouse * element
}
println factorial
//=====
def list = [2, 3, 4, 5]
def closure = {
    previous, element ->
        previous * element
}

def factorial = list.inject (1,closure)
println factorial

闭包的其他特性

由于闭包也是一个对象,因此它可以作为方法的参数,同时也可以作为另一个闭包的参数。

作为方法的参数

def filter(list, predict) {
    return list.findAll(predict)
}

def isEven = {
    x ->
        return x % 2 == 0
}

def isOdd = {
    x ->
        return !isEven(x)
}

def table = [11, 12, 13, 14]

def evens = filter(table,isEven)
println evens

def odds = filter(table,isOdd)
println odds

作为另一个闭包的参数

def takewhile = {
    predicate, list ->
        def result = []
        for (element in list) {
            if (predicate(element)) {
                result << element
            &#125; else &#123;
                return result
            &#125;
        &#125;
&#125;

def isEven = &#123;
    x ->
        return x % 2 == 0
&#125;

def isOdd = &#123;
    x ->
        return !isEven(x)
&#125;

def table1 = [12, 14, 15, 18]
def table2 = [11, 13, 15, 16, 18]

def evens = takewhile.call(isEven,table1)
println evens

作为返回值

def multiply(x)&#123;
    return &#123;
        y->
            return x * y
    &#125;
&#125;

def twice = multiply(2)

println twice(4)

嵌套闭包

选择排序算法的实现

def selection = &#123;
    list ->
  //交换两个位置的数
        def swap = &#123;
            sList, p, q ->
                def temp = sList[p]
                sList[p] = sList[q]
                sList[q] = temp
        &#125;
  //找出最小数位置
        def minimumPosition = &#123;
            pList, from ->
                def mPos = from
                def nextFrom = 1 + from
                for (j in nextFrom..<pList.size) &#123;
                    if (pList[j] < pList[mPos]) &#123;
                        mPos = j
                    &#125;
                &#125;
                return mPos
        &#125;
        def size = list.size() - 1
        for (k in 0..<size) &#123;
            def minPos = minimumPosition(list, k)
            swap(list, minPos, k)
        &#125;
        return list
&#125;

def table = [13, 14, 12, 11, 14]
def sorted = selection(table)
println sorted

关于闭包的更多信息

闭包和不确定性

先看一个例子,闭包在实参列表外部。如果方法的最后一个参数是闭包,那么可以将它从实参列表中删除,并放在气候的括号后面。

demo(closure) 变为 demo closure

def greeting = 'Hello'
def clo = &#123;
    param ->
        println "$greeting $param"
&#125;

def demo(clos) &#123;
    def greeting = 'Bonjour'
    clos.call('Ken')
&#125;
//demo()clo error null point exception,解释见后。
 
demo clo

demo() &#123;
    param ->
        println "welcome $param"
&#125;

假设clos是一个闭包变量,{...}是一个闭包体。假设xy是两个随意设置的值,则查看如下不同的方法调用语句:

method(x,y,&#123;...&#125;) //ok
method(x,y)&#123;...&#125; //ok
method(x,y,clos) //ok
method(x,y)clos //error no such method

第二行代码把闭包体直接放在调用参数的后面。第四行代码说明,同样的做法也许不适用闭包变量。Groovy解释器无法忍冬clos标识符是该方法的调用的一部分,因此Groovy解释器就会报错,说明找不到只有具有两个参数的method方法。

如果method标识具有一个参数的方法:

def method(c)&#123;
  ...
&#125;

则clos是个闭包变量,{…}是闭包体,如下调用语句

method(&#123;...&#125;)  //ok
method()&#123;...&#125;  //ok
method&#123;...&#125;    //ok    
method(clos)   //ok
method()clos   //error null point exception 
method clos    //ok

第五行代码中,调用方法时不适用任何实参,在该方法体中,形参将被初始化为null,在这个方法中使用闭包变量将会导致系统错误。

闭包和方法

对于如下两个结构

def mDouble = &#123;
    n ->
          return 2*n
&#125;

def mDouble(n)&#123;
    return 2*n
&#125;

前者是一个闭包定义,后者是一个方法定义。注意,任何闭包引用肯定有固定的作用域。不能出现同名的闭包变量,但是可以出现同名方法(方法重载)

默认参数

像方法一样,闭包也可以被分配带默认值的参数。

def greeting = &#123;
    msg, name = "leo" ->
        println "$msg $name"
&#125;

greeting("hello ")
greeting("hello", "julia")

闭包和作用域

上面讲到的局部闭包实现的选择性排序算法。下面讲一个使用冒泡算法排序。

def bubbleSort = &#123;
    list ->
        def swap = &#123;
            sList, p, q ->
                def temp
                temp = sList[q]
                sList[q] = sList[p]
                sList[p] = temp
        &#125;

        def size = list.size()
        def sortedPosition = 0

        while (sortedPosition < size) &#123;
            for (index in 1..<(size - sortedPosition)) &#123;
                if (list[index] < list[index - 1])
                    swap(list, index, index - 1)
            &#125;
            sortedPosition ++ 
        &#125;
        return list
&#125;

def list = [1,5,4,6,2,7,0,78,54,23]
bubbleSort(list)
println list

需要注意的是,闭包只能由语句组成,这意味着闭包中不能有方法定义,因此局部闭包swap不能使用方法定义来替代。

递归闭包

用递归的方法求n!

def factorial = &#123;
    n ->
        return n == 0 ? 1 : n * call(n - 1)
&#125;

println factorial(5)

状态类型

可以使用参数和返回值的动态类型来定义闭包。这个特性使得闭包更加通用。

闭包的动态类型

def times = &#123;
    x, y ->
        return x * y
&#125;

def times = &#123;
    x, y ->
        return x * y
&#125;

println times(3,4)
println times(3.2,4.15)
println times('hello',4)

在调用时,才确定x和y的类型

12
13.280
hellohellohellohello

静态指定参数类型的闭包

def times = &#123;
    Number x, Number y ->
        return x * y
&#125;

这种方式就限定了闭包的参数类型,不能是Number及其子类意外的其他类型。

有关实参的约定

在调用闭包时,实参的数量必须严格匹配闭包定义时的形参数量。否则,Groovy解释器就会报错,指示参数不正确。

def clos = &#123;
    a,b,c ->
   "clos($a$b$c)"
&#125;

clos(1,2,3) //ok
clos(1) //error
clos(1,2,3,4) //error

闭包、集合、范围

列表、映射、和范围等类型提供很多实用闭包作为参数的方法,这有利于迭代处理集合或者范围中的每个元素,并执行指定的任务。

迭代器方法

方法名称 说明
any*
collect*
collect*
each*
every*
find*
findAll*
findIndexOf*
inject*
reverseEach*
sort*

Return 语句

我们先看一个代码范例:

def isMemberA(item, list) &#123;
    def size = list.size()
    for (index in 0..<size) &#123;
        if (list[index] == item)
            return true
    &#125;
    return false
&#125;

def isMemberB(item, list) &#123;
    list.each &#123;
        if (it == item)
            return true
    &#125;
    return false
&#125;
def numbers = [11, 12, 13, 14]

println isMemberA(15,numbers)
println isMemberA(13,numbers)

println isMemberB(15,numbers)
println isMemberB(13,numbers)  //这个有问题

输出为:

false
true
false
false

显然,Groovy方式的返回是有问题的。这个是为什么呢?

在我们的范例中,作为实参的闭包包含一个return语句。闭包返回true,就会导致while循环继续处理列表中的下一个项目。

可以看一下each的实现方式

    public static <T> Iterator<T> each(Iterator<T> self, @ClosureParams(FirstGenericType.class) Closure closure) &#123;
        while(self.hasNext()) &#123;
            Object arg = self.next();
            closure.call(arg);
        &#125;

        return self;
    &#125;

可以看到return是指返回的closure.call()但是外层循环还是在的,并不能直接终止循环,因此最终所有的数都会跑一边,这个是一个比较需要注意的地方,在使用each进行迭代的时候一定要注意哦!

高级闭包

假设某家超时使用特殊的商品价格来吸引新客户,以及维护已经有的客户。在一周内,也许早餐谷类视频的价格优惠1/3;在另一周内,优惠政策可能变化,化妆品销售按照买一送一的方式进行销售。在这种优惠政策经常变化的环境中,需要某种灵活的方式来定义这些“业务规则”,并且能够灵活的修改和调整。

此处的思路来源多范式编程方式,这是Groovy的一个非常重要的特性。借助更高级的函数,函数编程范式综合了多台、组合以及计算模式。

简单闭包

以前,把闭包描述为一个代码块。闭包实现参数化更加有价值,可以被引用,可以被当做参数传入方法,以及可以与call消息一起被调用。

范例:计算两个数值参数的乘积

def multiply = &#123;
    x, y ->
        return x * y
&#125;

println "multiply(3,4) = $&#123;multiply(3,4)&#125;"
println "multiply(3.4,5.6) = $&#123;multiply(3.4,5.6)&#125;"

由于闭包可以引用对象的状态。

范例:作用域和闭包

def multiplier = 2 
def multiply = &#123;
    x -> 
        return x * multiplier
&#125;
println "multiply(3) = $&#123;multiply(3)&#125;"
println "multiply(5.6) = $&#123;multiply(5.6)&#125;"

multiplier = 3

println "multiply(3) = $&#123;multiply(3)&#125;"
println "multiply(5.6) = $&#123;multiply(5.6)&#125;"

在闭包定义范围内的变量可以从闭包代码内访问。

闭包是可以将另一个闭包返回的,类似一个二阶的函数

范例:闭包返回值

def add = &#123;
    x, y ->
        return x + y
&#125;

def subtract = &#123;
    x, y ->
        return x - y
&#125;

def multiply = &#123;
    x, y ->
        return x * y
&#125;

def divide = &#123;
    x, y ->
        return x / y
&#125;

def arithmetic = &#123;
    arith ->
        switch (arith) &#123;
            case 'ADD':
                return add
            case 'SUBTRACT':
                return subtract
            case 'MULTIPLY':
                return multiply
            case 'DIVIDE':
                return divide
            default:
                return add
        &#125;
&#125;

def addOperation = arithmetic('ADD')
def mulOperation = arithmetic('MULTIPLY')

println "addOperation(3,4) = $&#123;addOperation(3,4)&#125;"
println "mulOperation(3,4) = $&#123;mulOperation(3,4)&#125;"
println arithmetic('DIVIDE')(3,4)

部分应用

在上面的范例2中,multiply闭包计算一个参数和内部变量multiplier的乘积。现在,如果把multiplier作为闭包的参数,这个闭包返回multiply闭包,后者计算自己的参数与multiplier的成绩,这就是闭包部分应用的范例。

范例:partial applicaiton

def multiply = &#123;
    x, y ->
        return x * y
&#125;

def tripple = multiply.curry(3)
def quadroup = multiply.curry(4)

def twelve = multiply.curry(3,4)
println tripple(3)
println quadroup(5)
println twelve()

**注意:表达式multiply.curry(3)**表示把参数乘以3的闭包。然后,被返回的闭包使用一个参数进行调用。在数学家Haskell Curry之后,闭包的部分应用被称为修正。闭包triple的定义如下:

def triple = &#123;
    y ->
          return 3 * y
&#125;

其中,第一个参数被删除,所有出现第一个参数的地方使用数字3来替代。

加法和惩罚被称为可交换操作。这意味着A+B = B+A and A*B = B*A。但是减法和除法是不可交换的操作。我们可以事件类似于multiply的闭包。

范例:实现可交换操作

def rSubtract = &#123;
    y, x ->
        return x - y
&#125;
def lSubtract = &#123;
    x, y ->
        return x - y
&#125;
def subtract10 = rSubtract.curry(10)
def subractFrom20 = lSubtract.curry(20)

println subtract10(22)
println subractFrom20(14)

关于修正闭包的一个重点是:curry方法的实参数量绝对不能超过该必报所需的实参实际数量。比如该闭包有有三个参数,则调用curry方法时,可以使用零个,一个,两个,或者三个实参。

部分应用可以被认为是一种简化形式,是个复杂任务可以被分解为多个子任务。

组合

结构化顺序的一种组织方式是顺序执行多个任务。通常而言,每部分任务被分别设计和实现。在此,也许认为,一个闭包标识需要执行的某个简单任务。适用组合概念进行任务合并,可以非常容易构造出复杂的任务。并且,借助不同的祝贺方式,可以较快的创建超时所需要的新任务,本文开篇所述案例。

范例:闭包组合

范例演示了composition闭包,这个闭包有两个参数f和g,两个参数代表闭包,把闭包g应用到x比如g(x),把闭包f应用到处理结果比如(fg(x))

def composition = &#123;
    f, g, x ->
        println "f:$&#123;f.class&#125; g: $&#123;g.class&#125; x: $&#123;x.class&#125;"
        return f(g(x))
&#125;

def composition2 = &#123;
    f, g, x ->
        return f(x) * g(x)
&#125;

def multiply = &#123;
    x, y ->
        return x * y
&#125;

def triple = multiply.curry(3)
def quadroup = multiply.curry(4)

def twelveTimes = composition.curry(triple, quadroup)
def multiplyCompo = composition2.curry(triple,quadroup)

println "twelveTimes(12) = $&#123;twelveTimes(12)&#125;" //(3*(4 * 12)) = 144
println "multiplyCompo(12) = $&#123;multiplyCompo(12)&#125;" // 3*12 * 4*12 = 1728

闭包triple有一个参数,并把这个参数与3相乘。闭包twelveTimes是闭包tripe和quadroup的组合。结果是,闭包twelveTimes把参数乘以4,然后把乘法结果与3相乘。

范例:组合与集合

def composition = &#123;
    f, g, x ->
        return f(g(x))
&#125;

def multiply = &#123;
    x, y ->
        return x * y
&#125;

def triple = multiply.curry(3)
def quadroup = multiply.curry(4)

def twelveTimes = composition.curry(triple,quadroup)

def table = [1,2,3,4].collect&#123;
    element ->
        return twelveTimes(element)
&#125;
println "tabl = $table "

计算模式

介绍一种机制,是的闭包包含一种计算模式。

范例是对列表中的每个元素按照某中形式进行转换。这种转换通常被命名为map

范例:映射

//map closure
def map = &#123;
    clos, list ->
        return list.collect(clos)
&#125;

//composition closure
def compositon = &#123;
    f ,g ,x ->
        return f(g(x))
&#125;

def multiply = &#123;
    x , y ->
        return x * y
&#125;

def triple = multiply.curry(3)
def quadroup = multiply.curry(4)

def tripleAll = map.curry(triple)


def table = tripleAll([1,2,3,4])
println table

分析:

//实际的trpleAll将是一个CurryedClosure
//quadroup的结构
quadroup = &#123;
    list -> 
          return list.collect&#123;
              element ->
                  return 3 * element 
          &#125;
&#125;

关于映射的一个等级模式是,如果把闭包(比如f)映射到列表x,然后把闭包(比如g)应用到前一个闭包的处理结果。那么上述过程就等价于把g和f的组合应用到这个列表x。

范例:处理等价关系

//map closure
def map = &#123;
    clos, list ->
        return list.collect(clos)
&#125;

//composition closure
def compositon = &#123;
    f, g, x ->
        return f(g(x))
&#125;

def multiply = &#123;
    x, y ->
        return x * y
&#125;

def triple = multiply.curry(3)
def quadroup = multiply.curry(4)

def composeMapMap = compositon.curry(map.curry(triple), map.curry(quadroup))

def table = composeMapMap([1,2,3,4]) // each element * 4 * 3

println table

业务规则

现在我们考虑如何计算一个Book的净价,在计算过程要考虑到上品折扣和政府税金,比如增值税。

关于业务规则的实际实现

范例:净价计算

class Book &#123;
    def name
    def author
    def price
    def category
&#125;

def bk = new Book(name: 'Groovy', author: 'KenB', price: 25, category: 'CompSci') // 参数列表初始化

def discountRate = 0.1
def taxRate = 0.17

def rMultiply = &#123;
    y , x ->
        return x * y
&#125;

def lMultiply = &#123;
    x , y ->
        return x * y
&#125;

def composition = &#123;
    f,g,x ->
        return f(g(x))
&#125;

def calDiscounteedPrice = rMultiply.curry(1-discountRate)
def calTax = rMultiply.curry(1 + taxRate)
def calcNetPrice = composition.curry(calTax,calDiscounteedPrice)

def netPrice = calcNetPrice(bk.price)

println "netPrice = $&#123;netPrice&#125;"

范例:有上限折扣

class Book &#123;
    def name
    def author
    def price
    def category
&#125;

def bk = new Book(name: 'Groovy', author: 'KenB', price: 25, category: 'CompSci')

def discountRate = 0.1
def taxRate = 0.17
def maxDiscount = 3

def rMultiply = &#123;
    y, x ->
        return x * y
&#125;

def lMultiply = &#123;
    x, y ->
        return x * y
&#125;

def subtract = &#123;
    x, y ->
        return x - y
&#125;

def rSubtract = &#123;
    y, x ->
        return x - y
&#125;

def lSubtract = &#123;
    x, y ->
        return x - y
&#125;

def min = &#123;
    x, y ->
        return x < y ? x : y
&#125;

def id = &#123;
    x ->
        return x
&#125;

def composition = &#123;
    f, g, x ->
        return f(g(x))
&#125;

def bComposition = &#123;
    h, f, g, x ->
        return h(f(x), g(x))
&#125;

def calcDiscount = rMultiply.curry(discountRate)
def calcActuralDiscount = bComposition.curry(min, calcDiscount, id)
def calcDiscountedPrice = bComposition.curry(subtract, id, calcActuralDiscount)
def calcTax = rMultiply.curry(1 + taxRate)
def calcNetPrice = composition.curry(calcTax,calcDiscountedPrice)

println "bk.price : $bk.price"

def netPrice = calcNetPrice(bk.price)

println "netprice = $netPrice"

打包

以上范例开发了大量非常有价值的闭包,可以非常灵活的合并使用。因此,有必要把这些闭包都打包到一个类中去,这样更加容易引入到应用程序。

abstract class Functor {
    public static Closure bAdd = {
        x, y -> return x + y
    }

    public static Closure rAdd = {
        y, x -> return x + y
    }
    
    public static Closure bSubtract = {
        x , y -> return x - y
    }
    //blabla
}

这个类包含了算数的闭包。

闭包与协程

调用一个函数或方法会在程序的执行序列创建一个新的作用域.我们会在一个入口点进入函数.在方法完成之后,回到调用者的作用域.

协程(Coroutine)则支持多个入口点,每个入口点都是上次挂起调用的位置.我们可以进入一个函数,执行部分代码,挂起,在回到调用者的上下文或作用域内执行一些代码.之后我们可以在挂起的地方恢复该函数的执行.

写成对于实现某些特殊的逻辑或算法非常方便,比如用着生产者-消费者问题中.生产者会接受一些输入,对输入做一些厨师处理,通知消费者拿走处理过的值并做进一步计算,并输出或存出结果.消费者处理它那部分工作,完成之后通知生产者以获取更多输入.

在Java中,wait() and notify()与多线程借个使用,可以实现协程.闭包会让人产生”协程是在一个线程中执行”的感觉or错觉.

def iterate(n, closure)&#123;
    1.upto(n)&#123;
        println "In iterate with value $&#123;it&#125;"
        closure(it)
    &#125;
&#125;

println "Calling iterate"
total = 0
iterate(4)&#123;
    total += it
    println "In closure total so far is $&#123;total&#125;"
&#125;
println "Done"
/**
每次调用闭包,我们都会从上一次调用中回复total的值.  //可是我并没有从例子中看出什么是协程????
*/

协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。 协程还是比较复杂,嗯后续在研究研究.

闭包委托

Groovy的闭包支持方法委托,而且提供了方法分派能力.

this,owner and delegate是闭包的三个属性,用于确定有那个对象处理闭包内的方法调用,一般而言,delegate 会被设置为 owner,但是对其进行修改,可以有非常好的元编程能力.

利用闭包委托,可以更好的写出DSL,将在DSL好好的讲述本内容.

题外话

有关函数式编程,可参考阮一峰老师的博客,在这里引用阮一峰老师的博客,特此注明,并非常感谢老师能够出一片教程给大家科普一下。

函数式编程入门教程–阮一峰

ps:自己的一些理解

对于函数式编程中的柯里化,对应高中数学的只是,其实就是函数的一个组合。为了组合函数,需要将多个多参数函数转换成单参数函数。

举个🌰:
$$
z = x + y
$$

$$
z = x * y
$$

如果我想得到一个复合函数使得2->映射到8,计算过程为2 * 3 + 2

那么我怎么得到这个函数呢?定义这个映射为f
$$
f(x) = x * 3 + 2
$$
那么在程序中我们怎么体现呢

//这里的函数都是以闭包形式表示的准确来说这不是一个"方法",而是一个闭包的对象
//首先我们需要一个加法函数,这个加法函数是一个广义的
def add = &#123;
    x, y ->
        return x + y
&#125;
//然后我们需要一个乘法函数,同样是一个广义的
def multiple = &#123;
    x, y ->
        return x * y
&#125;
//然后我们定义一个组合,这个组合是嵌套函数,两个映射f,g和一个自变量x
def compose = &#123;
    f, g, x ->
        return f(g(x))
&#125;

//add.curry(2)  :  y = 2 + z
//multiple.curry(2)  : z = 3 * x 注:这里只是为了方便,变量名并不代表带入关系
//compose.curry(add2,multiple3) = : z = 2 + (3 * x)
def add2 = add.curry(2)
def multiple3 = multiple.curry(3)

def addAndMultiple = compose.curry(add2,multiple3)
println addAndMultiple(2)

Comments

2017-08-22

⬆︎TOP