Part III — Scala for scalable software · Lesson 5

Object-oriented programming in Scala

~25 min of readingdeck 05-scala-oop, lab 05

In this lesson

  1. Why mix OO and FP?
  2. How do traits, classes and constructors look?
  3. How does Scala interoperate with Java?
  4. Why objects instead of static members?
  5. Is everything really an object?
  6. What is uniform access?
  7. What do case classes and companion objects give you?
  8. How does pattern matching work on objects?
  9. How do control structures and "fancy syntax" work?
  10. Lab 05
  11. Check your understanding

The lesson in one paragraph. Scala's OOP fragment maps almost one-to-one onto Java (traits, classes, fields, methods), but it is purer and terser. Every value is an object, every operation is a method call, and operators, function application, indexing, getters and setters are all method calls. Class parameters replace most constructors; object replaces static, serving both as a module and as a real singleton; companion objects hold factories (apply) and extractors (unapply); case classes generate equality, toString, copy and pattern matching. Because fields and parameterless methods look the same to clients, exposing a field does not break encapsulation. Interoperability with Java is smooth in one direction.

1. Why mix OO and FP?

OO bringsFP brings
scalability and maintainability of codeflexibility and reuse of code
encapsulation of data and operationsaggressive identification of layers of abstraction
a small gap between problem and solution domaindeclarativity: confidence in correctness, good fit for concurrency
productivity in the large, for non-expertsproductivity in the small, for experts

Mixing them, the direction Scala pioneered, aims to keep both: promote immutable structures and declarative code, allow mutable state and side effects where needed. The design space is very wide, so code must be disciplined. After the purely functional fragment (lessons 2–4), this lesson sticks to Scala's OOP fragment, pure and modern; how to trade OOP against FP comes from lesson 6 on.

From the slides: Scala's features and roots

Scala is OO (data and operations packed together, in a pure way), functional (first-class functions, side-effect-free operations promoted), Java-compatible, concise, high-level and statically typed (and sophisticated). Its roots: syntax from Java/C#/C++, lately moving towards Python; basic types, libraries and execution model from Java and the JVM; the uniform object model from Smalltalk, then Ruby; universal nesting from Algol and Simula; uniform access for methods and fields from Eiffel; functional features and typing from ML; advanced function features such as type classes from Haskell; actors from Erlang. A slide by M. Odersky places Scala, in 2010 and in 2023, among languages plotted by strength of static typing.

2. How do traits, classes and constructors look?

Basic Java OO constructs map one-to-one (the FP ones of lessons 2–4 do not). A trait is a type, like an interface, but also a mixin. A class is a template for many objects and their (super)type; each object has an identity and possibly mutable fields. An object is a module and a singleton. That is essentially all OOP adds to the FP fragment.

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

  class CounterImpl extends Counter:
    private var v: Int = 0
    override def increment(): Unit = v = v + 1
    override def value: Int = v

  @main def tryCounters() =
    val counter: Counter = new CounterImpl  // "()" typically always optional
    counter.increment()
    counter.increment()
    counter.increment()
    println("Result is " + counter.value)   // Result is 3

public is not a keyword: it is the default (section 6 explains why). override is a keyword, too important to be an annotation, and mandatory when a concrete member is overridden (for an abstract member, as here, it is optional but good style). @main turns a method into an entry point.

From the slides: the same counter in Java-like Scala 2 syntax
// A trait defines a type, it includes a contract and possibly an incomplete class
trait Counter {
  def increment(): Unit // Unit is the equivalent of "void"
  def getValue(): Int
}

// a class with no constructors, i.e., only default constructor
class CounterImpl extends Counter {
  private var value: Int = 0 // a mutable field
  // public is the default visibility
  // override is a keyword, mandatory for actual overriding
  override def increment(): Unit = {  // return type could be omitted
    this.value = this.value+1 // operator ++ not available...
  }
  override def getValue(): Int = {
    return this.value // return keyword not needed, but possible
  }
}

// a module, with a main
object UseCounter { // note an object could extend a class..
  def main(args: Array[String]) = {   // Array as a standard generic class
    val counter: Counter = new CounterImpl()  // "()" typically always optional
    counter.increment()
    counter.increment()
    counter.increment()
    println("Result is "+counter.getValue()) // Result is 3
  }
}

