Higher-order functions and currying

Scala · Practical · 12 min

Higher-order functions remove duplication and are the core of functional design in Scala.

A higher-order function takes a function as a parameter or returns one. map, filter and foldLeft are examples; writing your own lets you abstract over behaviour, not just values: def retry[A](n: Int)(action: => A): A, def timed[A](label: String)(body: => A): A.

Returning functions builds behaviour from parts: def multiplier(k: Int): Int => Int = x => x * k. The returned function is a closure that captures k.

Currying means splitting parameters into separate lists: def add(a: Int)(b: Int) = a + b. Supplying the first list gives a function waiting for the rest: val inc = add(1) (in Scala 3 this eta-expands automatically). Multiple parameter lists also help type inference: in foldLeft(0)(_ + _) the type Int is fixed by the first list before the lambda is checked. A function value can be curried with .curried and uncurried with Function.uncurried.

Composition combines functions: f andThen g applies f then g, f compose g applies g then f.

Partial functions (PartialFunction[A, B]) are defined only for some inputs, written as a block of case clauses. collect uses them to filter and map in one step, and isDefinedAt checks the domain.

Prefer small, pure functions combined this way; they're trivially testable.

Example

def multiplier(k: Int): Int => Int = x => x * k
val triple = multiplier(3)
println(triple(5))                               // 15

def add(a: Int)(b: Int): Int = a + b
val inc = add(1)
println(List(1, 2, 3).map(inc))                  // List(2, 3, 4)

val trim: String => String = _.trim
val shout: String => String = _.toUpperCase + "!"
println((trim andThen shout)("  hi "))           // HI!

def retry[A](times: Int)(action: => A): A =
  try action
  catch case e: Exception if times > 1 => retry(times - 1)(action)

var attempts = 0
val result = retry(3) { attempts += 1; if attempts < 3 then sys.error("fail") else "ok" }
println(s"$result after $attempts attempts")     // ok after 3 attempts

val parseEven: PartialFunction[String, Int] = {
  case s if s.toIntOption.exists(_ % 2 == 0) => s.toInt
}
println(List("2", "x", "7", "10").collect(parseEven))  // List(2, 10)

Remember

  • Functions can take and return functions (closures capture values)
  • Multiple parameter lists enable currying and better type inference
  • andThen/compose chain functions
  • collect with a partial function filters and maps at once

Watch out for

  • Calling a partial function on an input it's not defined for, which throws MatchError; check isDefinedAt or use lift.

Check yourself

  1. val f = (x: Int) => x + 1; val g = (x: Int) => x * 2. What is (f andThen g)(3)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scala-lang.org/scala3/book/fun-hofs.html