Currying turns a function of several arguments into a chain of functions that each take one argument: (A, B) => C becomes A => B => C. The idea is named after Haskell Curry, though Moses Schönfinkel described it earlier. In Haskell every function is curried, so the idea is unavoidable. Scala is different: you can write curried function values, methods with several parameter lists, or ordinary methods, and the three behave differently in inference, evaluation and cost.

Fixing some arguments to get a smaller function, placeholder syntax and when fixed arguments are evaluated are covered in Scala partial application. How methods become function values is covered in Scala methods versus functions. This article is about currying itself: the types, the conversions, why Scala APIs split parameter lists the way they do, how to make each stage do useful work, and what it costs.

Advertisement

Curried function types

The function type arrow is right associative, so Int => Int => Int means Int => (Int => Int): a function that takes an Int and returns another function. Applying it twice is two calls to apply, and each intermediate result is an ordinary value you can store, pass or return.

val add: (Int, Int) => Int = (a, b) => a + b
val addC: Int => Int => Int = a => b => a + b

addC(2)(3)                     // 5: two apply calls
val plus2: Int => Int = addC(2) // stored intermediate stage
List(1, 2, 3).map(plus2)       // List(3, 4, 5)

// Standard conversions
val c1: Int => Int => Int = add.curried          // Function2 has curried and tupled
val u1: (Int, Int) => Int = Function.uncurried(addC)
val t1: ((Int, Int)) => Int = add.tupled          // takes a single pair
List((1, 2), (3, 4)).map(add.tupled)             // List(3, 7)

curried is defined on the function traits from Function2 upwards, and Function.uncurried and Function.untupled in the scala.Function object go back the other way for small arities. tupled is the one you will use most, because collections of pairs are common and map(add.tupled) avoids a pattern-matching lambda.

Methods with several parameter lists are not curried functions

A method such as def add(a: Int)(b: Int): Int looks curried, but it is one method. The compiler flattens the lists, so the JVM sees a single method taking two ints, and Java callers call it with both arguments at once. A full call add(1)(2) allocates nothing. You only get a function value when you stop early, which forces eta-expansion: Scala 2.13 requires you to ask for it with add(1) _ or an expected function type, while Scala 3 expands add(1) automatically when a function is needed.

The practical difference is when the body runs. A curried function value can run code at each stage. A method with several lists runs nothing until every list is supplied; eta-expansion just builds a closure that remembers the arguments so far. That matters as soon as a stage could prepare something expensive.

// Method: the regex is compiled on every call, even through a stored stage.
def matches(pattern: String)(s: String): Boolean = pattern.r.matches(s)
val isSkuSlow: String => Boolean = matches("[A-Z]{3}-[0-9]{4}")   // eta-expanded stage

// Curried value: the regex is compiled once, when the pattern arrives.
def matcher(pattern: String): String => Boolean = {
  val re = pattern.r
  s => re.matches(s)
}
val isSku: String => Boolean = matcher("[A-Z]{3}-[0-9]{4}")
skus.filter(isSku)   // one compile, n matches
Curried function value versus method with several parameter listsval price: Catalog => Rules => Order => Longprice(catalog)builds SKU index onceclosure 1captures indexclosure 2captures index, rulesLongper order(rules)(order)Each stage is an object; work placed before the inner lambda runs once per stage.def price(catalog: Catalog)(rules: Rules)(order: Order): LongCompiled formone JVM method price(c, r, o)Eta-expansion of price(c)closure that waits for r, oBody runsonly when all lists are filledNo allocation for direct calls, but nothing can be precomputed between lists.Choose the curried value when stages should do work; choose the method for plain calls.
A curried function value is a chain of objects, and work placed before the inner lambda runs once per stage. A method with several parameter lists compiles to one JVM method and runs its body only when every list is filled.
Advertisement

Parameter lists steer type inference

The most common reason Scala libraries split parameter lists is type inference, not currying. The compiler types arguments one parameter list at a time, and type parameters fixed by an earlier list are known when later lists are checked. The standard example is def foldLeft[B](z: B)(op: (B, A) => B): B on collections. Because z is in its own list, B is inferred from it first, and the lambda in the second list gets its parameter types for free.

