Paradigmi di Programmazione e Sviluppo — Prof. Viroli

PPS-04-Scala-OOP

Object-Oriented Programming in Scala

In this lesson

1. Basic OO: Traits, Classes, and Objects

Scala is a pure object-oriented language: every value is an object, and every operation is a method call. The core building blocks are traits, classes, and objects. A trait defines a type contract (like Java interfaces but richer — they are mixins). A class provides an implementation. An object is a singleton — a module that groups static-like members.

Key idea

Unlike Java, public is the default visibility in Scala — no keyword needed. The override keyword is mandatory when overriding a method, catching mistakes at compile time where Java would silently introduce a new method.

Here is a Scala 3 idiomatic version of a Counter — notice the significant indentation (no curly braces for the class body) and the compact syntax:

The trait Counter declares two abstract members: increment() and value. The class CounterImpl implements them with a private mutable field v. The @main annotation turns any method into a program entry point.

Editor's note

In Scala 3, class bodies use indentation-based syntax (like Python) rather than curly braces. A colon at the end of the class/trait/object header opens the indented body.

2. Constructors and Idiomatic Scala 3

Scala has a primary constructor that is part of the class signature itself. Constructor parameters appear directly after the class name. Any "floating" code in the class body is executed as part of the constructor, and auxiliary constructors delegate to the primary one via this(...).

Guideline

Auxiliary constructors are rarely used in practice. Prefer factory methods (apply in the companion object) or Builder patterns for flexible object creation.

In the Lamp example below, the primary constructor takes a Boolean parameter. Floating code runs during construction, and an auxiliary constructor provides a no-argument default:

class Lamp(on: Boolean):
  private var state: Boolean = on
  private val firstUse: java.util.Date = new java.util.Date()

  // floating code — runs as part of the primary constructor
  println("primary constr. of: " + this)

  // auxiliary constructor, delegating to the primary
  def this() =
    this(false)
    println("auxiliary constr. of: " + this)

  def isOn: Boolean = state
  def switchOn(): Unit = state = true
  def switchOff(): Unit = state = false
  override def toString(): String =
    s"Lamp { state } first use { firstUse }; identity { super.toString }"

An even more idiomatic Scala 3 version leverages class parameters directly as fields, drops the new keyword at the call site, and uses single-expression method bodies:

Notice the private var _state convention: underscore-prefixed names for private mutable fields. The isOn getter has no parentheses (no side-effect), while mutating methods on()/off() retain them.

3. Inheritance and Mixins

Inheritance in Scala follows the familiar pattern: extends for one class/trait, with for additional traits. The protected modifier gives visibility to subclasses, just as in Java.

trait Counter:
  def value: Int
  def increment(): Unit

class CounterImpl(protected var _value: Int) extends Counter:
  override def value: Int = _value
  override def increment(): Unit = _value = _value + 1

trait DecrementCounter extends Counter:
  def decrement(): Unit

class DecrementCounterImpl() extends CounterImpl(0) with DecrementCounter:
  override def decrement(): Unit = _value = _value - 1
Key insight

A trait can extend another trait, forming a stackable mixin hierarchy. The with keyword lets you compose multiple traits into a single class — more flexible than Java's single-inhibition + interface model, and closer to a true mixin system.

Call-site usage is uniform: CounterImpl(0).increment() or DecrementCounterImpl().decrement(), with the same method-call syntax regardless of whether the object's concrete type is known.

4. Objects as Singletons and Modules

The object keyword creates a singleton: exactly one instance exists, accessed by name. It serves two purposes at once: a module for grouping static-like members, and a true OOP singleton that can extend traits and be passed around polymorphically.

object Singleton extends App:
  trait SequenceGenerator:
    def get: Int

  class SequenceFromSupplier(f: () => Int) extends SequenceGenerator:
    override def get: Int = f()

  val s: SequenceGenerator = SequenceFromSupplier(() => 1)
  println(s.get)  // 1

  object Zeros extends SequenceFromSupplier(() => 0)

  object GlobalCounter extends SequenceGenerator:
    private var n: Int = 0
    override def get: Int =
      n = n + 1
      n - 1

  val s2: SequenceGenerator = GlobalCounter
  println(s2.get)         // 0
  println(GlobalCounter.get)  // 1
