Skip to content
AgileLogics
7 min read

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