Functional Programming

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14). Groovy is not a purely functional language, but closures are first-class values and the language and the GDK ship the usual functional toolkit: partial application, memoization, composition, tail-call elimination, method pointers, interoperability with the JDK functional interfaces, immutable data and, new in 6.0, a comprehension macro for monadic code. Closures themselves (syntax, parameters, calling) are on Closures; the higher-order collection methods (collect, findAll, inject) are on Working with Collections. For the same ideas in other languages on this site see Java Functional Programming, Java Lambdas & Method References and Kotlin Lambdas & Higher-order Functions.

Currying

Currying here means partial application: fixing some of a closure’s arguments to get a closure with fewer parameters. Groovy has three methods on Closure, differing in which arguments they fix:

  • curry(a, b, ...) fixes the leading parameters.

  • rcurry(a, b, ...) fixes the trailing parameters.

  • ncurry(index, a, b, ...) fixes parameters starting at a given zero-based index.

def volume = { double rate, int minutes, String plant -> "${plant}: ${rate * minutes} L" }
def at2 = volume.curry(2.0d)
def at2for10 = at2.curry(10)
println at2for10('tomatoes')
println volume.rcurry('basil')(1.5d, 4)
println volume.ncurry(1, 5)(0.5d, 'mint')       // fix the parameter at index 1
println volume.curry(2.0d, 10, 'x').maximumNumberOfParameters
println at2.parameterTypes*.simpleName

def adder = { a, b -> a + b }
println adder.curry(10)(5)
// closures are not auto-curried
try { adder.curry(1)() } catch (e) { println e.class.simpleName }
tomatoes: 20.0 L
basil: 6.0 L
mint: 2.5 L
0
[int, String]
15
MissingMethodException

Notes on the output:

  • A curried closure is a new closure; its parameterTypes list only the parameters that are still open ([int, String] after fixing the first one).

  • Groovy never curries automatically: calling a two-argument closure with one argument is a MissingMethodException, not a partial application. Call curry explicitly.

  • Fixing every parameter gives a closure with no parameters (maximumNumberOfParameters is 0) that still must be called.

Memoization

A pure closure can cache its results by argument list. memoize() returns a closure that computes each distinct argument list once; the cache is unbounded. The bounded variants are memoizeAtMost(max), memoizeAtLeast(protected) and memoizeBetween(protected, max). The first evicts the least recently used entry beyond max; the others keep the protected most recent entries strongly referenced and let the rest be reclaimed by the garbage collector under memory pressure. For methods, the AST transformation @Memoized does the same with protectedCacheSize and maxCacheSize parameters (see AST Transformations: Code Generation & Class Design).

import groovy.transform.Memoized
int calls = 0
def slow = { int n -> calls++; n * n }
def fast = slow.memoize()
[3, 3, 4, 3].each { fast(it) }
println "calls: $calls"
calls = 0
def bounded = slow.memoizeAtMost(2)
[1, 2, 3, 1, 1].each { bounded(it) }
println "memoizeAtMost(2) calls: $calls"
calls = 0
def between = slow.memoizeBetween(1, 2)
[1, 2, 3, 1].each { between(it) }
println "memoizeBetween(1,2) calls: $calls"
calls = 0
def atMost0 = slow.memoizeAtLeast(1)
[5,5,5].each { atMost0(it) }
println "memoizeAtLeast(1) calls: $calls"

class Sensor {
    static int reads = 0
    @Memoized(maxCacheSize = 10)
    double calibrated(int id) { reads++; id * 1.5 }
    @Memoized
    String banner() { reads++; 'sensor' }
}
def s = new Sensor()
3.times { s.calibrated(1) }
s.calibrated(2); s.banner(); s.banner()
println "reads: ${Sensor.reads}"

def fib
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoize()
println fib(80)
calls: 2
memoizeAtMost(2) calls: 4
memoizeBetween(1,2) calls: 4
memoizeAtLeast(1) calls: 1
reads: 3
23416728348467685

The first block counts how often the closure body runs: two for four calls with 3, 3, 4, 3, four when only two entries are kept (the least recently used one is dropped and recomputed). The recursive Fibonacci closure at the end is fast only because the memoized closure itself is what the recursion calls. Memoize only side-effect-free closures, since later calls return the cached value without running the body, and remember that the cache holds its arguments alive.

