Source: src/hql/lib/stdlib/js/internal/seq-protocol.js, src/hql/lib/stdlib/js/core.js, src/hql/lib/stdlib/stdlib.hql
lazy-seq ::= '(' 'lazy-seq' body ')'
cons ::= '(' 'cons' head tail ')'
seq ::= '(' 'seq' coll ')'
delay ::= '(' 'delay' expr ')'
force ::= '(' 'force' delayed ')'
realized ::= '(' 'realized' delayed ')'
HQL implements Clojure-inspired lazy sequences built on a Symbol-based protocol system. Lazy sequences defer computation until values are needed, enable infinite data structures, and provide memory-efficient processing of large collections.
The sequence protocol is defined using JavaScript Symbols:
SEQ = Symbol.for("hql.seq") // ISeq: first(), rest(), seq()
COUNTED = Symbol.for("hql.counted") // Counted: count() in O(1)
INDEXED = Symbol.for("hql.indexed") // Indexed: nth(i) in O(1)
Any object implementing the SEQ protocol can participate in HQL's sequence operations. The protocol requires three methods:
| Method | Return | Description |
|---|---|---|
first() | any | Returns the first element |
rest() | seq | Returns the rest of the sequence (never null; returns EMPTY) |
seq() | seq or null | Returns the sequence itself, or null if empty (nil-punning) |
Singleton empty sequence (like Clojure's PersistentList.EMPTY):
first() returns undefinedrest() returns itselfseq() returns nullcount() returns 0Immutable pair of (first, rest):
(cons 1 (cons 2 (cons 3 null)))
;; => (1 2 3)
Time complexity:
first(): O(1)rest(): O(1)seq(): O(1);; Prepend to a sequence
(cons 0 [1 2 3]) ;; => (0 1 2 3)
;; Build a list
(cons 1 (cons 2 null)) ;; => (1 2)
Lazy sequence that defers computation until first access:
(lazy-seq
(cons (compute-head)
(lazy-seq (compute-rest))))
Properties:
seq() returns null for empty lazy sequencesTime complexity:
first(): O(1) amortized (first call evaluates thunk)rest(): O(1)seq(): O(1) amortizedEfficient sequence view over a JavaScript array:
(seq [1 2 3]) ;; => ArraySeq wrapping [1, 2, 3]
Properties:
count(): O(1)nth(i): O(1)Chunked sequences for batch optimization:
first and a lazy seq as restChunked processing amortizes the overhead of lazy thunk evaluation across multiple elements.
seq FunctionConverts collections to lazy sequences with nil-punning:
(seq [1 2 3]) ;; => ArraySeq
(seq "hello") ;; => seq over characters
(seq #{1 2 3}) ;; => seq over Set values
(seq {a: 1}) ;; => seq over [key, value] entries
(seq []) ;; => null (nil-punning)
(seq null) ;; => null
Supported input types:
Array.from(set)Array.from(map)Object.entries().seq()Wraps an expression in a deferred computation:
(let d (delay (expensive-computation)))
;; computation not yet executed
Forces evaluation of a delayed value:
(force d) ;; => evaluates and caches the result
(force d) ;; => returns cached result (no recomputation)
Checks if a delay has been forced:
(let d (delay 42))
(realized d) ;; => false
(force d) ;; => 42
(realized d) ;; => true
Non-delay values always return true for realized.
Generates a lazy sequence of numbers:
(range) ;; => 0, 1, 2, 3, ... (infinite)
(range 5) ;; => 0, 1, 2, 3, 4
(range 2 8) ;; => 2, 3, 4, 5, 6, 7
(range 0 10 2) ;; => 0, 2, 4, 6, 8
Creates an infinite lazy sequence of a single value:
(take 3 (repeat "x")) ;; => ("x" "x" "x")
Creates an infinite lazy sequence by cycling through a collection:
(take 7 (cycle [1 2 3])) ;; => (1 2 3 1 2 3 1)
Creates an infinite lazy sequence by repeatedly applying a function:
(take 5 (iterate inc 0)) ;; => (0 1 2 3 4)
(take 5 (iterate (fn [x] (* x 2)) 1)) ;; => (1 2 4 8 16)
These operations return lazy sequences:
| Operation | Description | Example |
|---|---|---|
map | Transform each element | (map inc [1 2 3]) => (2 3 4) |
filter | Keep matching elements | (filter isOdd [1 2 3]) => (1 3) |
take | First n elements | (take 3 (range)) => (0 1 2) |
drop | Skip first n elements | (drop 2 [1 2 3 4]) => (3 4) |
takeWhile | Take while predicate holds | (takeWhile isEven [2 4 5]) => (2 4) |
dropWhile | Drop while predicate holds | (dropWhile isEven [2 4 5]) => (5) |
concat | Concatenate sequences | (concat [1 2] [3 4]) => (1 2 3 4) |
mapcat | Map then concatenate | (mapcat (fn [x] [x x]) [1 2]) => (1 1 2 2) |
interpose | Insert separator | (interpose ", " ["a" "b"]) => ("a" ", " "b") |
interleave | Interleave sequences | (interleave [1 2] [3 4]) => (1 3 2 4) |
distinct | Remove duplicates | (distinct [1 2 1 3]) => (1 2 3) |
flatten | Flatten nested seqs | (flatten [[1 2] [3]]) => (1 2 3) |
partition | Group into fixed-size chunks | (partition 2 [1 2 3 4]) => ((1 2) (3 4)) |
partitionAll | Like partition, keep remainder | (partitionAll 2 [1 2 3]) => ((1 2) (3)) |
partitionBy | Group by predicate changes | (partitionBy isOdd [1 3 2 4]) => ((1 3) (2 4)) |
These operations force evaluation:
| Operation | Description | Example |
|---|---|---|
reduce | Fold left | (reduce + 0 [1 2 3]) => 6 |
count | Count elements | (count [1 2 3]) => 3 |
first | First element | (first [1 2 3]) => 1 |
last | Last element | (last [1 2 3]) => 3 |
nth | Element at index | (nth [10 20 30] 1) => 20 |
some | First truthy result | (some isEven [1 2 3]) => true |
every | All match predicate | (every isPositive [1 2 3]) => true |
into | Collect into target | (into [] (range 5)) => [0 1 2 3 4] |
null from seq(), enabling (if (seq coll) ...) idiomfirst, rest, seq are O(1) for all sequence typesInfinite sequences must be consumed with take, takeWhile, or similar bounded operations:
;; CORRECT: bounded consumption
(take 10 (range))
;; DANGER: infinite loop
;; (reduce + (range)) -- never terminates
(first []) ;; => undefined
(rest []) ;; => EMPTY (not null)
(seq []) ;; => null
(count []) ;; => 0
Side effects in lazy sequences are deferred:
;; Side effects happen only when the sequence is consumed
(let logged (map (fn [x] (print x) x) [1 2 3]))
;; Nothing printed yet
(reduce + 0 logged) ;; Prints 1, 2, 3, returns 6
src/hql/lib/stdlib/js/internal/seq-protocol.jssrc/hql/lib/stdlib/js/core.jssrc/hql/lib/stdlib/stdlib.hqlsrc/hql/lib/stdlib/js/transducers.jstests/unit/stdlib.test.ts, tests/unit/lazy-seq.test.ts