What you'll learn

  • this, owner and delegate, and resolve strategies
  • Building a tiny DSL with a delegate
  • curry, rcurry and ncurry
  • Composing closures with >> and <<
  • memoize and trampoline

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/ncurry specialise closures; >> and << compose them.
  • memoize caches pure functions; trampoline makes deep recursion safe.
  • tap and with make object setup and transformation concise.

Example files