Scala - Currying with Examples

Currying converts a function that takes multiple arguments into a sequence of functions, each taking a single argument. Instead of `f(a, b, c)`, you get `f(a)(b)(c)`. This transformation enables...

Key Insights

  • Currying transforms multi-parameter functions into a chain of single-parameter functions, enabling partial application and creating specialized function variants from generic ones
  • Scala supports currying through multiple parameter lists and the curried method, making it seamless to build function pipelines and domain-specific abstractions
  • Currying excels in dependency injection, creating configurable functions, and building fluent APIs where context flows naturally through function chains

Understanding Currying Fundamentals

Currying converts a function that takes multiple arguments into a sequence of functions, each taking a single argument. Instead of f(a, b, c), you get f(a)(b)(c). This transformation enables powerful functional programming patterns.

// Standard function
def add(x: Int, y: Int): Int = x + y
add(3, 4) // 7

// Curried version
def addCurried(x: Int)(y: Int): Int = x + y
addCurried(3)(4) // 7

// Partial application
val add3 = addCurried(3) _
add3(4) // 7
add3(10) // 13

The curried version allows you to fix the first parameter and create specialized functions. The underscore after addCurried(3) tells Scala you want a partially applied function.

Converting Between Curried and Uncurried Forms

Scala provides built-in methods to convert between curried and uncurried function forms. This flexibility lets you adapt functions to different contexts.

// Uncurried function
val multiply: (Int, Int) => Int = (x, y) => x * y
multiply(3, 4) // 12

// Convert to curried form
val multiplyCurried = multiply.curried
multiplyCurried(3)(4) // 12

val multiplyBy5 = multiplyCurried(5)
multiplyBy5(10) // 50

// Convert back to uncurried
val multiplyUncurried = Function.uncurried(multiplyCurried)
multiplyUncurried(3, 4) // 12

The curried method works on functions with up to 22 parameters (Scala’s function arity limit). Use Function.uncurried to reverse the transformation.

Practical Pattern: Configuration and Context

Currying shines when you need to configure functions with context that remains constant across multiple invocations. This pattern appears frequently in database access, logging, and API clients.

case class DatabaseConfig(host: String, port: Int, timeout: Int)

// Curried function for database operations
def executeQuery(config: DatabaseConfig)(query: String)(params: Seq[Any]): Unit = {
  println(s"Connecting to ${config.host}:${config.port}")
  println(s"Executing: $query with params: $params")
  println(s"Timeout: ${config.timeout}ms")
}

// Create a configured executor
val prodConfig = DatabaseConfig("prod.db.com", 5432, 5000)
val prodExecutor = executeQuery(prodConfig) _

// Reuse the configured executor
prodExecutor("SELECT * FROM users")( Seq())
prodExecutor("SELECT * FROM orders WHERE id = ?")( Seq(123))

// Different configuration for testing
val testConfig = DatabaseConfig("localhost", 5432, 30000)
val testExecutor = executeQuery(testConfig) _
testExecutor("SELECT * FROM users")( Seq())

This pattern eliminates repetitive configuration passing and creates clear separation between setup and execution.

Building Fluent APIs with Multiple Parameter Lists

Multiple parameter lists in Scala enable DSL-like syntax. Each parameter list can serve a different purpose: configuration, context, and action.

object HttpClient {
  case class Request(method: String, url: String, headers: Map[String, String], body: String)
  
  def request(method: String)(url: String)(headers: Map[String, String] = Map.empty)
             (body: String = ""): Request = {
    Request(method, url, headers, body)
  }
}

// Create specialized request builders
val get = HttpClient.request("GET") _
val post = HttpClient.request("POST") _

// Use them fluently
val apiGet = get("https://api.example.com/users") _
val userRequest = apiGet(Map("Authorization" -> "Bearer token123"))()

val apiPost = post("https://api.example.com/users") _
val createUser = apiPost(Map("Content-Type" -> "application/json"))
createUser("""{"name": "John", "email": "john@example.com"}""")

Each parameter list adds specificity, creating a natural flow from general to specific.

Type-Safe Dependency Injection

Currying provides compile-time safe dependency injection without frameworks. Dependencies become function parameters, and partial application wires them together.

trait Logger {
  def log(message: String): Unit
}

trait MetricsCollector {
  def recordMetric(name: String, value: Double): Unit
}

class ConsoleLogger extends Logger {
  def log(message: String): Unit = println(s"[LOG] $message")
}

