λ Scala → Haskell · ch.1 · the mental shift
🧭 Chapter 1 · What actually changes

You already think in types.
Haskell just removes the escape hatches.

You know Scala: functions, types, immutability, pattern matching. Haskell is the same instincts with the safety rails bolted on — no var, no sneaky side effects, no eager evaluation — and this chapter is the map of what's different before we touch a single new idea.

"Learn You a Haskell" is charming, but if you came from Scala it spends a lot of breath on things you already do every day. So let's skip the poetry. This chapter is one promise: you don't have to relearn how to think — you just have to notice which doors Haskell quietly nails shut.

Everything in this chapter anchors to Scala you already know. No monads, no functors, no jargon avalanche — just the handful of habits that actually change when you cross over.

1Two languages, one family

Scala and Haskell share parents. Both are statically typed functional languages with roots in the same type system (Hindley-Milner — the engine that figures out types so you don't have to). When you write Scala in its best, purest style — small functions, immutable data, pattern matching, types that describe what code does — you're already writing something that reads a lot like Haskell with curly braces.

The difference is what each language lets you do when you're not at your best. Scala leaves several escape hatches open:

  • var — reassign a binding, mutate state in place.
  • return — jump out of the middle of a method.
  • throw anywhere — toss an exception from any expression, typed or not.
  • eager evaluation — every argument is computed the moment you call.

Haskell takes those hatches and welds them shut. There's no var, no return statement, side effects are tracked in the type, and nothing is evaluated until something actually needs it. Same instincts, fewer escape hatches. That's the whole mental shift — the next six sections are just the specifics.

🧭
Keep this frame the whole chapter: every "new" Haskell rule is really an old Scala option being removed. You lose the escape hatch; in exchange the compiler can promise you more.

2Everything is an expression

In Scala you've already half-internalized this: an if isn't a statement that does something, it's an expression that evaluates to a value. val x = if (c) 1 else 2 just works. Haskell takes that idea all the way down — there are no statements at all, only expressions that produce a value.

Where Scala lets you write an imperative block (assign, mutate, return), Haskell gives you two ways to name intermediate values, both of which are themselves expressions:

  • let x = … in body — bind x, then use it in body. The whole thing is a value.
  • body where x = … — same idea, definitions trailing after the expression that uses them.

There's no "and then do this, and then do that." Every piece evaluates to something, and you build bigger values out of smaller ones. Drag the toggle below to watch a statement-style snippet morph into the equivalent Haskell expression.

Interactive · statement ⇄ expression Flip the toggle
Imperative · statements
Haskell · one expression
every node evaluates to a value
💡
Because there are no statements, there's nothing to "fall through" and no return to forget. The value of a function is the expression on the right of the =.

3Pure by default

In Scala, any method can quietly do something behind your back. You can slip a println, bump a counter, or fire off a network call inside a function whose signature says Int => Int — and the type won't bat an eye. The signature is a half-truth.

Haskell won't allow it. Anything that touches the outside world — printing, reading a file, getting the time, mutating a reference — shows up in the type, marked IO. A function that prints has type … -> IO (); you can't smuggle that effect into something typed String -> Int.

The payoff is the part Scala can't give you: a signature without IO is a promise. parse :: String -> Int can read its input, compute, and return — and that's all it can do. No logging, no surprise mutation, same output every time for the same input. The type tells the truth.

🔒
This is just a preview — Chapter 7 is all about IO and how Haskell still gets real work done while keeping effects honest. For now, the takeaway: no IO in the type means no side effects, guaranteed.

4Lazy by default

Scala is strict: when you call f(g(x)), it computes g(x) first, right now, whether or not f ends up needing it. Haskell flips the default — it doesn't evaluate an argument until something actually demands the value. Until then it sits as an unevaluated promise (a "thunk").

That sounds like a micro-optimization, but it changes what's expressible. You can write [1..] — the list of all the positive integers — and then take what you need:

take 5 [1..] ⟹ [1,2,3,4,5]

A strict language would try to build the entire infinite list before take ever runs — and hang forever. Haskell only forces the first five thunks. Step through it below.

Interactive · lazy stepper take 3 (map (*2) [1..])
nothing forced yet
⚠️
A strict language would hang here. It would try to build the whole infinite list [1..] before take got a chance to stop it. Laziness only forces the three thunks that are actually needed.
🔒
Scala isn't without laziness — you reach for it explicitly with lazy val or LazyList. In Haskell that opt-in is the default, and the opt-out (forcing things eagerly) is the thing you ask for. Full treatment in Chapter 8.

5Type inference everywhere

You already lean on inference in Scala — val xs = List(1, 2, 3) and you never wrote List[Int]. But Scala still makes you annotate the return type of public defs, and inference gives up at API boundaries. Haskell's inference (the Hindley-Milner engine again) is stronger: it can figure out almost every type in a program, top-level functions included, with no annotations at all.

So why does idiomatic Haskell write a type signature on every top-level function anyway? Not because the compiler needs it — because you do. The signature is living documentation that the compiler keeps honest:

greet :: String -> String

Inside a function you almost never annotate locals — inference handles them. At the top level you write the signature on purpose, as a promise you're asking the compiler to check. It's the opposite of Scala's "annotate because you must"; here you annotate because it's good manners.

6Meet GHCi

GHCi is Haskell's REPL — like the Scala REPL or a worksheet, but the fastest tool you have for actually learning the language. The trick that makes it special: you can ask it about types, live, without running anything. Three commands do most of the work:

  • :t expr — what's the type of this expression? (your most-used command by far)
  • :i name — info: where it's defined, what typeclass it belongs to.
  • :l file.hs — load a source file so you can poke at its functions.

The way you learn Haskell isn't by reading — it's by typing :t at things until the types stop surprising you. Click an expression below and watch GHCi tell you its inferred type.

Interactive · faux GHCi · :t Click an expression
ghci> .

7Your first program

Here's the whole thing side by side. Same logic, same shape — a pure greet function and an entry point that prints. The differences are exactly the four hatches from Section 1: no return, the effect lives in IO, types stated up front, evaluation handled for you.

The line that trips up newcomers is main :: IO (). It looks like a function, but there are no arguments — main is just a value whose type happens to be IO (): "an effectful action that, when run, produces nothing useful (() is Haskell's Unit)." The runtime takes that value and executes it. You don't call main; you describe it, and GHC runs it for you.