Currying & Parameter Lists
|
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 two clause-interleaving error demos were also compiled with 3.3.8 and 3.5.2, as stated where
they appear. Everything except clause interleaving works unchanged on the 3.3 LTS and 3.9 LTS lines; clause
interleaving needs Scala 3.6 or later (it compiled on 3.5.2 only behind import scala.language.experimental.clauseInterleaving,
and it does not exist on 3.3.8).
A Scala method may declare several parameter lists: def pace(distanceKm: Double)(ascentM: Int)(restMinutes: Int).
The arguments are supplied list by list, and a call that supplies only the first lists is a partial application that
yields a function waiting for the rest. This is the mechanism behind foldLeft(zero)(op), the (using ...) lists of
Givens & Using Clauses, and the "control structure" call
style timed("sum"): body. The basics of declaring a method are in
Methods & Parameters; the difference between a method and
a function value is in Functions as Values.
The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails.
Multiple parameter lists
Each parameter list is a separate pair of parentheses. A call must supply the lists in order, and a method that has been given only some of them is turned into a function value by eta-expansion, which Scala 3 performs automatically.
//> using scala 3.10.0
def pace(distanceKm: Double)(ascentM: Int)(restMinutes: Int): Int =
math.round(distanceKm * 12 + ascentM / 100.0 * 10).toInt + restMinutes
@main def c1(): Unit =
println(pace(7.5)(640)(15))
val forThisTrail = pace(7.5) // partial application: Int => Int => Int
val withAscent = forThisTrail(640)
println(withAscent(15))
println(withAscent(30))
val viaUnderscore = pace(7.5)(_: Int)(_: Int) // a two-argument function
println(viaUnderscore(100, 0))
val all = pace // eta-expansion of the whole method
println(all(1.0)(0)(0))
println(Seq(0, 10, 20).map(withAscent))
169
169
184
100
12
List(154, 164, 174)
What the example shows:
-
pace(7.5)is not an error: the missing lists make the expression a functionInt => Int => Int. In Scala 2 this needed a trailing_(pace(7.5) _); Scala 3 expands it wherever a function is expected, and for a method value with no expected type as well (val all = pace). -
An argument placeholder is a lambda around the whole call:
pace(7.5)(_: Int)(_: Int)is(a, b) => pace(7.5)(a)(b), a function of two arguments, not a curried one. The type ascriptions are required here; without them the compiler reportsMissing parameter type. -
Passing a value of the intermediate function where a function is expected needs no wrapper:
Seq(0, 10, 20).map(withAscent).
Writing too few lists is only an error when the result is used as a value of the wrong type, and too many is an error at the extra list:
//> using scala 3.10.0
def pace(distanceKm: Double)(ascentM: Int): Int = ascentM
@main def c10(): Unit =
val f = pace(7.5)
println(f(1))
println(pace(7.5)(1)(2))
println(pace(7.5, 1))
[error] ./c10.scala:6:11
[error] method pace does not take more parameters
[error] println(pace(7.5)(1)(2))
[error] ^^^^^^^^^^^^
[error] ./c10.scala:7:16
[error] Found: (Double, Int)
[error] Required: Double
[error] println(pace(7.5, 1))
[error] ^^^^^^
Compilation failed
Source: Scala 3 Book: Methods
The second error is worth remembering: a call with the arguments of two lists squeezed into one list is not "too many
arguments"; Scala wraps them in a tuple and complains about the type, because the first list takes a single Double.
Why several lists: inference, blocks and defaults
Multiple lists are not only a way to partially apply; each list is also a separate stage for type inference and for call syntax.
Inference flows from one list to the next
The compiler infers the type parameters of a method from the arguments of the first list, fixes them, and only then checks the next list. A lambda in a later list therefore gets its parameter types for free. In a single list the arguments are typed left to right, so a lambda placed before the argument that determines its types fails.
//> using scala 3.10.0
def foldWith[A, B](xs: List[A])(zero: B)(step: (B, A) => B): B =
xs.foldLeft(zero)(step)
@main def c2(): Unit =
val legs = List(2.5, 4.0, 1.5)
println(foldWith(legs)(0.0)((sum, km) => sum + km))
println(foldWith(legs)(0.0)(_ + _))
println(foldWith(List("a", "b"))(0)(_ + _.length))
8.0
8.0
2
//> using scala 3.10.0
def foldOneList[A, B](xs: List[A], step: (B, A) => B, zero: B): B =
xs.foldLeft(zero)(step)
@main def c2d(): Unit =
println(foldOneList(List(1, 2, 3), (acc, x) => acc + x, 0))
[error] ./c2d.scala:6:50
[error] value + is not a member of Any
[error] println(foldOneList(List(1, 2, 3), (acc, x) => acc + x, 0))
[error] ^^^^^
Compilation failed
Scala 3 is better than Scala 2 at inferring within one list (a lambda that comes after the arguments that fix its types
compiles even in a single list), but it still processes a list left to right, so foldOneList fails above because B
is not yet known when the lambda is typed. Splitting zero and step into their own lists removes the dependency on
argument order. The standard library follows this convention: xs.foldLeft(zero)(op) and xs.scanLeft(zero)(op) put the
accumulator in its own list for exactly this reason. See Type Inference.
Block-like call syntax
A trailing parameter list that takes a function or a by-name value reads like a language construct, especially with the fewer-braces style (see Optional Braces & Indentation):
//> using scala 3.10.0
def timed[T](label: String)(body: => T): T =
val result = body
println(s"$label done")
result
def repeatUntil(limit: Int)(step: Int => Boolean): Int =
var i = 0
while i < limit && !step(i) do i += 1
i
@main def c11(): Unit =
val total = timed("sum"):
(1 to 4).sum
println(total)
val found = repeatUntil(10): i =>
i * i > 20
println(found)
sum done
10
5
timed("sum"): ... is the same call as timed("sum")((1 to 4).sum). The body is a by-name parameter (see
Methods & Parameters), so it runs inside timed, after
the first list has already been supplied. Putting the function last is what makes this readable; a method that takes the
function first and the data second cannot be called this way.
Defaults and dependent types
A default argument may refer to a parameter of an earlier list, but not to one of the same list. And a result type may
depend on a value from an earlier list (a dependent method type), which is impossible within one list in the same
form: def surfaceOf(t: Trail)(using o: Ordering[Int]): t.Surface is a signature that mentions t after it has been
introduced. See Dependent & Polymorphic Function Types.
//> using scala 3.10.0
def f(a: Int)(b: Int = a * 2): Int = b
def g(a: Int, b: Int = a * 2): Int = b
@main def d1(): Unit = println(f(4)())
[error] ./d1.scala:3:24
[error] Not found: a
[error] def g(a: Int, b: Int = a * 2): Int = b
[error] ^
Compilation failed
Without g, the program prints 8: the default of f sees a because it sits in a later list.
Currying as function values
Currying is turning a function of several arguments into a chain of functions of one argument. It is not a feature
of the method syntax but a property of the function types: (Int, Int) => Int and Int => Int => Int are different
types, and the standard library converts between them.
//> using scala 3.10.0
def add(a: Int)(b: Int): Int = a + b
def addPair(a: Int, b: Int): Int = a + b
@main def c3(): Unit =
val curried = (addPair).curried // Int => Int => Int
println(curried(1)(2))
val uncurried = Function.uncurried(curried)
println(uncurried(3, 4))
val tupled = (addPair).tupled
println(tupled((5, 6)))
val lam: Int => Int => Int = a => b => a + b
println(lam(1)(2))
val m = add // method with two lists -> curried function
println(m(7)(8))
val e = add(1)
println(e(2))
3
7
11
3
15
3
Comparison of the three forms:
| Form | Type | Partial application |
|---|---|---|
|
method, one list |
needs a lambda: |
|
method, two lists |
|
|
function value, curried |
|
The difference between a method with two lists and a curried function value matters in two places. First, a method with
several lists can have a using list, type parameters per list, defaults and by-name parameters in any list, none of
which a function type can express (a polymorphic or context function needs its own types; see
Dependent & Polymorphic Function Types
and Context Functions). Second, a call that supplies all lists of a method is an ordinary method call with all of the
arguments, whereas the Int => Int => Int form creates a function object for each partial result.
Java has no currying syntax (a Function<Integer, Function<Integer, Integer>> is the equivalent), and Kotlin has none
built in either; both write the lambda chain by hand. Scala 2 had the same multiple parameter lists but needed _
for partial application and was less willing to infer lambda parameter types.
Clause interleaving (SIP-47, Scala 3.6)
Before Scala 3.6, a method could have type parameters only in the first position, before all value parameters.
Clause interleaving (SIP-47) lets type-parameter clauses and value-parameter
clauses alternate, so a later type can be inferred from the value arguments in between, or can depend on them. It is
stable since 3.6.0 (behind import scala.language.experimental.clauseInterleaving in 3.5.x; the 3.6.2 release notes
list it with the other 3.6 language changes), and works on 3.9 LTS but not on 3.3 LTS.
//> using scala 3.10.0
trait Trail:
type Surface
def surface: Surface
class Gravel
class Gravel2 extends Trail:
type Surface = Gravel
def surface = Gravel()
def pair[A](a: A)[B](b: B): (A, B) = (a, b)
def describe(t: Trail)[S <: t.Surface](s: S): S = s
@main def c5(): Unit =
println(pair(1)[String]("x"))
println(pair("a")(2))
val t = Gravel2()
println(describe(t)(Gravel()).getClass.getSimpleName)
(1,x)
(a,2)
Gravel
The second type clause [B] is supplied explicitly in the first call and inferred in the second. In describe, the bound
S <: t.Surface refers to the value parameter t of the previous clause, which is the case that cannot be written
with all type parameters first. The same syntax is valid for extension methods, methods in classes, and a using clause
between the value clauses (all compiled on 3.10.0):
//> using scala 3.10.0
extension (s: String)
def times(n: Int)[T](f: String => T): List[T] = List.fill(n)(f(s))
class Trail[A]:
def mark(a: A)[B](b: B): (A, B) = (a, b)
def tuple[A](a: A)(using ord: Ordering[A])[B](b: B): (A, B) = (a, b)
@main def c7(): Unit =
println("ab".times(2)(_.length))
println(Trail[Int]().mark(1)("x"))
println(tuple(1)("y"))
println(tuple[Int](1)[String]("y"))
List(2, 2)
(1,x)
(1,y)
(1,y)
Rules and errors, from compiling on three versions:
-
Type arguments follow the clause they belong to.
pair[Int](1)[String]("x")is correct;pair[Int, String](1)("x")reportsToo many type arguments for pair[A](expected[A], actual[Int, String]). -
Names must be distinct across clauses.
def bad[A](a: A)[A](b: A)is rejected withA is already defined as type A. -
Not on 3.3 LTS. Compiling
pairwith Scala 3.3.8 gives'=' expected, but '[' foundat the second type clause, and 3.5.2 gives the same error unless the language import above is present.
Pitfalls
-
A missing list is not a missing argument.
val h: Int = f(1)fails withFound: Int => Int, Required: Int, which usually means a parameter list was left off. -
Argument order inside one list matters for inference, as
foldOneListshowed; give the lambda its own list, last. -
_creates a multi-argument function, not a curried one:pace(7.5)(_: Int)(_: Int)takes two arguments at once. -
Clause interleaving is not on the 3.3 LTS line. A library that must compile on 3.3 cannot expose it.
Where the books are outdated
-
Books written for Scala 2 teach
add _oradd(1) _for partial application and explain "currying" as a workaround for weaker inference; both are unnecessary in Scala 3. -
The Scala 3 Book’s methods chapter and the Tour describe multiple parameter lists but not clause interleaving (3.6), which the Reference documents under "Generalized Method Syntax".
-
Material from before 3.6 says "type parameters must come first"; that rule no longer holds.