Skip to main content
Scala beginner Lesson 5 of 10

Functions as Values

Pass functions as arguments, return them from methods, compose them, and use by-name parameters to build your own control structures.

In Scala a function is a value: you can put one in a val, pass it to a method, return one, and store a list of them. Most of the collections API in lesson 2 exists because of this.

A function in a val

@main def run(): Unit =
  val double: Int => Int = x => x * 2
  val add: (Int, Int) => Int = (a, b) => a + b
  val describe: Double => String = amt => if amt > 50 then "large" else "small"

  println(double(21))
  println(add(3, 4))
  println(describe(75.0))
  println(List(1, 2, 3).map(double))
42
7
large
List(2, 4, 6)

Int => Int is a type like any other. The last line passes double where map expects a function — no wrapping needed, because it already is one.

Taking a function as a parameter

case class Order(id: Int, country: String, status: String, amount: Double)

val orders = List(
  Order(1001, "GB", "completed", 25.50),
  Order(1002, "US", "completed", 12.00),
  Order(1003, "GB", "returned",  40.00),
  Order(1004, "GB", "completed",  8.75),
)

def summarise(os: List[Order], keep: Order => Boolean, value: Order => Double): Double =
  os.filter(keep).map(value).sum

@main def run(): Unit =
  println(summarise(orders, _.status == "completed", _.amount))
  println(summarise(orders, _.country == "GB", _.amount))
  println(summarise(orders, _ => true, _ => 1.0))
46.25
74.25
4.0

One method, three questions, because the caller supplies the policy. That is the whole idea behind filter, map, sortBy and the rest — they take the varying part as a function.

Returning a function

def discountFor(tier: String): Double => Double = tier match
  case "gold"   => amount => amount * 0.80
  case "silver" => amount => amount * 0.90
  case _        => amount => amount

@main def run(): Unit =
  val gold = discountFor("gold")
  val none = discountFor("bronze")

  println(gold(100.0))
  println(none(100.0))
  println(orders.map(o => o.copy(amount = gold(o.amount))).map(_.amount))
80.0
100.0
List(20.4, 9.6, 32.0, 7.0)

discountFor runs the match once and returns a function specialised to that tier. Applying it to a million orders does not re-run the decision.

Closures

def counter(): () => Int =
  var n = 0
  () =>
    n += 1
    n

@main def run(): Unit =
  val next = counter()
  println(next())
  println(next())
  println(next())

  val other = counter()
  println(other())
1
2
3
1

The returned function captured n — it outlives the call to counter that created it. Each call to counter() gets a fresh n, which is why other starts at 1.

This is one of the few legitimate var uses, and it is also a warning: a closure over mutable state is not thread-safe, and two threads calling next() can see the same number.

Composition

@main def run(): Unit =
  val addVat: Double => Double = _ * 1.20
  val addShipping: Double => Double = _ + 3.99
  val round: Double => Double = a => BigDecimal(a).setScale(2, BigDecimal.RoundingMode.HALF_UP).toDouble

  val checkout = addVat andThen addShipping andThen round
  val reversed = round compose addShipping compose addVat

  println(checkout(25.50))
  println(reversed(25.50))

  val steps = List(addVat, addShipping, round)
  println(steps.reduce(_ andThen _)(25.50))
34.59
34.59
34.59

f andThen g applies f first; g compose f is the same thing written the other way round. The last line composes a list of transformations — the shape of a configurable pipeline where the steps are chosen at runtime.

Currying

def between(lo: Double)(hi: Double)(o: Order): Boolean =
  o.amount >= lo && o.amount <= hi

@main def run(): Unit =
  val midRange = between(10.0)(30.0)
  println(orders.filter(midRange).map(_.id))

  val cheap = between(0.0)
  println(orders.filter(cheap(10.0)).map(_.id))
List(1001)
List(1004)

Multiple parameter lists let you apply some arguments now and the rest later. The other reason to curry is type inference:

def process[A, B](xs: List[A])(f: A => B): List[B] = xs.map(f)

@main def run(): Unit =
  println(process(orders)(o => o.amount * 2))
List(51.0, 24.0, 80.0, 17.5)

A is fixed by the first list, so the compiler already knows o is an Order when it checks the lambda. With one combined list you would have to annotate it. This is exactly why foldLeft(z)(op) has two lists.

By-name parameters

