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.
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:
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.
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:
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.
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):
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.
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:
Scala's collections framework is one of the richest in any language, declared goals: easy, concise, safe, fast, parallel, universal. The architecture is layered:
Iterable, Seq, Set, Map)The root trait is IterableOnce, which only requires an iterator method. Iterable adds template methods like fold, flatMap, map, etc.
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.
Working with the main collection types:
| Collection | Type | Performance note |
|---|---|---|
List | Immutable LinearSeq | Fast head/tail, slow indexed access |
LazyList | Immutable lazy LinearSeq | Elements computed on demand; memoised |
Vector | Immutable IndexedSeq | Fast apply/length, good all-around |
Array | Mutable IndexedSeq (JVM array) | Fixed length, low-level performance |
ArrayBuffer | Mutable IndexedSeq | Variable length, good general-purpose |
ListBuffer | Mutable LinearSeq | Efficient append to lists |
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.
Scala offers three strategies for managing state, each with different trade-offs:
| Pattern | Example | Style | When to use |
|---|---|---|---|
| Mutable val | private val buf = ListBuffer() | Imperative/Java-like | Only after precise performance evaluation |
| Immutable var | private var list = Seq() | Mixed imperative-declarative | When the domain concept calls for mutability |
| Immutable val | private val list = List() | FP style | Preferred; always push side-effects outward |
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.
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.
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.
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:
| Modifier | Meaning |
|---|---|
private | Visible only within the enclosing class/object |
protected | Visible to subclasses |
private[package] | Visible within the specified package |
protected[package] | Visible to subclasses or within the package |
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.
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 quintessential abstract modelling technique: define a system as a trait of abstract concepts (Student, Teacher, Course, Evaluation), then refine it progressively with concrete implementations.
This approach cannot be easily achieved in other mainstream languages. It lays the foundation for family polymorphism and mixin-based system composition.
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.
Scala 3 introduces export, which eliminates the boilerplate of delegation. Instead of manually writing forwarding methods, you export the members of a delegated object:
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:
Scala uses declaration-site variance (unlike Java's use-site wildcards). A type parameter can be:
+A): Container[Cat] <: Container[Animal]-A): Handler[Animal] <: Handler[Cat]Common library examples:
Covariant stack: making a Stack covariant requires a more general push method that uses a lower type bound:
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].
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:
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).
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.
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:
for-comprehension is syntactic sugar for chains of map, flatMap, filter, and foreach. It is a key mechanism for encoding sequential, exploratory computations.
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:
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:
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.
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).
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)))
:: 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”.
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.
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.
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.
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.
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.