Primary and auxiliary constructors

Modern OOP prefers factories to constructors. Scala's primary constructor takes the class parameters, and "floating" code in the class body runs as part of it (put it at the top, after the fields). Auxiliary constructors are Java-like, must start by calling this(…), and are discouraged: use factories or builders.

class Lamp(on: Boolean):  // a class with two constructors
  private var state: Boolean = on
  private val firstUse: java.util.Date = new java.util.Date()

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

  // auxiliary constructor, necessarily calling the primary one
  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}"

@main def tryLamp =
  println("starting..")
  val v = new Lamp(false)
  println(v)
  println(v.isOn) // omitting () at the call site, for getters
  v.switchOn()
  println(v.isOn)
  val v2 = new Lamp(true)
  println(v2.isOn)
  val v3 = new Lamp  // omitting () at the call site, for constructors
  println(v3.isOn)
starting..
primary constr. of: Lamp false first use Fri Sep 25 11:42:40 CEST 2026; identity Lamp@45c8e616
Lamp false first use Fri Sep 25 11:42:40 CEST 2026; identity Lamp@45c8e616
false
true
primary constr. of: Lamp true first use Fri Sep 25 11:42:40 CEST 2026; identity Lamp@7e0babb1
true
primary constr. of: Lamp false first use Fri Sep 25 11:42:40 CEST 2026; identity Lamp@6debcae2
auxiliary constr. of: Lamp false first use Fri Sep 25 11:42:40 CEST 2026; identity Lamp@6debcae2
false

new Lamp runs the primary constructor (fields, then floating code) before the body of the auxiliary one. The slide's toString is written s"Lamp {state} first use {firstUse}; identity {super.toString}", without $, and prints those braces literally; the version above interpolates.

Class parameters or fields?

Constructor arguments and state are different things in principle, but often the same in practice, especially in FP-like code, and repeating them violates DRY. A class parameter automatically becomes a field when:

  1. it is declared with val or var (possibly private);
  2. it is used in some method body, in which case it becomes a private val;
  3. it belongs to a case class, where it is a public immutable field.

Whether a parameter is stored is just an optimisation: classes are mere implementations, and what matters is creating them smoothly. The idiomatic Scala 3 lamp:

// use significant indentation!
object IdiomaticLamp:  // not required, but often wrap into an object module

  class Lamp2(private var _state: Boolean):  // underscore for private vars
    def isOn: Boolean = _state // almost never use curly brackets
    def on() = _state = true   // Unit return type as sort of default
    def off() = _state = false
    override def toString = s"Lamp $_state identity ${super.toString}"

  @main def useLamp() =
    val v = Lamp2(false) // 'new' is optional: won't use it
    println(v)           // Lamp false identity u05.IdiomaticLamp$Lamp2@4b6995df
    println(v.isOn)      // false
    v.on()
    println(v.isOn)      // true

  @main def usePoint() =
    // the degenerate case of a one-line class
    class Point(val x: Int, val y: Int)
    val p = Point(10, 20)
    println(s"${p.x}, ${p.y}")   // 10, 20

The slide writes s"$p.x, $p.y", which prints …[email protected], …[email protected]: a selection needs braces (the Apply slides have the same slip). Guidelines: class parameters, no parentheses for getters, an underscore for private vars, one-expression bodies.

Inheritance

object CountersAndInheritance:
  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

  @main def main() =
    val c: Counter = new CounterImpl(0)
    c.increment()
    c.increment()
    println(c.value)    // 2
    val c2: DecrementCounter = new DecrementCounterImpl
    c2.decrement()
    println(c2.value)   // -1

As in Java, one class and many traits: the first after extends, the others after with. Java's super(…) goes in the extends clause (CounterImpl(0)); protected works as expected. Mixins are in lesson 7.

3. How does Scala interoperate with Java?

Scala code mostly has a simple Java equivalent (JLamp below translates Lamp line by line), the compiler emits Java-compatible bytecode (it also targets JavaScript and native), and packages work alike. Scala uses Java classes smoothly (new JLamp); Java uses Scala classes less smoothly, since, unlike Kotlin, that direction was never a goal. Mixed projects work in IntelliJ and SBT (mind javacOptions).

From the slides: JLamp, the Java equivalent, and its output
public class JLamp {
    private boolean state;
    private final java.util.Date firstUse = new java.util.Date();

    public JLamp(final boolean on){
        this.state = on;
        System.out.println("primary constr. of "+this);
    }
    public JLamp(){
        this(false);
        System.out.println("auxiliary constr. of "+this);
    }
    public boolean isOn(){ return state; }
    public void switchOn(){ state = true; }
    public void switchOff(){ state = false; }

    @Override
    public String toString() {
        return "Lamp " + state + " first use " + firstUse +
                " identity " + super.toString();
    }
}

public class JUseLamps {
    public static void main(String[] args) {
        System.out.println("starting..");
        final JLamp v = new JLamp(false);
        System.out.println(v);
        System.out.println(v.isOn());
        v.switchOn();
        System.out.println(v.isOn());
        final JLamp v2 = new JLamp(true);
        System.out.println(v2.isOn());
        final JLamp v3 = new JLamp();
        System.out.println(v3.isOn());
    }
}
starting..
primary constr. of Lamp false first use Fri Sep 25 11:43:23 CEST 2026 identity JLamp@6e75aa0d
Lamp false first use Fri Sep 25 11:43:23 CEST 2026 identity JLamp@6e75aa0d
false
true
primary constr. of Lamp true first use Fri Sep 25 11:43:24 CEST 2026 identity JLamp@6a01e23
true
primary constr. of Lamp false first use Fri Sep 25 11:43:24 CEST 2026 identity JLamp@5a955565
auxiliary constr. of Lamp false first use Fri Sep 25 11:43:24 CEST 2026 identity JLamp@5a955565
false

The two classes are in separate files in the course code (u05/JLamp.java, u05/JUseLamps.java); the output is the same as the Scala tryLamp. tryLamp ends with val v4 = new JLamp; println(v4.isOn): Java's isOn() is called without parentheses, as a Scala getter.

4. Why objects instead of static members?

Java uses static for a module (helper functions) or a singleton, but static calls are not polymorphic. Scala's object serves both purposes: members are reached through its global name, yet it is an object that can extend a class or trait. A client can depend on the trait and receive the singleton as an injected dependency, so Singleton stops being an anti-pattern.

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

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

  val s: SequenceGenerator = new SequenceFromSupplier(() => 1)
  println(s.get)             // 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(s2.get)            // 1
  println(GlobalCounter.get) // 2

println itself is a method of such an object, scala.Predef, imported by default.

5. Is everything really an object?

Scala is first of all a pure OO language: primitive values are objects (optimised to JVM values), all types have a common ancestor, all operations are methods, and special syntax recovers the non-OO look. Newcomers see the syntax as tricks; experienced programmers enjoy both the coherent object model and the sugar.

From the slides: the Java weaknesses Scala addresses

Primitive types and operators versus OOP; arrays versus classes; fields versus getters and setters; static versus instance members; constructors versus factories; equals, hashCode, toString. Sections 5–7 take them in turn.

Any AnyVal AnyRef= java.lang.Object Double Float Long IntShort Byte Char Boolean Unit String List, Option YourClass Null Nothing value types: JVM primitiveswhen possible reference types type of null Nothing: type of throw and ???, subtype of every type
The Scala type lattice, as in the deck. Arrows go from supertype to subtype. AnyVal includes Unit; AnyRef is java.lang.Object; Scala strings are java.lang.String.

The two bottom types make many expressions typeable. The deck suggests trying these in the REPL; here are the types Scala 3.3 infers:

ExpressionInferred type
() => { ??? }() => Nothing (??? throws NotImplementedError)
() => null() => Null
() => throw new Exception()() => Nothing
() => if (true) then 10 else ???() => Int: Nothing adds nothing to the join
() => if (true) then 10 else true() => Int | Boolean
Beyond the slides

The deck calls the type of if b then 1 else true a least upper bound, AnyVal, as in Scala 2. Scala 3 infers the union Int | Boolean; it widens a union only to a proper common class (subclasses B, C of A give A). val any: AnyVal = … still compiles.

Operators and functions are method calls