def logIfEnabled(enabled: Boolean)(message: => String): Unit =
  if enabled then println(message)

def expensive(): String =
  println("  (building the message)")
  "order summary: " + orders.map(_.amount).sum

@main def run(): Unit =
  logIfEnabled(true)(expensive())
  println("---")
  logIfEnabled(false)(expensive())
  println("done")
  (building the message)
order summary: 86.25
---
done

With enabled = false, expensive() never ran — no “building the message” line. A by-name parameter (=> String) is evaluated where it is used, not at the call site. This is how logging libraries avoid paying for messages nobody prints.

It also lets you write things that look like syntax:

def retry[A](times: Int)(body: => A): A =
  var attempt = 1
  while true do
    try return body
    catch
      case e: Exception if attempt < times =>
        println(s"  attempt $attempt failed: ${e.getMessage}")
        attempt += 1
  throw new IllegalStateException("unreachable")

@main def run(): Unit =
  var calls = 0
  val result = retry(3) {
    calls += 1
    if calls < 3 then throw new RuntimeException(s"boom $calls")
    s"succeeded on attempt $calls"
  }
  println(result)
  attempt 1 failed: boom 1
  attempt 2 failed: boom 2
succeeded on attempt 3

retry(3) { ... } reads like a built-in construct. The block is passed unevaluated and run once per attempt.

PartialFunction

@main def run(): Unit =
  val statusLabel: PartialFunction[Order, String] =
    case o if o.status == "completed" => s"${o.id}: paid £${o.amount}"
    case o if o.status == "returned"  => s"${o.id}: refunded"

  println(orders.collect(statusLabel))
  println(statusLabel.isDefinedAt(Order(9, "GB", "pending", 1.0)))

  val amounts = List("25.50", "n/a", "12.00")
  println(amounts.collect { case s if s.toDoubleOption.isDefined => s.toDouble })
List(1001: paid £25.5, 1003: refunded, 1004: paid £8.75)
List(25.5, 12.0)

collect keeps only the elements the partial function is defined for — filter and map in one pass. Calling a partial function on an input it does not handle throws MatchError, so use collect, isDefinedAt, or applyOrElse rather than applying it directly.

Practice

1. Write a method taking a predicate and use it three ways.
def countWhere(os: List[Order])(p: Order => Boolean): Int = os.count(p)

println(countWhere(orders)(_.amount > 20))
println(countWhere(orders)(_.country == "GB"))
println(countWhere(orders)(o => o.status == "completed" && o.amount < 20))
2
3
2

One method, three questions. Adding a fourth needs no change to countWhere.

2. Compose two transformations and apply them to a list.
val pipeline = ((_: Double) * 1.2) andThen (_ + 3.99)
println(orders.map(o => pipeline(o.amount)))
List(34.59, 18.39, 51.99, 14.49)

The type annotation on the first lambda is needed because andThen gives the compiler nothing to infer from on its own.

3. Use a by-name parameter to skip expensive work.
  (building the message)
order summary: 86.25
---
done

The second call printed nothing from expensive(). Change the parameter to message: String and the side effect happens on both calls — proof that the => is doing the work.

4. Use collect with a partial function.
println(orders.collect { case Order(id, "GB", "completed", amt) => (id, amt) })
List((1001,25.5), (1004,8.75))

Non-matching elements are dropped rather than causing an error, which is the difference from map. It is the neatest way to filter by shape and extract fields in one step.

Next: traits and given instances — Scala 3’s approach to abstraction and type classes.

Frequently Asked Questions

What is the difference between a method and a function in Scala?
A method is defined with `def` and belongs to a class or object; a function is a value of type `Function1`, `Function2` and so on. Scala converts a method to a function automatically where one is expected — eta-expansion — so the distinction rarely surfaces.
What is a by-name parameter?
A parameter declared `x: => T` whose argument is not evaluated at the call site but each time it is used in the body. It is how you write logging that skips expensive message construction, and how library authors build control structures that look like language syntax.
What is currying used for in Scala?
Splitting parameters into multiple lists so some can be supplied now and the rest later. It also improves type inference — a type fixed in the first list is known when the compiler checks the lambda in the second, which is why `foldLeft` takes two lists.
What is a PartialFunction?
A function defined only for some inputs, written as a block of `case` clauses, with `isDefinedAt` to test applicability. `collect` uses it to filter and map in one step, keeping only the elements a case matches.