Free Handbook · Every example compiled & verified

Functions

def, default and named arguments, recursion with @tailrec, lambdas and the _ placeholder, methods vs functions, currying, by-name parameters and varargs.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 03 · what you'll be able to do

  • Define methods with def, know when the return type can be inferred and when it must be written
  • Call functions with default and named arguments instead of overloads
  • Write recursive functions and make them stack-safe with an accumulator and @tailrec
  • Pass lambdas and _ placeholders to methods, and explain the difference between a method and a function value
  • Use multiple parameter lists, by-name parameters and varargs, the building blocks of Scala APIs
01

Defining functions with def

def name(param: Type, ...): ReturnType = body defines a function. Parameters always need their types; the return type can usually be inferred from the body. The body is an expression — often a single line — and its value is what the function returns. There is no need for return. A multi-line body is an indented block, and its last expression is the result.

scalaMain.scala
def square(x: Int): Int = x * x

def area(width: Double, height: Double) = width * height

def greet(name: String): Unit =
  println(s"Hello, $name")

def describe(n: Int): String =
  val sign = if n < 0 then "negative" else "non-negative"
  s"$n is $sign"

@main def run(): Unit =
  println(square(7))
  println(area(2.5, 4))
  greet("Asha")
  println(describe(-3))
Outputcompiled & run with real Scala
49
10.0
Hello, Asha
-3 is negative

area has no declared return type — the compiler infers Double. area(2.5, 4) passes an Int where a Double is expected; Scala widens it for you because no precision is lost.

Your turn

Write def isAdult(age: Int): Boolean and call it with 17 and 18.

When to write the return type anyway
Always write it on public methods: it documents the contract, keeps a small change in the body from silently changing the API, and makes compile errors point at the right place. Inferring is fine for short private helpers and local values.
Error you will hit

Recursive method needs return type

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

@main def run(): Unit =
  println(factorial(5))
-- [E044] Cyclic Error: Main.scala:2:28
2 |  if n <= 1 then 1 else n * factorial(n - 1)
  |                            ^
  |                Overloaded or recursive method factorial needs return type
  |
  |                 Run with -explain-cyclic for more details.
1 error found
Compilation failed
Why the compiler said that

To infer the return type the compiler has to type the body, but the body calls factorial, whose type is the very thing it is trying to work out. It refuses to guess.

The fix

Declare the return type on every recursive (and overloaded) method.

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

@main def run(): Unit =
  println(factorial(5))
02

Default and named arguments

A parameter can have a default value, used when the caller leaves it out. Callers can also pass arguments by name — ssl = false — which lets them skip earlier defaults, pass arguments in any order, and makes a call with several booleans or numbers readable. Between them, these two features replace most of the overloaded methods and builder classes you would write in Java.

scalaMain.scala
def connect(host: String, port: Int = 5432, ssl: Boolean = true): String =
  s"$host:$port ssl=$ssl"

@main def run(): Unit =
  println(connect("db.local"))
  println(connect("db.local", 6543))
  println(connect("db.local", ssl = false))
  println(connect(port = 3306, host = "mysql.local"))
Outputcompiled & run with real Scala
db.local:5432 ssl=true
db.local:6543 ssl=true
db.local:5432 ssl=false
mysql.local:3306 ssl=true

The third call skips port by naming ssl; the fourth passes both named arguments in a different order from the definition.

Your turn

Add a parameter timeoutSec: Int = 30 and call connect changing only the timeout.

In real jobs
Name any argument whose meaning is not obvious at the call site. retry(3, 500, true) forces a reader to open the definition; retry(times = 3, delayMs = 500, jitter = true) does not. Case class copy (Module 05) is built entirely on named and default arguments.
Error you will hit

Missing argument for a parameter without a default

scala
def greet(name: String, greeting: String = "Hello"): String =
  s"$greeting, $name!"

@main def run(): Unit =
  println(greet(greeting = "Hi"))
-- [E171] Type Error: Main.scala:5:15
5 |  println(greet(greeting = "Hi"))
  |          ^^^^^^^^^^^^^^^^^^^^^^
  |missing argument for parameter name of method greet: (name: String, greeting: String): String
1 error found
Compilation failed
Why the compiler said that

Only greeting has a default. Naming it lets you skip nothing else — name still has no value, and the compiler prints the full signature so you can see which parameter is missing.

The fix

Pass the required argument, positionally or by name.

scala
def greet(name: String, greeting: String = "Hello"): String =
  s"$greeting, $name!"

@main def run(): Unit =
  println(greet("Ravi", greeting = "Hi"))
03

Recursion, @tailrec and nested functions