@main def typesExample() =  // a top-level main method
  val a: AnyVal = 1.2
  val b: Any = Object()
  val c: Null = null
  val e: java.util.Date = null // null is a Java thing..
  // val n: Nothing = throw new Error()
  // val iterable: Nothing = ??? // scala.NotImplementedError

  val i = 10
  val j = 10 + 20
  val k = i.+(20) // binary arguments are just method calls

  val f: Function1[Int, Int] = x => x+1  // package scala imported by default
  val g: Int => Int = f // Function1[Int,Int] an alias for (Int)=>(Int)
  println(f)          // u05.Types$package$$$Lambda/0x00007f52d4001408@63d4e2ba
  println(f(1))       // 2
  println(f.apply(1)) // 2, calling a function is invocation of apply
  println((f compose g compose f)(1))       // 4
  println((f.compose(g.compose(f)))(1))     // 4

  val f2: Function2[Int, Int, Int] = _ + _  // same as val f2: (Int, Int) => Int
  println(f2)         // u05.Types$package$$$Lambda/0x00007f52d4093b78@3aa9e816
  println(f2(1, 2))   // 3

10 + 20 is 10.+(20). A => B is syntax for the trait Function1[A, B] and f(1) for f.apply(1); tuple types are syntax for Tuple2, Tuple3…; constructors are called with method syntax. println(f) prints <function1> in Scala 2, as the slide says, and the lambda class in Scala 3.

6. What is uniform access?

Since operators are methods, method names need not differ from mathematical or logical operators, and whether an operator is a standard or an extension method hardly matters. One class shows all the pieces of uniform access:

object UniformAccess extends App:
  class A:
    var field = 10
    def meth(s: String):String = s + 10
    def ***(s: String):String = s + 10   // just a method called ***
    def prop: Int = field                // a getter, note lack of "()"
    def prop_=(i: Int): Unit = field=i   // corresponding setter
    def apply(index: Int): String = "indexer-get"
    def update(index: Int, s: String): Unit = println("indexer-set")
    def +:(i: Int):A = { field = field + i; this } // +: gives right-associativity

  val obj = new A
  val str = obj.meth("ciao") // four equivalent versions..
  val str2 = obj meth "ciao"
  val str3 = obj *** "ciao"
  val str4 = obj.***("ciao")
  println(str3)              // ciao10
  println(obj.prop)          // 10
  obj.prop = 1
  println(obj.prop)          // 1
  println(obj(10))           // indexer-get, java's obj[10]
  obj(11) = "casa"           // indexer-set, java's obj[11]="casa"
  obj.update(11, "casa")     // same as above: indexer-set
  10 +: 20 +: 30 +: obj      // obj.+:(30).+:(20).+:(10)
  println(obj.prop)          // 61

Fields and parameterless methods

Two consequences. Every runtime entity is an object: no wrapper confusion as in Java, generics over any type (Array[Int], Array[Lamp]), yet AnyVals and arrays compile to Java primitives. And every operation is a method call, field access included: exposing a field is the same as exposing a getter (and setter), so it does not violate encapsulation, and it is efficient. It is a deeper version of C# properties, and the reason public is the default rather than a keyword.

The members differ only in when they are evaluated: a def runs its body at every call, a val is evaluated once at construction, a var also has a setter. Hence the rules for contracts:

Methodologically, a class may expose val/var fields to its clients and later turn them into defs that add behaviour, without changing any client (OCP). One trait, three implementations:

// A person with immutable name/surname, mutable married flag, and toString
trait Person:
  def name: String
  def surname: String
  def married: Boolean
  def married_=(state: Boolean): Unit
  // a template method
  override def toString(): String = s"$name $surname $married"

// easiest implementation, with overriding class parameters becoming fields
class PersonImpl1(override val name: String,
                  override val surname: String,
                  override var married: Boolean) extends Person

// surname as a constant, name with overridden getter
class PersonImpl2(_name: String,
                  override var married: Boolean) extends Person:
  override def name =
    require(_name != null) // may throw IllegalArgumentException
    _name
  override val surname = "?"

// overridden getters and setters of married
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

@main def usePerson() =
  val p: Person = PersonImpl1("mario", "rossi", false)
  println(p)
  val p2: Person = PersonImpl2("mario", false)
  println(p2)
  val p3: Person = PersonImpl3("mario", "rossi", false)
  println(p3)
  p3.married = true
  println(p3)
mario rossi false
mario ? false
getting married property
mario rossi false
getting married property
mario rossi true

override var married in PersonImpl1 implements both married and married_=. PersonImpl3 replaces them with methods that log and validate (a married person cannot be set back to unmarried). The client code is the same for all three.

7. What do case classes and companion objects give you?

Any defines the final == and !=, which call equals and handle null, ##, which calls hashCode, and the overridable equals, hashCode, toString; AnyRef adds eq. Use == for equality and eq for identity. Writing equals by hand is trickier than in Java:

object Equality extends App:
  class Pair[A,B](val x: A, val y: B)

  class PairWithEq[A,B](val x: A, val y: B):
    // IntelliJ generation of code, with few adjustments
    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 CasePair[A,B](x: A, y: B)

  println(new Pair(10,20) == new Pair(10,20))              // false
  println(new PairWithEq(10,20) == new PairWithEq(10,20))  // true
  println(CasePair(10,20) == CasePair(10,20))              // true.. don't need 'new'
  val p = new Pair(10,20)
  println(p eq p)                                          // true.. is reference equality!

Scala 3 compiles PairWithEq with a warning: the type test case that: PairWithEq[A,B] cannot be checked at run time, because type arguments are erased; PairWithEq[?,?] says what is actually tested and compiles cleanly.

A case class is a record-like class whose construction mirrors its internal structure, so matching, equality and string representation can be generated. It gets automatically: a public immutable field per class parameter; toString, equals and hashCode based on the parameters; unapply in the companion object, for pattern matching; copy, to clone with some fields changed. The factory apply is added too. The slide says "unless the companion object is explicitly programmed", but a hand-written companion keeps it (Scala 3.3 replaces it only with an apply you define yourself), and in Scala 3 even plain classes are created without new. Every default can be reprogrammed.

apply and unapply

apply in an object O lets you write O(…), as for functions; it is typically a factory (in a class, it enables o(…)), and it supports DIP/OCP, since O(…) may be a constructor or a factory. unapply, the extractor, destructures an object and is called when matching case O(…); it returns Boolean, Option[X] or Option[(X1,…,Xn)] (scala.Option, the standard Optional). Write it only together with apply. Together they hide an implementation:

object ApplyUnapply extends App:
  // another, important application: factories hiding implementation
  trait Person:
    def name: String
    def surname: String

  object Person: // the companion object of trait/class Person
    def apply(name: String, surname: String): Person =
      PersonImpl(name,surname)
    def unapply(p: Person): Option[(String, String)] =
      Some(p.name, p.surname)
    private class PersonImpl(override val name: String,
                             override val surname:String) extends Person:
      assert(name != null && surname != null)

  // Showcasing apply
  val p: Person = Person("mario", "rossi")  // Hiding PersonImpl
  println(s"$p, ${p.name}, ${p.surname}")   // u05.ApplyUnapply$Person$PersonImpl@61a52fbd, mario, rossi
  // Showcasing unapply
  println(p match {case Person(n, s) => n+s})        // mariorossi
  println(Person.unapply(p).map((n, s) => n+s).get)  // mariorossi: roughly equivalent
  val Person(n, s) = p: @unchecked                   // matching in a val definition
  println(n + s)                                     // mariorossi

A companion object is the object C of a class or trait C: it holds C's "static" properties and shares private access with it. It typically contains factories, apply/unapply, utilities (as in java.util.Collections), would-be static nested classes, and hidden implementations of the trait.

8. How does pattern matching work on objects?

Pattern matching (lesson 2) abstracts algorithms, reads well and lets the compiler catch bugs. On objects it matches types (a safe instanceof) or structures given by extractors, with conditions, variable capture, _, backquoted variables in scope, or-patterns, and a default case that makes it total. The rough translation:

class Pair[A,B](val x: A, val y: B)
object Pair:
  def apply[A,B](x: A, y: B) = new Pair(x,y)
  def unapply[A,B](p: Pair[A,B]): Option[(A,B)] = Some((p.x,p.y))

class MyPair(override val x: Double, override val y: Double) extends Pair(x,y)

def match1(p: Pair[Double,Double]): String = p match
  case p: MyPair => "MYPAIR"         // matching by type: a safe 'instanceof'
  case Pair(_,0) => "X-axys"         // using unbounded parameters
  case Pair(x,y) if x==y => "X=Y"    // conditional case
  case _ => "?"

// Rough translation into equivalent code
def match2(p: Pair[Double,Double]): String =
  val unapplyResult = Pair.unapply(p)
  if p.isInstanceOf[MyPair] then "MYPAIR"
  else if unapplyResult.collect{case (_, 0) => true}.isDefined then "X-axys"
  else if unapplyResult.filter{case (x, y) => x==y}.isDefined then "X=Y"
  else "?"

println(match1(Pair(10,0)))        // X-axys
println(match1(Pair(0,2)))         // ?
println(match1(new MyPair(0,2)))   // MYPAIR

Every kind of pattern at once, with a custom extractor OfLength that matches only points farther than 1 from the origin:

object OfLength: // extracting distance to 0 when greater than 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"              // matching a structure, defined
  case Pair(2, Pair(0, _)) => "B"              // matching a structure, partially specified
  case Pair(3, null) | Pair (4, null) => "C"   // or
  case Pair(5, Pair(x, y)) if x == y => "D"    // conditional case
  case Pair(6, pair @ Pair(_,_)) if pair.y == 1  => "E" // capturing variable
  case Pair(7, Pair(`z`,`z`)) => "F" + z       // the value of z, not a new variable
  case Pair(8, OfLength(n)) => "G" + n         // using a custom extractor
  case Pair(9, _: MyPair) => "H"               // using a type check
  case _ => "?"                                // default case

println(process(Pair(1,Pair(1,1))))       // A
println(process(Pair(2,Pair(0,0))))       // B
println(process(Pair(3,null)))            // C
println(process(Pair(5,Pair(1,1))))       // D
println(process(Pair(6,Pair(1,1))))       // E
println(process(Pair(7,Pair(1,2))))       // ?
println(process(Pair(7,Pair(2,2))))       // F2
println(process(Pair(8,Pair(1,1))))       // G1.4142135623730951
println(process(Pair(8,Pair(0.2,0.2))))   // ?
println(process(Pair(9,new MyPair(10,10)))) // H

Patterns also work in variable declarations:

val t = (10, 20)
val (x, y) = t                 // same as x = t._1, y = t._2
val p = Pair(10, 15)
val Pair(z,w) = p              // exception if it does not match
val p2 = Pair(10, Pair(20, 30))
val Pair(_, Pair(a, b)) = p2   // a = 20, b = 30

Since Scala 3.2, a pattern definition that uses an extractor which may fail (such as Pair.unapply, returning an Option) compiles with a warning, pattern binding uses refutable extractor; writing = p: @unchecked states that a MatchError is acceptable. Tuples and case classes are irrefutable and give no warning.

9. How do control structures and "fancy syntax" work?

Imperative constructs are refurbished in a functional way: they are expressions, they translate into ordinary method calls, and most of them could be library code. if and while are straightforward, match is the switch, and for is a comprehension that works on any object with a foreach method.

val b = true
println(if (b) "OK" else "NO")  // if is actually an expression
println(if b then "OK" else "NO") // Scala 3 version without parenthesis
println(b match {case true => "OK" case _ => "NO"}) // equivalent version
val s = if b then  // recall that bodies are a special case of expr
  println("OK"); "OK"
else "NO"

val any: AnyVal = if b then 1 else true         // inferred Int | Boolean, see section 5
val i: Int = if b then 1 else throw new Error() // Int vs Nothing

def gcd(x: Int, y: Int): Int = // imperative style gcd
  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
// Basic uses of iterative for
for i <- 1 to 4 do print(i + " ")             // guess what 'to' is..
println()                                     // 1 2 3 4
for i <- 1 until 4 do print(i + " ")
println()                                     // 1 2 3
(4 to 1 by -1).foreach(i => print(i + " "))
println()                                     // 4 3 2 1

// Custom iterations, with a class and its factory in a companion obj
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 + " ")
println()                                     // 0 1 2 3 4 5 6 7 8 9

// A variant of for yield: a sequence of flatMap and foreach at the end
for
  i <- 0 until 10
  if i % 3 == 0
  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

