Factor Stack Based Concatenative Language Assignment

Factor is not your typical programming language. check my site Born from a lineage that traces back to Forth and Joy, it is a stack-based, concatenative language that challenges many assumptions we hold about how code is structured. There are no function arguments enclosed in parentheses, no operator precedence to memorize, and perhaps most disorienting of all—no traditional variables as first-class citizens. For a newcomer, the first question is often: “How do I store a value?” The answer reveals a deep philosophical shift. In Factor, “assignment” is not a built-in syntax; it is a library, a pattern, and sometimes an antipattern. To grasp assignment in Factor is to understand the very soul of concatenative programming.

The Concatenative Aesthetic

A concatenative language is one where programs are built by composing words (functions) in sequence. Each word consumes arguments from a shared data stack and pushes results back onto it. Because data flows implicitly through the stack, the need for named parameters dissolves. Consider a simple arithmetic expression:

text

2 3 + .

This pushes 2, then 3, then calls +, which pops both, adds them, pushes 5, and . prints it. The beauty is that any two code snippets can be concatenated, and the output stack of the first becomes the input stack of the second. There is no variables, no assignment, just pure data flow.

In such a world, the classic x = x + 1 is an unnatural act. The stack already holds the value; to “increment” we just duplicate, add, and let the result take its place: dup 1 +. The idea of naming a storage location and mutating it over time is, at best, a necessary evil when state becomes complex. Factor, as a modern, practical language, provides tools for that necessity, but it encourages you to think differently.

Stack Shuffling as the First Alternative

Before reaching for variables, a Factor programmer learns to shuffle. The language has a rich vocabulary of words that rearrange the top elements of the stack: swaprot-rotdupdropnipover, and the combinator keep. These allow you to route values to where they are needed without ever giving them a name.

For instance, suppose you want to compute the area of a triangle given base and height, and then print a message. In an imperative language, you would store intermediate results in variables. In Factor, you might write:

text

: triangle-area ( base height -- area ) * 2 / ;
: print-area ( base height -- ) 2dup triangle-area "Area: %d" printf ;

2dup duplicates the top two stack items so that after computing the area, base and height are still available for formatting, though here we just use area. Once you internalize the stack effect comments (the ( base height -- area ) notation), the code becomes a concise narrative of data transformation. This is the “zero-assignment” ideal. No names, no mutable state, no side channels.

When the Stack Gets Tangled

The stack approach breaks down when the same value is needed in multiple, deeply nested places, or when data must persist across event loops and callbacks. Imagine a complex GUI application where a piece of configuration needs to be read from a file at startup and then referenced by dozens of word definitions scattered across modules. click for more info Duplicating and threading that value through every function’s stack effect would be a nightmare—polluting stack effects and making composition brittle.

Factor recognizes this and provides dynamic variables as a pragmatic escape hatch. Dynamic variables are not global variables in the usual sense; they are stack-bound, thread-local, and their bindings can be shadowed lexically. They offer controlled, localized mutability that respects Factor’s functional core.

Dynamic Variables: Assignment with Discipline

A dynamic variable is created with the SYMBOL: defining word. You can then bind it with set and retrieve its value with get.

text

SYMBOL: current-user

"Alice" current-user set
current-user get print   ! outputs "Alice"

This looks like simple assignment, but the power lies in with-variable and with-variable* combinators that establish a new binding scope:

text

"Bob" current-user [
    current-user get print   ! "Bob"
] with-variable

current-user get print       ! still "Alice"

Inside the quotation, the dynamic variable is shadowed; outside, it reverts. This is conceptually identical to a dynamically scoped variable in Lisp, and it allows for safe, temporary redefinition. Because Factor threads each have their own namespace, dynamic variables are inherently thread-local unless explicitly shared. This eliminates whole classes of concurrency bugs.

Dynamic variables are used pervasively in Factor’s own libraries. Standard input/output streams are stored in dynamic variables, enabling redirection. The prettyprinter’s limits, error handlers, and even the current vocabulary search path are all managed this way. They provide a global “assignment” mechanism that is controlled, testable, and composable.

The Locals Library: A Familiar Face

For programmers transitioning from mainstream languages, Factor offers a locals vocabulary that mimics named local variables with lexical scope. It relies on parsing words to introduce a syntax that feels much like let in Scheme or local declarations in Rust:

text

USE: locals

:: greet ( name -- greeting )
    name "Hello, " prepend ;

Or multi-line with assignments:

text

:: describe-rectangle ( w h -- str )
    w h * :> area
    area "Area is %d" sprintf ;

Here, :> is the assignment operator inside a lambda word defined with ::. Under the hood, the locals library performs a stack-shuffling analysis and compiles down to equivalent stack code. It is a zero-cost abstraction: no hidden allocation, just a syntax sugar that allows you to name intermediate values arbitrarily. Purists may frown, but in practice this is invaluable for complex math or when interfacing with algorithms ported from other languages.

Crucially, Factor’s locals are single-assignment by default. You cannot reassign area after binding it. This immutability is intentional; it aligns with the concatenative philosophy that values are transformed, not mutated. However, you can declare a mutable local with :: and [let constructs if needed, but the compiler often warns that it cannot optimize as well. The message is clear: use assignment sparingly.

Word Definitions and the Illusion of Assignment

It’s worth noting that Factor does have a form of permanent, global assignment: defining a word. When you write : sq ( x -- y ) dup * ;, you are assigning the name sq to a quotation in the dictionary. This is the ultimate “assignment”—building a vocabulary that transforms the stack. In that sense, every Factor program is a series of re-definitions, extending the language. The dictionary itself is mutable; you can redefine words on the fly, which is how interactive development works. But this is meta-programming, not data assignment.

Objects, Slots, and Mutation

For structured data, Factor uses tuples (similar to classes/records). A tuple’s slots have reader and writer words. For example:

text

TUPLE: point x y ;
point new 3 >>x 4 >>y

The >> word sets a slot and returns the modified object. This is mutation, but it’s confined to the heap. Factor’s tuple system is designed to work with the stack: slot accessors pop the tuple and push the value; setters pop a value and the tuple, and push the modified tuple. This allow chaining:

text

point new
    3 >>x
    4 >>y
    [ x>> 2 * ] keep >>x   ! double the x coordinate

Assignment to slots is explicit and local; you see exactly what is being mutated. There are no hidden side effects because the stack makes the flow of the object visible. Many Factor programs that need complex state model it as a tuple that is passed from word to word, with transformation words that return a “new” tuple (often using clone and >>). This mimics persistent data structures while remaining efficient.

Choosing the Right Abstraction

Factor’s multi-paradigm nature means you have a spectrum of “assignment” tools:

  • Pure stack shuffling: for trivial local state within a short word.
  • Locals: for mathematical clarity and when algorithms are inherently name-heavy.
  • Dynamic variables: for context, configuration, and threading.
  • Tuple slots: for encapsulated, structured state that evolves over time.

A seasoned Factor developer learns to bias toward the simplest tool that avoids unnecessary naming. An enlightening exercise is to port a small script to Factor and resist the urge to use locals or dynamic variables. You’ll often discover that the stack-based version is shorter, more readable, and easier to refactor because data flow is explicit. When you finally deploy a SYMBOL:, you do so with intention, knowing exactly which scope it pollutes and for how long.

The Bigger Picture

The disdain for assignment in concatenative languages stems from a deeper principle: compositionality. If a word’s behavior depends on some invisible global assignment, you can’t understand it by looking at its inputs and outputs. Stack effects become meaningless; you lose the ability to reason algebraically about code. Factor’s design preserves this property by making assignment an opt-in, scoped mechanism rather than the default.

Slava Pestov, Factor’s creator, once noted that the language doesn’t prevent you from writing imperative spaghetti—it just makes the path of least resistance lead toward clean, factored code. When you must assign, you do it in a way that remains testable: dynamic variables can be rebound in tests; tuples can be mocked; locals are immutable by default.

Thus, “assignment” in Factor is not a missing feature but a deliberately constrained concept. It is an acknowledgment that while pure concatenative programming is beautiful, real-world systems need a bridge. Factor builds that bridge with dynamic bindings, a transparent locals layer, and slot setters that never lose sight of the stack. The result is a language where you can think in pure transformations and still store state when genuinely necessary. In mastering these techniques, you don’t just learn a new syntax for assignment—you unlearn the idea that every datum needs a name, and in doing so, a fantastic read you become a more versatile programmer.