A recursive function calls itself on a smaller problem until it reaches a base case. Each call that is still waiting for a result takes a frame on the JVM call stack, and the stack is only a few thousand frames deep — so a naive recursion over a million items crashes with StackOverflowError. The fix is tail recursion: if the recursive call is the very last thing the function does, the Scala compiler turns it into a loop. @tailrec asks the compiler to prove it did.

scalaMain.scala
import scala.annotation.tailrec

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

@tailrec
def sumTo(n: Int, acc: Long = 0): Long =
  if n == 0 then acc else sumTo(n - 1, acc + n)

@main def run(): Unit =
  println(factorial(5))
  println(factorial(25))
  println(sumTo(4))
  println(sumTo(1_000_000))
Outputcompiled & run with real Scala
120
15511210043330985984000000
10
500000500000

factorial is not tail recursive — after the call returns it still has to multiply by n. sumTo carries the running total in the accumulator acc, so the call is the last step and a million levels run in constant stack space. BigInt never overflows, which is why 25! prints exactly.

VisualizesumTo(4) with an accumulatorStep 1 / 7
def sumTo(n: Int, acc: Long = 0): Long =
if n == 0 then acc else sumTo(n - 1, acc + n)
println(sumTo(4))
Line 4

sumTo(4) is called; acc takes its default, 0.

Variables now
n4
acc0
All 7 steps as a table
StepLineWhat happenedVariables now
14sumTo(4) is called; acc takes its default, 0.n = 4 acc = 0
22n is not 0, so it calls sumTo(3, 0 + 4). Nothing is left to do afterwards, so the compiler reuses the same frame — a jump, not a new call.n = 3 acc = 4
32Again: sumTo(2, 4 + 3).n = 2 acc = 7
42sumTo(1, 7 + 2).n = 1 acc = 9
52sumTo(0, 9 + 1).n = 0 acc = 10
62Base case: n == 0, so the answer is already in the accumulator. No pending multiplications to unwind.n = 0 acc = 10
74The result is printed.
Error you will hit

@tailrec: the call is not in tail position

scala
import scala.annotation.tailrec

@tailrec
def factorial(n: Int): Long =
  if n <= 1 then 1L else n * factorial(n - 1)

@main def run(): Unit =
  println(factorial(5))
-- Error: Main.scala:5:38
5 |  if n <= 1 then 1L else n * factorial(n - 1)
  |                             ^^^^^^^^^^^^^^^^
  |                 Cannot rewrite recursive call: it is not in tail position
1 error found
Compilation failed
Why the compiler said that

After factorial(n - 1) returns, the function still multiplies by n, so the call is not the last step and cannot become a loop. Without @tailrec this would compile and blow the stack on large inputs; with it, you find out now.

The fix

Move the pending work into an accumulator parameter so the recursive call is the whole result.

scala
import scala.annotation.tailrec

@tailrec
def factorial(n: Int, acc: Long = 1): Long =
  if n <= 1 then acc else factorial(n - 1, acc * n)

@main def run(): Unit =
  println(factorial(5))

An accumulator parameter is an implementation detail callers should not see. Hide it in a nested function: a def inside another def is visible only there, and it can read the outer function's parameters and vals directly.

scalaMain.scala
import scala.annotation.tailrec

def isPalindrome(text: String): Boolean =
  val clean = text.toLowerCase.filter(c => c.isLetterOrDigit)

  @tailrec
  def check(i: Int, j: Int): Boolean =
    if i >= j then true
    else if clean(i) != clean(j) then false
    else check(i + 1, j - 1)

  check(0, clean.length - 1)

@main def run(): Unit =
  println(isPalindrome("Never odd or even"))
  println(isPalindrome("Scala"))
Outputcompiled & run with real Scala
true
false

check uses clean from the enclosing function without it being passed in, and nobody outside isPalindrome can call it.

Your turn

Write a tail-recursive countDigits(n: Long): Int with a nested helper.

04

Function values, lambdas and _

Functions in Scala are values: you can store one in a val, pass it to a method, and return it from another function. The literal syntax is a lambda (also called an anonymous function): (x: Int) => x * 2. Its type is written Int => Int, or (Int, Int) => Int for two parameters. When the compiler already knows the parameter types — for example inside map on a List[Int] — you can leave them out.

scalaMain.scala
@main def run(): Unit =
  val double = (x: Int) => x * 2
  val add: (Int, Int) => Int = (a, b) => a + b
  println(double(21))
  println(add(2, 3))

  val nums = List(1, 2, 3, 4)
  println(nums.map(double))
  println(nums.map(n => n + 1))
  println(nums.map(_ * 10))
  println(nums.filter(_ % 2 == 0))
  println(nums.reduce(_ + _))

  val shout: String => String = _.toUpperCase + "!"
  println(shout("hi"))
