Scala - Closures with Examples

A closure is a function that references variables from outside its own scope. When a function captures variables from its surrounding context, it 'closes over' those variables, creating a closure....

Key Insights

  • Closures in Scala are functions that capture and reference variables from their enclosing scope, maintaining access to these variables even after the outer scope has finished executing
  • Understanding closures is essential for functional programming patterns like partial application, currying, and callback mechanisms where state needs to be preserved across function invocations
  • Scala’s closure implementation handles both immutable and mutable variable capture, with important implications for concurrent programming and state management

What Are Closures

A closure is a function that references variables from outside its own scope. When a function captures variables from its surrounding context, it “closes over” those variables, creating a closure. The function maintains access to these variables even when executed in a different scope or after the original scope has terminated.

def makeAdder(x: Int): Int => Int = {
  (y: Int) => x + y
}

val add5 = makeAdder(5)
val add10 = makeAdder(10)

println(add5(3))   // Output: 8
println(add10(3))  // Output: 13

In this example, the anonymous function (y: Int) => x + y is a closure because it references x from the enclosing makeAdder function’s scope. Each call to makeAdder creates a new closure with its own captured value of x.

Variable Capture Mechanics

Closures capture variables by reference, not by value. This distinction becomes critical when dealing with mutable variables.

var factor = 2

val multiplier: Int => Int = (x: Int) => x * factor

println(multiplier(5))  // Output: 10

factor = 3
println(multiplier(5))  // Output: 15

The closure captures the reference to factor, so changes to the variable affect subsequent closure invocations. This behavior differs from languages that capture by value.

For immutable values, the distinction is less apparent but still important:

def createCounters(start: Int): (Int => Int, Int => Int) = {
  var count = start
  
  val increment: Int => Int = (n: Int) => {
    count += n
    count
  }
  
  val decrement: Int => Int = (n: Int) => {
    count -= n
    count
  }
  
  (increment, decrement)
}

val (inc, dec) = createCounters(10)
println(inc(5))   // Output: 15
println(dec(3))   // Output: 12
println(inc(2))   // Output: 14

Both closures share the same count variable, demonstrating that closures capture the actual variable binding, not a copy.

Practical Applications in Collections

Closures shine when working with Scala’s collection APIs, enabling elegant filtering, mapping, and transformation operations.

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

val products = List(
  Product("Laptop", 1200.0, "Electronics"),
  Product("Mouse", 25.0, "Electronics"),
  Product("Desk", 300.0, "Furniture"),
  Product("Chair", 150.0, "Furniture")
)

def createCategoryFilter(category: String): Product => Boolean = {
  product => product.category == category
}

def createPriceRangeFilter(min: Double, max: Double): Product => Boolean = {
  product => product.price >= min && product.price <= max
}

val electronicsFilter = createCategoryFilter("Electronics")
val affordableFilter = createPriceRangeFilter(0, 500)

val electronics = products.filter(electronicsFilter)
val affordable = products.filter(affordableFilter)
val affordableElectronics = products.filter(p => 
  electronicsFilter(p) && affordableFilter(p)
)

println(electronics.map(_.name))              // List(Laptop, Mouse)
println(affordable.map(_.name))               // List(Mouse, Desk, Chair)
println(affordableElectronics.map(_.name))    // List(Mouse)

This pattern creates reusable, composable filters that maintain their configuration state through closures.

Closures in Event Handling and Callbacks

Closures are fundamental to event-driven programming and asynchronous operations, where callbacks need to maintain context.

import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.ExecutionContext.Implicits.global

class DataProcessor(val processorId: String) {
  private var processedCount = 0
  
  def processAsync(data: List[Int]): Future[String] = {
    Future {
      val result = data.map(_ * 2).sum
      processedCount += data.length
      s"Processor $processorId: Processed $processedCount items, result: $result"
    }
  }
  
  def createLogger(prefix: String): String => Unit = {
    message => println(s"[$processorId][$prefix] $message")
  }
}

val processor = new DataProcessor("PROC-001")
val logger = processor.createLogger("INFO")

