A Scala crash course for FP ideas
Lesson 01 gave us the operational definition of FP — pure functions over immutable data, with meaning fixed by the substitution model ("replace an expression with its value; the meaning is unchanged"). But every example from here on is written in Scala, and you cannot follow an argument in a language you cannot read. This lesson is a targeted tour: just enough Scala to read and write every later snippet — not a language manual. We pick exactly the features the FP ideas need (immutable bindings, expressions, case classes, sum types, pattern matching, generics, List/Option, lambdas) and skip the rest.
New capability: read and write enough Scala to follow every snippet in Lessons 03–14: bindings, methods, expressions, case classes and sealed traits, pattern matching, generics, the core collections, and lambdas.
val vs var and why we default to val. (2) def, method syntax, and type annotations. (3) everything is an expression — the link back to substitution. (4) case class as an immutable record. (5) sealed trait + cases as a sum type. (6) pattern matching. (7) generics. (8) the collections you'll see: List, Option, Map. (9) lambdas and methods that take functions. We close with one end-to-end worked example that uses almost all of it.1 · val vs var — and why val wins by default
Scala has two ways to name a value. A val is a binding you cannot reassign; a var is a mutable variable you can.
val x = 10 // immutable binding: x is 10, forever
var y = 10 // mutable variable
y = 11 // legal — y now names 11
// x = 11 // compile error: reassignment to val
We default to val everywhere in this series, and reach for var almost never. The reason is exactly Lesson 01's substitution model: if x can never be reassigned, then wherever you see x you can replace it with 10 and the meaning is unchanged — you reason locally. A var breaks that: y means different things at different times, so you must track when you are reading it. This is the same "mutable state is an engine of complexity" point from Lesson 00, now visible as a one-keyword choice. (Lesson 03 builds the full case for immutability on top of this default.)
2 · def, method syntax, and type annotations
def defines a method (a named, possibly parameterized expression). Parameters are written name: Type; the return type goes after the parameter list, then =, then the body.
def add(a: Int, b: Int): Int = a + b // one-expression body, no braces needed
def square(n: Int): Int = n * n
val r = add(2, square(3)) // r == 11
Read a: Int as "a, which is an Int." Scala can often infer types (we wrote val x = 10 without saying Int), but parameter types are required, and annotating a method's return type is good practice — it is a checked piece of documentation. You will see Int, String, Boolean, Double, and Unit (the type with a single value (), used like void — a method returning Unit exists only for its side effect, a smell we will learn to push to the shell).
3 · Everything is an expression
This is the feature the whole series leans on. In Scala, constructs that are statements in many languages are expressions that evaluate to a value. An if returns a value; a { ... } block returns its last expression.
val sign = if (n > 0) "pos" else if (n < 0) "neg" else "zero" // if is an expression
val area = { // a block is an expression
val w = 3
val h = 4
w * h // the block's value is its last line: 12
}
There is no separate "ternary operator" and no need to assign inside an if; the if is the value. This is precisely what makes the substitution model from Lesson 01 usable: every piece of code stands for a value, so you can replace it with that value and reason about the result. Keep this in mind — when we later say "a program is one big expression you evaluate," this is the mechanism.
4 · case class — an immutable record with value equality
A case class is Scala's immutable record. One line gives you a constructor (no new needed), public read-only fields, a sensible toString, structural (value) equality, and a .copy for making a modified new instance.
case class Point(x: Int, y: Int)
val p = Point(1, 2) // construct; p.x == 1, p.y == 2
val q = p.copy(y = 5) // NEW Point(1, 5); p is untouched
p == Point(1, 2) // true — equality compares fields (value equality)
q == p // false
Note .copy: you never mutate p; you derive a new value from it. That is "update by building a new value," the heart of immutability (Lesson 03), and the case class makes it a one-liner. The value equality matters too: two case-class instances with equal fields are ==, unlike ordinary objects which compare by reference.
5 · sealed trait + cases — a sum type
Often a value is "one of a fixed set of shapes." A sealed trait with case classes / case objects expresses exactly that. Sealed means all subtypes live in this file, so the compiler knows the full list.
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
case object Unit2D extends Shape // a case OBJECT: a single value, no fields
A value of type Shape is a Circle or a Rect or Unit2D — nothing else. This is a sum type ("a value is one of these alternatives"), the dual of the case class's product type ("a value is this field and that field"). Lesson 06 makes "types as sets" precise and shows how sum + product types let you make illegal states unrepresentable; for now, just recognize the shape: sealed trait at the top, the alternatives as cases below.
6 · Pattern matching
Pattern matching is how you take a sum type apart. x match { case ... } tests x against patterns top to bottom and runs the first that fits — and because it is an expression, the whole match returns a value.
def area(s: Shape): Double = s match {
case Circle(r) => 3.14159 * r * r // binds r from the Circle
case Rect(w, h) => w * h // destructures both fields
case Unit2D => 0.0
}
Because Shape is sealed, the compiler checks the match is exhaustive and warns if you forget a case — a real safety win. You can match on Option the same way (we meet Option next):
def greet(name: Option[String]): String = name match {
case Some(n) => s"Hello, $n" // s"..." is an interpolated string
case None => "Hello, stranger"
}
7 · Generics
A method or type can be parameterized by a type, written in square brackets. [A] reads "for any type A."
def firstOf[A](xs: List[A]): A = xs.head // works for List[Int], List[String], ...
def identity[A](a: A): A = a
The same machinery names container types: List[A], Option[A]. You will see [A], [A, B] constantly from Lesson 05 onward — they mean the code does not care which type, only its shape, which is what lets one function compose with many.
8 · The collections you'll see: List, Option, Map
List[A] is an immutable singly-linked list. It is built from two pieces: the empty list Nil, and the cons operator :: which prepends an element. 1 :: 2 :: Nil is the list List(1, 2).
val xs = List(1, 2, 3) // sugar for 1 :: 2 :: 3 :: Nil
val ys = 0 :: xs // List(0, 1, 2, 3) — a NEW list; xs unchanged
xs.head // 1 (first element)
xs.tail // List(2, 3) (everything but the head)
Option[A] models "a value that might be absent" — Scala's typed replacement for null. It is itself a sum type: Some(a) (present) or None (absent).
val found: Option[Int] = Some(42)
val missing: Option[Int] = None
def lookup(m: Map[String, Int], k: String): Option[Int] = m.get(k) // returns Some/None
One more you'll meet in examples: Map[K, V] is an immutable key→value dictionary, and crucially its .get(k) returns an Option[V] — Some(v) if the key is present, None if not — rather than throwing or returning null. That is the typed-absence idea again (Lesson 11), built into the standard lookup.
Because absence is in the type, the compiler forces you to handle it (via the match above), so you cannot forget the missing case the way a null lets you. Lesson 11 builds the full story of typed failure on Option, Try, and Either.
9 · Lambdas, the underscore, and methods that take functions
A lambda (anonymous function) is written (param: Type) => body. Functions are ordinary values, so you can pass one to a method. The collection methods you'll use most — map, filter — take a function.
val inc = (x: Int) => x + 1 // a function value of type Int => Int
inc(4) // 5
List(1, 2, 3).map(x => x * 2) // List(2, 4, 6) — apply the function to each element
List(1, 2, 3).map(_ * 2) // same, using the _ shorthand: _ stands for the argument
List(1, 2, 3, 4).filter(_ % 2 == 0) // List(2, 4)
The underscore _ is shorthand for a single argument: _ * 2 means x => x * 2. This "pass a function to a method" move is the seed of Lesson 05 (functions as values, higher-order functions) and of the whole functor/monad story (Lessons 08, 10) — map is the first higher-order function you'll see everywhere.
1 + 2 is really 1.+(2), and xs.map(f) can read xs map f. So :: above is just a method named ::. Don't overuse it; just recognize it.for { } yield: you will see blocks like for { a <- optX; b <- optY } yield a + b. Despite the keyword, this is not a loop — it is sugar for flatMap/map that sequences context-carrying steps. We defer its full meaning entirely to Lesson 10; for now, read it as "do these steps in order, producing a result," and do not think of it as iteration.List, and pattern matching all at once.
case class Money(cents: Int) // product type (a record)
sealed trait Outcome // sum type: a payment is one of...
case class Paid(amount: Money) extends Outcome // ...succeeded with an amount
case object Declined extends Outcome // ...was declined (no data)
case class Failed(reason: String) extends Outcome // ...errored with a reason
// total only the successful payments; everything is an expression
def collected(outcomes: List[Outcome]): Money = {
val cents = outcomes.map { // map a function over the list
case Paid(Money(c)) => c // nested pattern: pull out the Int
case Declined => 0
case Failed(_) => 0 // _ matches anything, binds nothing
}.sum
Money(cents)
}
val ledger = List(Paid(Money(500)), Declined, Paid(Money(250)), Failed("network"))
collected(ledger) // Money(750)
Trace it: map turns each Outcome into an Int by pattern matching (the exhaustive, sealed match the compiler checks), .sum folds the List[Int] to 750, and the block's last expression Money(cents) is its value. No var, no mutation, no null — and every line is replaceable by its value. That is the entire idiom you will read for the rest of the series, in miniature.Common mistakes / failure modes
var by reflexval plus map/filter/fold. If you typed var, pause and ask whether a transformation expression would do.== surprises== on a case class is value equality (compares fields), which is what you want. But it is easy to assume ordinary (non-case) classes compare by value too — they compare by reference. If equality "should" work and doesn't, check whether the class is a case class.equals when a one-word case class would have given correct structural equality, hashCode, and .copy for free.for is a loopfor { } yield is sugar for flatMap/map (Lesson 10). Reading it as iteration will mislead you the moment the "loop" is over an Option or an Either, where there is nothing to iterate.Checkpoint exercise
next that returns the light that follows the standard cycle (Green → Yellow → Red → Green). Write it as a sealed trait with three case objects, plus a next: TrafficLight => TrafficLight using a pattern match. Confirm to yourself that the match is exhaustive (the compiler would warn if you dropped a case), and that no var appears anywhere. Sketch: sealed trait TrafficLight; case object Red/Yellow/Green extends TrafficLight; def next(t: TrafficLight) = t match { case Green => Yellow; case Yellow => Red; case Red => Green }.Where this points next
You can now read every snippet the series will throw at you. The two features we leaned on hardest — val and case class with .copy — are not just syntax; they are the working tools of immutability. Lesson 03 takes them up directly: it shows the aliasing and shared-mutation bugs that mutable data causes, draws the line between a value and its identity, and explains copy-on-write updates via .copy — so the habits this lesson made mechanical (default to val, derive new values instead of mutating) get their full justification. It will also raise the cost worry ("isn't building new values wasteful?") that Lesson 12 later answers.
val (immutable binding, the default) vs var (mutable, avoided); def with name: Type annotations; everything is an expression (if and blocks return values — the substitution model made concrete); the case class as an immutable record with .copy and value equality (a product type); the sealed trait + cases as a sum type ("one of these"); pattern matching to take sum types apart, checked for exhaustiveness; generics [A]; the core collections List (with ::/Nil) and Option (Some/None, typed absence); and lambdas (x: Int) => ... / the _ shorthand passed to map/filter. Two syntaxes were flagged to merely recognize — infix method calls and for { } yield (not a loop; deferred to Lesson 10). With val and case class in hand, Lesson 03 builds the discipline of immutability on top of them.Interview prompts
- Why does idiomatic Scala default to
valovervar? (§1 — avalcan't be reassigned, so it means one thing forever and you can substitute its value anywhere;varmeans different things at different times and forces non-local reasoning.) - What does "everything is an expression" mean, and why does FP care? (§3 —
ifand{}blocks evaluate to a value rather than just doing something; this is what makes the substitution model usable — every construct stands for a value you can replace it with.) - What do you get for free from a
case class? (§4 — a no-newconstructor, read-only fields,toString, structural/value equality andhashCode, and.copyfor deriving a modified new instance without mutation.) - Distinguish a product type from a sum type in Scala. (§4–5 — a
case classis a product ("this field AND that field"); asealed traitwith cases is a sum ("a value is one of these alternatives"). Lesson 06 formalizes both.) - Why does sealing a trait matter for pattern matching? (§5–6 — sealed means the compiler knows the complete set of subtypes, so it can check the
matchis exhaustive and warn about a missing case.) - How does
Optionimprove onnull? (§8 — absence is encoded in the type asNonevsSome(a), so the compiler forces you to handle the missing case (viamatch), whereas anullcan silently slip through. Full story in Lesson 11.) - Is
for { } yielda loop? (§9 callout — no; it is syntactic sugar forflatMap/mapthat sequences context-carrying steps, fully explained in Lesson 10. Reading it as iteration breaks down overOption/Either.)