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.