Scala Functions, Lambdas, and Higher-Order Functions
Edited by Dev Kilaru, Senior Data Analyst. How we make our content
Introduction
Functions are central to Scala. You can define them with names, write them inline without names, store them in values, pass them to other functions, and return them from functions. Mastering this gives you the vocabulary for the collection operations in the next lesson, where you will transform data with map, filter, and fold instead of writing loops.
Prerequisites
- Values, types, and control flow from the previous lesson
Step-by-Step Guide
Step 1: Define a method with def
A method is defined with def, a name, typed parameters in parentheses, a return type after a colon, and a body after an equals sign. The value of the last expression is the result, so there is no return keyword in normal Scala code. Parameters must always have types, but the return type can often be inferred. Writing it anyway on public methods is good practice, because it documents the contract and keeps the compiler's inference from changing the type when you edit the body.
def add(a: Int, b: Int): Int = a + b
def greet(name: String): String =
val greeting = s"Hello, $name"
greeting.toUpperCase
println(add(2, 3)) // 5
println(greet("Asha")) // HELLO, ASHAStep 2: Use default and named arguments
Parameters can have default values, so callers can leave them out. Callers can also name the arguments, in any order, which makes calls with several parameters easier to read and prevents mixing up two values of the same type. Together they replace the many overloaded versions and builder classes that other languages need. When a function has a boolean flag, naming it at the call site makes the meaning obvious.
def connect(host: String, port: Int = 5432, secure: Boolean = true): String =
val scheme = if secure then "https" else "http"
s"$scheme://$host:$port"
connect("db.example.com") // https://db.example.com:5432
connect("db.example.com", secure = false) // http://db.example.com:5432
connect(port = 8080, host = "localhost")Step 3: Write anonymous functions
A function literal, also called a lambda, is written with parameters, an arrow, and a body. It has a function type such as Int => Int, which you can store in a val and call like a method. When the compiler already knows the expected parameter type, you can leave the type out. The underscore shorthand makes short lambdas even terser, where each underscore stands for the next parameter, but it is best kept for very simple expressions because heavy use hurts readability.
val double = (x: Int) => x * 2
val isEven: Int => Boolean = n => n % 2 == 0
println(double(21)) // 42
println(isEven(4)) // true
List(1, 2, 3).map(_ * 10) // List(10, 20, 30)Step 4: Pass functions to other functions
A higher-order function takes a function as a parameter or returns one. This lets you separate what to do from how to repeat it. In the example, applyTwice does not know whether it is doubling or adding, and the caller decides. A method can also be used wherever a function is expected, because Scala converts it automatically. This pattern underlies the collection methods you use daily, such as map and filter, which accept a function that is applied to each element.
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 10)) // 16
println(applyTwice(double, 5)) // 20
def isPositive(n: Int): Boolean = n > 0
List(-2, 5, 0, 8).filter(isPositive) // List(5, 8)Step 5: Return functions and capture variables
A function can build and return another function. The returned function remembers the values it used from the scope where it was created, which is called a closure. In the example, makeMultiplier produces a function that keeps its own factor. This is a clean way to create configured behavior without a class. Because the captured value is a val, the closure stays predictable. Capturing a var is allowed, but then the function's behavior can change under you, which is a strong reason to prefer vals.
def makeMultiplier(factor: Int): Int => Int =
x => x * factor
val triple = makeMultiplier(3)
println(triple(7)) // 21Step 6: Use recursion with @tailrec
Functional code often uses recursion instead of loops. A plain recursive call uses stack space for each level, so a very deep recursion can throw a stack overflow. If the recursive call is the very last thing the function does, which is called tail position, the compiler can turn it into a loop. Adding the tailrec annotation makes the compiler check this for you, and fail to compile if the function is not actually tail recursive. An inner helper with an accumulator parameter is the standard way to get there.
import scala.annotation.tailrec
def factorial(n: Int): BigInt =
@tailrec
def loop(i: Int, acc: BigInt): BigInt =
if i <= 1 then acc
else loop(i - 1, acc * i)
loop(n, BigInt(1))
println(factorial(20)) // 2432902008176640000FAQs
- What is the difference between a method and a function in Scala?
- A method is defined with def and belongs to a class, object, or scope. A function is a value of a function type like Int => Int that you can store and pass around. Scala converts a method into a function automatically when a function is expected, so the difference rarely matters in practice.
- Is the underscore shorthand good style?
- It is fine for very short lambdas like _ * 2 or _.name. If you use the same underscore twice, or the expression is longer, name the parameter instead. Readability counts more than saving a few characters.
- Why does Scala not have a return keyword in normal code?
- Because the last expression is the result. Using return is allowed but discouraged, since in lambdas it can behave surprisingly. Structuring a function as a single expression is clearer and avoids the problem.
Summary
You can define methods with default and named parameters, write lambdas, pass and return functions, understand closures, and write safe tail recursion. These ideas are the foundation for working with collections, where a small function describes the work and a collection method applies it to every element. That is the topic of the next lesson.
Related tutorials
- Schedule and Debug Shell Scripts: cron, bash -x, and Portability
- Shell Scripting Project: A Backup Script with Rotation and a Log Report
- Writing Safe Bash Scripts: set -euo pipefail, getopts, and trap
- Shell Pipes, Redirection, grep, sed, and awk for Text Processing
- Bash Loops, Arrays, and Functions Explained
Comments
Log in or create a free account to join the discussion.