processor.processAsync(List(1, 2, 3, 4, 5)).foreach(logger)
processor.processAsync(List(10, 20, 30)).foreach(logger)

// Output (order may vary due to async execution):
// [PROC-001][INFO] Processor PROC-001: Processed 5 items, result: 30
// [PROC-001][INFO] Processor PROC-001: Processed 8 items, result: 120

The logger closure captures both processorId and prefix, while the Future’s closure captures the mutable processedCount variable.

Closure Scope and Lifetime

Understanding closure lifetime is crucial for avoiding memory leaks and unexpected behavior.

class ResourceManager {
  private val resources = scala.collection.mutable.Map[String, String]()
  
  def createResourceAccessor(resourceId: String): () => Option[String] = {
    resources(resourceId) = s"Resource-$resourceId"
    
    () => resources.get(resourceId)
  }
  
  def removeResource(resourceId: String): Unit = {
    resources.remove(resourceId)
  }
}

val manager = new ResourceManager
val accessor1 = manager.createResourceAccessor("R1")
val accessor2 = manager.createResourceAccessor("R2")

println(accessor1())  // Some(Resource-R1)
println(accessor2())  // Some(Resource-R2)

manager.removeResource("R1")

println(accessor1())  // None - closure sees the updated state
println(accessor2())  // Some(Resource-R2)

The closures maintain references to the resources map, so they reflect changes made through the manager. This can prevent garbage collection if not managed carefully.

Closures vs. Function Objects

Scala implements closures as function objects under the hood. Understanding this helps optimize performance-critical code.

// Closure approach
def multiplyBy(factor: Int): Int => Int = {
  x => x * factor
}

// Equivalent explicit function object
class Multiplier(factor: Int) extends Function1[Int, Int] {
  def apply(x: Int): Int = x * factor
}

val closureMultiplier = multiplyBy(5)
val objectMultiplier = new Multiplier(5)

println(closureMultiplier(10))  // 50
println(objectMultiplier(10))   // 50

// Performance consideration: reusing closures
val numbers = (1 to 1000000).toList
val multiplier = multiplyBy(3)  // Create once

// Efficient: reuses the same closure
val result1 = numbers.map(multiplier)

// Less efficient: creates new closure on each iteration
val result2 = numbers.map(x => multiplyBy(3)(x))

Creating closures inside hot loops can impact performance due to object allocation overhead.

Thread Safety Considerations

Closures that capture mutable state require careful handling in concurrent environments.

import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

// Unsafe: mutable variable captured by closure
def unsafeCounter(): () => Int = {
  var count = 0
  () => {
    count += 1
    count
  }
}

// Safe: using atomic operations
def safeCounter(): () => Int = {
  val count = new java.util.concurrent.atomic.AtomicInteger(0)
  () => count.incrementAndGet()
}

val unsafe = unsafeCounter()
val safe = safeCounter()

val unsafeFutures = Future.sequence((1 to 100).map(_ => Future(unsafe())))
val safeFutures = Future.sequence((1 to 100).map(_ => Future(safe())))

val unsafeResult = Await.result(unsafeFutures, 5.seconds)
val safeResult = Await.result(safeFutures, 5.seconds)

println(s"Unsafe max: ${unsafeResult.max}")  // Often less than 100
println(s"Safe max: ${safeResult.max}")      // Always 100

When closures capture mutable state accessed from multiple threads, use thread-safe constructs or immutable data structures.

Best Practices

Keep closure scope minimal by capturing only necessary variables. Avoid capturing entire objects when only specific fields are needed.

// Poor: captures entire configuration object
class ConfigurableService(config: AppConfig) {
  def createProcessor(): String => String = {
    input => s"${config.prefix}$input${config.suffix}"
  }
}

// Better: captures only required fields
class OptimizedService(config: AppConfig) {
  def createProcessor(): String => String = {
    val prefix = config.prefix
    val suffix = config.suffix
    input => s"$prefix$input$suffix"
  }
}

This approach reduces memory footprint and makes closure dependencies explicit, improving code maintainability and testability.

Liked this? There's more.

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