Blog › ICP guides
Factor developer on retainer: quotations, combinators, stack effects, dynamic variables, and Factor concatenative stack programming on monthly retainer
November 15, 2026 · ~16 min read
A Factor program using quotations as first-class values was producing four wrong writes per run. The program used a combinator — bi* — to apply two different quotations to two objects in sequence. One of the quotations called a word that mutated a dynamic variable using set. The dynamic variable had been established in the outer calling context with with-variable, which creates a dynamically scoped binding that exists for the duration of the thunk passed as its second argument. The intent was for the quotation to update the dynamic variable and for the updated value to be available in subsequent computation. The failure: the quotation was stored in a Factor variable and passed to bi* as a callable; later in the application flow, the same quotation was re-executed in a deferred context — a callback registered for later dispatch — where the original with-variable scope had already unwound. When the quotation called set inside this deferred context, the dynamic variable was no longer bound in the current dynamic scope chain. The write silently executed against an unbound variable. Four wrong writes per run. The Factor developer on retainer diagnosed the dynamic variable lifetime mismatch: the quotation was designed to be called exactly once, within the with-variable scope, but was reused as a general-purpose callback without that lifetime constraint. The fix was to restructure the computation to pass the value explicitly through the quotation’s stack inputs using curry, eliminating the dynamic variable dependency entirely and making the quotation self-contained regardless of when or where it was called. Wrong writes per run: 4 → 0.
The work log entry read “fixed dynamic variable scope bug, 14h.” It names the symptom and duration. It cannot explain to a client why Factor’s dynamic variables use a scope chain that is constructed at call time rather than definition time — a quotation closed over a word that calls get on a dynamic variable does not capture the binding at the moment the quotation is constructed; it resolves the binding at the moment get is called, walking up the current call stack looking for a with-variable binding for that variable — which means a quotation that behaves correctly when called inside a with-variable scope will behave incorrectly (returning the default value, or a value from a different scope) when called outside it. It cannot explain why the fix required auditing every site where the quotation was stored, passed, or executed to determine which execution contexts were inside the original with-variable scope and which were not (a quotation executed synchronously by bi* in the original call chain is always inside the scope; a quotation registered with add-hook or stored as a callback for later dispatch may execute in any future context, which may not have any with-variable binding for the variable the quotation depends on; identifying which pattern applies requires reading the combinator that ultimately calls the quotation, not just the site where the quotation is defined). It cannot explain why curry was the correct restructuring primitive rather than compose or explicit closure construction (curry partially applies one stack item to a quotation, producing a new quotation that takes one fewer argument from the stack at call time; the restructured quotation received the dynamic variable’s value as a direct stack input at creation time via curry, making the value a literal embedded in the quotation body rather than a runtime dynamic variable lookup). The 14 hours of scope chain analysis, quotation lifetime mapping, and curry composition design across every callsite are invisible in the diff beyond the removed get call and the added curry partial application.
Factor quotations as first-class values: combinators, stack effects, and the concatenative programming model
A Factor quotation is a first-class sequence of code items enclosed in square brackets: [ word1 word2 word3 ]. A quotation is a value on the Factor data stack like any other — an integer, a string, a TUPLE instance. It can be passed as an argument to a word, stored in a variable, returned from a word, or called with call (which pops the quotation from the stack and executes it). Combinators are words that accept quotations as arguments and apply them in specific patterns. The Factor standard library provides a systematic collection of combinators organized by the number of quotations and the arrangement of stack values. bi applies two quotations to one value: x [ p ] [ q ] bi is equivalent to x p x q — it evaluates p with a copy of x, then evaluates q with a copy of x. bi* applies two quotations to two values: x y [ p ] [ q ] bi* is equivalent to x p y q — it applies p to x and q to y in sequence. bi@ applies the same quotation to two values: x y [ p ] bi@ is equivalent to x p y p.
Stack effect declarations are Factor’s mechanism for documenting and statically verifying the stack behavior of every word. A word definition’s stack effect is declared in parentheses immediately after the word name: : double ( n -- n ) 2 * ; declares that double takes one item off the stack (n) and pushes one item back (n). Factor’s stack checker verifies at load time that the declared stack effect matches the actual implementation for every word in every vocabulary. A word with a wrong stack-effect declaration — one that declares ( x -- y ) but actually consumes two items or pushes two items — is caught by the stack checker when the vocabulary loads. Words called through combinators can produce incorrect stack depths when their stack effects are wrong in ways that compose incorrectly with the combinator’s own stack manipulation: if bi* applies [ p ] to x and p actually leaves two items on the stack instead of the declared one, the subsequent [ q ] application sees an unexpected value on the stack below y, and all subsequent stack operations are offset by one. Factor’s strict stack checker prevents most such errors at vocabulary load time, but only for code that is reachable from the vocabulary root through USE: imports. Dead code and dynamically loaded words bypass static checking; their stack-effect errors surface at runtime.
dip and keep are the two combinators most central to Factor idiom. dip takes a quotation and a value on top: x y [ p ] dip is equivalent to x p y — it temporarily saves y, executes p on the remaining stack (which has x on top), then restores y. dip is used whenever you need to apply an operation to a value below the top of the stack without consuming the top. keep executes a quotation and then restores the original top-of-stack value: x [ p ] keep is equivalent to x p x. curry partially applies a value to a quotation: x [ q ] curry produces a new quotation equivalent to [ x q ] — a quotation that, when called, will have x available as if it had just been pushed. compose concatenates two quotations: [ p ] [ q ] compose produces a quotation equivalent to [ p q ]. The distinction between curry and compose is the source of the restructuring choice in the dynamic variable repair: curry embeds a value into a quotation at construction time, making the quotation independent of whatever is on the stack when it is eventually called; compose sequences two quotations, which does not embed a value.
Factor’s concatenative programming model means that the meaning of a program is determined entirely by the composition of word definitions: every word is a function from stack states to stack states, and program composition is word sequencing. This model makes refactoring mechanically straightforward — a sequence of words can be extracted into a new word definition without changing semantics, because the stack effects compose identically. The challenge for retainer work is that the concatenative model’s uniformity conceals the distinction between values that are “data on the stack” and values that are “in the dynamic environment.” A dynamic variable is not a stack value; it is a thread-local binding that persists across word calls without appearing on the stack. In a purely concatenative program, all values flow through the stack. Dynamic variables are a pragmatic concession: sometimes a value needs to be accessible across many levels of call without being threaded through every intermediate word’s stack signature. The cost of this convenience is that dynamic variable access creates implicit dependencies that are not visible in stack-effect declarations. Retainer work on Factor dynamic variable bugs is therefore partly about identifying which values are implicitly accessed through the dynamic environment and whether each access point is correctly inside the scope where the binding was established.
Factor dynamic variables: with-variable scope, get/set, make-parameter, and the quotation lifetime hazard
Factor dynamic variables are defined with SYMBOL: or SYMBOL: declarations and are scoped using with-variable. The with-variable word takes a value, a variable symbol, and a quotation: value var [ quot ] with-variable establishes a new dynamic binding where var resolves to value for the duration of the quotation’s execution. Within the quotation, any call to var get returns value; any call to var set writes to the current innermost binding for var. The scope is dynamic, not lexical: the binding is available to every word called directly or transitively within the quotation, not just words that appear textually inside the quotation’s brackets. This is the power of dynamic scope — you can establish a binding in a high-level control word and have it be visible deep in the call tree without threading it through every intermediate word’s stack signature. It is also the source of the scope hazard: a quotation that is constructed inside a with-variable scope and immediately called is safe; a quotation that is stored and called later may execute in a context where the with-variable binding has already unwound.
The distinction between get/set and make-parameter is important for understanding the scope model. var get reads the current dynamic binding for var; if no with-variable has established a binding for var in the current call chain, get returns the variable’s global default value. var set writes to the innermost current dynamic binding for var. If set is called outside any with-variable scope for var, it writes to the global binding, affecting all threads. This is a different behavior from what a caller inside a with-variable scope expects: inside the scope, set writes to the thread-local binding established by with-variable, visible only within that scope. A quotation that uses set while inside the scope behaves predictably; the same quotation called outside the scope writes to the global value, which is visible to all subsequent get calls from any thread. Four wrong writes per run occurred because the quotation’s deferred execution wrote to the global binding, and the subsequent reads in the main computation read the wrong global value instead of the per-invocation value the with-variable scope was designed to provide. make-parameter creates a combined getter/setter closure that carries its own binding state, making it independent of the dynamic scope chain; it is the correct primitive when a value needs to be per-invocation without relying on the caller to establish a with-variable scope.
The quotation lifetime hazard arises whenever quotations are separated from the dynamic scope that gives them meaning. The canonical categories: (1) quotation registered as a callback with add-hook, add-output-stream, or an event dispatch mechanism — these quotations execute in the event loop’s dynamic scope, not the scope at registration time; (2) quotation wrapped in a curry and returned as a closure — the curried value is embedded, but any dynamic variable accesses inside the quotation are resolved at call time; (3) quotation stored in a TUPLE slot and called later — same issue; (4) quotation passed to a word that may call it in a different thread — Factor’s dynamic variable bindings established with with-variable are per-thread, so a quotation passed to a newly spawned thread has access only to that thread’s dynamic scope, which has no with-variable bindings unless explicitly established. In each category, the diagnostic question is the same: at the moment the quotation’s get or set call executes, is the with-variable binding still active in the call chain? If not, get returns the global default and set writes to the global binding.
The canonical repair is to eliminate the dynamic variable dependency from quotations that may outlive their originating scope. The primary technique is curry-based value embedding: capture the current value with var get at the moment the quotation is constructed (while inside the with-variable scope), and embed it into the quotation with curry. The resulting quotation carries the value as a literal and does not need to look it up via the dynamic scope at call time. A secondary technique is to pass the value explicitly through the quotation’s stack inputs: redesign the combinator call to push the value onto the stack before applying the quotation, so the quotation receives the value as a direct stack input rather than accessing it from the dynamic environment. Both techniques make the quotation’s dependency on the value explicit in the quotation’s stack effect rather than implicit in the dynamic scope chain, which makes the dependency visible to the stack checker and to future readers of the code. The trade-off is that the quotation’s stack effect becomes more complex: a quotation that was ( -- ) (calling get with no stack inputs) becomes ( x -- ) (receiving the value as a stack input). Every combinator call that uses the quotation must be updated to push the value beforehand, which may require additional dip manipulations to place the value in the correct stack position.
Factor word definitions, TUPLE: classes, GENERIC: dispatch, and the vocabulary system
Factor word definitions use the : (colon) syntax: : word-name ( stack-effect ) body ;. The stack effect is a declaration in parentheses listing the input names (before --) and output names (after --). The body is a sequence of words and literals. Factor uses postfix notation: 3 4 + is equivalent to 3 + 4 in infix notation. Word definitions are compiled into Factor’s native code generation backend; the image-based development model means that compiled word definitions persist across Factor image saves and loads. Modifying a word definition and refreshing it (with refresh-all or reload) recompiles the word and all words that call it transitively; this live-patching capability is central to Factor’s development workflow. The stack checker runs on every refresh-all and reports stack-effect declaration mismatches before the new word definitions are committed to the image.
TUPLE: classes are Factor’s primary data abstraction. A TUPLE: declaration defines a class with named slots: TUPLE: point x y ; declares a tuple class point with slots x and y. Factor automatically generates accessor words: point-x (getter), point-x>> (getter, postfix style), and >>x (setter). Constructor words are generated from TUPLE: declarations: point new creates a point with all slots set to f (Factor’s false/null value); point boa (by-order-of-arguments) pops values for each slot off the stack in declaration order and creates a fully initialized instance. Slot defaults can be specified with initial: annotations: TUPLE: point { x initial: 0 } { y initial: 0 } ; creates points with x=0 and y=0 by default when using new. TUPLE: slots are read and written through generated accessor words; direct slot manipulation is not available, which means TUPLE: accessors are the natural points for type checking, validation, or caching logic via method decoration.
GENERIC: words provide Factor’s object-oriented dispatch. A GENERIC: declaration names a generic word: GENERIC: area ( shape -- area ). Method implementations are added with M: syntax: M: circle area ( circle -- area ) radius>> sq pi * ;. Factor dispatches on the class of the first stack argument. UNION: and INTERSECTION: type declarations allow dispatch on class combinations: UNION: shape circle rectangle triangle ; defines a union class that matches any instance of its constituent classes. PREDICATE: allows dispatch on a runtime predicate: PREDICATE: positive-integer < integer [ 0 > ] ; defines a class that matches integers satisfying the predicate. The dispatch order is: most-specific class first, with specificity determined by the inheritance hierarchy; TUPLE: subclasses are more specific than their superclasses; PREDICATE: classes are checked in definition order when their base class matches. Retainer work on GENERIC: dispatch bugs typically involves either adding missing M: methods for new TUPLE: subclasses (a GENERIC: dispatch falling through to the wrong default method because a new TUPLE: subclass was added without adding the corresponding M: specialization) or resolving dispatch ambiguity when a TUPLE: instance matches multiple UNION: classes with overlapping M: methods.
The Factor vocabulary system organizes words into named namespaces. A vocabulary is a directory containing vocab.factor (word definitions), optionally vocab-tests.factor (test definitions), and optionally vocab-summary.factor (documentation). USE: vocabulary-name imports all words from a vocabulary into the current parsing context; USING: vocab1 vocab2 ... ; imports multiple vocabularies. Factor’s vocabulary loader resolves vocabulary names to filesystem paths using a root search path. The image-based development model means vocabulary loads are cached: once a vocabulary is loaded into the Factor image, its words are available until the image is reset or the vocabulary is explicitly refreshed. Factor’s history: Slava Pestov began designing Factor in 2003, influenced by Joy (John Manookin’s concatenative calculus), Forth, Lisp, and Smalltalk. Factor’s design goal was to make Joy-style concatenative programming practical for real applications by providing a rich standard library, a native code compiler, and an interactive development environment. The Factor standard library covers sequences (arrays, vectors, linked lists), assocs (hash tables, association lists), io (streams, files, sockets, HTTP), math (integers, rationals, floats, complex), strings (unicode), and reflection (class introspection, word lookup). Factor’s image-based development is similar to Smalltalk’s: the live running system is the development environment, word definitions are modified in place, and the modified image is saved rather than source files being separately compiled.
How HourTab tracks Factor developer retainer hours
Factor retainer work shares the invisible-work problem common to all systems programming retainers, compounded by the concatenative programming model’s tendency to produce very short diffs relative to the diagnostic work. A dynamic variable scope fix is a diff with a removed get call, an added curry application, and updated combinator calls to push the value before applying the quotation; the value is elimination of all wrong writes caused by quotation re-execution outside the with-variable scope, a correct understanding of which quotation execution contexts are inside the originating scope, and a curry-composition discipline that prevents the same class of scope error from appearing in other quotations stored for deferred execution. A stack-effect declaration audit that corrects mismatches across a vocabulary is a diff with a handful of changed parenthetical declarations; the value is a vocabulary whose stack checker passes on every refresh, correct stack depth at every combinator application, and a set of word definitions whose actual behavior matches their declared specifications. A GENERIC: dispatch refactor that adds missing M: methods and resolves dispatch ambiguity is a diff with new M: specializations and possibly a UNION: redefinition; the value is correct dispatch for all current and future TUPLE: subclasses without silent fallthrough to wrong default methods.
HourTab gives Factor developers a public retainer-hours URL they send to clients — typically functional programming consultancies working on Factor vocabulary systems, research groups using Factor for live programming experiments, and organizations that adopted Factor for its image-based development workflow — at the start of an engagement. For Factor retainers, each work log entry should name the mechanism (quotation pipeline design; combinator selection: bi/tri/bi*/tri*/bi@/tri@; dip/keep composition; curry/compose partial application; dynamic variable scope analysis: with-variable binding scope; get/set mutation; quotation lifetime mapping; explicit-value threading redesign via curry; stack-effect declaration audit; TUPLE: slot and constructor design; GENERIC: dispatch chain analysis; vocabulary USE:/USING: import repair; image refresh cycle), the specific words and quotations involved in the bug, and the before/after metric. Factor retainers are often compared to Forth developer retainers for stack-based programming work and to Racket developer retainers for functional language engineering with an emphasis on first-class procedures and macro systems. HourTab’s work log makes the dynamic variable scope analysis, quotation lifetime mapping, and curry composition design visible to clients who would otherwise see only the symptom — four wrong writes per run — and not understand why the fix required auditing every quotation storage and execution site, mapping which contexts were inside the with-variable scope, and restructuring the combinator calls to thread values explicitly rather than relying on the dynamic scope chain.
Track Factor developer retainer hours without the status emails
HourTab gives Factor developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Factor developer retainers
What does a Factor developer on retainer typically do?
A Factor developer on monthly retainer covers four principal service areas: quotation and combinator design (quotation-as-first-class-value pipeline architecture; bi/tri/bi*/tri*/bi@/tri@ combinator selection; dip/keep/curry/compose higher-order quotation design; stack-effect declaration auditing); dynamic variable scope management (with-variable binding scope analysis; get/set vs make-parameter discipline; quotation lifetime mapping; explicit-value threading via curry); data model design (TUPLE: slot declaration; new vs boa constructor patterns; GENERIC: dispatch; UNION: and INTERSECTION: type declarations; PREDICATE: dispatch); and vocabulary system architecture (USE:/USING: import declarations; private word organization; MAIN: entry point; Factor image-based development workflow).
What Factor work is most commonly underlogged in a retainer?
Dynamic variable scope audit (quotation stored in variable, passed to bi*, inner quotation called set inside with-variable scope, quotation re-executed in deferred context outside with-variable scope, set wrote to global binding, 4 wrong writes per run, restructured with curry to thread value explicitly, wrong writes: 4/run → 0, 12–20 hrs invisible in scope chain analysis, quotation lifetime mapping, and curry composition design); stack-effect declaration repair (word with wrong stack-effect caught only at load time for reachable words; words called through combinators produce wrong stack depths; 8–14 hrs invisible in stack-effect auditing across vocabulary); and TUPLE: slot initializer design (initial: annotations, boa constructor discipline; 6–10 hrs invisible in constructor design analysis).
What are typical Factor developer retainer rates?
Entry-level Factor developers (1–2 years, basic word definitions, quotations, standard combinators) bill at $70–$120/hr. Mid-level Factor engineers (2–4 years, dynamic variable scope management, advanced combinator composition, GENERIC: dispatch design, vocabulary architecture) bill at $115–$195/hr. Senior Factor architects (4–8 years, full image-based system design, native code generation optimization, C library FFI with alien vocabulary, Factor bootstrap contribution, complex GENERIC: dispatch hierarchy) bill at $165–$295/hr. Monthly retainer ranges: $1,800–$4,500/mo advisory (15–25 hrs), $6,000–$16,000/mo for full Factor system development engagements.
What should a Factor developer retainer agreement include?
A Factor developer retainer agreement should specify: quotation scope (quotation-as-first-class-value pipeline design; bi/tri/bi*/tri*/bi@/tri@ combinator selection; dip/keep/curry/compose composition patterns; stack-effect declaration auditing); dynamic variable scope (with-variable binding scope analysis; get/set vs make-parameter discipline; quotation lifetime mapping; explicit-value threading via curry); data model scope (TUPLE: slot declaration; new vs boa constructor patterns; GENERIC: multi-method dispatch; UNION: and INTERSECTION: type hierarchy); vocabulary architecture scope (USE:/USING: import discipline; private word organization; MAIN: entry point design; image refresh and reload workflow); and hour logging format (advisory category, before/after metric, Factor version, whether fix required curry composition addition, with-variable scope expansion, or stack-effect declaration correction).
How should Factor developer retainer hours be logged?
Log each Factor retainer session with: advisory category (quotation pipeline design; combinator selection: bi/tri/bi*/tri*/bi@/tri@ choice; dip/keep composition; curry/compose partial application; dynamic variable scope analysis: with-variable binding scope; get/set mutation; quotation lifetime mapping; explicit-value threading redesign via curry; stack-effect declaration audit; TUPLE: slot and constructor design; GENERIC: dispatch chain analysis; vocabulary USE:/USING: import repair; image refresh cycle); the specific words and quotations involved in the bug (bi* applied quotation stored in variable; inner quotation called set inside with-variable scope; quotation re-executed in deferred context outside with-variable scope; set wrote to global binding; 4 wrong writes per run; restructured with curry to thread value explicitly; wrong writes: 4/run → 0); and the before/after observable metric. Include Factor version and whether fix required curry addition, scope expansion, stack-effect correction, or combinator substitution.