class SimpleMetrics extends MetricsCollector {
  def recordMetric(name: String, value: Double): Unit = 
    println(s"[METRIC] $name: $value")
}

// Business logic with curried dependencies
def processOrder(logger: Logger)(metrics: MetricsCollector)
                (orderId: String, amount: Double): Unit = {
  logger.log(s"Processing order $orderId")
  metrics.recordMetric("order.amount", amount)
  logger.log(s"Order $orderId completed")
}

// Wire dependencies
val logger = new ConsoleLogger
val metrics = new SimpleMetrics
val processWithDeps = processOrder(logger)(metrics) _

// Use the configured function
processWithDeps("ORD-123", 99.99)
processWithDeps("ORD-124", 149.50)

// Easy to test with mocks
class TestLogger extends Logger {
  var messages: List[String] = List.empty
  def log(message: String): Unit = messages = messages :+ message
}

val testLogger = new TestLogger
val testProcess = processOrder(testLogger)(metrics) _
testProcess("TEST-1", 10.0)
println(testLogger.messages) // List([LOG] Processing order TEST-1, ...)

This approach makes dependencies explicit and testing straightforward without reflection or runtime magic.

Currying with Higher-Order Functions

Combine currying with map, filter, and fold operations to create powerful data transformation pipelines.

case class Product(name: String, price: Double, category: String)

val products = List(
  Product("Laptop", 999.99, "Electronics"),
  Product("Mouse", 29.99, "Electronics"),
  Product("Desk", 299.99, "Furniture"),
  Product("Chair", 199.99, "Furniture")
)

// Curried filter predicates
def inCategory(category: String)(product: Product): Boolean = 
  product.category == category

def priceAbove(threshold: Double)(product: Product): Boolean = 
  product.price > threshold

def priceBelow(threshold: Double)(product: Product): Boolean = 
  product.price < threshold

// Create specialized filters
val electronicsFilter = inCategory("Electronics") _
val expensiveFilter = priceAbove(100) _
val affordableFilter = priceBelow(500) _

// Compose filters
val expensiveElectronics = products
  .filter(electronicsFilter)
  .filter(expensiveFilter)

println(expensiveElectronics) // List(Product(Laptop,999.99,Electronics))

// Curried transformations
def applyDiscount(percentage: Double)(product: Product): Product =
  product.copy(price = product.price * (1 - percentage / 100))

val apply20Percent = applyDiscount(20) _

val discountedFurniture = products
  .filter(inCategory("Furniture"))
  .map(apply20Percent)

discountedFurniture.foreach(p => println(s"${p.name}: ${p.price}"))
// Desk: 239.992
// Chair: 159.992

Curried predicates and transformations become reusable building blocks for complex data processing.

Performance Considerations

Currying introduces additional function objects and calls. For performance-critical code, measure before optimizing.

// Multiple function objects created
def curriedSum(a: Int)(b: Int)(c: Int): Int = a + b + c

// Single function call
def directSum(a: Int, b: Int, c: Int): Int = a + b + c

// Benchmark example (conceptual)
val iterations = 1000000

// Curried version creates intermediate functions
val start1 = System.nanoTime()
(1 to iterations).foreach(_ => curriedSum(1)(2)(3))
val time1 = System.nanoTime() - start1

// Direct version is faster for hot paths
val start2 = System.nanoTime()
(1 to iterations).foreach(_ => directSum(1, 2, 3))
val time2 = System.nanoTime() - start2

println(s"Curried: ${time1}ns, Direct: ${time2}ns")

Use currying for API design and code organization. Use direct calls in tight loops where microseconds matter. The JVM’s JIT compiler optimizes many cases, but profiling reveals actual bottlenecks.

Common Pitfalls

Avoid over-currying simple functions. Not every function benefits from currying.

// Unnecessary currying
def getName(first: String)(last: String): String = s"$first $last"

// Better as standard function
def getName(first: String, last: String): String = s"$first $last"

// Good use of currying: separating concerns
def formatWithTemplate(template: String)(values: Map[String, String]): String = {
  values.foldLeft(template) { case (result, (key, value)) =>
    result.replace(s"{$key}", value)
  }
}

val emailTemplate = formatWithTemplate("Hello {name}, your order {orderId} is ready") _
emailTemplate(Map("name" -> "Alice", "orderId" -> "123"))

Reserve currying for scenarios where partial application provides clear value: configuration, dependency injection, or building specialized functions from generic ones.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.