val xs = List(1, 2, 3)
xs.foldLeft(0)((acc, x) => acc + x)          // B = Int from the first list; no annotations

xs.foldLeft(Nil)((acc, x) => x :: acc)        // Scala 2: B fixed to Nil.type, type mismatch
xs.foldLeft(List.empty[Int])((acc, x) => x :: acc)   // portable fix: give the type in list one

// Your own APIs: put what determines the type first, functions last.
def retryUntil[A](attempts: Int)(run: => A)(accept: A => Boolean): Option[A] =
  Iterator.continually(run).take(attempts).find(accept)

retryUntil(5)(fetchToken())(t => t.expiresInSeconds > 60)   // A known before the lambda

The same mechanism explains a design rule for your own APIs. If a function argument's parameter types depend on a type parameter, put the arguments that fix that type parameter in an earlier list and the function in a later one. Scala 2 also cannot infer lambda parameter types from other arguments in the same list, so map2(oa, ob, (a, b) => a + b) fails there with "missing parameter type"; Scala 3 infers more of these cases, but the split-list version works in both and reads better.

Block arguments and control abstractions

A parameter list with exactly one parameter can be supplied with braces instead of parentheses. Combined with a by-name parameter (=> A, evaluated each time it is used rather than once at the call site), the last list becomes a block, and your function reads like a built-in control structure. This is the second big reason for several parameter lists: the leading lists carry configuration and the last carries the code to run.

import scala.concurrent.duration._

def retry[A](times: Int, backoff: FiniteDuration)(body: => A): A = {
  var attempt = 1
  while (true) {
    try return body
    catch {
      case e: java.io.IOException if attempt < times =>
        Thread.sleep(backoff.toMillis * attempt)
        attempt += 1
    }
  }
  throw new IllegalStateException("unreachable")
}

val page = retry(times = 3, backoff = 200.millis) {
  fetchPage("https://example.com/catalog")   // re-evaluated on each attempt
}

The standard library uses the same shape: scala.util.Using.resource(open())(r => ...) closes the resource after the block, and Future(...) takes its execution context in a separate implicit list. Two cautions apply. A by-name parameter is re-evaluated every time it is referenced, so reference it once per intended execution. And return inside a lambda is a non-local return, deprecated in Scala 3; the example uses it inside a method body, which is fine.

Later lists can depend on earlier parameters

Each parameter list is in scope for the lists after it. That gives three abilities a single list does not have. Defaults can refer to earlier parameters: def window(start: Long)(end: Long = start + 3600) is legal. Types can refer to earlier parameters, the dependent method types feature: in def put(store: Store)(key: store.Key, value: store.Value) the second list's types are chosen by the actual store passed. Context parameters go in their own list: in Scala 2 a single implicit list must come last, while Scala 3 allows using clauses alongside other lists, which is how type class evidence is threaded through APIs. The given and using mechanics are covered in Scala 3 contextual abstraction.

trait Store {
  type Key
  type Value
  def write(k: Key, v: Value): Unit
}

// The second list's types are chosen by the store passed in the first.
def put(store: Store)(key: store.Key, value: store.Value): Unit = store.write(key, value)

// A default that reads an earlier list.
def window(start: Long)(end: Long = start + 3600): (Long, Long) = (start, end)
window(1000)()      // (1000, 4600)

Neither trick works with a single parameter list, because parameters in the same list cannot see each other. That is a reason to split lists even when nobody will ever partially apply the method.

Worked example: a validation pipeline configured once

An order service validates incoming orders against rules loaded from configuration at startup. Each rule is a function built by currying its settings into it, the pipeline is built once, and the per-order path only applies functions. The code is written for Scala 3; in Scala 2.13, wrap the definitions in an object.

final case class Order(id: String, country: String, totalCents: Long, lines: Int)
type Check = Order => Either[String, Order]

def maxTotal(limitCents: Long): Check =
  o => if (o.totalCents <= limitCents) Right(o) else Left(s"${o.id}: total above $limitCents")

