Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

genfunc: Named Generators

A generator is a function that can pause and resume, producing values lazily via yield. Lykn’s genfunc form mirrors func — keyword-labeled clauses, typed parameters, and a new :yields annotation for per-yield type checking.

Basic Generator

(genfunc range
  :args (:number start :number end)
  :yields :number
  :body
  (for (let i start) (< i end) (+= i 1)
    (yield i)))
function* range(start, end) {
  if (typeof start !== "number" || Number.isNaN(start))
    throw new TypeError(
      "range: arg 'start' expected number, got " + typeof start);

  if (typeof end !== "number" || Number.isNaN(end))
    throw new TypeError(
      "range: arg 'end' expected number, got " + typeof end);

  for (let i = start; i < end; i += 1) {
    yield (() => {
      const yv__gensym0 = i;
      if (typeof yv__gensym0 !== "number" || Number.isNaN(yv__gensym0))
        throw new TypeError(
          "range: yield expected number, got " + typeof yv__gensym0);
      return yv__gensym0;
    })();
  }
}

What :yields Does

The :yields :number annotation generates a runtime type check on every yield expression — the yielded value is wrapped in an IIFE that validates the type before yielding. This catches type errors at the production site, not at the consumption site.

Without :yields, yield passes values through unchecked:

(genfunc count-up
  :body
  (let i 0)
  (while true
    (yield i)
    (+= i 1)))

Consuming Generators

Generators are iterable — they implement the iteration protocol automatically:

;; for-of
(for-of n (range 0 5)
  (console:log n))      ;; 0, 1, 2, 3, 4

;; Spread into array
(bind nums (array (spread (range 0 5))))  ;; [0, 1, 2, 3, 4]

;; Manual iteration
(bind r (range 0 3))
(console:log (r:next))  ;; { value: 0, done: false }
(console:log (r:next))  ;; { value: 1, done: false }
(console:log (r:next))  ;; { value: 2, done: false }
(console:log (r:next))  ;; { value: undefined, done: true }

yield* — Delegation

yield* delegates to another iterable, yielding each of its values:

(genfunc concat-iters
  :args (:any a :any b)
  :body (yield* a) (yield* b))

(for-of n (concat-iters #a(1 2) #a(3 4))
  (console:log n))      ;; 1, 2, 3, 4

Infinite Generators

Generators can be infinite — they yield forever until the consumer stops asking:

(genfunc fibonacci
  :yields :number
  :body
  (let a 0)
  (let b 1)
  (while true
    (yield a)
    (let temp a)
    (= a b)
    (= b (+ temp b))))

Consume with break or take:

(bind count (cell 0))
(for-of n (fibonacci)
  (console:log n)
  (swap! count (fn (:number c) (+ c 1)))
  (if (>= (express count) 10) (break)))

Generators are lazy — they compute the next value only when next() is called. This is what makes infinite sequences practical: memory usage is constant regardless of how many values are produced.