Chapter 17 — Coming from Haskell

If you know Haskell, Lean 4 will feel familiar — same ML-family lineage, same emphasis on types and purity — but the differences matter in daily use, and there is no Hackage, so finding an API works differently. This chapter is a translation guide plus a "how do I look things up" workflow.

17.1 Syntax and idiom, side by side

ConceptHaskellLean 4
define a valuex = 1def x := 1
functionf x = x + 1def f x := x + 1
lambda\x -> x + 1fun x => x + 1
type signaturef :: Int -> Intdef f : Int → Int
type applicationf @Intf (α := Int) / @f Int
algebraic data type`data T = A \B Int``inductive T \A \B (n : Int)`
recorddata P = P { x :: Int }structure P where x : Int
type classclass Show a where …class ToString (α) where …
instanceinstance Show T where …instance : ToString T where …
MaybeMaybe a / Just / NothingOption α / some / none
EitherEither e a / Left / RightExcept ε α / .error / .ok
list[1,2,3], x:xs[1,2,3], x :: xs
list map/filtermap f xs, filter p xsxs.map f, xs.filter p
string interpolationprintf / Text.printfs!"value is {x}"
do (monadic)do { x <- act; … }do let x ← act; …
bind / pure>>= / return>>= / pure (return also works)
alternative`<>` (Alternative)`<> (OrElse / Alternative`)
where clausef = … where g = …where/let … := … (or a top-level def)
guards`f x \p x = …`if p x then … else … / match
newtypenewtype N = N Intstructure N where val : Int (or def N := Int)

Two habits to unlearn:

s.toUpper, arr.push x. The function usually lives in the type's namespace (List.map, String.toUpper), and x.f a means T.f x a.

implicit type arguments in { } are inferred, like Haskell's, but you can always pass them explicitly with (α := …).

17.2 Mutation: IORef, MVar, STRefIO.Ref, let mut

Haskell hides mutation behind IORef / STRef / MVar. Lean has the same tools, plus an ergonomic let mut inside do (still pure — desugars to a state-passing fold, no actual mutation escapes):

HaskellLean 4
newIORef xIO.mkRef x
readIORef rr.get
writeIORef r xr.set x
modifyIORef r fr.modify f
runST + STRefrunST + ST.mkRef
MVar (concurrency)Std.Mutex / Std.Channel (Ch 13)
— (no direct analogue)let mut x := 0 inside do
#eval show IO Nat from do
  let r ← IO.mkRef 0
  for i in [1:5] do
    r.modify (· + i)      -- 1 + 2 + 3 + 4
  r.get
10

let mut is the one Haskell doesn't have — a loop-local mutable name that reads like imperative code but compiles to a pure fold. Reach for IO.Ref only when state must be shared across actions. See Ch 9 §9.8 and Ch 13 for the full story.

17.3 Finding an API without Hackage

There is no cabal search / Hackage. Instead the type is the search key, and the tools are in the compiler and editor:

Hover shows the type and doc-string; <kbd>F12</kbd> jumps to the definition; .-autocomplete lists everything in a type's namespace (type str. and see every String.*).

  #check   @List.foldl          -- show its full type
  #print   List.foldl           -- show its definition
  #eval    "abc".toList         -- run it

the goal you want and let Lean search the library for you:

  example (xs : List Nat) : List Nat := by exact?   -- suggests a fit

type signature, name substring, or a subexpression: String → List Char, List _ → Nat, |- _ ++ _ = _. This is the closest thing to "Hoogle for Lean".

over Mathlib and the stdlib.

.lake/packages/; the stdlib and Std/Batteries are plain Lean files. grep -r "def toUpper" .lake finds the real definition fast.

The mindset shift: in Haskell you search Hackage for a package; in Lean you already have the whole library, and you search it by type (Loogle / exact?) or by namespace (editor autocomplete).

17.4 Where to go next

behaves more like C/Python than like GHC, and when that makes Lean the better choice for an application.