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.
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.
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.
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(...).
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.
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
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.
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
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.
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:
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.
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
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.
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.
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.
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
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.
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.
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.
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
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.
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:
Boolean — a simple "does it match?" testOption[T] — extracts a single valueOption[(T1, T2, ...)] — extracts multiple values (as used above)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
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
Below is a table of what process returns for each input:
| Input | Result |
|---|---|
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 |
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
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.
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
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.
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
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
}
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.
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.
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).
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.
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.
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.
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).
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.
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) =>.