Chapter 01 — Lean 4 for ML Engineers¶
You don't need to be a Lean expert to use Hesper. This chapter teaches just enough to read the rest of the tutorial. If you've used Haskell, OCaml, or even just Python with type hints, most of it will feel familiar — Lean is a strict, ML-style functional language with a dependent type system on top.
Types and definitions¶
def square (x : Float) : Float := x * x
#eval square 3.14 -- 9.8596
#check @square -- @square : Float → Float
def name (args) : ReturnType := body. Type annotations on arguments
and return type are usually optional — Lean infers them — but in
tutorial code we'll write them out for clarity.
The Array, List, and String types¶
Lean's standard library gives you the usual collections:
def xs : Array Nat := #[1, 2, 3, 4]
def ys : List Float := [1.0, 2.0, 3.0]
#eval xs.map (· + 1) -- #[2, 3, 4, 5]
#eval ys.foldl (· + ·) 0.0 -- 6.0
(· + 1) is shorthand for fun x => x + 1. The dot-notation xs.map
desugars to Array.map xs.
Structures and product types¶
structure Shape where
rows : Nat
cols : Nat
deriving Repr
def s : Shape := { rows := 3, cols := 4 }
#eval s.rows * s.cols -- 12
We'll use small structures a lot — Tensor, Config, Layer, etc.
The IO monad¶
GPU side-effects and FFI calls live inside IO. You write do-blocks
just like Haskell or Rust:
def hello : IO Unit := do
IO.println "hello GPU"
#eval hello
← binds the result of an IO action; let x := ... is pure binding.
The s!"..." syntax is string interpolation.
Dependent types (the part that's new)¶
Lean lets a type depend on a value. This is what makes the shader DSL type-safe:
inductive Kind | f32 | i32 | u32
-- A type that depends on a value of Kind. We use `abbrev` (not `def`)
-- so that `Carrier .f32` reduces to `Float` transparently — otherwise
-- the elaborator can't see through the alias when it needs to find a
-- `OfNat Float 1` instance below.
abbrev Carrier : Kind → Type
| .f32 => Float
| .i32 => Int
| .u32 => UInt32
def oneOf : (k : Kind) → Carrier k
| .f32 => 1.0
| .i32 => 1
| .u32 => 1
In Hesper, Exp (.scalar .f32) and Exp (.scalar .i32) are different
types, so a stray i32 cannot sneak into an f32 shader expression.
Typeclasses¶
Typeclasses look like Haskell:
class Approximable (α : Type) where
approxEq : α → α → Bool
instance : Approximable Float where
approxEq a b := (a - b).abs < 1e-6
#eval Approximable.approxEq (1.0 : Float) 1.0000001
-- true
Hesper uses typeclasses for GPUBackend (Ch05), Differentiable (Ch03),
and for tensor element types.
Module imports¶
A real Lean file starts with all its imports up top, like Python or Haskell. In the rest of this tutorial each chapter's notebook places its imports in the first code cell:
import Hesper.WGSL.DSL -- the type-safe shader DSL
import Hesper.Compute -- high-level compute API
import Hesper.Models.BitNet -- BitNet inference
What's next¶
- Chapter 01b — Your First Hesper Project: set up a fresh package that uses Hesper as a dependency.
- Chapter 02 — The Shader DSL: the type-safe expression language we actually compile to WGSL and PTX.