Scala - Functions - Define and Call
The `def` keyword defines methods in Scala. These are the most common way to create reusable code blocks:
Key Insights
- Scala functions are first-class citizens that can be assigned to variables, passed as parameters, and returned from other functions, making them more flexible than traditional methods
- Function definitions use
deffor methods andvalwith lambda syntax for function values, each serving different purposes in functional programming patterns - Understanding the distinction between methods (evaluated on call), function values (objects), and by-name parameters (lazy evaluation) is critical for writing idiomatic Scala code
Basic Function Definition with def
The def keyword defines methods in Scala. These are the most common way to create reusable code blocks:
def greet(name: String): String = {
s"Hello, $name!"
}
// Call the function
val message = greet("Alice")
println(message) // Output: Hello, Alice!
For single-expression functions, omit the curly braces:
def add(x: Int, y: Int): Int = x + y
println(add(5, 3)) // Output: 8
Scala infers return types, but explicit typing improves readability for public APIs:
def multiply(x: Int, y: Int) = x * y // Type inferred as Int
def divide(x: Double, y: Double): Double = x / y // Explicit return type
Function Values and Lambda Syntax
Function values are objects that implement FunctionN traits. Assign them to variables using lambda syntax:
val square: Int => Int = (x: Int) => x * x
println(square(4)) // Output: 16
Shortened syntax when types are inferred:
val numbers = List(1, 2, 3, 4, 5)
// Full syntax
val doubled = numbers.map((x: Int) => x * 2)
// Type inference
val tripled = numbers.map(x => x * 3)
// Underscore shorthand for single parameter
val quadrupled = numbers.map(_ * 4)
println(doubled) // Output: List(2, 4, 6, 8, 10)
println(tripled) // Output: List(3, 6, 9, 12, 15)
println(quadrupled) // Output: List(4, 8, 12, 16, 20)
Multi-parameter function values:
val combine: (Int, Int) => Int = (x, y) => x + y
println(combine(10, 20)) // Output: 30
// Using with higher-order functions
val result = List(1, 2, 3).foldLeft(0)(combine)
println(result) // Output: 6
Methods vs Function Values
Methods and function values behave differently. Methods are not objects; function values are:
def methodAdd(x: Int, y: Int): Int = x + y
val functionAdd: (Int, Int) => Int = (x, y) => x + y
// Convert method to function value (eta expansion)
val methodAsFunction = methodAdd _
// All three work the same when called
println(methodAdd(2, 3)) // Output: 5
println(functionAdd(2, 3)) // Output: 5
println(methodAsFunction(2, 3)) // Output: 5
// But only function values are objects
val functions = List(functionAdd, methodAsFunction)
// functions.foreach(f => println(f(1, 1)))
This distinction matters for partial application and higher-order functions:
def processData(data: List[Int], operation: Int => Int): List[Int] = {
data.map(operation)
}
def increment(x: Int): Int = x + 1
// Automatic eta expansion in function parameter position
val result1 = processData(List(1, 2, 3), increment)
println(result1) // Output: List(2, 3, 4)
// Explicit conversion
val result2 = processData(List(1, 2, 3), increment _)
println(result2) // Output: List(2, 3, 4)
Default Parameters and Named Arguments
Default parameters eliminate overloading:
def createUser(name: String, age: Int = 18, active: Boolean = true): String = {
s"User: $name, Age: $age, Active: $active"
}
println(createUser("Bob")) // Output: User: Bob, Age: 18, Active: true
println(createUser("Alice", 25)) // Output: User: Alice, Age: 25, Active: true
println(createUser("Charlie", 30, false)) // Output: User: Charlie, Age: 30, Active: false
Named arguments improve clarity and allow parameter reordering:
println(createUser(name = "David", active = false, age = 40))
// Output: User: David, Age: 40, Active: false
def connect(host: String, port: Int = 8080, timeout: Int = 5000): String = {
s"Connecting to $host:$port with timeout $timeout ms"
}
println(connect("localhost"))
// Output: Connecting to localhost:8080 with timeout 5000 ms
println(connect("api.example.com", timeout = 10000))
// Output: Connecting to api.example.com:8080 with timeout 10000 ms
Variable-Length Arguments (Varargs)
Use * to accept variable arguments:
def sum(numbers: Int*): Int = {
numbers.foldLeft(0)(_ + _)
}
println(sum(1, 2, 3)) // Output: 6
println(sum(10, 20, 30, 40)) // Output: 100
// Pass a sequence using :_*
val nums = List(5, 10, 15)
println(sum(nums: _*)) // Output: 30
Combine with regular parameters (varargs must be last):
def formatList(prefix: String, items: String*): String = {
items.map(item => s"$prefix$item").mkString(", ")
}
println(formatList("- ", "Apple", "Banana", "Cherry"))
// Output: - Apple, - Banana, - Cherry
Higher-Order Functions
Functions that accept or return other functions:
def applyTwice(f: Int => Int, x: Int): Int = {
f(f(x))
}
def addTwo(x: Int): Int = x + 2
println(applyTwice(addTwo, 5)) // Output: 9 (5 + 2 + 2)
// With anonymous functions
println(applyTwice(x => x * 2, 3)) // Output: 12 (3 * 2 * 2)
Returning functions:
def multiplier(factor: Int): Int => Int = {
(x: Int) => x * factor
}
val triple = multiplier(3)
val double = multiplier(2)
println(triple(5)) // Output: 15
println(double(10)) // Output: 20
// Practical example: creating validators
def rangeValidator(min: Int, max: Int): Int => Boolean = {
(value: Int) => value >= min && value <= max
}
val isValidAge = rangeValidator(0, 120)
val isValidPercentage = rangeValidator(0, 100)
println(isValidAge(25)) // Output: true
println(isValidPercentage(150)) // Output: false
Currying and Partial Application
Currying transforms multi-parameter functions into chains of single-parameter functions:
def add(x: Int)(y: Int): Int = x + y
println(add(5)(3)) // Output: 8
// Partial application
val add5 = add(5) _
println(add5(3)) // Output: 8
println(add5(10)) // Output: 15
Practical use case with configuration:
def query(connection: String)(timeout: Int)(sql: String): String = {
s"Executing '$sql' on $connection with timeout $timeout ms"
}
// Configure connection and timeout once
val prodQuery = query("prod-db")(5000) _
println(prodQuery("SELECT * FROM users"))
// Output: Executing 'SELECT * FROM users' on prod-db with timeout 5000 ms
println(prodQuery("SELECT * FROM orders"))
// Output: Executing 'SELECT * FROM orders' on prod-db with timeout 5000 ms
By-Name Parameters
By-name parameters use => Type syntax and evaluate lazily:
def measure(block: => Unit): Long = {
val start = System.nanoTime()
block // Evaluated here
System.nanoTime() - start
}
val elapsed = measure {
Thread.sleep(100)
println("Task completed")
}
println(s"Elapsed: ${elapsed / 1000000} ms")
Difference between by-value and by-name:
def byValue(x: Int): Unit = {
println(s"First call: $x")
println(s"Second call: $x")
}
def byName(x: => Int): Unit = {
println(s"First call: $x")
println(s"Second call: $x")
}
var counter = 0
def increment(): Int = {
counter += 1
counter
}
byValue(increment())
// Output:
// First call: 1
// Second call: 1
counter = 0
byName(increment())
// Output:
// First call: 1
// Second call: 2
This distinction enables control structures like custom conditionals:
def unless(condition: Boolean)(block: => Unit): Unit = {
if (!condition) block
}
var x = 5
unless(x > 10) {
println(s"x is not greater than 10: $x")
}
// Output: x is not greater than 10: 5
Recursive Functions
Recursive functions require explicit return types:
def factorial(n: Int): Int = {
if (n <= 1) 1
else n * factorial(n - 1)
}
println(factorial(5)) // Output: 120
// Tail-recursive version for better performance
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, accumulator: Int = 1): Int = {
if (n <= 1) accumulator
else factorialTail(n - 1, n * accumulator)
}
println(factorialTail(5)) // Output: 120
The @tailrec annotation ensures compiler optimization and fails compilation if tail recursion isn’t possible, preventing stack overflow on large inputs.