Intermediate

Functions in Groovy

Learn how to define and call functions in Groovy - parameters, default values, return values, recursion, closures, and higher-order functions with Docker-ready examples

Introduction

Functions are the building blocks of reusable code. They let you name a piece of behavior, pass data into it, and get a result back—turning long scripts into small, composable pieces. In Groovy, functions are remarkably flexible: you can write them with or without type annotations, omit the return keyword, give parameters default values, and treat blocks of code as values you can pass around.

Because Groovy is a multi-paradigm language—object-oriented, functional, imperative, and scripting all at once—it offers two closely related concepts: methods (defined with def or a return type) and closures (anonymous functions assigned to variables). Methods feel like the functions you know from Java or Python, while closures bring first-class functions, capturing their surrounding environment and enabling functional-style programming.

In this tutorial you’ll learn how to define and call methods, work with parameters and return values, supply default arguments, write recursive functions, understand variable scope, and harness closures as higher-order functions. Every example is a complete, runnable Groovy script you can execute with Docker.

Defining and Calling Methods

A method in a Groovy script is declared with either an explicit return type or the def keyword for dynamic typing. The return statement is optional—if you omit it, the value of the last expression is returned automatically.

Create a file named functions.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Method with an explicit return type and parameter type
int square(int n) {
    return n * n
}

// Dynamic return type using def; the last expression is returned
def greet(name) {
    "Hello, ${name}!"
}

// A method with no parameters
def currentYear() {
    2026
}

println square(5)
println greet("Groovy")
println currentYear()

Here square uses static types (int) for clarity and tooling support, while greet uses def and relies on Groovy’s implicit return—the GString "Hello, ${name}!" is the last expression, so it becomes the return value. Notice you call methods just like in Java, but parentheses can be omitted in many contexts.

Parameters, Defaults, and Varargs

Groovy makes parameter handling concise. You can give parameters default values so callers can omit them, and you can accept a variable number of arguments using varargs (the ... syntax).

Create a file named parameters.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Default parameter value: exponent defaults to 2 if not supplied
def power(base, exponent = 2) {
    base ** exponent
}

// Varargs: accept any number of integer arguments
def sum(int... numbers) {
    int total = 0
    for (n in numbers) {
        total += n
    }
    total
}

println power(3)         // uses the default exponent of 2
println power(2, 10)     // overrides the default
println sum(1, 2, 3, 4, 5)
println sum()            // no arguments at all

The ** operator is Groovy’s power operator, so power(3) computes 3 squared. With varargs, numbers arrives as an array, so sum can be called with any number of values—including none, which yields 0.

Recursion

A recursive function calls itself to solve a problem by breaking it into smaller subproblems. Groovy handles recursion naturally, and the optional ternary operator keeps the code compact.

Create a file named recursion.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Classic factorial: n! = n * (n-1) * ... * 1
def factorial(n) {
    if (n <= 1) {
        return 1
    }
    return n * factorial(n - 1)
}

// Fibonacci using a ternary expression
def fib(n) {
    n < 2 ? n : fib(n - 1) + fib(n - 2)
}

println factorial(5)
println fib(10)

// Ranges combine nicely with recursion
(0..7).each { println "fib($it) = ${fib(it)}" }

factorial(5) multiplies 5 × 4 × 3 × 2 × 1 = 120. The fib method returns n directly for the base cases (0 and 1) and otherwise sums the two preceding Fibonacci numbers. The (0..7).each { ... } line uses a range and a closure to print each result—a preview of the closure power covered below.

Variable Scope

Variables declared inside a method are local—they exist only while that method runs and are invisible elsewhere. Closures, however, can capture variables from the scope where they were created, keeping them alive afterward.

Create a file named scope.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Local variables live only inside the method that declares them
def scale(value) {
    def factor = 3        // 'factor' exists only inside scale()
    return value * factor
}

println scale(5)

// Closures capture variables from their enclosing scope
def makeCounter() {
    def count = 0
    return { -> count += 1 }   // the returned closure remembers 'count'
}

def counter = makeCounter()
println counter()
println counter()
println counter()

The factor variable cannot be seen outside scale. The interesting part is makeCounter: it returns a closure that still has access to count even after makeCounter has finished. Each call to counter() increments and returns the captured value, demonstrating a closure maintaining private state.

Closures and Higher-Order Functions

Closures are Groovy’s first-class functions: blocks of code you can store in variables, pass as arguments, and return from methods. A closure with a single parameter can use the implicit name it. Higher-order functions are methods or closures that accept or return other functions—the foundation of Groovy’s functional style.

Create a file named closures.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// A closure assigned to a variable
def triple = { x -> x * 3 }
println triple(7)

// A closure using the implicit 'it' parameter
def shout = { it.toUpperCase() }
println shout("groovy")

// Higher-order function: takes a closure as an argument
def applyTwice(value, Closure operation) {
    operation(operation(value))
}
println applyTwice(5) { it + 10 }

// Closures power Groovy's collection methods
def numbers = [1, 2, 3, 4, 5, 6]
println numbers.collect { it * it }
println numbers.findAll { it % 2 == 0 }
println numbers.inject(0) { acc, n -> acc + n }

applyTwice accepts a Closure and applies it twice—calling applyTwice(5) { it + 10 } adds 10 to get 15, then again to get 25. Groovy lets you pass a trailing closure outside the parentheses, which is why { it + 10 } sits after the call. The collection methods show idiomatic Groovy: collect transforms (map), findAll filters, and inject reduces a list to a single value.

Running with Docker

Run each example with the official Groovy image—no local Groovy installation required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Pull the official image
docker pull groovy:4.0-jdk17-alpine

# Run the basic methods example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy functions.groovy

# Run the parameters and varargs example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy parameters.groovy

# Run the recursion example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy recursion.groovy

# Run the scope example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy scope.groovy

# Run the closures example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy closures.groovy

On Windows PowerShell, replace $(pwd) with ${PWD}.

Expected Output

Running functions.groovy:

25
Hello, Groovy!
2026

Running parameters.groovy:

9
1024
15
0

Running recursion.groovy:

120
55
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13

Running scope.groovy:

15
1
2
3

Running closures.groovy:

21
GROOVY
25
[1, 4, 9, 16, 25, 36]
[2, 4, 6]
21

Key Concepts

  • Methods vs. closures: Methods are declared with def or a return type; closures are anonymous functions assigned to variables. Closures are first-class values you can pass around.
  • Implicit return: The value of the last expression is returned automatically, so the return keyword is optional.
  • Optional typing: Use def for dynamic typing or explicit types (int, String) for clarity and tooling—mix them freely in the same script.
  • Default parameters and varargs: Parameters can have default values, and Type... accepts any number of arguments as an array, reducing the need for overloads.
  • Closures capture scope: A closure remembers the variables from where it was defined, enabling private state and patterns like counters and factories.
  • Higher-order functions: Passing closures to methods like collect, findAll, and inject is the idiomatic Groovy way to transform, filter, and reduce collections.
  • The implicit it: A single-parameter closure can reference its argument as it without declaring it explicitly.
  • Recursion is natural: Groovy supports recursion directly, and concise ternary expressions keep recursive definitions readable.

Running Today

All examples can be run using Docker:

docker pull groovy:4.0-jdk17-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining