λ Scala → Haskell · ch.10 · type-level
🔭 Chapter 10 · Types that compute

Types that compute
and your road ahead.

Haskell's type system isn't just generics — it can carry real facts and rule out whole classes of bug at compile time. This last chapter samples that power, then maps where to go after the series.

You've spent nine chapters learning that Haskell pushes work into the type system: errors become Either, effects become IO, absence becomes Maybe. This chapter is about the next step — making the type system itself do more, so that whole categories of bug simply can't compile.

None of this is exotic. It's the same instinct you already have when you reach for an opaque type in Scala 3 to stop a raw String from masquerading as a validated one. Haskell just gives you sharper tools for the same goal. We'll sample the headline ones, then close the series:

  • GADTs — constructors with precise result types, so a typed mini-AST can't be misbuilt.
  • Phantom types & newtypes — tags that live only at compile time, with zero runtime cost.
  • Type families & kinds — functions on types, and the "types of types" themselves.
  • The ecosystem & what's next — the tools and libraries you'll actually use, plus a roadmap.

1Types as a language

In Scala, generics top out around List[A] — types parameterised by other types. Useful, but the type system is mostly a filing system: it tracks what shape things are, and that's it.

Haskell lets the type system carry facts. Not just "this is a list," but "this is a list of length 3," or "this expression evaluates to an Int, never a Bool," or "this Double is in dollars and must never be added to euros." Once a fact lives in the type, the compiler enforces it for free — and the error shows up at compile time, not in production.

You already know the slogan: make illegal states unrepresentable. In Scala that's mostly a design discipline — pick your case classes so bad combinations can't be constructed. In Haskell it becomes literal: you encode the constraint in the type, and the type checker is your proof that no illegal state exists. The rest of this chapter is four concrete ways to do that.

💡
Think of the type checker as a small theorem prover that runs every compile. The more facts you hand it, the more bugs it rules out before your code ever runs. That's the whole game of type-level Haskell.

2GADTs — constructors that know their result

Recall ordinary data from Chapter 3: every constructor of data Expr a produces the same Expr a, whatever a happens to be. That's too loose if you're building a typed mini-language — nothing stops you from adding a number to a boolean.

A GADT (generalised algebraic data type) fixes that by letting each constructor declare its own precise result type. Turn on the GADTs extension and write the constructors with explicit signatures:


        

Read it line by line: IntLit builds an Expr Int, BoolLit builds an Expr Bool, and crucially Add only accepts two Expr Ints. There is no way to write Add on a boolean — it doesn't type-check. The evaluator can't go wrong, because the types it has to handle have already been narrowed.

If you've used Scala 3's enum Expr[A] with per-case extends Expr[Int], this is exactly the same idea — Scala calls them GADT-style enums. Haskell's version is just the original, and the syntax is closer to "here's each constructor's type signature." Build an expression below and watch valid ones evaluate while type-invalid ones get rejected.

Interactive · build a typed expression Pick pieces · valid trees evaluate · ill-typed ones are rejected
Outer node: Left: Right:

3Phantom types & newtypes — tags that cost nothing

Here's a bug every codebase has shipped: a Double in dollars added to a Double in euros, or meters mixed with feet. Both are Double, so the compiler is happy and the rocket lands in the sea.

A newtype wraps a value with a distinct type but zero runtime overhead — at runtime it is the underlying value, the wrapper exists only for the type checker:


        

The tag parameter is a phantom type: it appears on the left of the = but in no constructor on the right. It carries no runtime value — it's pure compile-time labelling. So Tagged "USD" Double and Tagged "EUR" Double are different types that hold the same bytes. Try to add a USD to a EUR and it won't compile; add two USDs and you're fine.

This is Scala 3's opaque type or the older tagged-type trick — a nominal wrapper the compiler distinguishes but the runtime forgets. Zero-overhead safety: you pay nothing and you can never mix the tags. Try it below.

Interactive · the phantom guard Pick two tags · same tag adds · mismatched tags won't compile
Left value: Right value:

4Type families — functions on types

You write functions on values every day: give them a value, they compute a value. A type family is the same idea one level up — give it a type, it computes a type. It's a function whose inputs and outputs are types.

A quick taste. Suppose every container type has an "element type." A type family lets you write that mapping down:


        

Here Elem is a function from a type to a type: feed it [Int] and it computes Int; feed it Text and it computes Char. Now a generic function can talk about "the element type of c" without knowing which container c is.

