Functions in Scala: Higher-Order Functions, Currying and Anonymous Functions
Scala treats functions as values you can store in a variable, pass as an argument, and return from another function — the same way you’d treat an Int or a String. This isn’t a minor convenience; it’s the foundation the entire functional-programming half of the language is built on. This guide covers function definitions, anonymous functions, higher-order functions, and currying — the vocabulary for treating behavior itself as data.
Defining Functions With def
def add(a: Int, b: Int): Int = a + b
def greet(name: String): String = { val greeting = "Hello" s"$greeting, $name!"}def declares a method. For a single-expression body, the = form (def add(a: Int, b: Int): Int = a + b) is idiomatic — no braces needed, since the function body is just one expression. A multi-line body uses a { } block, whose value (per the previous guide’s coverage of block expressions) becomes the method’s return value automatically, with no return keyword needed.
// Default parameter valuesdef greet(name: String, greeting: String = "Hello"): String = s"$greeting, $name!"
greet("Alice") // "Hello, Alice!"greet("Alice", "Hey") // "Hey, Alice!"greet(name = "Alice", greeting = "Hi") // named arguments — order doesn't matter with namesgreet(greeting = "Hi", name = "Alice") // also valid — same resultNamed arguments let you call a function specifying which parameter each value belongs to, independent of declaration order — genuinely useful once a function has several parameters of the same type, where positional arguments alone become error-prone (swapping two same-typed arguments silently compiles but does the wrong thing).
Anonymous Functions (Function Literals)
val square = (x: Int) => x * xprintln(square(5)) // 25
val add = (a: Int, b: Int) => a + bprintln(add(2, 3)) // 5The (params) => body syntax defines a function value directly, without a name — assignable to a val like any other value, exactly as the earlier guide’s “everything is an object” framing implied. square here has type Int => Int (read: “a function from Int to Int”), which is a genuine, first-class type you can use anywhere a type is expected — as a parameter type, a return type, or a field type.
// Passing an anonymous function directly as an argument — extremely commonval numbers = List(1, 2, 3, 4, 5)val doubled = numbers.map(x => x * 2)val evens = numbers.filter(x => x % 2 == 0)
// Scala's underscore shorthand — a common abbreviation once the intent is clear from contextval doubledShort = numbers.map(_ * 2)val evensShort = numbers.filter(_ % 2 == 0)The _ (underscore) shorthand stands in for “the argument,” used when a function literal’s parameter is referenced exactly once and its type is unambiguous from context — _ * 2 means the same thing as x => x * 2. This is genuinely just abbreviation, not a different mechanism, and reaching for it is a matter of readability judgment: a short, single-use transformation reads cleanly with _, while anything with the parameter used more than once, or requiring a descriptive name for clarity, should use the explicit named form instead.
Higher-Order Functions — Functions That Take or Return Functions
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(x => x + 3, 10) // 16 — applies (x => x+3) twice: 10 -> 13 -> 16// Returning a function from a functiondef multiplier(factor: Int): Int => Int = { (x: Int) => x * factor}
val triple = multiplier(3)triple(5) // 15A higher-order function is simply a function that takes another function as a parameter, returns one, or both — applyTwice takes a function; multiplier returns one. This is what makes .map(), .filter(), and every other collection transformation possible: map itself is a higher-order function, defined once on the collection type, parameterized by whatever specific transformation you pass it.
Reusing applyTwice with entirely different behavior (x => x + 3 vs x => x * x) without touching applyTwice’s own definition is the whole point — the what to do (the function passed in) is decoupled from the how to apply it twice (applyTwice’s own logic), which is the core reusability benefit higher-order functions provide over hardcoding one specific operation.
Currying — Splitting Multiple Parameters Into Sequential Function Calls
// Ordinary multi-parameter functiondef add(a: Int, b: Int): Int = a + badd(2, 3) // 5
// Curried version — parameters split into separate parameter LISTSdef addCurried(a: Int)(b: Int): Int = a + baddCurried(2)(3) // 5 — looks similar, but is structurally very differentaddCurried(2) on its own is a valid, complete expression — it returns a new function of type Int => Int (specifically, “add 2 to whatever comes next”), which is then applied to 3 in the second set of parentheses. This is fundamentally different from add(2, 3), where both arguments must be supplied together, in one call, or not at all.
// Currying's actual practical value: creating specialized, partially-applied functionsdef addCurried(a: Int)(b: Int): Int = a + b
val add5 = addCurried(5) _ // partially applied — add5 is now Int => Intadd5(10) // 15add5(20) // 25
val increment = addCurried(1) _increment(41) // 42This is where currying earns its keep: addCurried(5) _ fixes the first argument and produces a new, reusable, specialized function (add5) — genuinely useful for building families of related functions from one general one, without writing add5 and increment as separate hand-written functions. The trailing _ here explicitly signals “treat this as a partially-applied function value,” a Scala-specific syntax quirk needed because otherwise the compiler would expect a second argument list to be supplied immediately.
// Currying is exactly why some standard library methods take multiple parameter listsList(1, 2, 3).foldLeft(0)((acc, x) => acc + x) // 6// ^ ^// first list second list — this shape exists specifically to help type inferencefoldLeft’s multi-parameter-list shape isn’t arbitrary — splitting the initial value (0) from the combining function (acc, x) => acc + x into separate parameter lists lets Scala’s type inference use the first list’s type to infer the second list’s function parameter types automatically, without needing explicit type annotations on acc and x. This is a genuinely practical reason currying appears throughout Scala’s standard library beyond the “partial application” use case shown above.
By-Name Parameters — Deferred Evaluation
def logIfDebug(condition: Boolean, message: => String): Unit = { if (condition) println(message)}
logIfDebug(debugEnabled, expensiveMessageComputation())message: => String (note the => before the type, not after) is a by-name parameter — instead of evaluating expensiveMessageComputation() before calling logIfDebug (as a normal parameter would), the expression is passed unevaluated and only computed the moment message is actually referenced inside the function body. If condition is false, expensiveMessageComputation() never runs at all — genuinely skipped, not just its result discarded.
// Contrast with an ordinary parameter, which is always evaluated eagerly, regardless of usedef logIfDebugEager(condition: Boolean, message: String): Unit = { if (condition) println(message)}
logIfDebugEager(false, expensiveMessageComputation())// expensiveMessageComputation() STILL runs here, even though its result is thrown away — wasted workThis distinction matters for exactly the case shown: expensive computations, logging that’s usually disabled, or lazy default values — by-name parameters let the caller pass an expression whose cost is only paid if it’s genuinely needed, which is a real, measurable difference in production logging code that’s disabled most of the time in a hot path.
Multiple Return Values via Tuples
def divideWithRemainder(a: Int, b: Int): (Int, Int) = (a / b, a % b)
val (quotient, remainder) = divideWithRemainder(17, 5)println(s"$quotient remainder $remainder") // "3 remainder 2"Since Scala has no dedicated multiple-return-value syntax the way some languages do, returning a tuple (covered in the previous guide) and destructuring it at the call site is the standard idiom — clean for a small, fixed number of related values, though a named case class (covered in its own guide) is the better choice once the returned values need clearer field names than _1/_2 would provide.
Recursion Instead of Loops
def factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)
def sumList(numbers: List[Int]): Int = numbers match { case Nil => 0 case head :: tail => head + sumList(tail)}// Tail recursion — the compiler can optimize this into a loop internally, avoiding stack growthimport scala.annotation.tailrec
@tailrecdef factorialTailRec(n: Int, acc: Int = 1): Int = if (n <= 1) acc else factorialTailRec(n - 1, n * acc)Idiomatic Scala reaches for recursion over manual mutable loops more often than Java or Python code typically does, aligning with the immutability-first style covered in depth in a later guide. The @tailrec annotation is worth knowing specifically because it’s a compile-time check, not just documentation: if the annotated function isn’t actually written in a tail-recursive form (the recursive call must be the very last operation, with nothing left to do after it returns), compilation fails with an explicit error — catching a function that would otherwise silently blow the stack on large inputs, before that ever happens in production.
Frequently Asked Questions
When should I use the underscore shorthand (_) instead of a named parameter in a lambda? When the transformation is short and the parameter is used exactly once — _ * 2 reads cleanly. Once a lambda’s parameter is used more than once, or a descriptive name would clarify what it represents, switch to the explicit x => ... form.
Is currying actually used in real Scala code, or is it mostly a functional-programming curiosity? It’s genuinely practical — beyond the partial-application pattern shown above, many standard library methods (foldLeft, foldRight) use multiple parameter lists specifically to improve type inference, and it’s common in configuration/builder-style APIs where fixing some parameters upfront and supplying the rest later is a natural fit.
What’s the actual performance cost of a by-name parameter? By-name parameters are implemented as a small wrapper function under the hood, evaluated each time they’re referenced — if referenced multiple times inside the function body, the underlying expression is recomputed each time, unlike a normal (eager) parameter, which is computed once upfront. If you need “compute once, but only if actually used,” a lazy val inside the function body wrapping the by-name parameter’s single use is the correct combination.
Why does addCurried(5) _ need a trailing underscore? Without it, Scala’s parser expects the second parameter list to follow immediately — the underscore explicitly tells the compiler “I want the partially-applied function value itself, not an error about a missing second argument list.”
Should I prefer recursion or a while loop for iterative logic? For anything reasonably transformable into a @tailrec-checked recursive function, recursion is the more idiomatic Scala style and pairs naturally with immutable data. For genuinely performance-critical hot loops, or logic that’s naturally imperative (polling, retry loops with side effects), a while loop with var state remains a legitimate, unapologetic choice — Scala’s hybrid design (from the first guide in this series) means neither is “wrong.”
What’s Next
Functions as values, higher-order functions, and currying are the functional-programming building blocks this series will keep returning to. Next: Classes and Objects in Scala, where we shift to the object-oriented half of the language — constructors, fields, and methods — and start building toward traits and case classes.