Classic infinite sequences — primes, Hamming numbers, Fibonacci, Catalan — as lazy Haskell streams, printed from the command line. Two hinges: (1) laziness — an expression isn't evaluated until it's needed, and only as far as needed, so you can define an infinite list as a value and
primes 15orfib nth 100forces exactly that much; (2) self-reference — a list whose tail is computed from the list itself (fibs = 0:1:zipWith (+) fibs (tail fibs)), which diverges instantly in a strict language but works under laziness because each element is produced just before it's read back. A lazy-evaluation turn after the strict Rust/Go/Zig/OCaml entries.
📦 GitHub: https://github.com/sen-ltd/lazy-streams-hs
Why laziness
The recent entries were all strict — Rust, Go, Zig, and even OCaml evaluate
eagerly. Haskell is lazy: an expression isn't evaluated until its value is
needed, and only as far as needed. That changes what a "value" can be. Here every
sequence is a complete, infinite list, defined once; asking for primes 15 or
fib nth 100 forces exactly that much and no more.
$ stream primes 15
primes (first 15): 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
$ stream fib nth 100 # deep into an infinite list — only this is forced
fib #100 = 218922995834555169026
Self-reference: a list defined from itself
The purest demonstration is a list whose tail is computed from the list. In a
strict language this diverges instantly; under laziness each element is produced
just before it's read back, so it works:
-- each Fibonacci number is the sum of the two before it, read out of the
-- very list being defined
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- Hamming numbers (only prime factors 2,3,5): the stream is the merge of
-- itself scaled by 2, 3 and 5, with duplicates dropped
hamming :: [Integer]
hamming = 1 : merge (map (*2) hamming)
(merge (map (*3) hamming) (map (*5) hamming))
factorials = 1 : zipWith (*) factorials [1..] is the same trick. A test drives
each stream far in — fibs !! 100, the 1000th prime, 500 Hamming numbers — and
checks exact Integer results, that Hamming numbers stay 5-smooth and strictly
increasing, and that the sieve's outputs are all actually prime.
The sieve, lazily
Primes are a lazy incremental sieve: emit the head, then sieve the tail by
filtering its multiples — over an infinite list, so you decide where to stop by
how much you consume:
primes :: [Integer]
primes = sieve [2..]
where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
A strict language would blow up trying to build [2..] in the first place.
Laziness is what makes "write the infinite, use the finite" natural.
Zero dependencies
No external packages — base library only. ghc -O2 compiles it to a single
binary, no cabal/stack needed. Because Integer is arbitrary precision,
values like factorial nth 25 = 620448401733239439360000 come out exact.
src/Streams.hs — the sequences: fibs, primes, hamming, catalans, factorials, …
app/Main.hs — CLI: stream <name> <count> / stream <name> nth <k> / stream list
test/Spec.hs — known prefixes, deep indices, invariants, primality, laziness
Takeaway
Laziness lets you write the infinite as a value and pay only for the finite part
you touch. Its essence is the self-referential infinite list — fibs,
hamming, factorials — and the prime sieve rides the same idea. I built it in
Haskell to add a lazy axis next to the strict Rust/Go/Zig/OCaml entries.

Top comments (0)