Composition

The operators >> (right shift) and << (left shift) compose closures into a new closure. f >> g calls f first and passes its result to g, that is g(f(x)); f << g is f(g(x)). Any callable works as an operand, including a method pointer. The composed closure is a new closure; the last line of the example shows that it reports one parameter, like the first closure in the chain.

def celsius = { double f -> (f - 32) / 1.8 }
def round1  = { double d -> Math.round(d * 10) / 10.0 }
def label   = { it -> "${it} C" }

def pipeline = celsius >> round1 >> label        // label(round1(celsius(x)))
println pipeline(98.6d)
def reversed = label << round1 << celsius        // the same chain written right to left
println reversed(98.6d)

def up = String.&toUpperCase
def exclaim = { it + '!' }
def shout = up >> exclaim
println shout('ok')
println pipeline.maximumNumberOfParameters
37 C
37 C
OK!
1

The pipeline reads left to right: Fahrenheit to Celsius, round to one decimal, then format. Both spellings give the same result, so use whichever reads better; >> matches the data-flow direction. The operator-to-method mapping (rightShift, leftShift) is listed on Operator Overloading.

Trampolines and tail recursion

The JVM does not eliminate tail calls, so a deeply recursive closure or method overflows the stack. Groovy offers two remedies.

  • trampoline() turns a closure into one that returns a TrampolineClosure instead of calling itself: the recursive call is written name.trampoline(args) and the trampoline runs the pending calls in a loop.

  • @TailRecursive (from groovy.transform) rewrites a method whose recursive call is in tail position into a loop at compile time. The call must be the last thing the method does; @TailRecursive is described with the other class-design transformations in AST Transformations: Code Generation & Class Design.

import groovy.transform.TailRecursive
def sumTo
sumTo = { long n, long acc = 0 -> n == 0 ? acc : sumTo.trampoline(n - 1, acc + n) }.trampoline()
println sumTo(100000)

def deep
deep = { long n -> n == 0 ? 0L : 1L + deep(n - 1) }
try { deep(200000) } catch (StackOverflowError e) { println 'StackOverflowError' }

class Counter {
    @TailRecursive
    long countdown(long n, long acc = 0) {
        n == 0 ? acc : countdown(n - 1, acc + 1)
    }
    @TailRecursive
    static BigInteger factorial(int n, BigInteger acc = 1G) {
        n <= 1 ? acc : factorial(n - 1, acc * n)
    }
}
println new Counter().countdown(500000)
println Counter.factorial(25)
5000050000
StackOverflowError
500000
15511210043330985984000000

The plain recursive closure overflows the stack at a depth of 200 000, while the trampolined sum to 100 000 and the @TailRecursive methods run in constant stack.

Method pointers and method references

A method pointer obj.&method turns a method into a closure (a MethodClosure) bound to the receiver. Type.&method points to a static method, or, for an instance method, to an unbound method that takes the receiver as its first argument. Since Groovy 3 the Java method reference syntax Type::method and the constructor reference Type::new also work and are what you pass where a java.util.function interface is expected. The .& form picks the overload at run time from the actual argument types, so it can point to a group of overloaded methods; the :: form is closer to Java’s.

The functional interfaces of java.util.function (Function, Predicate, BiFunction, Supplier, UnaryOperator, Consumer, Comparator) interoperate with closures in both directions. A closure is coerced to a functional interface automatically wherever the target type is known (an assignment, a parameter), This is how Groovy code uses Java streams.

import java.util.function.*
import java.util.stream.*

class Zone { String id; double temp }
String describe(Zone z) { "${z.id}=${z.temp}" }
def z = new Zone(id: 'bench', temp: 21.5)

def ptr = this.&describe                 // method pointer to a script method
println ptr(z)
println ptr.class.simpleName
def up = 'hello'.&toUpperCase            // bound to an instance
println up()
def max = Math.&max                      // static method
println max(3, 9)
println([1, 5, 3].collect(Integer.&toBinaryString))
def len = String.&length                 // unbound: the receiver is the first argument
println len('abcd')
println(['a', 'bb', 'ccc'].collect(String::length))   // method reference operator
def create = Zone::new                   // constructor reference
println create().class.simpleName

