Add loop combinators to stdlib: times, each-integer, while #394

Closed
opened 2026-04-10 23:29:43 +00:00 by navicore · 3 comments
navicore commented 2026-04-10 23:29:43 +00:00 (Migrated from github.com)

Summary

Add basic loop combinators as stdlib words. These are table-stakes for concatenative languages (Factor has each, times, while, until) and are identified as tier 1 in LANGUAGE_GAPS.md.

Motivation

Recursion-only iteration is correct (TCO guarantees it) but not ergonomic. Simple counted loops require defining two words (the entry point and the recursive helper), managing an explicit index/counter on the stack, and getting the termination condition right.

Example — printing 5 shares currently requires:

: print-points-loop ( List Int -- )
  over list.length over i.<= if
    drop drop
  else
    over over list.get drop print-point
    1 i.+ print-points-loop
  then
;
: print-points ( List -- )
  dup 0 print-points-loop drop
;

With each-integer, this becomes:

: print-points ( List -- )
  dup list.length [ over swap list.get drop print-point ] each-integer drop
;

Proposed Words

These can be pure stdlib (no compiler changes), backed by recursion + TCO:

times — execute quotation N times

# ( Int Quotation -- )
# 3 [ "hello" io.write-line ] times

each-integer — call quotation with 0, 1, ..., n-1

# ( Int Quotation -- )
# 5 [ int->string io.write-line ] each-integer
# prints: 0 1 2 3 4

while — loop while predicate returns true

# ( Quotation Quotation -- )
# [ dup 0 i.> ] [ dup int->string io.write-line 1 i.- ] while drop
# prints: 5 4 3 2 1

Risk

None. Pure stdlib additions — no compiler or runtime changes. TCO guarantee means these perform identically to hand-written recursion. No new syntax; the concatenative composition model is preserved.

Note

These combinators inherit the current limitation that their quotation arguments cannot use >aux/aux> (see #393) and cannot auto-capture values from the enclosing scope (see #394 if filed). They still eliminate boilerplate for simple iterations.


Context: discovered while implementing Shamir's Secret Sharing in Seq. Multiple recursive loop helpers (split-byte-loop, print-points-loop, str-to-bytes-loop, etc.) could each be replaced by a single each-integer or times call.

## Summary Add basic loop combinators as stdlib words. These are table-stakes for concatenative languages (Factor has `each`, `times`, `while`, `until`) and are identified as tier 1 in `LANGUAGE_GAPS.md`. ## Motivation Recursion-only iteration is correct (TCO guarantees it) but not ergonomic. Simple counted loops require defining two words (the entry point and the recursive helper), managing an explicit index/counter on the stack, and getting the termination condition right. Example — printing 5 shares currently requires: ```seq : print-points-loop ( List Int -- ) over list.length over i.<= if drop drop else over over list.get drop print-point 1 i.+ print-points-loop then ; : print-points ( List -- ) dup 0 print-points-loop drop ; ``` With `each-integer`, this becomes: ```seq : print-points ( List -- ) dup list.length [ over swap list.get drop print-point ] each-integer drop ; ``` ## Proposed Words These can be pure stdlib (no compiler changes), backed by recursion + TCO: ### `times` — execute quotation N times ```seq # ( Int Quotation -- ) # 3 [ "hello" io.write-line ] times ``` ### `each-integer` — call quotation with 0, 1, ..., n-1 ```seq # ( Int Quotation -- ) # 5 [ int->string io.write-line ] each-integer # prints: 0 1 2 3 4 ``` ### `while` — loop while predicate returns true ```seq # ( Quotation Quotation -- ) # [ dup 0 i.> ] [ dup int->string io.write-line 1 i.- ] while drop # prints: 5 4 3 2 1 ``` ## Risk None. Pure stdlib additions — no compiler or runtime changes. TCO guarantee means these perform identically to hand-written recursion. No new syntax; the concatenative composition model is preserved. ## Note These combinators inherit the current limitation that their quotation arguments cannot use `>aux`/`aux>` (see #393) and cannot auto-capture values from the enclosing scope (see #394 if filed). They still eliminate boilerplate for simple iterations. --- *Context: discovered while implementing Shamir's Secret Sharing in Seq. Multiple recursive loop helpers (`split-byte-loop`, `print-points-loop`, `str-to-bytes-loop`, etc.) could each be replaced by a single `each-integer` or `times` call.*
navicore commented 2026-04-11 12:34:58 +00:00 (Migrated from github.com)

we'll hold off on implementing "while". Requires either a typechecker special case (the dip/keep/bi pattern is established and works) or accepting a fixed-shape while (e.g., Int -> Int only).

but we are implementing the other 2 combinations via github.com/navicore/patch-seq@34a0acdae7

we'll hold off on implementing "while". Requires either a typechecker special case (the dip/keep/bi pattern is established and works) or accepting a fixed-shape while (e.g., Int -> Int only). but we are implementing the other 2 combinations via https://github.com/navicore/patch-seq/commit/34a0acdae75677ddf8824c25e02bd1ba804ae4f8
navicore commented 2026-04-11 12:35:50 +00:00 (Migrated from github.com)
https://github.com/navicore/patch-seq/blob/main/docs/design/LOOP_COMBINATORS_PHASE1.md
navicore commented 2026-04-12 04:25:02 +00:00 (Migrated from github.com)
https://github.com/navicore/patch-seq/pull/399
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
navicore/patch-seq#394
No description provided.