λ Scala → Haskell · ch.3 · data & patterns
🧩 Chapter 3 · Shapes of data

Your sealed trait and case classes,
in one keyword.

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:

  • Sum + product types — your sealed trait hierarchy, as a single data declaration.
  • Pattern matching — Scala's match, with a compiler that nags about missing cases.
  • Maybe and EitherOption and Either, no nulls, errors as values.
  • Records and recursion — named fields for free, and lists as a sum type you fold over.

1Sum and product, in one keyword

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:

  • "One of these choices" is a sum type — the | in Haskell, the sealed hierarchy in Scala. A Shape is a Circle or a Rect, never both.
  • "Bundles some fields" is a product type — a 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.

Interactive · build the ADT Toggle constructors · pick a value to match
Sample value: pick a value to highlight its clause

2Defining data

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 =.

💡
Because a constructor is a function, you can use it anywhere a function fits — map Circle [1.0, 2.0] gives you [Circle 1.0, Circle 2.0]. No .apply, no new, no companion object.

3Pattern matching

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.

Interactive · trace a match Click a value to run it through the case
pick a value above
⚠️
Cover every case. If 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.

4Maybe — 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.

5Either — errors as values

When 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.

6Records — name the fields

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:

  • Accessor functions. 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.
  • Record-update syntax. 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.

7Recursion over data

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.

Interactive · the cons list Prepend with cons · transform with map
[] — empty list

8The same idea, in both languages

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.

9Check yourself

3 questions · instant feedback 0 / 3

10Where this goes next

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.

🧰
Up next · Chapter 4
Typeclasses
The given/using you already know — Haskell's original take on ad-hoc polymorphism.