1 to 4 is 1.to(4), a range. for … do becomes foreach, a guard withFilter: the last loop is (0 until 10).withFilter(i => i % 3 == 0).foreach(i => (0 until 2).foreach(j => …)). So ZeroTo needs only foreach; yield would need map/flatMap (lesson 4). Scala 3 deprecates i + " "; prefer s"$i ".

Variable arguments and arrays

def sum(args: Int*): Int =
  var s = 0
  for i <- args do s = s + i
  s
println(sum(1,2,3,4,5))     // 15

def printall(args: Int*) =
  args foreach (print(_))   // infix notation
printall(1,2,3,4,5)
println()                   // 12345

// Arrays are generic classes with proper apply method..
val a: Array[Int] = Array(1,2,3,4,5,6) // a Java-compatible array
a(2)=30
println(s"$a, ${a.apply(1)}, ${a(2)}, ${a.length}")      // [I@48140564, 2, 30, 6

// equivalent code thanks to Scala's apply, update and varargs
val b: Array[Int] = Array.apply(1,2,3,4,5,6)
b.update(2,30)
println(s"$b, ${b.apply(1)}, ${b.apply(2)}, ${b.length}") // [I@58ceff1, 2, 30, 6

The last parameter can have type T*, a sequence of actual arguments seen as a Seq[T], which has foreach. Array[T] is a generic class, compiled to a Java array, whose apply/update give the indexing syntax and whose companion's apply takes varargs.

Default and named arguments, braces, by-name parameters

Flexible syntax lets libraries look like new constructs, script-like but strongly typed: besides infix calls, apply and varargs, there are default values, named arguments, and braces for a single-argument list.

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"))                  // Person(bob,hope,1900): skipping args with a default
println(makePerson(name = "bob", surname = "hope")) // Person(bob,hope,1900): named args
println(makePerson(surname = "hope", name = "bob")) // Person(bob,hope,1900): no order
val p = makePerson(name = "bob", surname = "hope", year = 1920)
println(p)                                          // Person(bob,hope,1920)
println(p.copy(year = 1921))                        // Person(bob,hope,1921): copy of case classes
println(makePerson(name = "bob", "hope"))           // Person(bob,hope,1900)
println(makePerson("bob"))                          // Person(bob,?,1900)
println{makePerson{"bob"}}                          // Person(bob,?,1900): curly braces

@annotation.tailrec // call by-name
def mywhile(condition: => Boolean)(body: =>Unit): Unit =
  if condition then {body; mywhile(condition)(body)}

var n = 5
var res = 1
mywhile( n>=1 ) { // the same as while!!
  res = res * n
  n = n - 1
}
println("factorial "+res)                           // factorial 120

Defaults, named arguments and copy often make a Builder unnecessary. mywhile is a loop because its parameters are by name (=> Boolean, => Unit), re-evaluated at each use (lesson 3); currying plus braces make it look built in.

From the slides: formatting conventions (docs.scala-lang.org/style)

10. Lab 05

The lab (unibo-pps/pps-lab05) asks for idiomatic Scala OOP classes, Scala implementations of the logic of Java GUI exams, and an extractor.

From the lab: the four exercises, with sketches of Vector2D and sameCategory
  1. In it.unibo.pps.ex: an immutable Vector2D (every "modifying" method returns a new vector), then a mutable OnlineCoursePlatform (its methods return Unit). Ideally use private nested (case) classes in the companion object, and follow the comments and the mains.
  2. In java/polyglot/a05b: keep the Java GUI and implement its Logics interface as a compact Scala class LogicsImpl in scala/polyglot/a05b, using only the course's data structures (Sequence, Optional…, no Scala collections) and scala.util.Random (e.g. Random(42).nextInt(10)). It follows the structure of Prof. Viroli's OOP exams.
  3. The same for another OOP exam (or java/polyglot/01a/01b), this time writing Logics as a Scala trait called from the GUI, without looking at the OOP solution.
  4. An extractor sameCategory(cat) on a sequence of Courses, extracting the category common to all of them, if any; it must use scala.Option.

The lab's own interfaces may differ. The vector hides its implementation, a private case class, behind a trait and a factory, and its operations are symbolic methods:

trait Vector2D:
  def x: Double
  def y: Double
  def +(other: Vector2D): Vector2D
  def *(scalar: Double): Vector2D
  def dot(other: Vector2D): Double
  def magnitude: Double

object Vector2D:
  def apply(x: Double, y: Double): Vector2D = Vector2DImpl(x, y)

  private case class Vector2DImpl(x: Double, y: Double) extends Vector2D:
    def +(other: Vector2D): Vector2D = Vector2DImpl(x + other.x, y + other.y)
    def *(scalar: Double): Vector2D = Vector2DImpl(x * scalar, y * scalar)
    def dot(other: Vector2D): Double = x * other.x + y * other.y
    def magnitude: Double = math.sqrt(this dot this)
    override def toString = s"Vector2D($x, $y)"

@main def tryVector =
  val v = Vector2D(3, 4)
  val w = v + Vector2D(1, 1) * 2   // * binds tighter than +
  println(w)                       // Vector2D(5.0, 6.0)
  println(v.magnitude)             // 5.0
  println(v dot w)                 // 39.0
  println(Vector2D(1, 2) == Vector2D(1, 2))  // true: case-class equality

The extractor must return a scala.Option. It matches a non-empty sequence whose courses all share one category:

case class Course(title: String, category: String)

object sameCategory:
  def unapply(courses: Sequence[Course]): Option[String] = courses match
    case Cons(c, Nil()) => Some(c.category)
    case Cons(c, rest)  => unapply(rest).filter(_ == c.category)
    case Nil()          => None

// courses match
//   case sameCategory(cat) => println(s"$courses have same category $cat")
//   case _                 => println(s"$courses have different categories")
// Scala + Java (Programming): same category: Programming
// Scala + ML: different categories;  empty sequence: different categories

Check your understanding

When does a class parameter become a field?

When it is declared val/var, when a method body uses it (it becomes a private val), and always in a case class (public and immutable).

In what order does new Lamp run code, for a class with an auxiliary constructor def this() = { this(false); … }?

The primary constructor first (field initialisers and floating code, in textual order), then the rest of the auxiliary constructor's body.

Why does Scala's object make Singleton acceptable again?

Because an object is a real object: it can extend a trait, so clients can depend on the trait and receive the singleton (or any other implementation) as a dependency. Java static members are not polymorphic.

Where do Null and Nothing sit in the type hierarchy, and what are they for?

Null is a subtype of every reference type (the type of null); Nothing is a subtype of every type and has no values (the type of throw and ???). So if b then 1 else throw new Error() has type Int.

Rewrite 10 +: 20 +: obj and obj(3) = "x" as explicit method calls.

obj.+:(20).+:(10), since operators ending in : are right-associative and belong to the right operand; obj.update(3, "x").

Why is exposing a public val not a violation of encapsulation in Scala?

Clients access fields and parameterless methods with the same syntax, so the val can later become a def (or a var a getter/setter pair) with validation or logging, without changing any client.

Can a trait's val x: Int be implemented by def x = 1? And the other way round?

No: a val promises a stable value, so only a val can implement it. A def can be implemented by a def or a val, which is why contracts should prefer def.

What does a case class generate, and what do apply and unapply do?

Public immutable fields, equals/hashCode/toString on the parameters, copy, and a companion with apply (factory) and unapply (extractor, used by case C(…) patterns).

What is the minimum a class needs to be used in for x <- c do …? And in for x <- c yield …?

A foreach method for the first; map (and flatMap with more generators, withFilter with guards) for the second.

Why does mywhile(n >= 1) { … } behave like while?

Both parameter lists are by name, so the condition and the body are re-evaluated at every recursive call; currying lets the body be passed in braces.

Where this lesson comes from

Deck 05 – Modern, pure and terse object-orientation with Scala (64 slides: basic OO, constructors, Java interoperability, objects as singletons, the type hierarchy, uniform access, equality, case classes, apply/unapply, pattern matching, companion objects, control structures, arrays and varargs, fancy syntax, style) · lab 05 – OOP in Scala (repository pps-lab05) · course code code-scala/src/main/scala/u05 (all Scala files, JLamp.java, JUseLamps.java). All examples were compiled and run with Scala 3.3.6 and Java 21; dates and identity hash codes in the outputs change at every run.