Outputcompiled & run with real Scala
42
5
List(2, 4, 6, 8)
List(2, 3, 4, 5)
List(10, 20, 30, 40)
List(2, 4)
10
HI!

_ * 10 is shorthand for n => n * 10. Each _ stands for the next parameter, so _ + _ is (a, b) => a + b — exactly what reduce wants.

Your turn

Use filter and a placeholder to keep only the numbers greater than 2.

Error you will hit

Two placeholders mean two parameters

scala
@main def run(): Unit =
  val nums = List(1, 2, 3)
  println(nums.map(_ * _))
-- [E086] Syntax Error: Main.scala:3:19
3 |  println(nums.map(_ * _))
  |                   ^^^^^
  |                   Wrong number of parameters, expected: 1
1 error found
Compilation failed
Why the compiler said that

The intention was "square each number", but _ * _ expands to (a, b) => a * b, a two-parameter function. map passes one value at a time. A placeholder can be used only once per parameter.

The fix

When you need the same parameter twice, name it.

scala
@main def run(): Unit =
  val nums = List(1, 2, 3)
  println(nums.map(n => n * n))
When to use _
Use the placeholder for short, obvious lambdas: _.name, _ > 0, _ + _. As soon as the body grows past one small operation, a named parameter reads better. Higher-order functions like map, filter and foldLeft get a full module: Module 07.
05

Methods vs function values

A def defines a method: it belongs to an enclosing class, object or file and is not itself a value. A lambda is a function value: an ordinary object with an apply method, an instance of Function1, Function2 and so on. double(4) is really double.apply(4). When you use a method where a function value is expected — nums.map(square) or val f = square — Scala 3 converts it automatically. This is called eta-expansion.

scalaMain.scala
def square(x: Int): Int = x * x

@main def run(): Unit =
  val f = square
  val inc = (x: Int) => x + 1
  println(f(5))
  println(inc.apply(5))
  println(List(1, 2, 3).map(square))

  val incThenSquare = inc.andThen(square)
  val squareThenInc = inc.compose(square)
  println(incThenSquare(2))
  println(squareThenInc(2))
Outputcompiled & run with real Scala
25
6
List(1, 4, 9)
9
5

andThen and compose exist because function values are objects with methods. inc.andThen(square) runs inc first; inc.compose(square) runs square first.

Method (def)Function value (lambda)
Is it a value?No — it becomes one via eta-expansionYes — an object you can store and pass
Type parametersdef first[A](xs: List[A])Only with Scala 3 polymorphic function types
Default / named argsYesNo
Multiple parameter lists, by-name, varargsYesNo (a curried lambda returns a lambda)
Typical useThe API of a class or objectArguments to map, callbacks, strategies
06

Currying and multiple parameter lists

A method can have more than one parameter list: def add(a: Int)(b: Int). Call it with both, add(2)(3), or supply only the first to get back a function waiting for the rest — add(10) is an Int => Int. This is currying. In everyday Scala it matters for two reasons: partial application, and letting the last argument be a block in braces, which is how Scala libraries make method calls look like built-in syntax.

scalaMain.scala
def add(a: Int)(b: Int): Int = a + b

def timed(label: String)(body: => Int): Int =
  println(s"[$label] start")
  val result = body
  println(s"[$label] result $result")
  result

@main def run(): Unit =
  println(add(2)(3))
  val addTen = add(10)
  println(addTen(5))
  println(List(1, 2, 3).map(add(100)))

  val total = timed("sum") {
    (1 to 4).sum
  }
  println(total)
  println(List(1, 2, 3).foldLeft(0)(_ + _))
Outputcompiled & run with real Scala
5
15
List(101, 102, 103)
[sum] start
[sum] result 10
10
6

timed("sum") { ... } reads like a control structure, but it is a normal method call whose second parameter list gets a block. foldLeft(0)(_ + _) in the standard library uses two lists so the type of the start value (Int) is known before the lambda is checked.

Your turn

Write def repeat(times: Int)(body: => Unit): Unit and use it as repeat(3) { println("hi") }.

Type inference flows left to right
Scala infers types one parameter list at a time. That is the practical reason foldLeft is curried: with foldLeft(0) the compiler fixes the accumulator type as Int, then checks _ + _ against it. Module 09 uses a separate parameter list for using clauses too.
07

By-name parameters

