Higher-Order Functions & Closures
|
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 Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala CLI and Scala 3.10.0
on JDK 26 on 2026-10-15 (the examples were not re-run on 3.9 or 3.3). Nothing used here is newer than Scala 3.0, so it
applies unchanged to the 3.9 LTS and 3.3 LTS lines, and with the syntax differences noted below to Scala 2.13. A
higher-order function is a function or method that takes a function as a parameter, returns a function as its result,
or both. A closure is a function value that uses variables defined outside its own body and keeps them alive for as
long as the function lives. Almost all of the Scala collections API (map, filter, foldLeft, sortBy, …) is
higher-order, and almost every lambda you write is a closure.
This page is about writing and using such functions. The function types and lambda syntax themselves are in
Functions as Values; functions defined by cases are in
Partial Functions; several parameter lists are in
Currying & Parameter Lists; recursion, including
@tailrec, is in Recursion & Tail Calls; and the
collection operations themselves are in Collection Operations.
The Java and Kotlin counterparts are Java Lambdas & Method References
and Kotlin Lambdas & Higher-Order Functions.
The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails.
Methods that take functions
A parameter can have a function type such as Trail => Boolean (read "a function from Trail to Boolean`"). The caller
passes any function value of that type: a lambda, a method converted to a function, or a `val holding a function. Inside
the method the parameter is called like any function.
//> using scala 3.10.0
case class Trail(name: String, km: Double, ascentM: Int)
val trails = List(Trail("Aguja", 7.5, 640), Trail("Lago", 3.2, 120), Trail("Cumbre", 12.0, 1100))
def keepIf(ts: List[Trail], p: Trail => Boolean): List[Trail] =
ts.filter(p)
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
def describeAll(ts: List[Trail])(label: Trail => String): List[String] =
ts.map(label)
@main def e1(): Unit =
println(keepIf(trails, t => t.km > 5).map(_.name))
println(keepIf(trails, _.ascentM < 500).map(_.name))
println(applyTwice(_ + 3, 10))
println(describeAll(trails)(t => s"${t.name} (${t.km} km)"))
val byPrecedence = trails.sortBy(t => -t.ascentM).map(_.name)
println(byPrecedence)
List(Aguja, Cumbre)
List(Lago)
16
List(Aguja (7.5 km), Lago (3.2 km), Cumbre (12.0 km))
List(Cumbre, Aguja, Lago)
Things worth noticing:
-
The parameter type tells the compiler what the lambda looks like. In
applyTwice(_ + 3, 10)there is no type on the lambda’s parameter: the expected typeInt => Intsupplies it. This is why the function parameter usually goes last, or in its own parameter list (as indescribeAll): the compiler infers type parameters from earlier arguments before it types the lambda, which is what makesfoldLeft(0)((acc, x) => acc + x)work without annotations. See Currying & Parameter Lists. -
The placeholder
_stands for the lambda’s next parameter, one per occurrence, and the lambda extends to the nearest enclosing expression:_.ascentM < 500ist => t.ascentM < 500. It cannot be used twice for the same argument, so_ + _is a two-parameter function. -
A block can be the argument. With a trailing lambda
xs.map: x =>followed by an indented body, Scala 3 lets you drop the parentheses (see Optional Braces & Indentation). -
Function types are ordinary types.
Trail => Booleanis shorthand forFunction1[Trail, Boolean], and(Double, Int) => IntforFunction2[Double, Int, Int]; they appear in generics, fields and collections.
Writing map and filter yourself
The standard collection methods are not magic. Writing them with a function parameter shows what the pattern really is:
an operation is left open as a parameter and the method supplies the traversal. Here map is written with recursion and
pattern matching, and filter with foldRight, itself a higher-order function.
//> using scala 3.10.0
// map and filter written by hand, first with recursion, then with a fold
def myMap[A, B](xs: List[A])(f: A => B): List[B] = xs match
case Nil => Nil
case head :: tail => f(head) :: myMap(tail)(f)
def myFilter[A](xs: List[A])(p: A => Boolean): List[A] =
xs.foldRight(List.empty[A])((x, acc) => if p(x) then x :: acc else acc)
@main def e2(): Unit =
val xs = List(1, 2, 3, 4, 5, 6)
println(myMap(xs)(_ * 10))
println(myFilter(xs)(_ % 2 == 0))
println(myMap(List("a", "bb", "ccc"))(_.length))
assert(myMap(xs)(_ + 1) == xs.map(_ + 1))
assert(myFilter(xs)(_ > 3) == xs.filter(_ > 3))
List(10, 20, 30, 40, 50, 60)
List(2, 4, 6)
List(1, 2, 3)
Two caveats apply to the recursive myMap. It is not tail recursive, so a list of a few hundred thousand elements
overflows the stack; the standard library’s map is a loop. The tail-recursive version with an accumulator, and the
trampolining alternative, are in Recursion & Tail Calls.
And A and B are inferred from the first parameter list, so myMap(xs)(_ * 10) needs no type annotation. In Scala 2
the function had to be in a second list for that reason: a single list myMap(xs, _ * 10) fails there with "missing
parameter type". Scala 3 types lambda arguments after the other arguments, so on 3.10.0 the single-list form compiles
too, and the separate list is now a style and API choice (it lets the caller pass a block with braces).
Methods that return functions
A method can return a function. The usual reasons are to configure a behaviour once and reuse it, to build a predicate
for filter, or to compose two functions into a third.
//> using scala 3.10.0
// methods that return functions
def scaler(factor: Double): Double => Double = x => x * factor
def greaterThan(limit: Int): Int => Boolean = _ > limit
def compose[A, B, C](f: A => B, g: B => C): A => C = a => g(f(a))
def adder(n: Int) = (x: Int) => x + n // result type inferred: Int => Int
@main def e3(): Unit =
val toFeet = scaler(3.28084)
println(toFeet(1000))
println(List(3, 8, 12).filter(greaterThan(5)))
val summit = compose(toFeet, (f: Double) => s"${f.round} ft")
println(summit(2000))
println(adder(4)(5))
println(scaler(2)(21)) // calling the returned function immediately
3280.84
List(8, 12)
6562 ft
9
42.0
scaler(3.28084) returns a new function each time it is called, and each returned function remembers its own
factor. That remembered variable is a closure, discussed next. The standard library already provides composition on
function values: f.andThen(g) is compose(f, g) above, and f.compose(g) applies g first. A method that returns a
function is not the same as a method with two parameter lists: adder(4)(5) works for both, but the first creates a
function object between the calls, while the second does not (see
Currying & Parameter Lists).
Closures
A function value whose body mentions a name from an enclosing scope closes over it: the compiler keeps a reference to
that binding alongside the code. Closing over a val captures an immutable value, so the behaviour is easy to reason about.
Closing over a var captures the variable itself, not its current value.
//> using scala 3.10.0
@main def e4(): Unit =
// a closure over a val
val base = 100
val addBase = (x: Int) => x + base
println(addBase(5))
// a closure over a var: it sees the variable, not a copy of its value
var total = 0
val record = (x: Int) => total += x
List(1, 2, 3).foreach(record)
println(total)
total = 1000
record(1)
println(total)
// each call of a function that creates a closure gets its own captured variable
def counter(): () => Int =
var n = 0
() => { n += 1; n }
val a = counter()
val b = counter()
println(List(a(), a(), a(), b()))
// the closure keeps seeing later changes to a var
var factor = 2
val times = (x: Int) => x * factor
println(times(10))
factor = 5
println(times(10))
105
6
1001
List(1, 2, 3, 1)
20
50
The results show the whole model:
-
Captured variables are shared, not copied.
recordadds to the sametotalthat the surrounding code reads and writes, so assigningtotal = 1000outside is visible inside, and the other way round. The compiler implements this by moving a capturedvarinto a heap-allocated reference cell (ascala.runtime.IntRefin this case), so a capturedvarcosts an allocation; a capturedvaldoes not. -
Every call of
countermakes a freshn, soaandbcount independently. This is the standard way to hide state without a class. -
A closure outlives the scope that created it.
nlives on aftercounter()returns, for as long asaorbis reachable. The same applies to everything else the lambda mentions, for example a large collection: a lambda stored in a long-lived field keeps all of it from being collected.
In Java a lambda may only capture local variables that are final or effectively final, so total += x inside a lambda is a
compile error and programmers use AtomicInteger or a one-element array. Scala has no such restriction because of the
reference-cell translation above. Kotlin behaves like Scala (captured var`s are wrapped in `Ref objects). The price is that
a mutable closure is shared state: it is unsafe to call from several threads without synchronization, and it breaks the
referential transparency that makes higher-order code easy to reason about. Prefer foldLeft, sum or count over
accumulating into a captured var.
The loop-capture pitfall
Because a closure sees the variable and not its value at creation time, creating closures in a while loop that mutates
a var is a classic trap. A for loop binds a new value each iteration, so it is safe.
//> using scala 3.10.0
import scala.collection.mutable.ListBuffer
// a classic pitfall: closures created in a loop share the same var
@main def e5(): Unit =
var i = 0
val shared = ListBuffer.empty[() => Int]
while i < 3 do
shared += (() => i)
i += 1
println(shared.map(f => f()).toList)
val own = ListBuffer.empty[() => Int]
for j <- 0 until 3 do own += (() => j)
println(own.map(f => f()).toList)
var k = 0
val copied = ListBuffer.empty[() => Int]
while k < 3 do
val snapshot = k
copied += (() => snapshot)
k += 1
println(copied.map(f => f()).toList)
List(3, 3, 3)
List(0, 1, 2)
List(0, 1, 2)
The first list is List(3, 3, 3): all three closures read the single i, which is 3 when the loop ends. The cure is
to copy the value into a fresh val per iteration (third case) or to use a construct that already does so (for,
foreach, map). The same trap exists in JavaScript with var and in Python with closures created in a loop.
Methods as functions: eta-expansion
A method is not a function value, but the compiler converts one into a function value when a function type is
expected. This conversion is called eta-expansion, and it is what makes List(1, 2, 3).map(double) work.
//> using scala 3.10.0
def double(x: Int): Int = x * 2
def isLong(t: String): Boolean = t.length > 3
def hike(km: Double, ascentM: Int): Int = (km * 12 + ascentM / 10.0).toInt
object Calc:
def square(x: Int): Int = x * x
def twice(f: Int => Int)(x: Int): Int = f(f(x))
@main def e6(): Unit =
println(List(1, 2, 3).map(double)) // method passed where a function is expected
println(List("ab", "abcd").filter(isLong))
val f = double // Scala 3: eta-expansion even with no expected type
println(f(21))
val g = hike // multi-parameter method becomes (Double, Int) => Int
println(g(7.5, 640))
println(List(1, 2, 3).map(Calc.square))
val quad = Calc.twice(Calc.square) // partial application of the first list
println(quad(3))
val h: (Int, Int) => Int = _ + _ // an operator as a function
println(List(1, 2, 3, 4).reduce(_ + _))
println(List("x", "y").map(String.valueOf)) // an overloaded Java method picked by expected type
println(h(2, 3))
List(2, 4, 6)
List(abcd)
42
154
List(1, 4, 9)
81
10
List(x, y)
5
In Scala 3 the conversion is applied to every method that has at least one parameter, whether or not a function type is
expected; Scala 2.13 still required one. The same program on Scala 2.13.18 rejects the bare val g = double:
[error] ./e9.scala:9:13
[error] missing argument list for method double in object E9 of type (x: Int): Int
[error] Unapplied methods are only converted to functions when a function type is expected.
[error] You can make this conversion explicit by writing `double _` or `double(_)` instead of `double`.
[error] val g = double
[error] ^^^^^^
so Scala 2 code writes double _ (or double(_)). Scala 3.10.0 still accepts double _ but warns "The trailing + _+ for eta-expansion is unnecessary" (verified; the warning exists since 3.4). In both versions
passing the method where a function is expected (as in .map(double)) works without it. The rules are described in
Scala 3 Reference: Automatic Eta Expansion,
and the difference between methods and function values in
Scala 3 Book: Eta Expansion.
A method with no parameter list (def x: Int) is never eta-expanded, because there is nothing to turn into a
function; wrap it in a lambda, () => x. See
Methods & Parameters.
Function values are not comparable
A function value is an object like any other, with the default identity equality. Two textually identical lambdas are
different objects, and Scala offers no way to compare functions by behaviour. Keep that in mind before using functions as
Map keys or inside case classes whose equals you rely on.
//> using scala 3.10.0
// a function that takes a function that returns a function
def retry[A](times: Int)(action: Int => Option[A]): Option[A] =
(1 to times).iterator.map(action).collectFirst { case Some(v) => v }
def logged[A, B](name: String)(f: A => B): A => B = a =>
val r = f(a)
println(s"$name($a) = $r")
r
@main def e8(): Unit =
println(retry(5)(n => if n >= 3 then Some(n * 10) else None))
val safeLen = logged("len")((s: String) => s.length)
println(List("ab", "cde").map(safeLen))
// function values compare by identity, not by behaviour
val f1 = (x: Int) => x + 1
val f2 = (x: Int) => x + 1
println(f1 == f2)
println(f1 == f1)
Some(30)
len(ab) = 2
len(cde) = 3
List(2, 3)
false
true
retry stops at the first success because iterator.map is lazy and collectFirst pulls elements one by one, so
action is called only for 1, 2 and 3. logged is a decorator: it takes a function and returns a function with the
same type and added behaviour, the functional replacement for a wrapper class.
Common mistakes
//> using scala 3.10.0
def double(x: Int): Int = x * 2
@main def e7(): Unit =
val xs = List(1, 2, 3)
val a = xs.map((x, y) => x + y)
val b = xs.map(double(_, 1))
val c: Int = xs.map(double)
[error] ./e7.scala:7:18
[error] Wrong number of parameters, expected: 1
[error] val a = xs.map((x, y) => x + y)
[error] ^^^^^^^^^^^^^^^
[error] ./e7.scala:8:25
[error] Found: (Int, Int)
[error] Required: Int
[error] val b = xs.map(double(_, 1))
[error] ^^^^
[error] ./e7.scala:9:16
[error] Found: List[Int]
[error] Required: Int
[error] val c: Int = xs.map(double)
[error] ^^^^^^^^^^^^^^
-
The arity of the lambda must match what the method expects:
mapwants a one-parameter function, so a two-parameter lambda is rejected. A collection of pairs is a different case: Scala 3 untuples a pair, soList((1, 2), (3, 4)).map((a, b) => a + b)compiles and givesList(3, 7)(verified on 3.10.0). -
double(_, 1)passes two arguments to a one-parameter method, so the compiler reports a pair(Int, Int)where anIntis required. The placeholder would have madex => double(x, 1); when a placeholder sits inside a nested call, write the lambda explicitly to keep the error readable. -
mapreturns a collection; the type error in the third case is the usual sign of callingmapwhereforeach,foldLeftor a plain call was meant. -
Side effects inside
mapandfilterrun once per element, in order, but for lazy collections (Iterator,LazyList, views) possibly later than expected or not at all. Useforeachfor effects.
Where the books are outdated
The books consulted for this section (see the section bibliography) were written for Scala 2 or the first Scala 3 releases:
-
f _andf(_)to turn a method into a function appear throughout Scala 2 material; Scala 3 converts methods automatically, and the underscore form draws a warning since 3.4. -
Single-abstract-method lambdas. Scala 2.12 and later convert a lambda to any SAM type (
Runnable,Comparator), and Scala 3 does too, with nonew Runnable { ... }boilerplate; the older books still show the anonymous class. -
Function1composition andandThen/composeare unchanged and still the idiom for building functions out of functions. -
Braces and parentheses. Scala 3 allows
xs.map: x =>with an indented body (optional braces); older material always showsxs.map { x => ... }. Both forms compile. -
Closures, higher-order methods and
map/filterare unchanged since Scala 2; the explanations still hold.
The run results above are from 2026-10-15 on Scala 3.10.0 (Scala 2.13.18 for the one Scala 2 comparison) and were not re-run on the 3.9 LTS or 3.3 LTS releases.