The examiner will ask

Why does object eliminate the Singleton anti-pattern? Because an object can extend a trait, so a client can depend on the abstraction (the trait) rather than the concrete singleton — enabling dependency injection and testability.

5. Scala Type Hierarchy

Scala's type system is a uniform lattice rooted at Any. Every type descends from Any, which splits into AnyVal (value types, optimised to primitives on the JVM) and AnyRef (reference types, an alias for java.lang.Object). At the bottom sit Null (subtype of all reference types, the type of null) and Nothing (subtype of all types, the type of throw and ???).

Click through the nodes in the explorer below to see how each type relates:

Why this matters

The presence of Nothing as a bottom type makes expressions like if (b) 1 else throw new Error() well-typed: the compiler unifies Int and Nothing to Int. Similarly, ??? (which returns Nothing) can stand in for any type during prototyping.

6. Operators Are Method Calls

In Scala there is no special treatment for operators — every operator like +, -, * is simply a method on the left operand. This is a direct consequence of the "everything is an object" philosophy.

val i = 10
val j = 10 + 20      // looks like a built-in operator
val k = i.+(20)      // but it's really a method call on Int

// Unary methods can be called with infix notation
val obj = new A
val str = obj meth "ciao"   // equivalent to obj.meth("ciao")

// Even symbolic names are just methods
val str2 = obj *** "ciao"   // method named ***
val str3 = obj.***("ciao")  // explicit equivalent
Design principle

This uniformity means DSLs and embedded languages feel natural. You can define methods with symbolic names (***, ++, ::) and call them with or without dots and parentheses, giving the appearance of custom syntax while staying within the type system.

7. Functions Are Objects: apply

Function types like Int => Int are syntactic sugar for Function1[Int, Int]. Calling a function f(x) desugars to f.apply(x). This is the fundamental insight that unifies FP with OO: functions are just objects with an apply method.

val f: Int => Int = x => x + 1

println(f)           // 
println(f(1))        // 2
println(f.apply(1))  // 2 — identical

// Composition works naturally
println((f compose f)(1))  // 3

// Multi-argument functions
val f2: Function2[Int, Int, Int] = _ + _
println(f2(1, 2))  // 3

The same apply mechanism powers factory methods on companion objects, array indexing, and tuple access — all are unified under a single syntactic idea.

8. Getters, Setters, and Indexers

Scala's uniform access principle means there is no syntactic difference between field access and method call at the call site. A no-argument method can be defined without parentheses (acting as a getter), and a method named prop_= acts as a setter invoked via assignment syntax.

Guidelines

Omit parentheses on no-argument methods that have no side effects (accessors, computations). Retain parentheses on methods that perform mutation or I/O. This convention is part of the official Scala style guide.

Indexing, too, is a method call: obj(i) desugars to obj.apply(i), and obj(i) = v desugars to obj.update(i, v). There is no special array syntax — it is all just method calls under the hood.

println(obj(10))     // calls obj.apply(10) — "indexer-get"
obj(11) = "casa"      // calls obj.update(11, "casa") — "indexer-set"
obj.update(11, "casa") // explicit alternative

9. Right-Associative Operators

Methods ending in a colon (:) are right-associative — the expression a :: b :: list becomes list.::(b).::(a) rather than the usual left-to-right evaluation. This is the mechanism behind Scala's immutable list construction (::) and is available for any custom method.

class A:
  var field = 10
  def +:(i: Int): A = { field = field + i; this }

// Right-associative means:
10 +: 20 +: 30 +: obj
// Evaluates as: obj.+:(30).+:(20).+:(10)
println(obj.field)  // 60

The colon method name is what triggers right-associativity. This is how Scala implements "cons" for lists: 1 :: 2 :: List(3) builds the list right-to-left, keeping natural reading order.

10. Uniform Access and Encapsulation

Because def (method), val (immutable field), and var (mutable field) share the same client syntax, a trait can declare a contract using any of them, and a class can implement it with any compatible member. This is the uniform access principle.

Open-Closed Principle in practice

Start by exposing a val or var field. Later, if you need validation or computation, switch to a def getter/setter pair without changing any client code. The contract uses def to be maximally abstract; implementations choose the appropriate storage strategy.