Normally an argument is evaluated once, before the function runs (by-value). Put => before a parameter's type — msg: => String — and it becomes by-name: the argument expression is passed unevaluated and runs every time the parameter is used, and not at all if it is never used. That is how getOrElse, loggers, assertions and the timed block above avoid doing work they do not need.

scalaMain.scala
def expensive(): Int =
  println("computing...")
  42

def logByValue(debug: Boolean, x: Int): Unit =
  if debug then println(s"value $x")

def logByName(debug: Boolean, x: => Int): Unit =
  if debug then println(s"value $x")

def twice(x: => Int): Int = x + x

@main def run(): Unit =
  logByValue(false, expensive())
  logByName(false, expensive())
  println("---")
  println(twice(expensive()))
Outputcompiled & run with real Scala
computing...
---
computing...
computing...
84

By-value computed the argument even though debug was off. By-name skipped it entirely — and then ran it twice inside twice, because it was used twice.

Evaluate once if you need the value twice
A by-name parameter re-runs its expression on every use. If it is expensive or has side effects and you need it more than once, copy it into a local: val v = x, then use v. For "compute at most once", that is exactly what lazy val v = x gives you.
08

Varargs

A last parameter typed Int* accepts any number of arguments, including zero. Inside the method it is a Seq[Int]. To pass an existing collection into a varargs parameter, spread it with xs* (Scala 2 wrote xs: _*). List(1, 2, 3) and println-style APIs are built on this.

scalaMain.scala
def sumAll(nums: Int*): Int = nums.sum

def log(level: String, parts: String*): Unit =
  println(s"[$level] ${parts.mkString(" ")}")

@main def run(): Unit =
  println(sumAll())
  println(sumAll(1, 2, 3))
  val xs = List(4, 5, 6)
  println(sumAll(xs*))
  log("INFO", "server", "started", "on", "8080")
  log("WARN")
Outputcompiled & run with real Scala
0
6
15
[INFO] server started on 8080
[WARN] 
Your turn

Write def maxOf(first: Int, rest: Int*): Int — requiring at least one argument — and call it with and without extra values.

Error you will hit

Passing a List where varargs are expected

scala
def sum(nums: Int*): Int = nums.sum

@main def run(): Unit =
  val list = List(1, 2, 3)
  println(sum(list))
-- [E007] Type Mismatch Error: Main.scala:5:14
5 |  println(sum(list))
  |              ^^^^
  |              Found:    (list : List[Int])
  |              Required: Int
1 error found
Compilation failed
Why the compiler said that

Int* means "any number of Int arguments", so each argument must be an Int. A whole List[Int] is one argument of the wrong type — the compiler does not unpack it for you.

The fix

Spread the collection with *.

scala
def sum(nums: Int*): Int = nums.sum

@main def run(): Unit =
  val list = List(1, 2, 3)
  println(sum(list*))
def
Defines a method: a named function belonging to an enclosing class, object or file.
Default argument
A parameter value used when the caller omits that argument.
Named argument
Passing an argument as name = value, in any order.
Tail recursion
Recursion where the recursive call is the last step, so the compiler can turn it into a loop.
@tailrec
An annotation that makes compilation fail unless the method really is tail recursive.
Accumulator
An extra parameter that carries a partial result through recursive calls.
Lambda
An anonymous function value, such as (x: Int) => x * 2.
Placeholder _
Shorthand for a lambda parameter; each _ is the next parameter.
Eta-expansion
Automatic conversion of a method into a function value when one is expected.
Currying
Splitting parameters into several lists so a function can be applied one list at a time.
By-name parameter
A parameter declared x: => T whose argument is evaluated each time it is used, not before the call.
Varargs
A last parameter T* that accepts any number of arguments as a Seq[T].
Quick check

Given def f(x: => Int) = x + x, how many times does f({ println("hi"); 1 }) print "hi"?

Quick check

Which call is tail recursive?

Frequently asked questions

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 file; it can have type parameters, default arguments and several parameter lists. A function is a value — an object with an apply method, usually written as a lambda. Scala 3 converts a method to a function value automatically wherever a function is expected (eta-expansion), so in practice you can pass either.
Does Scala optimise tail recursion?
Yes, for self-recursive methods that cannot be overridden (local functions, methods on objects, and final or private methods). The compiler rewrites the call into a loop. Add @tailrec so compilation fails if a later change breaks the tail position. The JVM itself does not optimise tail calls, so mutual recursion between two methods is not optimised.
What does the underscore mean in Scala lambdas?
In xs.map(_ * 2), _ is a placeholder for the lambda's parameter. Each underscore is a separate parameter in order, so _ + _ means (a, b) => a + b. Use a named parameter when you need the same value twice.

Finish the Scala handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.