Closures have a few more powers that turn them from “anonymous functions” into a tool for designing APIs. These are what make Gradle build files, Jenkins pipelines and Groovy builders look like configuration rather than code.
this, owner and delegate
Inside a closure, an unqualified name (a method call or property) is looked up through three objects:
- this — the class where the closure was defined.
- owner — the enclosing object: usually the same as
this, or the outer closure if the closure is nested. - delegate — an object you can set. By default it’s the owner.
class Report {
String title = 'Daily report'
def build() {
def c = { "title from owner: $title" } // finds title on the owner
c()
}
}
Delegates power DSLs
Change the delegate, and calls inside the closure go to an object of your choice:
class Email {
String to, subject, body
def to(String v) { to = v }
def subject(String v) { subject = v }
def body(String v) { body = v }
}
def email(Closure spec) {
def e = new Email()
spec.delegate = e
spec.resolveStrategy = Closure.DELEGATE_FIRST // look on the delegate first
spec()
e
}
def mail = email {
to 'ops@example.test'
subject 'Integration failed'
body '3 records rejected'
}
That block reads like configuration, but each line is a method call on Email. This is exactly how dependencies { … } in Gradle and pipeline { … } in Jenkins work.
Resolve strategies: OWNER_FIRST (default), DELEGATE_FIRST, OWNER_ONLY, DELEGATE_ONLY.
Currying: fix some arguments now
def price = { taxRate, amount -> amount * (1 + taxRate) }
def withUkVat = price.curry(0.20) // fix the first argument
def withIndiaGst = price.curry(0.18)
withUkVat(100) // 120.00
def subtract = { a, b -> a - b }
subtract.rcurry(1)(10) // fix the LAST argument: 9
ncurry(index, value) fixes an argument at any position. Currying is a neat way to create specialised functions from general ones — one per country, partner or environment.
Composition: build pipelines
def trim = { it.trim() }
def upper = { it.toUpperCase() }
def tag = { "<$it>" }
def clean = trim >> upper >> tag // left to right
clean(' po-4471 ') // <PO-4471>
(tag << upper)('acme') // right to left: <ACME>
Small closures, each doing one thing, snapped together — the same idea as Java’s Function.andThen (see Functional interfaces).
memoize: cache results
def slowLookup = { String sku -> sleep 50; sku.reverse() }.memoize()
3.times { slowLookup('AX-200') } // the slow work runs once
Memoized closures remember results per argument. Variants: memoizeAtMost(n) (LRU cache), memoizeAtLeast(n), memoizeBetween(min, max). Only memoize pure closures — same input, same output, no side effects.
trampoline: deep recursion safely
def sumTo
sumTo = { long n, long acc = 0 ->
n == 0 ? acc : sumTo.trampoline(n - 1, acc + n)
}.trampoline()
sumTo(100_000) // 5000050000 — no StackOverflowError
A trampolined closure turns recursion into a loop behind the scenes.
Handy helpers: with and tap
def order = [id: 'PO-1', lines: []].tap { it.lines << 'AX-200' } // configure, return the object
'boomi'.with { it.capitalize() + ' rocks' } // compute from the object
The full example
// closures_advanced.groovy — owner, delegate, currying, composition, memoize, trampoline.
// 1. this, owner and delegate
class Report {
String title = 'Daily report'
def build() {
def c = { "title from owner: $title" } // resolves to the enclosing object
c()
}
}
println new Report().build()
// 2. Delegates power Groovy DSLs (Gradle, Jenkins pipelines, builders)
class Email {
String to, subject, body
def to(String v) { to = v }
def subject(String v) { subject = v }
def body(String v) { body = v }
}
def email(Closure spec) {
def e = new Email()
spec.delegate = e // calls inside the closure go to e
spec.resolveStrategy = Closure.DELEGATE_FIRST
spec()
e
}
def mail = email {
to 'ops@example.test'
subject 'Integration failed'
body '3 records rejected'
}
println "${mail.to} | ${mail.subject} | ${mail.body}"
// 3. Currying: fix some arguments now, the rest later
def price = { taxRate, amount -> amount * (1 + taxRate) }
def withUkVat = price.curry(0.20) // taxRate fixed
def withIndiaGst = price.curry(0.18)
println withUkVat(100) // 120.00
println withIndiaGst(100) // 118.00
def subtract = { a, b -> a - b }
println subtract.rcurry(1)(10) // fixes the LAST argument: 9
// 4. Composition: chain closures with >> and <<
def trim = { it.trim() }
def upper = { it.toUpperCase() }
def tag = { "<$it>" }
def clean = trim >> upper >> tag // left to right
println clean(' po-4471 ') // <PO-4471>
println((tag << upper)('acme')) // right to left: <ACME>
// 5. Memoize: cache results of an expensive closure
def calls = 0
def slowLookup = { String sku -> calls++; sleep 50; sku.reverse() }.memoize()
3.times { slowLookup('AX-200') }
println "lookup ran $calls time(s)" // 1
// 6. Trampoline: deep recursion without StackOverflowError
def sumTo
sumTo = { long n, long acc = 0 -> n == 0 ? acc : sumTo.trampoline(n - 1, acc + n) }.trampoline()
println sumTo(100_000) // 5000050000
// 7. Closure parameters and the 'with' / 'tap' helpers
def order = [id: 'PO-1', lines: []].tap { it.lines << 'AX-200' }
println order
println 'boomi'.with { it.capitalize() + ' rocks' }
println "maximumNumberOfParameters: ${price.maximumNumberOfParameters}"
Try it yourself
Create a closure discount = { percent, amount -> … }, then use curry to make blackFriday (30% off) and staff (10% off), and compose staff >> vat where vat adds 20%.
Show a solution
def discount = { percent, amount -> amount * (1 - percent / 100) }
def blackFriday = discount.curry(30)
def staff = discount.curry(10)
def vat = { it * 1.2 }
println blackFriday(100) // 70.0
println((staff >> vat)(100)) // 108.00
Key takeaways
- Closures resolve names through this, owner and delegate; setting the delegate creates DSLs.
curry/rcurry/ncurryspecialise closures;>>and<<compose them.memoizecaches pure functions;trampolinemakes deep recursion safe.tapandwithmake object setup and transformation concise.