trait Person:
  def name: String
  def surname: String
  def married: Boolean
  def married_=(state: Boolean): Unit
  override def toString(): String = s"$name $surname $married"

// Implementation 1: direct fields via class parameters
class PersonImpl1(
  override val name: String,
  override val surname: String,
  override var married: Boolean
) extends Person

// Implementation 2: computed getter with validation
class PersonImpl2(_name: String, override var married: Boolean) extends Person:
  override def name =
    require(_name != null)
    _name
  override val surname = "?"

// Implementation 3: custom getter/setter with side effects
class PersonImpl3(
  override val name: String,
  override val surname: String,
  private var _married: Boolean
) extends Person:
  override def married =
    println("getting married property")
    _married
  override def married_=(m: Boolean) =
    require(_married != true || m != false)
    _married = m

The three implementations are all substitutable via the Person trait. Clients never know — nor need to know — which variant they are using.

11. Equality, Case Classes, and Companion Objects

Scala uses == for structural equality (calls equals internally, handles null safely) and eq for reference identity (available only on AnyRef). Writing correct equals/hashCode by hand is error-prone, which is where case classes shine.

class Pair[A, B](val x: A, val y: B)

class PairWithEq[A, B](val x: A, val y: B):
  def canEqual(other: Any): Boolean = other.isInstanceOf[PairWithEq[_, _]]
  override def equals(other: Any): Boolean = other match
    case that: PairWithEq[A, B] =>
      (that canEqual this) && x == that.x && y == that.y
    case _ => false
  override def hashCode(): Int =
    Seq(x, y).map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)

// Case class: all of the above for free
case class CasePair[A, B](x: A, y: B)

println(new Pair(10,20) == new Pair(10,20))      // false — reference equality
println(new PairWithEq(10,20) == new PairWithEq(10,20))  // true
println(CasePair(10,20) == CasePair(10,20))      // true — structural equality
The examiner will ask

What does a case class give you automatically? (1) Public immutable fields for each parameter, (2) toString, equals, hashCode based on all fields, (3) apply in the companion object (no new needed), (4) unapply for pattern matching, and (5) a copy method for creating modified clones.

A companion object is an object with the same name as a class/trait, placed in the same file. It holds "static" members and has access to the class's private members (and vice versa). Case classes automatically generate a companion object with apply, unapply, and other utilities.

12. apply and unapply: Factories and Extractors

The apply method on a companion object turns it into a factory: Person("Mario", "Rossi") instead of new PersonImpl("Mario", "Rossi"). The unapply method (also called an extractor) does the reverse: it destructures an object in pattern matching.

Using apply/unapply together, you can completely hide the concrete implementation class behind a trait:

The unapply method can return:

13. Pattern Matching in Depth

Pattern matching is a cornerstone of Scala. It combines type tests, structural destructuring, conditionals, variable binding, custom extractors, and more into a single, readable construct.

Here is a comprehensive example showing every pattern variant from the course slides:

object OfLength:
  // Extractor: returns distance from origin, but only if > 1
  def unapply(p: Pair[Double, Double]): Option[Double] =
    Some(Math.sqrt(p.x * p.x + p.y * p.y)).filter(_ > 1)

val z = 2
def process(p: Pair[Double, Pair[Double, Double]]): String = p match
  case Pair(1, Pair(1, 1))          => "A"  // structural, exact
  case Pair(2, Pair(0, _))          => "B"  // wildcard for unmatched part
  case Pair(3, null) | Pair(4, null) => "C"  // or-pattern
  case Pair(5, Pair(x, y)) if x == y => "D"  // conditional (guard)
  case Pair(6, pair @ Pair(_, _)) if pair.y == 1 => "E"  // variable binding
  case Pair(7, Pair(`z`, `z`))      => "F"  // stable identifier (backticks)
  case Pair(8, OfLength(n))         => "G"  // custom extractor
  case Pair(9, _: MyPair)           => "H"  // type test
  case _                             => "?"   // default
How pattern matching works

For case classes, case Pair(x, y) calls Pair.unapply on the value. For type tests, the compiler inserts isInstanceOf. Guards filter results. The compiler checks exhaustiveness: if you omit the default case on a sealed type, you get a warning.