Function<Integer, Integer> twice = { it * 2 }
Predicate<String> empty = { it.isEmpty() }
BiFunction<Integer, Integer, Integer> add = { a, b -> a + b }
Supplier<String> now = { 'tick' }
UnaryOperator<String> shout = { it.toUpperCase() }
println twice.apply(4)
println empty.negate().test('x')
println add.andThen(twice).apply(1, 2)
println now.get()
println shout.apply('x')
println IntStream.rangeClosed(1, 5).map { it * it }.sum()
println Stream.of('a', 'bb', 'ccc').filter { it.size() > 1 }.map { it.toUpperCase() }.collect(Collectors.toList())
Comparator<Zone> byTemp = { a, b -> a.temp <=> b.temp }
println([new Zone(id: 'b', temp: 30), z].toSorted(byTemp)*.id)
bench=21.5
MethodClosure
HELLO
9
[1, 101, 11]
4
[1, 2, 3]
Zone
8
true
6
tick
X
55
[BB, CCC]
[bench, b]

Method pointers are the groovy.lang.Closure-typed counterpart of Java method references, so they compose (>>) and curry like any closure. See also Object Operators for .& and ::.

Allocation-free GDK overloads in 6.0

Groovy 6.0 adds java.util.function overloads across the GDK (each, collect, findAll, inject and others). A method reference or a lambda passed to one of them is used directly, with no Closure object allocated for every call. The release notes list this under performance and describe it as a change with no change in semantics: the source code stays the same.

import java.util.function.*
def temps = [18.5d, 22.0d, 31.2d, 25.4d]
Predicate<Double> hot = { it > 24 } as Predicate
Function<Double, String> fmt = { String.format(Locale.ROOT, '%.1f C', it) }
println temps.findAll(hot)
println temps.collect(fmt)
println temps.inject(0d, (BiFunction<Double, Double, Double>) { a, b -> a + b })
temps.each((Consumer<Double>) { print "${it} " })
println()
// method reference works as a java.util.function argument
println temps.collect(Double::intValue)
println(['a', 'bb'].collect(String::length))
[31.2, 25.4]
[18.5 C, 22.0 C, 31.2 C, 25.4 C]
97.1
18.5 22.0 31.2 25.4
[18, 22, 31, 25]
[1, 2]

The typed arguments (Predicate, Function, BiFunction, Consumer) are the forms that select the new overloads; plain closures keep using the closure-based ones. The practical effect is in hot loops, where it removes allocation; do not rewrite working code for it without measuring. See Performance, invokedynamic & Native Images.

Immutability

Functional code prefers values that cannot change. Groovy offers three tools:

  • @Immutable (from groovy.transform) generates a class with final properties, a tuple constructor, equals, hashCode and toString, and defensive copies of mutable members such as lists, which it wraps in unmodifiable collections. copyWith = true adds a copyWith method that builds a changed copy.

  • record types are immutable by construction; Groovy’s @RecordOptions(copyWith = true) adds copyWith to a record (see Records & Sealed Types).

  • asImmutable() and asUnmodifiable() give read-only views of collections.

import groovy.transform.*

@Immutable(copyWith = true)
class Reading {
    String sensor
    double temp
    List<String> tags
}
def r = new Reading('s1', 20.5, ['a'])
try { r.temp = 3 } catch (ReadOnlyPropertyException e) { println e.message }
try { r.tags << 'b' } catch (UnsupportedOperationException e) { println 'tags: UnsupportedOperationException' }
def r2 = r.copyWith(temp: 25.0d)
println r2
println r

def list = [1, 2, 3].asImmutable()
try { list << 4 } catch (UnsupportedOperationException e) { println 'list is read-only' }

@RecordOptions(copyWith = true)
record Point(int x, int y) {}
println new Point(1, 2).copyWith(y: 5)
Cannot set read-only property: temp for class: Reading
tags: UnsupportedOperationException
Reading(s1, 25.0, [a])
Reading(s1, 20.5, [a])
list is read-only
Point[x=1, y=5]

