Paradigmi di Programmazione e Sviluppo — Prof. Viroli

PPS-05-Scala-Advanced

Advanced Scala: Scalability and Advanced Constructs

In this lesson

1. Functional Data Structures

Scala blends OOP and FP naturally. When building a data structure such as a linked list, we aim for immutability, rely on Option instead of exceptions, and support declarative operations like map, filter, and foreach. The idiomatic Scala approach defines the type with its OO-style method signatures, uses enums for the algebraic data type, and places factories or FP-style operations in the companion object.

Key idea

Scala's approach to data structures is OO/FP mixed: a single enum defines the contract, constructors (cases), and methods in one place. The companion object hosts factories and utility operations.

Consider our own List type—a cons-list similar to the standard library's, but built here to understand the mechanics:

2. Right-Associative Construction

Scala supports right-associative operators: any method whose name ends with : is right-associative. This is the secret behind the elegant 1 :: 2 :: 3 :: Nil syntax. Internally, a :: b is rewritten to b.::(a), so the list is built from right to left.

Oral exam

Be ready to explain: why does :: need right associativity? Because prepend to an immutable list is O(1), and chaining prepends naturally reads right-to-left: 10 :: 20 :: Nil creates Nil.::(20).::(10).

Three equivalent construction styles:

3. Folding: foldLeft and foldRight

Folding is the quintessential functional pattern for reducing a collection to a single value. Scala supports both left-to-right and right-to-left folds. Understanding the difference is critical for both correctness and performance.

Key insight

foldLeft is tail-recursive and processes elements left-to-right, accumulating a result. foldRight processes right-to-left using the stack—it is not tail-recursive, but it is natural for reconstructing structures (like map via foldRight).

Use the interactive stepper below to trace a foldLeft and foldRight evaluation on List(10, 20, 30):

4. OO/FP Mixed Design

The mixed OO/FP approach in Scala gives you method chaining (like Java streams), foreach/map support for for-comprehensions, and a clear separation of contract, construction, and algorithms. Many algorithms share a common fold structure and can be refactored to use foldRight.

Editor's note

map can be expressed as foldRight(Nil())(f(_) :: _), and filter as foldRight(Nil())((a,b) => if p(a) then a::b else b). This is the essence of fold-based refactoring.

The companion object provides factory methods like apply and of:

For mutable structures, we use a trait + private implementation pattern. Note that mutable operations have the same signature as their immutable counterparts but return this, enabling fluent chaining:

5. Scala Collections Framework

Scala's collections framework is one of the richest in any language, declared goals: easy, concise, safe, fast, parallel, universal. The architecture is layered:

The root trait is IterableOnce, which only requires an iterator method. Iterable adds template methods like fold, flatMap, map, etc.

Architecture

Seq splits into LinearSeq (optimised head/tail: List, LazyList) and IndexedSeq (optimised apply/length: Vector, Array, ArrayBuffer).

Explore the collection hierarchy in the diagram below:

classDiagram
  class IterableOnce { +iterator: Iterator }
  class Iterable { +map, +flatMap, +filter, +fold }
  class Seq
  class Set
  class Map
  class LinearSeq
  class IndexedSeq
  class View
  IterableOnce <|-- Iterable
  Iterable <|-- Seq
  Iterable <|-- Set
  Iterable <|-- Map
  Iterable <|-- View
  Seq <|-- LinearSeq
  Seq <|-- IndexedSeq

Aliases and constructors: without any import, Seq, Set, and Map refer to the immutable versions in scala.collection.immutable. Calling Seq(1,2,3) creates a List; IndexedSeq(1,2,3) creates a Vector; Set(1,2,3) creates a HashSet.

6. Collections in Practice

Working with the main collection types:

CollectionTypePerformance note
ListImmutable LinearSeqFast head/tail, slow indexed access
LazyListImmutable lazy LinearSeqElements computed on demand; memoised
VectorImmutable IndexedSeqFast apply/length, good all-around
ArrayMutable IndexedSeq (JVM array)Fixed length, low-level performance
ArrayBufferMutable IndexedSeqVariable length, good general-purpose
ListBufferMutable LinearSeqEfficient append to lists
Views

Views provide a lazy, immutable wrapper around a collection. Operations on a view are not computed until forced (via .toList or iteration). If the backing collection is mutable, changes to it are reflected in the view.

7. Immutability Strategies

Scala offers three strategies for managing state, each with different trade-offs:

PatternExampleStyleWhen to use
Mutable valprivate val buf = ListBuffer()Imperative/Java-likeOnly after precise performance evaluation
Immutable varprivate var list = Seq()Mixed imperative-declarativeWhen the domain concept calls for mutability
Immutable valprivate val list = List()FP stylePreferred; always push side-effects outward
Oral question

Compare the three Logger implementations. Why is private var strings = Seq() often preferred in Scala over a mutable val? Because it encapsulates mutability at the reference level, leaving the data structure itself immutable and sharable.

8. Nesting Constructs

Scala's constructs nest arbitrarily: classes inside classes (inner classes), classes inside objects (nested/static-like), methods inside methods (for recursive helpers), anonymous classes inside methods.

Scalability

The ability to nest constructs arbitrarily is what makes Scala scalable: the language grows with your needs, from a simple script to a large system.

9. Packages, Imports, Visibility

Packages in Scala are fully compatible with Java's but can also define namespaces directly in source files. Imports can appear at any point, are fine-grained, and support aliasing. Default imports include scala.*, java.lang.*, and scala.Predef.*.

Visibility modifiers in Scala:

ModifierMeaning
privateVisible only within the enclosing class/object
protectedVisible to subclasses
private[package]Visible within the specified package
protected[package]Visible to subclasses or within the package
Oral question

Why is private less frequent in Scala than in Java? Because the pattern "private field + public getter" is unnecessary thanks to uniform access. Also, private helper methods can be nested inside the single public method that uses them.

10. Abstract Types and Modelling

Scala supports abstract type members: a trait can declare type T, and subclasses concretise it with override type T = SomeType. This is an alternative to type parameterisation. Combined with alias creation, it enables powerful abstract modelling.

The Exams case study

The quintessential abstract modelling technique: define a system as a trait of abstract concepts (Student, Teacher, Course, Evaluation), then refine it progressively with concrete implementations.

Why this matters

This approach cannot be easily achieved in other mainstream languages. It lays the foundation for family polymorphism and mixin-based system composition.

11. Traits, Mixins, Linearization

Traits in Scala are richer than Java interfaces: they can carry fields, concrete methods, and be stacked using linearization. When a class mixes in multiple traits with super calls, Scala creates a linear chain that determines which implementation is invoked.

Rich interfaces define a few abstract methods and many template methods derived from them—Iterable is the canonical example:

Self-types express that a trait requires being mixed into a specific type, enabling a form of dependency injection without inheritance:

Linearization is how Scala resolves the diamond problem. For a class C extends Base with A with B, the linearization is computed as C → B → A → Base. The super call in each trait refers to the next element in this chain.

12. Exports and Delegation

Scala 3 introduces export, which eliminates the boilerplate of delegation. Instead of manually writing forwarding methods, you export the members of a delegated object:

Principle

Prefer delegation over inheritance (least power principle). Inheritance opens a class too much and hardly deals with multiple reuse. Exports make delegation zero-boilerplate.

Exports also serve as import units—reusable bundles of aliases and re-exports:

13. Variance in Generics

Scala uses declaration-site variance (unlike Java's use-site wildcards). A type parameter can be:

Common library examples:

Covariant stack: making a Stack covariant requires a more general push method that uses a lower type bound:

Oral question

Why can't Stack[T] be covariant? Because push(t: T) puts T in contravariant position. The solution is def push[R >: T](r: R): CoStack[R].

14. Extension Methods

Extension methods let you add new methods to existing types without modification. They are an application of the adapter pattern, automatically provided by the compiler when the extension is in scope:

Pure FP with extensions

Extension methods enable a purely functional style where you define types via enums/case classes and add operations as extensions in companion objects. This separates data from operations and recombines flexibly.

Do we really need OOP? Surprisingly, many cases can be solved with just enums + extension methods + delegation. Use OOP where it naturally fits (controllers, stateful models).

15. Contextual Programming

Scala 3's given/using mechanism separates inputs from context. Context parameters can be inferred from the scope, avoiding verbose explicit passing—perfect for strategies, configurations, and type classes.

Term inference

A given instance is a canonical term of a type that the compiler can summon automatically using using clauses or summon.

Context bounds (T: MyOrdering) enable ad-hoc polymorphism: a method works for any type T that has a MyOrdering[T] available:

16. For-Comprehension

for-comprehension is syntactic sugar for chains of map, flatMap, filter, and foreach. It is a key mechanism for encoding sequential, exploratory computations.

Oral question

Desugar the following for-comprehension into flatMap/map/filter calls. This is a classic oral exam question!

N-Queens—the canonical example of search-space exploration using for-comprehension:

17. ScalaTest

ScalaTest is the primary testing framework for Scala, offering multiple styles. The AnyFunSuite style is simple and familiar; AnyFlatSpec yields more readable output with should matchers that read like natural language:

DSL power

ScalaTest showcases how Scala's flexibility (extension methods, given instances, infix notation) enables beautiful DSLs. The chess board test example uses custom operators to literally draw the board in code.

Check Your Understanding

Explain the difference between foldLeft and foldRight. Which one is tail-recursive?

foldLeft processes elements left-to-right and is tail-recursive: the accumulator is computed first, then passed to the next iteration. foldRight processes right-to-left: it pushes recursive calls onto the stack and combines on return. foldRight is not tail-recursive, but it expresses structure-preserving transformations naturally (e.g., map, filter, concat).

Desugar the expression for x <- List(1,2); y <- List(3,4) if x+y > 3 yield (x,y) into flatMap/map/filter calls.

List(1,2).flatMap(x => List(3,4).filter(y => x + y > 3).map(y => (x, y)))

Why does :: need to be right-associative in Scala?

Because a :: b :: Nil should mean Nil.::(b).::(a) (prepend a then b). Right associativity makes the chain read naturally left-to-right: “prepend 10, prepend 20, prepend 30 to Nil”.

Compare the three immutability strategies. When would you use each?

Mutable val (e.g., val buf = ListBuffer()): for hot loops with verified performance need. Immutable var (e.g., var list = Seq()): for domain concepts that are naturally mutable (register state, counters). Immutable val (e.g., val list = List()): preferred FP style; the data never changes, new versions are created. Always push side-effects outward.

What is trait linearization? Give an example with NonEmpty and BasicParser.

Linearization resolves which super implementation is called when traits are stacked. For NonEmptyBasicParser extends BasicParser with NonEmpty[Char], the linearization is NonEmptyBasicParser → NonEmpty → BasicParser → Parser. NonEmpty's super.parse(t) calls BasicParser's parse.

How do you make Stack[T] covariant? What is the key change to the push method?

Declare trait CoStack[+T] and change push to use a lower type bound: def push[R >: T](r: R): CoStack[R]. This prevents inserting the wrong type by widening the result type.

What is the difference between import and export in Scala 3?

import brings names into the current scope for use. export re-exports stable members of a value to the enclosing scope, making them part of the public API—it is the inverse of import, and it enables zero-boilerplate delegation.

Explain how abstract types enable the Exams modelling approach.

The Exams trait declares abstract types (type Student, type Teacher, type Course, type Evaluation) that are concretised by subclasses (override type Student = String). This defines a whole “system” of concepts that can be refined progressively, keeping all mutual dependencies consistent without type parameters.