Intermediate

Functions in Scala

Learn how to define and use functions in Scala - methods, parameters, default arguments, recursion, higher-order functions, and closures with Docker-ready examples

Functions are where Scala’s dual nature really shines. As a language that unifies object-oriented and functional programming, Scala treats functions as both named blocks of behavior (methods, declared with def) and as honest-to-goodness values that can be stored in variables, passed as arguments, and returned from other functions. This is the heart of functional programming, and it’s available to you from the very first line of Scala you write.

In Java, a method is something attached to a class—it isn’t a value you can hand around. Scala keeps def methods but adds first-class function values on top, so x => x * 2 is an expression with a type (Int => Int) just like 42 has the type Int. This unlocks higher-order functions, closures, and the elegant collection operations (map, filter, reduce) that make Scala code so concise.

In this tutorial you’ll learn how to define methods, supply parameters and return values, use default and named arguments, write recursive functions, and—most importantly for a functional language—pass functions as arguments and return them as results.

Defining and Calling Functions

A Scala method is introduced with def, followed by a name, a parameter list with explicit types, an optional return type, and a body after =. Because Scala has powerful type inference, the return type can often be omitted—but it’s good practice (and required for recursive functions) to declare it.

Create a file named Functions.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
object Functions {

  // A simple method: typed parameters and an explicit return type.
  // The last expression in the body is automatically returned — no `return` keyword needed.
  def add(a: Int, b: Int): Int = a + b

  // A multi-line body uses braces; the final expression is the result.
  def greet(name: String): String = {
    val message = s"Hello, $name!"
    message
  }

  // Default parameter values: `exponent` defaults to 2 if not supplied.
  def power(base: Int, exponent: Int = 2): Int = {
    var result = 1
    for (_ <- 1 to exponent) result *= base
    result
  }

  // Recursion: a method may call itself. The return type is mandatory here.
  def factorial(n: Int): Int =
    if (n <= 1) 1
    else n * factorial(n - 1)

  // Higher-order function: `f` is itself a function of type Int => Int.
  def applyTwice(f: Int => Int, x: Int): Int = f(f(x))

  // Returning a function. The returned lambda "closes over" `factor` (a closure).
  def multiplier(factor: Int): Int => Int =
    (x: Int) => x * factor

  def main(args: Array[String]): Unit = {
    // Calling methods by name with positional arguments.
    println(s"add(3, 4) = ${add(3, 4)}")
    println(greet("Scala"))

    // Default parameter in action.
    println(s"power(5) = ${power(5)}")           // exponent defaults to 2
    println(s"power(2, 10) = ${power(2, 10)}")

    // Named arguments let you pass parameters in any order.
    println(s"power(exponent = 3, base = 2) = ${power(exponent = 3, base = 2)}")

    // Recursion.
    println(s"factorial(5) = ${factorial(5)}")

    // Higher-order functions: pass a function literal (lambda).
    // `_ + 3` is shorthand for `x => x + 3`.
    println(s"applyTwice(_ + 3, 10) = ${applyTwice(_ + 3, 10)}")

    // Closures: the returned function remembers the captured `factor`.
    val triple = multiplier(3)
    println(s"triple(7) = ${triple(7)}")

    // Functions are first-class values, so collections accept them directly.
    val numbers = List(1, 2, 3, 4, 5)
    val doubled = numbers.map(n => n * 2)
    val evens = numbers.filter(_ % 2 == 0)
    val total = numbers.reduce(_ + _)
    println(s"doubled = $doubled")
    println(s"evens = $evens")
    println(s"total = $total")
  }
}

This single file demonstrates the full progression: a plain method, a multi-line body, default and named arguments, recursion, a higher-order function, a closure, and first-class functions used with collections. Notice there is no return keyword anywhere—in Scala the value of the last expression is the result, which is why methods read like mathematical definitions.

Scope and Nested Functions

Scala is block-structured: a value or function is visible only within the block where it’s defined. Functions can be nested inside other functions, which is a clean way to hide helper logic that callers shouldn’t see. This example also highlights the distinction between a def method and a function value stored in a val.

Create a file named Scope.scala:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
object Scope {

  // Visible to every method in this object.
  val taxRate = 0.10

  def checkout(price: Double): Double = {
    // `tax` is a nested function — it exists only inside `checkout`
    // and can see both `price` and the enclosing `taxRate`.
    def tax(amount: Double): Double = amount * taxRate
    val withTax = price + tax(price)
    withTax
  }

  def main(args: Array[String]): Unit = {
    println(s"checkout(100.0) = ${checkout(100.0)}")

    // A function VALUE (stored in a val) versus a METHOD (declared with def).
    val square: Int => Int = x => x * x   // first-class function value
    def cube(x: Int): Int = x * x * x     // ordinary method

    println(s"square(4) = ${square(4)}")
    println(s"cube(4) = ${cube(4)}")

    // Because `square` is a value, it can be passed around like any other value.
    val operations = List(square, (x: Int) => x + 1)
    println(s"applied to 5: ${operations.map(op => op(5))}")
  }
}

The nested tax function keeps the calculation private to checkout, while still reaching outward to read taxRate—that outward reach is lexical scoping. The square value and the cube method behave identically when called, but only square can be dropped into a List and mapped over, because it’s a value.

Running with Docker

You can run both examples with the official Scala CLI image—no local Scala installation required.

1
2
3
4
5
6
7
8
# Pull the official image
docker pull virtuslab/scala-cli:latest

# Run the functions example
docker run --rm -v $(pwd):/app -w /app virtuslab/scala-cli:latest run Functions.scala

# Run the scope example
docker run --rm -v $(pwd):/app -w /app virtuslab/scala-cli:latest run Scope.scala

Expected Output

Running Functions.scala produces:

add(3, 4) = 7
Hello, Scala!
power(5) = 25
power(2, 10) = 1024
power(exponent = 3, base = 2) = 8
factorial(5) = 120
applyTwice(_ + 3, 10) = 16
triple(7) = 21
doubled = List(2, 4, 6, 8, 10)
evens = List(2, 4)
total = 15

Running Scope.scala produces:

checkout(100.0) = 110.0
square(4) = 16
cube(4) = 64
applied to 5: List(25, 6)

Key Concepts

  • def defines methods — A method takes a typed parameter list and returns the value of its last expression; there is no return keyword in idiomatic Scala.
  • Functions are first-class values — A lambda like x => x * 2 has the type Int => Int and can be stored in a val, passed as an argument, or returned from another function.
  • Higher-order functions — Methods such as applyTwice accept other functions as parameters, and collection methods like map, filter, and reduce are higher-order functions you’ll use constantly.
  • Default and named arguments — Parameters can declare default values (exponent: Int = 2), and callers can pass arguments by name in any order for clarity.
  • Recursion needs a return type — The compiler can’t infer the type of a recursive method, so functions like factorial must declare their result type explicitly.
  • Closures capture their environment — A function returned from multiplier remembers the factor value it closed over, even after the enclosing method has finished.
  • Lexical scope and nesting — Functions can be nested to hide helpers, and inner functions can read values from their enclosing scope while staying invisible to the outside.

Running Today

All examples can be run using Docker:

docker pull virtuslab/scala-cli:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining