In Scala you reach for a sealed trait plus a handful of case classes to model "a value is one of these shapes." Haskell does the whole thing with a single data declaration — and pattern matching falls straight out of it.
You already model data the right way. A sealed trait with a few case classes says "a value is exactly one of these shapes, and each shape carries its own fields." Haskell agrees with that design — it just spells it in one line and hands you exhaustive pattern matching for free.
This chapter walks the whole toolkit you reach for daily, one Scala anchor at a time:
sealed trait hierarchy, as a single data declaration.match, with a compiler that nags about missing cases.Option and Either, no nulls, errors as values.Picture a sealed trait Shape with case class Circle and case class Rect under it. You're saying two things at once: a Shape is one of these choices, and each choice bundles some fields.
Those are the two ways to combine types, and they have names:
| in Haskell, the sealed hierarchy in Scala. A Shape is a Circle or a Rect, never both.Rect holds a width and a height, like a tuple or a case class's constructor args.Scala makes you write a trait plus N case classes to get this. Haskell's data expresses both the sum (the choices) and the products (each choice's fields) in a single declaration. Add a constructor below, watch the area function grow a matching clause — that's the "add a case, handle a case" loop, made live.
Here's the whole declaration. Read the | as "or" and each capitalized name as one of the shapes:
data Shape = Circle Double | Rect Double Double
Each capitalized name — Circle, Rect — is a constructor. And here's the part that surprises Scala folks: a constructor is just a function that builds the value. Circle has type Double -> Shape — hand it a radius, get back a Shape. Rect is Double -> Double -> Shape.
That maps straight onto Scala's case class Circle(r: Double), whose companion apply is exactly that builder function. The difference is bookkeeping: Scala wants a class per case and the extends Shape wiring; Haskell lists them after one =.
map Circle [1.0, 2.0] gives you [Circle 1.0, Circle 2.0]. No .apply, no new, no companion object.Once a value was built by one of those constructors, you take it apart by asking "which one built it?" — and pull the fields back out in the same move. That's pattern matching, and it's Scala's match with less ceremony.
Two ways to write it. As a case expression:
area s = case s of
Circle r -> pi * r * r
Rect w h -> w * h
Or — more idiomatic — as separate function clauses, one per shape, matching right in the argument position:
area (Circle r) = pi * r * r
area (Rect w h) = w * h
The r, w, h are bindings — names that capture the fields of the matched constructor, exactly like case Circle(r) => in Scala. Step a value through the branches below and watch the matching clause light up.
area matches only Circle and forgets Rect, GHC won't refuse to compile — it'll emit a non-exhaustive patterns warning, then blow up at runtime the moment a Rect arrives. Compile with -Wall (or -Wincomplete-patterns) and treat it like Scala's exhaustiveness check on a sealed match: a missing case is a bug.Maybe — Haskell's Option"A value that might be missing" is something you model every day with Option. Haskell calls it Maybe a, and it's the same sum type you'd build yourself:
data Maybe a = Nothing | Just a
Just x is Scala's Some(x); Nothing is None. A lookup that may fail returns the absence in its type, so there's no null to forget about — the compiler makes you pattern-match the missing case before you can touch the value:
lookup 2 [(1,"a"),(2,"b")] -- => Just "b"
lookup 9 [(1,"a"),(2,"b")] -- => Nothing
Try both Just 5 and Nothing in the tracer above — they run through the same case machinery as everything else, because Maybe is nothing special. It's just a data declaration in the standard library.
Either — errors as valuesWhen you need to say why something failed, not just that it did, you reach for Either[A, B] in Scala. Haskell's is identical:
data Either a b = Left a | Right b
Left carries the error, Right carries the success — and the mnemonic writes itself: "right" means right. By convention the success path is always Right, the failure is Left.
The point is the same as Scala's: an error travels in the return type instead of flying out as a thrown exception. A function that might fail is honest about it right in its signature, and every caller has to deal with both branches:
safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "divide by zero"
safeDiv a b = Right (a `div` b)
Run Left "boom" and Right 42 through the tracer to see both branches in action.
Positional fields are fine for a Rect, but once a type has a name, an age, and an email, you want named fields — exactly what case class Person(name: String, age: Int) gives you. Haskell's record syntax is the same idea:
data Person = Person { name :: String, age :: Int }
You get two things for free, both of which mirror Scala:
name p pulls the name out of a Person — same as p.name, just function-first. Each field name becomes a getter of type Person -> String.p { age = 31 } builds a fresh Person with everything the same but age bumped — that's Scala's p.copy(age = 31). The original p is untouched; values are immutable.And you can still pattern-match a record positionally or by field, so records don't cost you any of the matching power from earlier.
Here's the idea that ties the chapter together: a list is itself a sum type. It's one of two shapes — empty, or a head stuck onto a tail. Written out, it's the data declaration you've already learned to read:
data [a] = [] | a : [a] -- empty, or head `cons` tail
[] is the empty list; x : xs is element x prepended ("consed") onto the rest xs. So [1, 2, 3] is really 1 : 2 : 3 : [] — sugar over nested constructors.
Because a list is a sum of two shapes, you write functions over it by matching those two shapes. One clause for empty, one clause for "head and tail" — and the function calls itself on the tail. That's structural recursion, the bread and butter of working with data:
len [] = 0
len (_:xs) = 1 + len xs
The _ says "there's a head here but I don't care about it." Build a list below, then map over it — every function on lists is a variation on this two-clause shape.
Side by side: the sealed trait + case classes you'd write in Scala, and the single data declaration plus matching clauses in Haskell. Same model, less ceremony.
You can now model any shape of data and tear it apart by pattern. The next question is how to write one function that works across many types — show on a Shape, a Person, an Int — without overloading by hand. That's the job of typeclasses, Haskell's take on the given/using you already use in Scala.