Closures in Depth
Prerequisites: Closures
Level 1 used closures as short blocks passed to methods. This lesson covers the features that make them the foundation of Groovy's DSLs.
Currying
Currying fixes some arguments of a closure and returns a new closure that takes the rest:
def volume = { length, width, height -> length * width * height }
def flatBox = volume.rcurry(1) // fix the last argument
def square10 = volume.curry(10, 10) // fix the first two
def midWidth = volume.ncurry(1, 5) // fix argument number 1 (0-based)
println flatBox(3, 4)
println square10(2)
println midWidth(2, 3)
12
200
30
Composition
def trim = { String s -> s.trim() }
def upper = { String s -> s.toUpperCase() }
def exclaim = { String s -> s + '!' }
def shout = trim >> upper >> exclaim // left to right
def shout2 = exclaim << upper << trim // right to left: same result
println shout(' hello ')
println shout2(' groovy ')
HELLO!
GROOVY!
f >> g means "call f, then pass the result to g".
Memoization
def calls = 0
def slowSquare = { n -> calls++; n * n }.memoize()
println slowSquare(9)
println slowSquare(9)
println slowSquare(4)
println "computed $calls times"
def fib
fib = { BigInteger n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoize()
println fib(100)
81
81
16
computed 2 times
354224848179261915075
memoize() caches results by argument, so repeated calls are instant.
Use memoizeAtMost(n) to limit the cache size.
Method pointers
def greet(String name) { "Hello, $name" }
def greeter = this.&greet // a method as a closure
println greeter('Ada')
println(['Ada', 'Linus'].collect(this.&greet))
def upper = String.&toUpperCase
println upper('groovy')
Hello, Ada
[Hello, Ada, Hello, Linus]
GROOVY
object.&method turns a method into a closure, so it can go anywhere a
closure can. (String::toUpperCase from Level 1 is the Java-style
equivalent.)
this, owner, and delegate
Every closure has three objects it can resolve names against:
- this — the enclosing class (or script).
- owner — the enclosing object, which may be another closure.
- delegate — an object you can set; by default it's the owner.
class Email {
String to, subject
void to(String value) { to = value }
void subject(String value) { subject = value }
String toString() { "To: $to | Subject: $subject" }
}
def email(Closure spec) {
def mail = new Email()
spec.delegate = mail
spec.resolveStrategy = Closure.DELEGATE_FIRST
spec()
mail
}
println email {
to 'ada@example.com'
subject 'Build finished'
}
To: ada@example.com | Subject: Build finished
Inside the closure, to '...' has no object in front of it. Because the
delegate is an Email and the strategy is DELEGATE_FIRST, Groovy
calls mail.to(...). This one trick is how Gradle's dependencies { },
Jenkins' pipeline { }, and every Groovy builder work.
Trampolines
def sum
sum = { n, acc = 0G -> n == 0 ? acc : sum.trampoline(n - 1, acc + n) }.trampoline()
println sum(100_000)
5000050000
Deep recursion normally overflows the JVM stack. trampoline() turns
tail calls into a loop, so this recursion 100,000 levels deep is fine.
Practice
Write a closure applyTwice that takes a closure f and a value x
and returns f(f(x)). Use it with a closure that adds 3, starting from
10.
Show solution
def applyTwice = { Closure f, x -> f(f(x)) }
def addThree = { it + 3 }
println applyTwice(addThree, 10)
println applyTwice(addThree >> { it * 2 }, 1)
16
22