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.
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:
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.
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.
return to forget. The value of a function is the expression on the right of the =.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.
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.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.
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.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.
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.
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.