def allowedCountries(codes: Set[String]): Check = {
  val upper = codes.map(_.toUpperCase)              // normalised once, at build time
  o => if (upper(o.country.toUpperCase)) Right(o) else Left(s"${o.id}: ships to ${o.country}")
}

def maxLines(n: Int): Check =
  o => if (o.lines <= n) Right(o) else Left(s"${o.id}: ${o.lines} lines")

def validate(checks: List[Check])(o: Order): Either[String, Order] =
  checks.foldLeft[Either[String, Order]](Right(o))((acc, check) => acc.flatMap(check))

// Startup: configuration is curried in exactly once.
val checks = List(maxTotal(cfg.maxCents), allowedCountries(cfg.countries), maxLines(cfg.maxLines))
val check: Order => Either[String, Order] = validate(checks)   // expected type triggers eta-expansion

// Request path
val results = incoming.map(check)

Three things are worth noticing. The rule builders are curried function values in disguise: a method taking settings and returning Check, with the setup work (upper) done before the returned lambda. validate is a method with two lists, so check is a cheap closure over the list of rules. And the foldLeft gives its type parameter explicitly, because Right(o) alone would infer a type too narrow for the accumulator. Each rule closes over only its settings; closure capture is covered in Scala closures.

Cost model

A full call to a method with several parameter lists costs the same as a call with one list. A curried function value costs one object per stage when it is built and one virtual apply call per stage when it is used. In the validation example that is a few objects at startup and a handful of calls per order, which is nothing. In a tight numeric loop, f(a)(b)(c) on a curried value creates two intermediate closures per iteration; the JIT can often remove them through inlining and escape analysis, but only when the call site is monomorphic and small enough. If a profiler shows closure allocation in a hot loop, apply the early stages outside the loop or switch to a method.

Primitive arguments add boxing. Scala 2 specialises Function1 for a few primitive types, but deeper curried chains of generic functions box Ints and Longs at each stage. Measure with JMH before and after, rather than assuming.

Currying or something else

Currying is one of several ways to supply some inputs early and the rest later, and the alternatives are often clearer. A class with a constructor fixes dependencies once and exposes named methods, which suits services with several operations. A case class of settings passed as one argument keeps names visible when there are many options. Context parameters suit values that are threaded everywhere and rarely chosen by hand, such as an execution context or type class evidence. Curried functions fit best when the result is genuinely a single function that will be passed around, such as a rule, a predicate or a request handler, and when the early stage does real work.

Pitfalls

  • Expecting staged work from a method. Partially applying def f(a)(b) does no work with a; the body still runs in full on every final call. Return a lambda after the setup code if the stage must do work.
  • Overloading with several lists. Scala 2 resolves overloads using only the first parameter list, so overloads that differ in later lists are ambiguous. Scala 3 can consult later lists, but avoid relying on it.
  • Inference surprises. foldLeft(Nil) and foldLeft(None) infer singleton types in Scala 2. Give the type in the first list.
  • Java interop. Java sees one flattened method and, for curried values, nested Function1 objects. Expose a plain method for Java callers.
  • Readability. A => B => C => D => E is hard to read and its stages are positional. Past three stages, a case class of settings or a small class with a constructor is clearer.

What to do next

  1. Find places where you call the same function with the same leading arguments many times, and curry those arguments into a stored stage.
  2. For each curried builder, move expensive setup (regex compilation, index building, normalisation) before the returned lambda, and check the method form is not silently redoing it.
  3. Review your public APIs: put arguments that fix type parameters first and function arguments last, so callers write lambdas without annotations.
  4. Turn retry, timing and resource-handling helpers into block-argument functions with a by-name last list.
  5. Replace foldLeft(Nil) and foldLeft(None) with typed empties across the codebase.
  6. Profile hot paths that apply curried values inside loops and hoist the early stages out.
Key takeaway: Currying turns a multi-argument function into a chain of single-argument functions, and in Scala it appears in three forms: curried function values with conversions such as curried, uncurried and tupled; methods with several parameter lists, which compile to one method; and eta-expanded stages of those methods. Use parameter lists to steer type inference, to take block arguments and to let later lists depend on earlier parameters. Use curried values when a stage should do work once, such as compiling a regex or building an index, and remember that a partially applied method does no work until its last list arrives.