Patterns can also be used in variable declarations for destructuring:

val t = (10, 20)
val (x, y) = t                    // destructuring a tuple
val p = Pair(10, 15)
val Pair(z, w) = p                // throws MatchError if no match
val p2 = Pair(10, Pair(20, 30))
val Pair(_, Pair(a, b)) = p2     // nested destructuring: a=20, b=30

Interactive: Try the pattern match results

Matcher results

Below is a table of what process returns for each input:

InputResult
Pair(1, Pair(1,1))"A" — exact structural match
Pair(2, Pair(0,0))"B" — wildcard on second coordinate
Pair(3, null)"C" — or-pattern matches
Pair(5, Pair(1,1))"D" — guard x == y passes
Pair(6, Pair(1,1))"E" — variable binding, pair.y == 1
Pair(7, Pair(2,2))"F2" — stable id matches z=2
Pair(8, Pair(1,1))"G1.414..." — OfLength extracts distance
Pair(9, MyPair(10,10))"H" — type test against MyPair
Pair(7, Pair(1,2))"?" — no case matches

14. Control Structures: if, while

Scala's control structures are expressions that return values, not statements. This makes them composable and concise.

val b = true

// if is an expression
println(if (b) "OK" else "NO")    // OK
println(if b then "OK" else "NO") // Scala 3 infix syntax

// The body of an if can be a block expression
val s = if b then
  println("OK")
  "OK"
else "NO"

// Types unify correctly thanks to the type lattice
val any: AnyVal = if b then 1 else true   // least upper bound: AnyVal
val i: Int = if b then 1 else throw new Error()  // Int vs Nothing -> Int

// while loops for imperative style
def gcd(x: Int, y: Int): Int =
  var a = x
  var b = y
  while (a != b)
    if a > b then a = a - b else b = b - a
  a

println(gcd(10, 18))  // 2
The examiner will ask

Why does if b then 1 else throw new Error() type-check as Int? Because throw has type Nothing, the bottom type. The compiler computes the least upper bound of Int and Nothing, which is Int.

15. For-Comprehension, foreach, and Custom Iterables

The for loop in Scala is not a built-in construct — it desugars into foreach, map, flatMap, and withFilter calls. Any object that provides these methods can be used in a for comprehension.

// Basic iteration
for i <- 1 to 4 do print(i + " ")  // 1 2 3 4
for i <- 1 until 4 do print(i + " ") // 1 2 3
(4 to 1 by -1).foreach(i => print(i + " ")) // 4 3 2 1

// A custom iterable class
class ZeroTo(val n: Int):
  def foreach(action: Int => Unit): Unit =
    for i <- 0 until n do action(i)

object ZeroTo:
  def apply(n: Int) = new ZeroTo(n)

for i <- ZeroTo(10) do print(i + " ")  // 0 1 2 ... 9

// For-comprehension with multiple generators and guards
for
  i <- 0 until 10
  if i % 3 == 0   // guard
  j <- 0 until 2
do print(s"$i-$j ")
// 0-0 0-1 3-0 3-1 6-0 6-1 9-0 9-1
The power of foreach

By implementing a single foreach method, your class becomes iterable with the for syntax. This is a lightweight way to integrate custom collections into the language's control flow — no interface or inheritance required.

16. Variable Arguments and Arrays

Variable argument lists are declared with * and are treated as Seq[T] inside the method. Arrays are generic classes with apply/update methods, not special syntax.

def sum(args: Int*): Int =
  var s = 0
  for i <- args do s = s + i
  s

println(sum(1, 2, 3, 4, 5))  // 15

// Arrays are generic classes
val a: Array[Int] = Array(1, 2, 3, 4, 5, 6)
a(2) = 30                       // a.update(2, 30)
println(a(2))                   // a.apply(2) -> 30
println(a.length)               // 6

// Equivalent explicit form
val b: Array[Int] = Array.apply(1, 2, 3, 4, 5, 6)
b.update(2, 30)
println(b.apply(2))  // 30

17. Fancy Syntax: Named Arguments, Defaults, Curly Braces

Scala provides several syntactic conveniences that reduce boilerplate without sacrificing type safety:

case class Person(name: String, surname: String, year: Int)

def makePerson(
  name: String,
  surname: String = "?",
  year: Int = 1900
) = Person(name, surname, year)

println(makePerson("bob", "hope"))           // skip surname default
println(makePerson(name = "bob", surname = "hope"))  // named args
println(makePerson(surname = "hope", name = "bob"))  // any order
println(makePerson(name = "bob", "hope"))             // mix named and positional
println(makePerson("bob"))                             // only required arg

// curly braces instead of parentheses for single lambda arg
println { makePerson { "bob" } }

// This enables writing control-structure-like libraries
@annotation.tailrec
def mywhile(condition: => Boolean)(body: => Unit): Unit =
  if condition then { body; mywhile(condition)(body) }

var n = 5
var res = 1
mywhile (n >= 1) {   // looks like built-in while!
  res = res * n
  n = n - 1
}
The examiner will ask

What is the effect of the => in condition: => Boolean? It makes the parameter call-by-name: the expression is re-evaluated every time it is used inside the method, like a lambda without creating a closure object. This is what makes mywhile work as a real loop.

Check Your Understanding

What is the difference between val, var, and def in a class body? When should you use each in a trait contract?

val is an immutable field (evaluated once at construction). var is a mutable field (read/write). def is a method (evaluated each call). In a trait, prefer def for the contract because it is maximally abstract — implementations can use def, val, or var as needed. Use val in a trait only to express that the value is guaranteed to be constant across calls.

Explain why 10 +: 20 +: obj evaluates as obj.+:(20).+:(10).

The method +: ends with a colon (:), making it right-associative. The expression is grouped right-to-left: 10 +: (20 +: obj). The left operand of the colon method is the receiver in right-associativity, so 20 +: obj becomes obj.+:(20), then 10 +: result becomes result.+:(10).

How does Scala unify functions and objects? What role does apply play?

Function types like Int => Int are syntactic sugar for Function1[Int, Int], which is a trait with an abstract apply method. Calling f(x) desugars to f.apply(x). This means functions are just objects with a specific interface. The same apply mechanism powers factories on companion objects, array indexing, and more — all unified under a single syntactic model.

What is the uniform access principle? Give a concrete example of how it supports the Open-Closed Principle.

Uniform access means there is no syntactic difference between val, var, and def at the call site — they are all accessed the same way (e.g., obj.prop). You can start with a var field, then later replace it with a def getter/setter pair that adds validation or logging, without changing any client code. As long as the trait declares def prop, implementations are free to choose the storage strategy.

What does a case class automatically generate? When would you write apply/unapply manually instead?

Case classes generate: (1) immutable fields, (2) toString, equals, hashCode based on all parameters, (3) apply in the companion (no new), (4) unapply for pattern matching, (5) copy method. You write apply/unapply manually when you want to hide the concrete implementation behind a trait (returning the abstract type), add validation, or customize the extraction logic.

Describe Scala's type hierarchy. Where does Nothing sit, and why is it useful?

The root is Any, which splits into AnyVal (value types: Int, Double, Boolean, Unit, etc.) and AnyRef (reference types, alias for java.lang.Object). Null is a subtype of all AnyRef types (it is the type of null). Nothing is a subtype of all types, including both AnyVal and AnyRef. It is the return type of throw and ???, allowing expressions like if (b) 1 else throw new Error() to type-check as Int (the least upper bound of Int and Nothing is Int).

How does for-comprehension desugar? What methods must a class provide to be usable in a for loop?

A minimal for i <- collection do body desugars to collection.foreach(i => body). With guards and multiple generators, it desugars into nested calls of withFilter, flatMap, and foreach. To be usable in a for, a class must implement at least foreach. For full comprehension support, it also needs map, flatMap, and withFilter.

Write a simple custom extractor sameCategory(category) that extracts a category from a list of courses, matching only if all courses share the same category.

object sameCategory:
  def unapply(courses: Sequence[Course]): Option[String] =
    if courses.isEmpty then None
    else
      val cat = courses.head.category
      if courses.tail.forall(_.category == cat) then Some(cat)
      else None
This extractor returns Some(category) only when every course in the sequence has the same category, enabling the pattern case sameCategory(cat) =>.