DEV Community

SEN LLC
SEN LLC

Posted on

Building an Infinite-Sequence CLI in Haskell — Laziness and Lists That Refer to Themselves, plus a Prime Sieve and Hamming Numbers

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 15 or fib nth 100 forces 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

Screenshot

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 listfibs,
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.

📦 GitHub: https://github.com/sen-ltd/lazy-streams-hs

Top comments (0)