@Immutable makes the tags list read-only (the example shows the rejected <<), and assigning a property gives a ReadOnlyPropertyException. The original and the changed copy are independent values. @Immutable is also covered in AST Transformations: Code Generation & Class Design.

Monadic comprehensions with the DO macro

Chains of flatMap over Optional, Stream or CompletableFuture nest quickly. Groovy 6.0 adds the DO macro (incubating, since 6.0, so it may change) to write them flat, in the style of a Scala for-comprehension or Haskell do-notation. It lives in the groovy-macro-library module, which is on the classpath of the groovy command and needs to be a compile-time dependency only in a project. A DO takes name in source generators followed by a body closure, and the body must itself produce a value of the carrier type (there is no implicit lifting). Each generator becomes a nested bind, a name bound by an earlier generator is visible to later ones, and an empty carrier short-circuits the rest.

def result = DO(a in Optional.of(2), b in Optional.of(3)) { Optional.of(a + b) }
println result
def chained = DO(a in Optional.of(10), b in Optional.of(a * 2)) { Optional.of(a + b) }
println chained
def none = DO(a in Optional.empty(), b in Optional.of(3)) { Optional.of(b) }
println none
import java.util.stream.Stream
def pairs = DO(x in Stream.of(1, 2), y in Stream.of('a', 'b')) { Stream.of("$x$y".toString()) }
println pairs.toList()
Optional[5]
Optional[30]
Optional.empty
[1a, 1b, 2a, 2b]

A realistic use is combining lookups that may miss. Here a plant and a reading are both optional; a missing reading produces an empty result without any null checks. The same shape works for CompletableFuture, which composes two asynchronous values without blocking on each.

import groovy.transform.Field
import groovy.transform.CompileStatic
import java.util.concurrent.CompletableFuture

record Plant(String name, double minTemp) {}
@Field Map<String, Plant> plants = [t: new Plant('tomato', 12.0d), b: new Plant('basil', 10.0d)]
@Field Map<String, Double> readings = [t: 18.5d]
Optional<Plant> findPlant(String id) { Optional.ofNullable(plants[id]) }
Optional<Double> reading(String id) { Optional.ofNullable(readings[id]) }

def ok = DO(p in findPlant('t'), r in reading('t')) { Optional.of("${p.name()}: ${r - p.minTemp()} above minimum") }
println ok
def missing = DO(p in findPlant('b'), r in reading('b')) { Optional.of("${p.name()}: ${r - p.minTemp()}") }
println missing
def f = DO(a in CompletableFuture.supplyAsync { 20 }, b in CompletableFuture.supplyAsync { 22 }) { CompletableFuture.completedFuture(a + b) }
println f.get()

@CompileStatic(extensions = 'groovy.typecheckers.MonadicChecker')
class Calc {
    static int sum() { DO(a in Optional.of(2), b in Optional.of(3)) { Optional.of(a + b) }.get() }
}
println Calc.sum()
Optional[tomato: 6.5 above minimum]
Optional.empty
42
5

Which types can be a carrier:

  • the types on the standard allow-list: Optional, Stream, CompletionStage (so CompletableFuture) and groovy.concurrent.Awaitable (so DataflowVariable);

  • any type with a one-argument flatMap (and map);

  • a class annotated @groovy.transform.Monadic, naming its own bind and map methods.

The macro does not enforce the monad laws; a user-defined carrier is responsible for them. Under @CompileStatic or @TypeChecked use the extension groovy.typecheckers.MonadicChecker, as in the Calc class above: without it the type of each bound name and of the whole expression falls back to Object. The companion checker MonadicShapeChecker lints hand-written flatMap chains. Both are described on Built-in Type Checkers. Use DO when two or more dependent flatMap steps would nest; for a single map or flatMap the plain method is shorter.

Where the book is outdated

The Manning book Groovy in Action (publisher page in the references) teaches curry and closure composition, and those descriptions still hold. What to check against the current documentation instead of the book:

  • rcurry and ncurry, memoization (memoize and @Memoized), trampoline and @TailRecursive, all added after the book was written.

  • Method references with :: and the java.util.function overloads, which need a JDK with lambdas; the book predates both.

  • @Immutable and records, and the 6.0 DO macro: the book has nothing on either.

For where these features appeared see What’s New & Migrating from Older Versions.