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 def for methods and val with 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.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.