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 function Int => 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 reports Missing 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

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

def add(a: Int, b: Int)

method, one list

needs a lambda: b => add(1, b), or add(1, _)

def add(a: Int)(b: Int)

method, two lists

add(1) (eta-expanded automatically)

val add: Int => Int => Int

function value, curried

add(1) (an ordinary application)

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") reports Too 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 with A is already defined as type A.

  • Not on 3.3 LTS. Compiling pair with Scala 3.3.8 gives '=' expected, but '[' found at 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 with Found: Int => Int, Required: Int, which usually means a parameter list was left off.

  • Argument order inside one list matters for inference, as foldOneList showed; 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 _ or add(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.