new if control for seq #430

Closed
opened 2026-04-25 18:01:55 +00:00 by navicore · 1 comment
navicore commented 2026-04-25 18:01:55 +00:00 (Migrated from github.com)

RFC: Replace if/else/then Syntax with Quotation-Based Combinators

Background

The language currently uses Forth-style control flow:

condition if
  then-branch
else
  else-branch
then

Example:

: abs ( Int -- Int )
  dup 0 i.< if
    0 swap i.-    # negate: 0 - n
  then
;

This works, but feels inconsistent with the rest of the language, which already
has first-class quotations, closures, and row-polymorphic stack effects.

Proposal

Remove if, else, and then as parser-level syntax. Replace them with a
single ordinary word:

: if ( ..a Bool [ ..a -- ..b ] [ ..a -- ..b ] -- ..b )

if becomes a normal combinator that takes three things off the stack: a
boolean and two quotations. No new syntax, no parser changes — the existing
quotation and row-polymorphism machinery already knows how to handle it.

The abs example becomes:

: abs ( Int -- Int )
  dup 0 i.< [ 0 swap i.- ] [ ] if
;

Or, with a when combinator (see below):

: abs ( Int -- Int )
  dup 0 i.< [ 0 swap i.- ] when
;

Why This Fits the Language

It's the natural payoff of row polymorphism

The signature ( ..a Bool [ ..a -- ..b ] [ ..a -- ..b ] -- ..b ) is only
expressible because ..a and ..b exist. In a parametric-only system you
couldn't type if precisely — you'd lose the guarantee that both branches
agree on stack effect.

Adopting quotation-based control flow cashes in a type-system feature the
language already has.

It unifies "control flow" with "everything else"

Loops (while, until, times), error handling (recover), iteration
(each, map, filter), and conditionals all become combinators with the
same shape. Users learn one pattern instead of N special forms.

It composes

[ ... ] [ ... ] if is a value. It can be stored, returned from a word, built
dynamically, passed to higher-order combinators. Forth-style if cannot do
any of that.

Derived Combinators (Library, Not Syntax)

Once if is a word, a family of related combinators becomes ordinary library
code:

: when    ( ..a Bool [ ..a -- ..a ] -- ..a )         # only true branch
: unless  ( ..a Bool [ ..a -- ..a ] -- ..a )         # only false branch
: cond    ( ..a { [ pred ] [ body ] ... } -- ..b )   # multi-way dispatch
: ?       ( ..a Bool A A -- ..a A )                  # value-returning ternary

Users can write their own. The parser doesn't need to know about any of them.

Translation From Existing Code

The migration is mechanical and local:

cond if A else B then     →    cond [ A ] [ B ] if
cond if A then            →    cond [ A ] [ ] if    (or: cond [ A ] when)

A migration tool can do this transformation automatically. Meeting existing
use cases isn't a hope — it's guaranteed by construction.

Nested Control Flow: A Concrete Win

The strongest argument for the change is how it handles deep nesting.

Today's pain

a if
  b if
    c if
      X
    else
      Y
    then
  else
    Z
  then
else
  W
then

Five-deep nesting accumulates then terminators at the bottom. They carry no
information — they're parser bookkeeping. You have to count them to know
you've closed the right number of blocks, and a missing or extra then
produces an error far from the actual mistake.

With quotations

a [
  b [
    c [ X ] [ Y ] if
  ] [
    Z
  ] if
] [
  W
] if

The ] characters carry the same bookkeeping the thens did, but they pair
visually with their opening [. Editors, linters, and humans all track the
nesting structure for free. No counting.

Nesting often becomes unnecessary

Forth-style syntax forces nesting because if is a statement. With
combinators, deeply nested decisions frequently flatten into a cond:

{ [ pred1 ] [ body1 ]
  [ pred2 ] [ body2 ]
  [ pred3 ] [ body3 ]
  [ t ]     [ default ]
} cond

A lot of code that looks like it needs nested ifs is really a flat
dispatch in disguise. Forth-style syntax hides that because there's no
convenient way to express it.

Branches can also be named and reused, since they're values:

: handle-overflow ( ..a -- ..b ) ... ;
: handle-normal   ( ..a -- ..b ) ... ;

overflow? [ handle-overflow ] [ handle-normal ] if

The factoring options Forth-style denies — naming a branch, reusing a branch,
building a branch dynamically — are exactly the tools needed to flatten deep
nesting. Quotations provide them.

Costs

To be honest about the tradeoffs:

  • Breaking change. Existing code stops parsing. Mitigated by a mechanical
    migration tool.
  • Slight verbosity in the simple case. cond [ A ] [ ] if is a few
    characters longer than cond if A then. The when combinator recovers
    most of that.
  • Empty-branch ergonomics. [ ] for "do nothing" is a little ugly. A
    nop or id word can help; some languages just let [ ] stand.
  • Compile-time optimization. Forth-style if compiles to a direct
    conditional jump trivially. Quotation-based if produces identical code
    if the compiler inlines literal quotations at the call site — a standard
    optimization (Factor does it). The cost only appears when the quotations
    are dynamic, and in that case the feature didn't exist in the old syntax
    at all.

Prior Art

Factor's kernel and combinators vocabularies are the canonical reference
for how far this style scales. Slava Pestov's writing on Factor's design
covers the rationale for quotations-over-syntax in depth. Joy is the original
quotation-based concatenative language and worth reading for the minimalist
take on the same idea.

# RFC: Replace `if/else/then` Syntax with Quotation-Based Combinators ## Background The language currently uses Forth-style control flow: ``` condition if then-branch else else-branch then ``` Example: ``` : abs ( Int -- Int ) dup 0 i.< if 0 swap i.- # negate: 0 - n then ; ``` This works, but feels inconsistent with the rest of the language, which already has first-class quotations, closures, and row-polymorphic stack effects. ## Proposal Remove `if`, `else`, and `then` as parser-level syntax. Replace them with a single ordinary word: ``` : if ( ..a Bool [ ..a -- ..b ] [ ..a -- ..b ] -- ..b ) ``` `if` becomes a normal combinator that takes three things off the stack: a boolean and two quotations. No new syntax, no parser changes — the existing quotation and row-polymorphism machinery already knows how to handle it. The `abs` example becomes: ``` : abs ( Int -- Int ) dup 0 i.< [ 0 swap i.- ] [ ] if ; ``` Or, with a `when` combinator (see below): ``` : abs ( Int -- Int ) dup 0 i.< [ 0 swap i.- ] when ; ``` ## Why This Fits the Language ### It's the natural payoff of row polymorphism The signature `( ..a Bool [ ..a -- ..b ] [ ..a -- ..b ] -- ..b )` is *only* expressible because `..a` and `..b` exist. In a parametric-only system you couldn't type `if` precisely — you'd lose the guarantee that both branches agree on stack effect. Adopting quotation-based control flow cashes in a type-system feature the language already has. ### It unifies "control flow" with "everything else" Loops (`while`, `until`, `times`), error handling (`recover`), iteration (`each`, `map`, `filter`), and conditionals all become combinators with the same shape. Users learn one pattern instead of N special forms. ### It composes `[ ... ] [ ... ] if` is a value. It can be stored, returned from a word, built dynamically, passed to higher-order combinators. Forth-style `if` cannot do any of that. ## Derived Combinators (Library, Not Syntax) Once `if` is a word, a family of related combinators becomes ordinary library code: ``` : when ( ..a Bool [ ..a -- ..a ] -- ..a ) # only true branch : unless ( ..a Bool [ ..a -- ..a ] -- ..a ) # only false branch : cond ( ..a { [ pred ] [ body ] ... } -- ..b ) # multi-way dispatch : ? ( ..a Bool A A -- ..a A ) # value-returning ternary ``` Users can write their own. The parser doesn't need to know about any of them. ## Translation From Existing Code The migration is mechanical and local: ``` cond if A else B then → cond [ A ] [ B ] if cond if A then → cond [ A ] [ ] if (or: cond [ A ] when) ``` A migration tool can do this transformation automatically. Meeting existing use cases isn't a hope — it's guaranteed by construction. ## Nested Control Flow: A Concrete Win The strongest argument for the change is how it handles deep nesting. ### Today's pain ``` a if b if c if X else Y then else Z then else W then ``` Five-deep nesting accumulates `then` terminators at the bottom. They carry no information — they're parser bookkeeping. You have to count them to know you've closed the right number of blocks, and a missing or extra `then` produces an error far from the actual mistake. ### With quotations ``` a [ b [ c [ X ] [ Y ] if ] [ Z ] if ] [ W ] if ``` The `]` characters carry the same bookkeeping the `then`s did, but they pair visually with their opening `[`. Editors, linters, and humans all track the nesting structure for free. No counting. ### Nesting often becomes unnecessary Forth-style syntax forces nesting because `if` is a statement. With combinators, deeply nested decisions frequently flatten into a `cond`: ``` { [ pred1 ] [ body1 ] [ pred2 ] [ body2 ] [ pred3 ] [ body3 ] [ t ] [ default ] } cond ``` A lot of code that *looks* like it needs nested `if`s is really a flat dispatch in disguise. Forth-style syntax hides that because there's no convenient way to express it. Branches can also be named and reused, since they're values: ``` : handle-overflow ( ..a -- ..b ) ... ; : handle-normal ( ..a -- ..b ) ... ; overflow? [ handle-overflow ] [ handle-normal ] if ``` The factoring options Forth-style denies — naming a branch, reusing a branch, building a branch dynamically — are exactly the tools needed to flatten deep nesting. Quotations provide them. ## Costs To be honest about the tradeoffs: - **Breaking change.** Existing code stops parsing. Mitigated by a mechanical migration tool. - **Slight verbosity in the simple case.** `cond [ A ] [ ] if` is a few characters longer than `cond if A then`. The `when` combinator recovers most of that. - **Empty-branch ergonomics.** `[ ]` for "do nothing" is a little ugly. A `nop` or `id` word can help; some languages just let `[ ]` stand. - **Compile-time optimization.** Forth-style `if` compiles to a direct conditional jump trivially. Quotation-based `if` produces identical code *if* the compiler inlines literal quotations at the call site — a standard optimization (Factor does it). The cost only appears when the quotations are dynamic, and in that case the feature didn't exist in the old syntax at all. ## Prior Art Factor's `kernel` and `combinators` vocabularies are the canonical reference for how far this style scales. Slava Pestov's writing on Factor's design covers the rationale for quotations-over-syntax in depth. Joy is the original quotation-based concatenative language and worth reading for the minimalist take on the same idea.
navicore commented 2026-04-25 20:50:51 +00:00 (Migrated from github.com)
https://github.com/navicore/patch-seq/pull/431
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#430
No description provided.