闭包
本章将介绍 Groovy 闭包。Groovy 中的闭包是一个开放的、匿名的代码块,它可以接收参数、返回值并赋值给变量。闭包可以引用其周围作用域中声明的变量。与闭包的形式化定义相反,Groovy 语言中的 Closure
也可以包含在其周围作用域之外定义的自由变量。虽然这打破了闭包的形式化概念,但它提供了本章中描述的各种优点。
1. 语法
1.1. 定义闭包
闭包定义遵循以下语法
{ [closureParameters -> ] statements }
其中 [closureParameters->]
是一个可选的逗号分隔参数列表,而 statements 是 0 或多个 Groovy 语句。参数看起来类似于方法参数列表,这些参数可以有类型,也可以无类型。
当指定参数列表时,->
字符是必需的,它用于将参数与闭包体分开。statements 部分由 0、1 或多个 Groovy 语句组成。
有效闭包定义的一些示例
{ item++ } (1)
{ -> item++ } (2)
{ println it } (3)
{ it -> println it } (4)
{ name -> println name } (5)
{ String x, int y -> (6)
println "hey ${x} the value is ${y}"
}
{ reader -> (7)
def line = reader.readLine()
line.trim()
}
1 | 一个引用名为 item 变量的闭包 |
2 | 可以通过添加箭头(-> )将闭包参数与代码显式分开 |
3 | 一个使用隐式参数(it )的闭包 |
4 | it 为显式参数的替代版本 |
5 | 在这种情况下,通常最好为参数使用一个显式名称 |
6 | 一个接受两个带类型参数的闭包 |
7 | 一个闭包可以包含多个语句 |
1.2. 作为对象的闭包
闭包是 groovy.lang.Closure
类的实例,使其可以像任何其他变量一样赋值给变量或字段,尽管它是一个代码块
def listener = { e -> println "Clicked on $e.source" } (1)
assert listener instanceof Closure
Closure callback = { println 'Done!' } (2)
Closure<Boolean> isTextFile = {
File it -> it.name.endsWith('.txt') (3)
}
1 | 您可以将闭包赋值给变量,并且它是 groovy.lang.Closure 的实例 |
2 | 如果不使用 def 或 var ,则使用 groovy.lang.Closure 作为类型 |
3 | 您可以选择使用 groovy.lang.Closure 的泛型类型来指定闭包的返回类型 |
1.3. 调用闭包
闭包作为匿名代码块,可以像任何其他方法一样被调用。如果您定义了一个不带参数的闭包,如下所示
def code = { 123 }
那么闭包内的代码只有在您 调用 闭包时才会执行,这可以通过像常规方法一样使用变量来完成
assert code() == 123
或者,您可以显式地使用 call
方法
assert code.call() == 123
如果闭包接受参数,原理是相同的
def isOdd = { int i -> i%2 != 0 } (1)
assert isOdd(3) == true (2)
assert isOdd.call(2) == false (3)
def isEven = { it%2 == 0 } (4)
assert isEven(3) == false (5)
assert isEven.call(2) == true (6)
1 | 定义一个接受 int 作为参数的闭包 |
2 | 它可以直接调用 |
3 | 或使用 call 方法 |
4 | 对于带隐式参数(it )的闭包也是如此 |
5 | 可以直接使用 (arg) 调用 |
6 | 或使用 call |
与方法不同,闭包在调用时总是返回一个值。下一节将讨论如何声明闭包参数,何时使用它们以及什么是隐式“it”参数。
2. 参数
2.1. 普通参数
闭包的参数遵循与常规方法参数相同的原则
-
可选类型
-
一个名称
-
可选的默认值
参数用逗号分隔
def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'
def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3
def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3
def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3
def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3
2.2. 隐式参数
当一个闭包没有明确定义参数列表(使用 ->
)时,闭包总是定义一个隐式参数,名为 it
。这意味着这段代码
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
严格等同于这段代码
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
如果您想声明一个不接受任何参数且必须限制为无参数调用的闭包,那么您必须使用显式的空参数列表来声明它
def magicNumber = { -> 42 }
// this call will fail because the closure doesn't accept any argument
magicNumber(11)
2.3. 可变参数
闭包可以像任何其他方法一样声明可变参数。可变参数方法是指如果最后一个参数是可变长度(或数组),则可以接受可变数量参数的方法,如下面的示例所示
def concat1 = { String... args -> args.join('') } (1)
assert concat1('abc','def') == 'abcdef' (2)
def concat2 = { String[] args -> args.join('') } (3)
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args -> (4)
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
1 | 一个接受可变数量字符串作为第一个参数的闭包 |
2 | 它可以不带显式数组包装地使用任意数量的参数调用 |
3 | 如果将 args 参数声明为数组,则直接提供相同的行为 |
4 | 只要最后一个参数是数组或显式可变参数类型 |
3. 委托策略
3.1. Groovy 闭包 vs lambda 表达式
Groovy 将闭包定义为Closure 类的实例。这使得它与Java 8 中的 lambda 表达式非常不同。委托是 Groovy 闭包中的一个关键概念,它在 lambda 中没有等效概念。改变闭包的委托或委托策略的能力使得在 Groovy 中设计漂亮的领域特定语言 (DSL) 成为可能。
3.2. Owner, delegate 和 this
为了理解委托的概念,我们首先必须解释闭包中 this
的含义。闭包实际上定义了 3 个不同的东西
-
this
对应于闭包定义的封闭类 -
owner
对应于闭包定义的封闭对象,它可以是类或闭包 -
delegate
对应于一个第三方对象,当消息的接收者未定义时,将在该对象上解析方法调用或属性
3.2.1. this 的含义
在闭包中,调用 getThisObject
将返回闭包定义所在的封闭类。它等同于使用显式的 this
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } (1)
assert whatIsThisObject() == this (2)
def whatIsThis = { this } (3)
assert whatIsThis() == this (4)
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } (5)
}
void run() {
def inner = new Inner()
assert inner.cl() == inner (6)
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } (7)
cl()
}
assert nestedClosures() == this (8)
}
}
1 | 一个闭包在 Enclosing 类中定义,并返回 getThisObject |
2 | 调用闭包将返回定义闭包的 Enclosing 实例 |
3 | 通常,您只想使用快捷方式 this 符号 |
4 | 并且它返回完全相同的对象 |
5 | 如果闭包定义在内部类中 |
6 | 闭包中的 this 将返回内部类,而不是顶级类 |
7 | 在嵌套闭包的情况下,例如这里 cl 在 nestedClosures 的作用域内定义 |
8 | 那么 this 对应于最近的外部类,而不是封闭闭包! |
当然,可以通过这种方式调用封闭类中的方法
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString() (1)
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
1 | 闭包在 this 上调用 toString ,这实际上会调用封闭对象上的 toString 方法,也就是说 Person 实例 |
3.2.2. 闭包的 Owner
闭包的 owner 与闭包中 this 的定义非常相似,但有一个细微的区别:它将返回直接的封闭对象,无论是闭包还是类
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() } (1)
assert whatIsOwnerMethod() == this (2)
def whatIsOwner = { owner } (3)
assert whatIsOwner() == this (4)
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } (5)
}
void run() {
def inner = new Inner()
assert inner.cl() == inner (6)
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner } (7)
cl()
}
assert nestedClosures() == nestedClosures (8)
}
}
1 | 一个闭包在 Enclosing 类中定义,并返回 getOwner |
2 | 调用闭包将返回定义闭包的 Enclosing 实例 |
3 | 通常,您只想使用快捷方式 owner 符号 |
4 | 并且它返回完全相同的对象 |
5 | 如果闭包定义在内部类中 |
6 | 闭包中的 owner 将返回内部类,而不是顶级类 |
7 | 但在嵌套闭包的情况下,例如这里 cl 在 nestedClosures 的作用域内定义 |
8 | 那么 owner 对应于封闭闭包,因此与 this 是不同的对象! |
3.2.3. 闭包的 Delegate
可以通过使用 delegate
属性或调用 getDelegate
方法来访问闭包的 delegate。它是 Groovy 中构建领域特定语言的强大概念。虽然this和owner引用闭包的词法作用域,但 delegate 是一个用户定义的对象,闭包将使用它。默认情况下,delegate 设置为 owner
class Enclosing {
void run() {
def cl = { getDelegate() } (1)
def cl2 = { delegate } (2)
assert cl() == cl2() (3)
assert cl() == this (4)
def enclosed = {
{ -> delegate }.call() (5)
}
assert enclosed() == enclosed (6)
}
}
1 | 您可以调用 getDelegate 方法获取闭包的 delegate |
2 | 或使用 delegate 属性 |
3 | 两者返回相同的对象 |
4 | 它是封闭类或闭包 |
5 | 特别是在嵌套闭包的情况下 |
6 | delegate 将对应于 owner |
闭包的 delegate 可以更改为任何对象。让我们通过创建两个类来演示这一点,这两个类不是彼此的子类,但都定义了一个名为 name
的属性
class Person {
String name
}
class Thing {
String name
}
def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')
然后我们定义一个从 delegate 获取 name
属性的闭包
def upperCasedName = { delegate.name.toUpperCase() }
然后通过更改闭包的 delegate,您可以看到目标对象会改变
upperCasedName.delegate = p
assert upperCasedName() == 'NORMAN'
upperCasedName.delegate = t
assert upperCasedName() == 'TEAPOT'
此时,行为与在闭包的词法作用域中定义 target
变量没有什么不同
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
然而,存在重大差异
-
在最后一个例子中,target 是闭包内部引用的局部变量
-
delegate 可以透明地使用,也就是说,不需要像下一段解释的那样,在方法调用前加上
delegate.
。
3.2.4. 委托策略
在闭包中,当访问属性时没有明确设置接收者对象,则会涉及委托策略
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } (1)
cl.delegate = p (2)
assert cl() == 'IGOR' (3)
1 | name 没有引用闭包词法作用域中的变量 |
2 | 我们可以将闭包的 delegate 更改为 Person 的实例 |
3 | 方法调用将成功 |
这段代码之所以有效,是因为 name
属性将在 delegate
对象上透明地解析!这是一种在闭包内部解析属性或方法调用的非常强大的方式。无需设置显式 delegate.
接收者:调用将发生,因为闭包的默认委托策略使其如此。闭包实际上定义了您可以选择的多种解析策略
-
Closure.OWNER_FIRST
是默认策略。如果属性/方法存在于 owner 上,则将在 owner 上调用它。如果不存在,则使用 delegate。 -
Closure.DELEGATE_FIRST
反转了逻辑:首先使用delegate,然后使用owner -
Closure.OWNER_ONLY
将仅在 owner 上解析属性/方法查找:delegate 将被忽略。 -
Closure.DELEGATE_ONLY
将仅在 delegate 上解析属性/方法查找:owner 将被忽略。 -
Closure.TO_SELF
可供需要高级元编程技术并希望实现自定义解析策略的开发人员使用:解析将不会在 owner 或 delegate 上进行,而只在闭包类本身上进行。只有当您实现自己的Closure
子类时,使用它才有意义。
让我们用这段代码说明默认的“owner first”策略
class Person {
String name
def pretty = { "My name is $name" } (1)
String toString() {
pretty()
}
}
class Thing {
String name (2)
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')
assert p.toString() == 'My name is Sarah' (3)
p.pretty.delegate = t (4)
assert p.toString() == 'My name is Sarah' (5)
1 | 为了说明,我们定义了一个引用“name”的闭包成员 |
2 | Person 和 Thing 类都定义了一个 name 属性 |
3 | 使用默认策略,name 属性首先在 owner 上解析 |
4 | 所以如果我们将 delegate 更改为 Thing 的实例 t |
5 | 结果没有变化:name 首先在闭包的 owner 上解析 |
但是,可以更改闭包的解析策略
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
通过更改 resolveStrategy
,我们正在修改 Groovy 解析“隐式 this”引用的方式:在这种情况下,name
将首先在 delegate 中查找,如果未找到,则在 owner 中查找。由于 name
在 delegate(一个 Thing
的实例)中定义,因此使用此值。
如果其中一个 delegate(或 owner)不具有此类方法或属性,则可以说明“delegate first”与“delegate only”或“owner first”与“owner only”之间的区别
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.delegate = p
assert cl() == 42 (1)
cl.delegate = t
assert cl() == 42 (1)
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42 (2)
cl.delegate = t
try {
cl() (3)
assert false
} catch (MissingPropertyException ex) {
// "age" is not defined on the delegate
}
1 | 对于“owner first”,delegate 是什么并不重要 |
2 | 对于“delegate only”,将 p 作为 delegate 成功 |
3 | 对于“delegate only”,将 t 作为 delegate 失败 |
在这个例子中,我们定义了两个类,它们都具有 name
属性,但只有 Person
类声明了 age
。Person
类还声明了一个引用 age
的闭包。我们可以将默认的解析策略从“owner first”更改为“delegate only”。由于闭包的 owner 是 Person
类,那么我们可以检查如果 delegate 是 Person
的实例,调用闭包会成功,但如果我们将它与 Thing
的实例作为 delegate 调用,它将以 groovy.lang.MissingPropertyException
失败。尽管闭包定义在 Person
类内部,但 owner 未被使用。
关于如何使用此功能开发 DSL 的全面解释可以在手册的专门部分中找到。 |
3.2.5. 元编程场景下的委托策略
在描述“owner first”委托策略时,我们谈到如果属性/方法在 owner 中“存在”则使用 owner 的属性/方法,否则使用 delegate 的相应属性/方法。对于“delegate first”也是类似的情况,只是顺序相反。与其使用“存在”这个词,不如使用“处理”这个词更准确。这意味着对于“owner first”,如果属性/方法存在于 owner 中,或者它有 propertyMissing/methodMissing 钩子,那么 owner 将处理成员访问。
我们可以通过稍微修改之前的示例来看到这一点
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
def propertyMissing(String name) { -1 }
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == -1
在此示例中,即使我们的 Thing
类实例(cl
最后使用的委托)没有 age
属性,它通过其 propertyMissing
钩子处理缺失属性的事实意味着 age
将是 -1
。
4. GString 中的闭包
请看下面的代码
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
代码按预期运行,但如果您添加
x = 2
assert gs == 'x = 2'
您会发现断言失败!这有两个原因
-
GString 只会惰性地评估值的
toString
表示 -
GString 中的语法
${x}
不代表一个闭包,而是一个表达式$x
,在 GString 创建时进行评估。
在我们的例子中,GString 是用一个引用 x
的表达式创建的。当 GString 被创建时,x
的值是 1,所以 GString 被创建时值为 1。当断言被触发时,GString 被评估,1 使用 toString
转换为 String
。当我们把 x
改为 2 时,我们确实改变了 x
的值,但这是一个不同的对象,GString 仍然引用旧的那个。
GString 只有在它引用的值发生变化时才会改变其 toString 表示。如果引用本身改变,则什么也不会发生。 |
如果您需要在 GString 中使用真正的闭包,例如强制变量的惰性求值,您需要使用替代语法 ${-> x}
,如下面修正后的示例所示
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
让我们用这段代码说明它与突变有何不同
class Person {
String name
String toString() { name } (1)
}
def sam = new Person(name:'Sam') (2)
def lucy = new Person(name:'Lucy') (3)
def p = sam (4)
def gs = "Name: ${p}" (5)
assert gs == 'Name: Sam' (6)
p = lucy (7)
assert gs == 'Name: Sam' (8)
sam.name = 'Lucy' (9)
assert gs == 'Name: Lucy' (10)
1 | Person 类有一个返回 name 属性的 toString 方法 |
2 | 我们创建第一个名为 Sam 的 Person |
3 | 我们创建另一个名为 Lucy 的 Person |
4 | 变量 p 设置为 Sam |
5 | 并创建了一个闭包,引用 p 的值,即 Sam |
6 | 所以当我们评估字符串时,它返回 Sam |
7 | 如果我们将 p 更改为 Lucy |
8 | 字符串仍然评估为 Sam,因为它是创建 GString 时 p 的值 |
9 | 所以如果我们修改 Sam 将名称更改为 Lucy |
10 | 这次 GString 被正确地修改了 |
因此,如果您不想依赖于变异对象或包装对象,您必须通过显式声明空参数列表来在 GString
中使用闭包
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
// Create a GString with lazy evaluation of "p"
def gs = "Name: ${-> p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Lucy'
6. 函数式编程
闭包,就像Java 8 中的 lambda 表达式一样,是 Groovy 中函数式编程范式的核心。一些函数上的函数式编程操作直接在 Closure
类上可用,如本节所示。
6.1. 柯里化
在 Groovy 中,柯里化指的是部分应用的概念。由于 Groovy 对闭包应用了不同的作用域规则,它不对应于函数式编程中柯里化的真实概念。Groovy 中的柯里化将允许您设置闭包的一个参数的值,并且它将返回一个接受少一个参数的新闭包。
6.1.1. 左柯里化
左柯里化是指设置闭包最左边的参数,如下面的例子所示
def nCopies = { int n, String str -> str*n } (1)
def twice = nCopies.curry(2) (2)
assert twice('bla') == 'blabla' (3)
assert twice('bla') == nCopies(2, 'bla') (4)
1 | nCopies 闭包定义了两个参数 |
2 | curry 将第一个参数设置为 2 ,创建一个接受单个 String 的新闭包(函数) |
3 | 所以新函数只能用 String 调用 |
4 | 它等同于用两个参数调用 nCopies |
6.1.2. 右柯里化
与左柯里化类似,也可以设置闭包最右边的参数
def nCopies = { int n, String str -> str*n } (1)
def blah = nCopies.rcurry('bla') (2)
assert blah(2) == 'blabla' (3)
assert blah(2) == nCopies(2, 'bla') (4)
1 | nCopies 闭包定义了两个参数 |
2 | rcurry 将最后一个参数设置为 bla ,创建一个接受单个 int 的新闭包(函数) |
3 | 因此,新函数只能用一个 int 调用 |
4 | 它等同于用两个参数调用 nCopies |
6.1.3. 基于索引的柯里化
如果闭包接受多于 2 个参数,则可以使用 ncurry
设置任意参数
def volume = { double l, double w, double h -> l*w*h } (1)
def fixedWidthVolume = volume.ncurry(1, 2d) (2)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d) (3)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) (4)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d) (5)
1 | volume 函数定义了 3 个参数 |
2 | ncurry 将第二个参数(索引 = 1)设置为 2d ,创建了一个接受长度和高度的新体积函数 |
3 | 该函数等同于调用 volume 时省略宽度 |
4 | 也可以设置多个参数,从指定索引开始 |
5 | 结果函数接受的参数数量等于初始函数减去 ncurry 设置的参数数量 |
6.2. 记忆化
记忆化允许缓存闭包调用的结果。如果函数(闭包)的计算很慢,但您知道该函数将经常使用相同的参数调用,那么它会很有趣。一个典型的例子是斐波那契数列。一个天真的实现可能看起来像这样
def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // slow!
这是一种天真的实现,因为 'fib' 经常递归调用相同的参数,导致指数级算法
-
计算
fib(15)
需要fib(14)
和fib(13)
的结果 -
计算
fib(14)
需要fib(13)
和fib(12)
的结果
由于调用是递归的,您已经可以看到我们将一遍又一遍地计算相同的值,尽管它们可以被缓存。这种天真的实现可以通过使用 memoize
缓存调用结果来“修复”
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize()
assert fib(25) == 75025 // fast!
缓存使用参数的实际值。这意味着如果您将记忆化用于除基本类型或包装基本类型之外的任何其他类型,则应非常小心。 |
可以通过替代方法调整缓存的行为
-
memoizeAtMost
将生成一个新闭包,它最多缓存 n 个值 -
memoizeAtLeast
将生成一个新闭包,它至少缓存 n 个值 -
memoizeBetween
将生成一个新的闭包,它至少缓存 n 个值,并且最多缓存 n 个值
所有 memoize 变体中使用的缓存都是 LRU 缓存。
6.3. 组合
闭包组合对应于函数组合的概念,也就是说通过组合两个或更多函数(链式调用)来创建新函数,如下例所示
def plus2 = { it + 2 }
def times3 = { it * 3 }
def times3plus2 = plus2 << times3
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))
def plus2times3 = times3 << plus2
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))
// reverse composition
assert times3plus2(3) == (times3 >> plus2)(3)
6.4. 蹦床
递归算法常常受到物理限制:最大栈高度。例如,如果您调用的方法递归调用自身过深,您最终会收到 StackOverflowException
。
在这种情况下,一种有用的方法是使用 Closure
及其蹦床功能。
闭包被包装在 TrampolineClosure
中。调用时,一个蹦床化的 Closure
将调用原始 Closure
并等待其结果。如果调用的结果是 TrampolineClosure
的另一个实例(可能是调用 trampoline()
方法的结果),则 Closure
将再次被调用。这种对返回的蹦床化 Closure
实例的重复调用将持续进行,直到返回一个非蹦床化 Closure
的值。该值将成为蹦床的最终结果。通过这种方式,调用是串行进行的,而不是填满堆栈。
以下是使用 trampoline()
实现阶乘函数的示例
def factorial
factorial = { int n, def accu = 1G ->
if (n < 2) return accu
factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()
assert factorial(1) == 1
assert factorial(3) == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits