Scala Functions

Functions are the building blocks of any Scala program. A function takes some inputs, does something with them, and returns a result. Writing good functions is one of the most important skills in Scala — and in programming generally.

Defining a Basic Function

def add(a: Int, b: Int): Int =
  a + b

val result = add(3, 7)
println(result)   // 10

def add ( a: Int , b: Int ) : Int =   a + b
 │    │    │              │   │        │
 │    │    └─ parameters  │   │        └─ body (expression)
 │    │                   │   └─ return type
 │    └─ function name     └─ parameter list end
 └─ keyword

Functions as Expressions

In Scala, every function body is an expression — it produces a value. The last expression in the body is the return value. You do not need a return keyword.

def multiply(x: Int, y: Int): Int =
  x * y   // this value is returned automatically

def isEven(n: Int): Boolean =
  n % 2 == 0

Using return is allowed but considered bad style in Scala. Functional programmers treat code as a series of expressions, not commands with explicit return statements.

Multi-Line Function Bodies

When a function needs more than one step, indent the body block:

def describeNumber(n: Int): String =
  val category = if n > 0 then "positive" else if n < 0 then "negative" else "zero"
  val parity = if n % 2 == 0 then "even" else "odd"
  s"$n is $category and $parity"

println(describeNumber(6))    // 6 is positive and even
println(describeNumber(-3))   // -3 is negative and odd
println(describeNumber(0))    // 0 is zero and even

Functions with No Parameters

def greet(): String =
  "Hello, learner!"

println(greet())   // Hello, learner!

When a function takes no parameters, you can define it with empty parentheses () or without parentheses at all. By convention, functions with side effects (like printing) use (). Functions that simply compute a value omit the parentheses.

Functions with Unit Return Type

A function that performs an action but returns no meaningful value has return type Unit:

def printBox(message: String): Unit =
  println("+-----------+")
  println("| " + message + " |")
  println("+-----------+")

printBox("Scala!")

Output:

+-----------+
| Scala! |
+-----------+

Function Call Diagram


Caller                 Function: square
──────                 ─────────────────
square(5)    ──→   receives n = 5
             ←──   returns n * n = 25
println(25)
def square(n: Int): Int = n * n

val x = square(5)
println(x)   // 25

Returning Multiple Values with Tuples

Scala functions return one value. To return multiple pieces of data, wrap them in a Tuple:

def minMax(numbers: List[Int]): (Int, Int) =
  (numbers.min, numbers.max)

val (lo, hi) = minMax(List(3, 1, 9, 4, 7))
println(s"Min: $lo, Max: $hi")   // Min: 1, Max: 9

Nested Functions

Scala allows defining functions inside other functions. Inner functions are only visible within the outer function — they are private helpers.

def computeArea(shape: String, a: Double, b: Double): Double =
  def rectangleArea = a * b
  def triangleArea = 0.5 * a * b

  shape match
    case "rectangle" => rectangleArea
    case "triangle"  => triangleArea
    case _           => 0.0

println(computeArea("rectangle", 5.0, 3.0))   // 15.0
println(computeArea("triangle", 6.0, 4.0))    // 12.0

Pure Functions

A pure function always returns the same output for the same input and has no side effects (it does not read files, print to screen, or modify external state). Pure functions are easy to test and reason about.

// Pure function — same input always gives same output
def celsius(fahrenheit: Double): Double =
  (fahrenheit - 32) * 5 / 9

// Impure function — depends on external state
var callCount = 0
def trackCalls(): Int =
  callCount += 1
  callCount

Prefer pure functions wherever possible. When a function must interact with the outside world (read a file, send a network request), isolate that impure behavior and keep the rest of the code pure.

Functions That Call Other Functions

def double(x: Int): Int = x * 2
def addTen(x: Int): Int = x + 10
def doubleThenAdd(x: Int): Int = addTen(double(x))

println(doubleThenAdd(5))   // double(5) = 10, addTen(10) = 20

Input: 5
  │
  ↓ double(5)
  10
  │
  ↓ addTen(10)
  20  ← final result

Type Annotations Are Required for Parameters

Scala infers types in many places, but function parameters require explicit type annotations. The compiler needs to know what types the caller must provide:

// WRONG — Scala cannot infer parameter types
def add(a, b) = a + b   // Error

// CORRECT
def add(a: Int, b: Int): Int = a + b

Return types can be inferred by the compiler, but writing them explicitly is good practice — it documents the function's contract and catches mistakes early.

Recursive Functions

A function that calls itself is recursive. Recursion is a core technique in functional programming:

def factorial(n: Int): Int =
  if n <= 1 then 1
  else n * factorial(n - 1)

println(factorial(5))   // 5 * 4 * 3 * 2 * 1 = 120

factorial(5)
  = 5 * factorial(4)
        = 4 * factorial(3)
              = 3 * factorial(2)
                    = 2 * factorial(1)
                          = 1

Tail-recursive functions are covered separately because they require a different pattern for large inputs. The example above works for small numbers but will cause a stack overflow for very large inputs like factorial(100000).

Function Overloading

You can define multiple functions with the same name as long as they have different parameter lists:

def describe(n: Int): String = s"Integer: $n"
def describe(s: String): String = s"Text: $s"
def describe(b: Boolean): String = s"Boolean: $b"

println(describe(42))       // Integer: 42
println(describe("hello"))  // Text: hello
println(describe(true))     // Boolean: true

Scala selects the correct function based on the argument type at compile time. This is called static dispatch.

Leave a Comment

Your email address will not be published. Required fields are marked *