If you've used Scala's type memberstrait Coll { type Elem } and c.Elem — that's the same move: a type that depends on another type. Haskell calls the per-instance version associated types, declared right inside a typeclass. You won't reach for these on day one, but they're how the heavyweight libraries stay both generic and precise. We'll leave it at the taste.

5Kinds — the types of types

If values have types, what do types have? Kinds. A kind is the "type of a type," and it answers one question: how many type arguments does this thing still need before it's a real, value-holding type?

  • Int :: * — a complete type, kind star. It needs no arguments; you can have a value of type Int right now.
  • Maybe :: * -> * — not yet a type. Hand it one type (Maybe Int) and it becomes one. It's a type constructor.
  • Either :: * -> * -> * — needs two type arguments before it's complete.

This is why Functor in Chapter 5 was fussy about what you give it. Functor f requires f :: * -> * — a type with exactly one hole, like Maybe or [] or IO. You can't make Int a Functor (kind *, no hole) and you can't directly make Either one (kind * -> * -> *, two holes) — the kinds don't fit. In Scala this is the higher-kinded F[_] versus F[_, _] distinction; kinds make it explicit and check it.

And one sentence on going further: the DataKinds extension promotes ordinary values like True and numbers up into the kind level, so you can index a type by an actual number — that's how length-checked vectors like Vec 3 Int become possible.

🔭
In GHCi, :kind Maybe prints Maybe :: * -> * the same way :type prints a value's type. Same tool, one level up.

6The ecosystem you'll actually use

Theory aside, here's the practical landscape — the Haskell equivalents of sbt, Maven Central, and the libraries you import without thinking.

Build & packages

  • Cabal and Stack — the two build tools. Stack pins a curated, known-good set of package versions (a "resolver"), which makes builds reproducible; Cabal is the lower-level standard with its own solver. Either is your sbt.
  • Hackage — the central package repository, Haskell's Maven Central. Stackage is the curated, tested snapshot of it that Stack resolves against.

Libraries you'll reach for

  • text / bytestring — efficient strings. Use Text over the default String (a linked list of chars) in real code.
  • containersMap, Set, Seq. Your scala.collection immutable workhorses.
  • aeson — JSON encode/decode, the circe/play-json of Haskell.
  • mtl — the monad-transformer classes from Chapter 9, in their standard packaging.
  • lens / optics — composable getters/setters for deep immutable updates; like monocle in Scala.
  • servant — describe a web API as a type, get the server and client for free. A very Haskell idea, and a great showcase of this chapter.

Extensions worth knowing

GHC ships behind {-# LANGUAGE … #-} pragmas. The friendly starter set: OverloadedStrings (string literals become Text/etc.), RecordWildCards and NamedFieldPuns (less record boilerplate), LambdaCase, DeriveFunctor, and the ones from this chapter — GADTs and DataKinds — when you need them.

7Where to go next

You've got the language. The fastest way to make it stick is to build something and read a book alongside it.

Books

  • Programming in Haskell (Graham Hutton) — short, rigorous, the best on-ramp if you liked this series' pace.
  • Haskell Programming from First Principles (Allen & Moronuki) — enormous and thorough; the "learn it properly, exercise by exercise" choice.
  • Optics By Example (Chris Penner) — once you want lens/optics to click.

Practice

  • Exercism's Haskell track — bite-sized problems with mentor feedback.
  • Advent of Code — small, self-contained puzzles; Haskell shines on the parsing-and-folding ones.
  • Port a small Scala service you know. Translating familiar code is the fastest way to feel the differences land.

The whole series, in one map

Ten chapters, each anchored to something you already do in Scala. Click any node to recall what it gave you and its Scala bridge — then go finish the ones you skipped.

Interactive · the series, end to end Click a chapter · open it from the recall card
Tap a chapter above
Each node shows what you learned and the Scala idea it bridged from.

8The same idea, in both languages

The GADT-style typed expression, side by side. Scala 3's per-case extends Expr[Int] and Haskell's per-constructor signatures are the same move — narrow each constructor's result type so the evaluator is total and safe.

9Check yourself

3 questions · instant feedback 0 / 3

10That's the series

You started by unlearning statements and mutation, and you're ending with a type system that can prove your code right. Everything in between — currying, data, typeclasses, functor/applicative/monad, IO, laziness, transformers — was building toward the same payoff: programs you can reason about, and a compiler that catches the rest. You can read and write real Haskell now. Go build something.

Series complete
You've finished Scala → Haskell
From the mental shift through functions, data, typeclasses, functor/applicative/monad, IO, laziness, transformers, to type-level Haskell — you can read and write real Haskell now.