Functions as Values
|
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). Everything shown here exists in the 3.3 LTS and
3.9 LTS lines too; the only version-dependent detail is the deprecation of the trailing underscore in eta-expansion
(3.4), covered below. In Scala a function is a value like any other: it can be written as a literal, stored in a val,
kept in a collection or a field, passed to a method and returned from one. A method (def) is not a value; the
language converts one into the other when the context asks for a function, and this page explains precisely when.
Methods that take and return functions are the subject of
Higher-Order Functions & Closures;
Partial Functions covers functions defined on part of their
domain, Currying & Parameter Lists covers methods with several
parameter lists, and Dependent & Polymorphic Function Types
and Context Functions cover the more advanced function types.
For the declaration side see Methods & Parameters.
The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails.
Function literals
A function literal (also called an anonymous function or lambda) is written (parameters) => body. The parameter types
may be omitted when the compiler can infer them from the expected type, and the parentheses may be omitted around a single
parameter without a type. The body extends as far to the right as possible; a multi-line body is an indented block.
//> using scala 3.10.0
case class Trail(name: String, km: Double, ascentM: Int)
@main def e1(): Unit =
val trails = List(Trail("Aguja", 7.5, 640), Trail("Lago", 4.2, 120), Trail("Cumbre", 11.0, 1100))
// a function literal, with parameter types written out
val minutes = (km: Double, ascentM: Int) => math.round(km * 12 + ascentM / 100.0 * 10).toInt
// types inferred from the declared function type
val steep: Trail => Boolean = t => t.ascentM > 500
val shortName: Trail => String = _.name.take(3) // placeholder syntax
val longer: (Trail, Trail) => Boolean = _.km > _.km // two placeholders, two parameters
println(minutes(7.5, 640))
println(trails.filter(steep).map(shortName))
println(longer(trails(0), trails(1)))
println(trails.map(_.km).sum)
println(trails.sortWith(_.km > _.km).map(_.name))
// a multi-line literal with a braceless body
val describe = (t: Trail) =>
val m = minutes(t.km, t.ascentM)
s"${t.name}: $m min"
println(trails.map(describe))
// the value is an instance of the Function1 / Function2 trait
println(steep.isInstanceOf[Function1[?, ?]])
println(minutes.apply(1.0, 0) == minutes(1.0, 0))
154
List(Agu, Cum)
true
22.7
List(Cumbre, Aguja, Lago)
List(Aguja: 154 min, Lago: 62 min, Cumbre: 242 min)
true
true
The notes that matter:
-
The function value is an object.
minutes(7.5, 640)is sugar forminutes.apply(7.5, 640); the last two lines of the example show it. There is no separate "function" concept in the runtime, only instances of theFunctionNtraits. -
Parameter types are inferred only from the expected type.
val steep: Trail => Boolean = t => ...works because the declared type tells the compiler whattis; a literal with no expected type needs annotated parameters (see the pitfalls section for the error). -
The body is a single expression or an indented block. Its last expression is the result, exactly as in a method. A literal whose body is indented under
=>needs no braces in Scala 3; see Optional Braces & Indentation. -
A literal is not a method. It has no name, no type parameters and no default or by-name parameters; for those use a
def(and see the eta-expansion section for turning it into a value).
Placeholder syntax
When each parameter is used exactly once, in order, the parameter list can be replaced by _ in the body. The two
forms below are equivalent:
val doubled = List(1, 2, 3).map(x => x * 2)
val doubled2 = List(1, 2, 3).map(_ * 2)
Each _ stands for the next parameter, so two placeholders mean a two-parameter function and the parameters are used in
order of appearance. The important subtlety is the scope of the placeholder: the underscore expands to a lambda around
the smallest enclosing expression that is not just the underscore itself, which in practice means the enclosing argument.
The next example shows the nested case and then the common mistake.
//> using scala 3.10.0
def inc(n: Int): Int = n + 1
@main def e9(): Unit =
val a = List(1, 2, 3).map(_ + 1)
val b = List(1, 2, 3).map(inc(_) * 2) // the lambda is the whole argument of map
val c = List(1, 2, 3).map(x => inc(x) * 2)
println((a, b, c))
val e = List(List(1, 2), List(3)).map(_.map(_ + 1)) // two nested lambdas, two scopes
println(e)
(List(2, 3, 4),List(4, 6, 8),List(4, 6, 8))
List(List(2, 3), List(4))
In inc(_) * 2 the underscore sits directly in the argument position of inc, so the compiler wraps a lambda around
inc(_) * 2, not around inc alone. Two common mistakes follow:
//> using scala 3.10.0
def inc(n: Int): Int = n + 1
@main def e9b(): Unit =
val d = List(1, 2, 3).map(inc(_ * 2))
val bad = List(1, 2, 3).map(math.max(_, _))
[error] ./e9b.scala:6:33
[error] Missing parameter type
[error]
[error] I could not infer the type of the parameter _$1
[error] in expanded function:
[error] _$1 => _$1 * 2
[error] Expected type for the whole anonymous function:
[error] Int
[error] val d = List(1, 2, 3).map(inc(_ * 2))
[error] ^
[error] ./e9b.scala:7:31
[error] Wrong number of parameters, expected: 1
[error] val bad = List(1, 2, 3).map(math.max(_, _))
[error] ^^^^^^^^^^^^^^
In inc(_ * 2) the underscore’s smallest enclosing expression is _ * 2, the argument of inc, so the lambda is
x => x * 2 handed to a method that wants an Int. In math.max(_, _) the two placeholders make a two-parameter lambda,
but map expects a one-parameter function. When the compiler’s reading is not obvious, write the parameter out.
Function types
The type of a function value is written A => B; with several parameters (A, B) => C; with none () => C. The arrow
is right associative, so Int => Int => Int is a function that takes an Int and returns a function from Int to Int.
These are shorthands for the standard traits Function0 to Function22: A => B is Function1[A, B], (A, B) => C is
Function2[A, B, C]. Scala 3 has no limit of 22 parameters for function types; a function of 23 or more parameters is an
instance of scala.runtime.FunctionXXL (a literal with 23 Int parameters was compiled and called on 3.10.0, and the
value is not a scala.Function, so it is rarely needed in practice).
//> using scala 3.10.0
@main def e2(): Unit =
val inc: Int => Int = _ + 1
val dbl: Int => Int = _ * 2
println((inc andThen dbl)(5)) // dbl(inc(5))
println((inc compose dbl)(5)) // inc(dbl(5))
val add: (Int, Int) => Int = _ + _
val addT: ((Int, Int)) => Int = add.tupled
val addC: Int => Int => Int = add.curried
println(addT((2, 3)))
println(addC(2)(3))
println(List((1, 2), (3, 4)).map(addT))
// parameterless, three parameters, and a function returning a function
val now: () => String = () => "now"
val f3: (Int, Int, Int) => Int = _ + _ + _
val adder: Int => Int => Int = a => b => a + b
println((now(), f3(1, 2, 3), adder(1)(2)))
// functions as map values
val ops: Map[String, (Double, Double) => Double] =
Map("+" -> (_ + _), "*" -> (_ * _), "max" -> math.max)
println(ops("max")(2, 7))
println(ops.keys.toList.sorted)
println(Function.const(7)("ignored"))
println(identity[Int](3))
12
11
5
5
List(3, 7)
(now,6,3)
7.0
List(*, +, max)
7
3
Source: Scala 3 Book: Function Variables
The members of Function1 that you will use most:
| Member | Meaning | Example result |
|---|---|---|
|
call the function |
|
|
run |
|
|
run |
|
|
take one tuple instead of several arguments |
|
|
turn |
|
|
the |
|
|
a function that ignores its argument |
|
andThen and compose build a new function value and do not call anything yet; the composed function evaluates only when
applied.
Pattern-matching lambdas
A block of case clauses can be written wherever a function is expected, and it matches on the parameter. This lets a
lambda destructure a tuple or a case class without naming the whole value, and _ ignores a component:
//> using scala 3.10.0
case class Trail(name: String, km: Double, ascentM: Int)
@main def e13(): Unit =
val pairs = List("Aguja" -> 640, "Lago" -> 120)
println(pairs.map { case (name, ascent) => s"$name climbs $ascent m" })
val trails = List(Trail("Aguja", 7.5, 640), Trail("Lago", 4.2, 120))
println(trails.map { case Trail(n, km, _) => s"$n=$km" })
val f: PartialFunction[Int, String] = { case 1 => "one" }
val g: Int => String = f // a PartialFunction is a Function1
println(g(1))
println(try g(2) catch case e: MatchError => "MatchError")
List(Aguja climbs 640 m, Lago climbs 120 m)
List(Aguja=7.5, Lago=4.2)
one
MatchError
Source: Scala 3 Book: Partial Functions
A PartialFunction[A, B] is itself an A => B. Passing one where a total function is expected compiles, and applying it to a
value outside its domain throws MatchError, which is why collect and lift exist (see
Partial Functions).
Methods versus functions
A method and a function value look alike at the call site, but they are different things:
Method (def) |
Function value | |
|---|---|---|
Is an object |
no, it belongs to a class, an object or the top level |
yes, an instance of |
Can be stored in a |
no, it must be converted |
yes |
Type parameters |
allowed ( |
not on a plain function type (see Dependent & Polymorphic Function Types) |
Several parameter lists, default and by-name parameters |
allowed |
not allowed |
Evaluated |
each time it is called |
the value is created once; the body runs at each call |
The conversion from the left column to the right is eta-expansion, and Scala 3 performs it automatically wherever it can.
Automatic eta-expansion
Scala 3 converts a method to a function value whenever a function type is expected, and also when no type is expected at
all. A method with parameters becomes a function; a method with an empty parameter list () becomes a () => R function.
//> using scala 3.10.0
def sign(n: Int): String = if n > 0 then "up" else if n < 0 then "down" else "flat"
def twice(f: Int => Int, x: Int): Int = f(f(x))
def inc(n: Int): Int = n + 1
def pair(a: Int, b: Int): Int = a + b
def poly[A](a: A): A = a
def now(): Long = 42L
def lazyLen(s: => String): Int = s.length
class Counter:
private var n = 0
def next(): Int = { n += 1; n }
@main def e3(): Unit =
val f = sign // no expected type: still eta-expanded in Scala 3
val g: Int => String = sign
val h = pair // multi-parameter method
val k: () => Long = now // empty-parameter-list method
println((f(1), g(-1), h(1, 2), k()))
println(twice(inc, 3))
println(List(1, 2, 3).map(inc))
println(List(1, 2, 3).map(poly))
val c = Counter()
val next: () => Int = c.next // captures the receiver c
println((next(), next(), c.next()))
val l: String => Int = lazyLen // a by-name parameter becomes a plain parameter
println(l("abc"))
(up,down,3,42)
5
List(2, 3, 4)
List(1, 2, 3)
(1,2,3)
3
Source: Scala 3 Book: Eta-Expansion
What the example demonstrates:
-
val f = signneeds nosign _; Scala 3 expands any method reference that is not already applied. In Scala 2 the same line failed with "missing argument list" unless an expected function type was present. -
A generic method is expanded too:
map(poly)instantiatesAtoInt. -
A method taken from an instance (
c.next) capturesc, so three calls give 1, 2, 3 on the same counter. -
The by-name parameter of
lazyLenbecomes an ordinary strict parameter of the function, sol("abc")evaluates its argument before the call; laziness is lost, as it must be for a plainString => Int.
The full rules are in Scala 3 Reference: Automatic Eta-Expansion.
The trailing underscore is deprecated
Scala 2 required f _ (a method name followed by a space and an underscore) to force the conversion. Scala 3 still
accepts the form but warns from 3.4 onward, and the warning becomes an error under -source:future:
//> using scala 3.10.0
def inc(n: Int): Int = n + 1
@main def e4(): Unit =
val f = inc _
println(f(1))
[warn] ./e4.scala:5:11
[warn] The trailing ` _` for eta-expansion is unnecessary;
[warn] the compiler performs eta-expansion automatically, so you can write the function value directly instead.
[warn] This construct can be rewritten automatically under -rewrite -source 3.4-migration.
[warn] val f = inc _
[warn] ^^^^^
2
With //> using option -source:future the same program fails to compile with the same text at [error] severity.
Write val f = inc, or val f: Int => Int = inc when you want the type to be explicit.
SAM conversion
A single abstract method (SAM) type is a trait or interface with exactly one abstract method. Wherever such a type is
expected, a function literal is converted into an instance of it. This is how lambdas work with Java interfaces such as
Runnable and Comparator, and it also works for traits you write yourself, even when they carry concrete members.
//> using scala 3.10.0
trait Scorer:
def score(km: Double): Int
trait Greeter:
def greet(name: String): String
def shout(name: String): String = greet(name).toUpperCase // concrete members are fine
@main def e6(): Unit =
val r: Runnable = () => println("running")
r.run()
val cmp: java.util.Comparator[String] = (a, b) => a.length - b.length
val words = java.util.ArrayList(java.util.List.of("ccc", "a", "bb"))
words.sort(cmp)
println(words)
val sc: Scorer = km => (km * 10).toInt // a user trait with one abstract method
println(sc.score(2.5))
val g: Greeter = n => s"hi $n"
println(g.shout("ana"))
val names = java.util.ArrayList(java.util.List.of("b", "a", "c"))
names.sort((a, b) => a.compareTo(b)) // a lambda handed to a Java method
println(names)
val t = new Thread(() => println("in thread"))
t.start(); t.join()
val x: Int => Int = _ + 1
println(x.isInstanceOf[Function1[?, ?]])
running
[a, bb, ccc]
25
HI ANA
[a, b, c]
in thread
true
Rules for SAM conversion:
-
The target must be a trait, interface or abstract class with exactly one abstract method, and that method must have a parameter list (possibly empty, as in
def name(): String); concrete members are allowed (Greeter.shout). -
The conversion happens only when the target type is expected. A parameterless abstract method does not qualify, so the next abstract class is not a SAM type:
//> using scala 3.10.0
abstract class Named:
def name: String
@main def e7(): Unit =
val n: Named = () => "x"
[error] ./e7.scala:7:18
[error] Found: () => String
[error] Required: Named
[error] val n: Named = () => "x"
[error] ^^^^^^^^^
-
e7fails becausedef name: Stringhas no parameter list, not becauseNamedis a class: the same error appears for a trait with that member. Declared asdef name(): String, the same abstract class accepts() => "x". -
A Scala
FunctionNis a SAM type itself, soInt => Intis the default target of a literal; SAM conversion is what applies to every other single-method type. In Scala 2 the conversion needed 2.12 or later (2.11 required-Xexperimental).
Java, Kotlin and other languages
| Language | Function type | Notes compared with Scala |
|---|---|---|
Scala 3 |
|
methods convert to functions automatically; any SAM type accepts a lambda |
Java |
|
a lambda needs a functional interface; there is no general function type; a method reference is |
Kotlin |
|
function types are built in; a lambda is |
Python |
|
any function is a value; there are no placeholders; lambdas are limited to one expression |
JavaScript |
|
arrow functions; no placeholders; methods lose |
A Java caller sees a Scala Function1 as scala.Function1, not as java.util.function.Function, so lambdas do not cross the
boundary without a conversion; see Calling Scala from Java and
Using Java from Scala (the SAM conversion above is the Scala
side of that bridge).
Putting it together
A small pipeline for the trailhead toolkit: a method promoted to a value and composed with andThen, functions stored in a
map, and a composed function whose building runs nothing until it is applied.
//> using scala 3.10.0
case class Trail(name: String, km: Double, ascentM: Int)
def minutes(t: Trail): Int = math.round(t.km * 12 + t.ascentM / 100.0 * 10).toInt
@main def e14(): Unit =
val trails = List(Trail("Aguja", 7.5, 640), Trail("Lago", 4.2, 120), Trail("Cumbre", 11.0, 1100))
val steep: Trail => Boolean = _.ascentM > 500
val label: Int => String = m => s"$m min"
val plan: Trail => String = minutes andThen label // a method promoted to a value, then composed
val pipeline: List[Trail] => List[String] = _.filter(steep).map(plan)
println(pipeline(trails))
val sortKeys: Map[String, Trail => Double] = Map("km" -> (_.km), "ascent" -> (_.ascentM.toDouble))
println(sortKeys("ascent").andThen(math.round)(trails(0)))
println(trails.sortBy(sortKeys("km")).map(_.name))
val calls = scala.collection.mutable.ListBuffer.empty[String]
val traced: Trail => Trail = t => { calls += t.name; t }
val composed = traced andThen steep
println(calls.isEmpty) // building it ran nothing
println(trails.map(composed))
println(calls.toList)
List(154 min, 242 min)
640
List(Lago, Aguja, Cumbre)
true
List(true, false, true)
List(Aguja, Lago, Cumbre)
Creating the function value does not run its body; each call does. The minutes andThen label line also shows
eta-expansion at work: the method minutes is expanded to a function so that andThen can be called on it.
Pitfalls
-
Missing parameter type. A literal with no expected type cannot infer its parameters:
//> using scala 3.10.0
@main def e8(): Unit =
val f = x => x + 1
val g = (x, y) => x + y
val h: Int => Int = x => x + 1
val xs = List(1, 2, 3).map(x => x + 1)
println((h(1), xs))
[error] ./e8.scala:4:11
[error] Missing parameter type
[error]
[error] I could not infer the type of the parameter x
[error] val f = x => x + 1
[error] ^
[error] ./e8.scala:5:12
[error] Missing parameter type
[error]
[error] I could not infer the type of the parameter x
[error] val g = (x, y) => x + y
[error] ^
[error] ./e8.scala:5:15
[error] Missing parameter type
[error]
[error] I could not infer the type of the parameter y
[error] val g = (x, y) => x + y
[error] ^
Source: Scala 3 Book: Function Variables
Lines 6 and 7, where a type is expected, compile. Annotate the parameters ((x: Int) => x + 1) or the value.
-
Arity mismatch. A two-parameter literal does not satisfy a one-parameter function type, even though both are "functions":
//> using scala 3.10.0
@main def e10b(): Unit =
val bad: Int => Int = (a: Int, b: Int) => a + b
[error] ./e10b.scala:4:25
[error] Found: (Int, Int) => Int
[error] Required: Int => Int
[error] val bad: Int => Int = (a: Int, b: Int) => a + b
[error] ^^^^^^^^^^^^^^^^^^^^^^^^^
Source: Scala 3 Book: Function Variables
Use tupled to turn a two-parameter function into a function of a pair, or write a lambda of one parameter.
-
Placeholder scope. See the
inc(_ * 2)error above: an underscore binds to the nearest enclosing argument, not the whole line. -
Java immutable collections. A lambda handed to
java.util.List.of(...).sort(...)compiles but throwsUnsupportedOperationExceptionat run time, because the list is immutable (this was seen while writing the SAM example; the fix is to copy into anArrayList). The language is not at fault, the Java collection is. -
Trailing underscore.
inc _still compiles on 3.10 with a warning but is an error under-source:future; drop it. -
Lost laziness. Eta-expanding a method with a by-name parameter produces a function that evaluates its argument eagerly.
Where the books are outdated
-
The Scala 2 books and many tutorials write
val f = inc _orinc(_); in Scala 3 the compiler expands a method reference by itself and the trailing underscore is deprecated (3.4). -
Older sources describe SAM conversion as an experimental flag (Scala 2.11). It is standard since Scala 2.12 and in Scala 3.
-
The "22 parameter" limit of
Function22in Scala 2 does not apply to Scala 3 function types. -
Tutorials on Java interop sometimes mention
scala.runtime.AbstractFunction1; with SAM conversion you rarely need it.
The run results above are from 2026-10-15 on Scala 3.10.0 and were not re-run on the 3.9 LTS or 3.3 LTS releases.