Blog › ICP guides
Factor developer on retainer: stack effect design, combinator programming, vocabulary system, object system, and concatenative language engineering on monthly retainer
October 18, 2026 · ~18 min read
A Factor-based configuration processing pipeline was producing corrupted output for two production jobs per day. The corruption was non-deterministic from the caller’s perspective — sometimes the wrong configuration object was returned, sometimes the result had extra items appended, sometimes it was missing fields entirely. The Factor developer on retainer diagnosed the root cause in four hours: the word normalize-config, declared with stack effect ( config -- normalized ), was actually consuming two stack items rather than one. The word had been refactored six months earlier to call a new sub-word that expected a config-slot object below the main config object on the stack, but the stack effect declaration had not been updated. The result was that every call to normalize-config silently consumed the item below the config on the stack — which could be anything from the previous computation — and used it as the config-slot input, producing wrong normalized output while discarding a legitimate stack value. Factor’s built-in stack checker was not run as part of the build process; when invoked after the fact, it surfaced the mismatch immediately. Fixing the word required decomposing normalize-config into four focused words, each with a verified stack effect matching its actual behavior, plus adding a check-stack step to the vocabulary load sequence. Stack corruptions: 2 per day → 0.
The work log entry read “fixed config normalization stack bug, 8h.” It names the symptom and the duration. It cannot explain to a client why the bug existed silently for six months, why it manifested non-deterministically, why the fix required decomposing one word into four rather than patching one stack effect comment, or what the Factor stack checker is and why not running it earlier was the root cause of the latency between introduction and discovery. The diagnosis required understanding that Factor’s stack effect declarations — the ( inputs -- outputs ) comments in word definitions — are not enforced at definition time unless the stack checker is explicitly invoked; they are documentation by default and enforced constraints only on demand. The six months between the refactor introducing the mismatch and the stack checker run that surfaced it represents the latency cost of not integrating the checker into the build process. The 8 hours of stack trace analysis (tracing the stack state through each call to normalize-config and its callers), decomposition design (identifying the four sub-responsibilities that should each be their own word with a clear stack effect), regression verification (running the stack checker on the full vocabulary to surface any other mismatches), and build integration (adding check-stack to the vocabulary load sequence) are not visible in the diff. The corruptions: gone. The stack effect guarantee: checker-verified.
The Factor stack model: stack effects, shuffle words, and combinator design
Factor is a concatenative, stack-based language where all computation operates by manipulating a shared data stack. Every word (Factor’s term for a function) takes values from the top of the stack and leaves values on the stack; the stack effect declaration ( inputs -- outputs ) documents the expected stack transformation. drop ( x -- ) removes the top item; dup ( x -- x x ) duplicates it; swap ( x y -- y x ) swaps the top two items; over ( x y -- x y x ) copies the second item to the top; rot ( x y z -- y z x ) rotates the third item to the top; -rot ( x y z -- z x y ) rotates in the reverse direction; 2dup ( x y -- x y x y ) duplicates the top two items; 2drop ( x y -- ) removes the top two. Shuffle words are necessary but dangerous at scale: a word that requires rot -rot over swap to arrange its arguments is difficult to read, difficult to verify, and easy to break when the surrounding call context changes. The primary technique for eliminating shuffle words is combinator adoption.
Quotations are Factor’s first-class function values: [ 2 * ] is a quotation containing the literal 2 and the word *; calling it with a number on the stack doubles the number. call ( ..a quot: ( ..a -- ..b ) -- ..b ) invokes a quotation. The core combinators: bi ( x p q -- ) applies quotation p then quotation q to the same x, leaving both results on the stack; tri ( x p q r -- ) applies three quotations to the same x; bi@ ( x y p -- ) applies quotation p to both x and y; bi* ( x y p q -- ) applies p to x and q to y; tri* ( x y z p q r -- ) applies three quotations to three arguments. dip ( x quot -- x ) calls quot with x temporarily removed from the top of the stack, then restores x afterward; 2dip does the same for the top two items. keep ( x quot -- x ) calls quot with x on the stack and then also leaves the original x; 2keep does the same for two items. spread ( x y [p] [q] -- ) applies [p] to x and [q] to y where the quotations are on the stack rather than inline. curry ( obj quot -- curried ) pushes obj as a literal into the front of quot, producing a new quotation that, when called, pushes obj and then executes quot; compose ( p q -- r ) creates a new quotation that executes p then q. Replacing over >r process-item r> finalize with [ process-item ] keep finalize makes the stack effect explicit: keep’s declared stack effect documents that the original item is preserved, whereas over >r ... r> requires the reader to mentally trace the return stack.
Factor’s sequence vocabulary provides map ( seq quot -- newseq ) for transformation, filter ( seq quot -- newseq ) for selection (the quotation must leave a boolean on the stack), filter-map ( seq quot -- newseq ) for combined transformation and selection, reduce ( seq identity quot -- result ) for folding, and each ( seq quot -- ) for iteration with side effects. map and filter preserve the sequence type: calling map on an array returns an array; calling it on a string produces a string. zip ( seq1 seq2 -- pairs ) produces a sequence of 2-element arrays from two parallel sequences; zip-with ( seq1 seq2 quot -- result ) combines them with a combining quotation. Sorting: sort ( seq -- sorted ) uses natural comparison; sort-by ( seq quot -- sorted ) sorts by applying a key extraction quotation; sort-with ( seq quot -- sorted ) sorts with a custom comparator quotation. Searching: find ( seq quot -- elt/f ) returns the first element satisfying the quotation or f (false); any? ( seq quot -- ? ) returns t if any element satisfies; all? ( seq quot -- ? ) returns t if all elements satisfy. The assoc vocabulary provides association list operations: at ( assoc key -- value/f ), set-at ( value assoc key -- ), keys ( assoc -- keys ), values ( assoc -- values ), and assoc-map ( assoc quot -- newassoc ).
Factor’s stack checker (tools.annotations and the check-stack word) statically verifies that every word’s implementation matches its declared stack effect. The checker performs abstract interpretation: it tracks the stack depth and type information (where available) through every call in a word’s definition and reports if the actual stack effect differs from the declared one. Running the checker on a vocabulary: USE: myapp.config myapp.config check-vocab verifies all words in the vocabulary. The stack checker also detects infinite loops in quotation inference (recursive words that expand indefinitely) and unclear cases in polymorphic dispatch. A retainer engagement integrating the stack checker into the build process typically involves adding a main.factor entry point that calls check-vocab for each vocabulary before loading the application, converting implicit stack effect comments to explicit DECLARE: ( ... ) declarations where the checker requires help with polymorphic words, and restructuring words whose stack effects are too complex (more than 4 inputs or outputs) into smaller composed words that the checker can verify individually.
Factor vocabulary system, object model, and parsing words
Factor organizes code into vocabularies, which are namespaces that can export words. IN: myapp.config declares the current vocabulary name. USING: kernel sequences io.files ; imports words from listed vocabularies into the current namespace. USE: io.encodings.utf8 imports a single vocabulary. The <private> marker in a vocabulary file starts a section of private words that are not exported; the matching </private> ends it. Vocabulary source files live at paths like vocab/myapp/config/config.factor; the vocabulary loader finds them based on vocabulary name conventions. vocab/myapp/config/config-tests.factor contains tools.test unit tests for the vocabulary. A vocabulary’s vocab/myapp/config/authors.txt and vocab/myapp/config/summary.txt provide metadata. The vocabulary system enables lazy loading: vocabularies are loaded on demand when first used, so large applications can have short startup times. Circular vocabulary dependencies cause load-time errors: vocabulary A that uses vocabulary B which uses vocabulary A creates a circular dependency that must be resolved by extracting the shared dependency into a third vocabulary C.
Factor’s object system is based on generic words and tuple classes. TUPLE: point { x float } { y float } ; defines a tuple class with two typed slots; point new creates an instance; >>x sets the x slot (consuming the new value and the point object from the stack, leaving the modified point); x>> gets the x slot. point boa (build-on-array) creates a point from the top two stack items in slot order. GENERIC: area ( shape -- area ) declares a generic word; M: point area ( point -- area ) ... ; provides a method for the point class; M: circle area ( circle -- area ) ... ; provides one for circles. Factor’s dispatch is single-dispatch on the class of the top-of-stack object. PREDICATE: positive-integer < integer ( n -- ? ) 0 > ; defines a predicate class — a subclass of integer that includes only positive values; methods can be dispatched to this subclass. UNION: shape point circle rectangle ; defines a union class that encompasses multiple tuple classes; methods dispatched on shape match any of its member classes. MIXIN: serializable ; declares a mixin class; adding a class to the mixin with INSTANCE: point serializable enables M: point serialize ... method dispatch without changing the class hierarchy. slot-access>> naming convention: slot readers are named slot-name>> and writers are >>slot-name.
Parsing words (defined with SYNTAX:) extend Factor’s reader at parse time, enabling DSL syntax that compiles to standard Factor words at vocabulary load time. SYNTAX: CONSTANT: scan-token create-word-in [ drop ] curry define-declared ; is a simplified version of a parsing word that reads the next token as a word name and defines a constant. \ word creates a quotation literal that pushes the word object itself (rather than calling it) when the surrounding quotation is called; \ normalize-config pushes the normalize-config word as a first-class value, enabling higher-order patterns. string>word ( string vocab -- word ) looks up a word by name in a vocabulary, enabling dynamic dispatch patterns. Factor’s I/O system: io.files provides <file-reader> and <file-writer> stream constructors; io.encodings.utf8 provides the utf8 encoding specification; with-file-reader ( path encoding quot -- ) opens a file reader and runs the quotation with it as the current input stream. The http.server vocabulary provides main-responder for defining an HTTP application entry point; define-dispatcher ( table -- ) sets up a URL dispatch table; respond-with-content-type ( content-type body -- response ) constructs a response. The io.sockets vocabulary provides TCP server and client support with <server>, <client>, accept, and with-socket stream management.
Factor’s development environment, the Factor listener, is an interactive REPL that also serves as the primary debugging environment. :help word-name shows documentation for a word including its stack effect, vocabulary, and any attached documentation string. :source word-name shows the source code. watch ( word -- ) wraps a word to print its inputs and outputs every time it is called — the Factor equivalent of adding print-debugging statements, but without modifying source code. profile ( quot -- ) runs a quotation under the profiler and prints a call count table. time ( quot -- ) measures wall-clock execution time. breakpoint stops execution and drops into the Factor debugger at that point. The Factor debugger shows the current data stack, the call stack (the sequence of word calls leading to the current point), and the restarts available (resume, retry, abort). The call stack in Factor is distinct from the data stack: the data stack holds computation values; the call stack holds the sequence of word activations. A retainer engagement debugging a stack corruption in a long-running Factor server involves correlating the call stack shown in the debugger with the data stack contents at the corruption point, identifying which word in the call chain is consuming more or fewer stack items than expected, and verifying the fix with the stack checker before deploying.
How HourTab tracks Factor developer retainer hours
Factor retainer work shares the invisible-work problem with all concatenative language retainers, with the specific challenge that Factor’s most common retainer tasks — stack effect diagnosis, combinator pipeline refactoring, vocabulary architecture restructuring, GENERIC:/METHOD: dispatch design — involve analytical work that leaves small diffs. Diagnosing a stack effect mismatch that caused 2 corruptions per day requires reading the call stack trace, tracing the stack depth through every word in the execution path, identifying the word where the declared effect diverged from the actual behavior, and decomposing that word into components whose effects can each be verified. The diff: four small word definitions replacing one incorrect one, plus a check-stack call in the vocabulary loader. The value: zero stack corruptions. Replacing 18 shuffle-heavy words with combinator pipelines produces a diff that rewrites 18 word definitions; the value is a codebase where stack flow can be understood without mentally simulating shuffle sequences, where the stack checker can verify every word, and where future modifications are less likely to introduce new mismatches. Splitting a 1,200-word vocabulary into 6 focused vocabularies is a diff that adds 5 new vocabulary files and modifies USING: declarations throughout; the value is a dependency graph that makes circular dependencies structurally impossible and enables vocabulary-level testing.
HourTab gives Factor developers a public retainer-hours URL they send to clients — typically research teams deploying Factor for data pipeline processing, DSL development, or rapid prototyping of stack-based systems — at the start of an engagement. For Factor retainers, each work log entry should name the mechanism (stack effect declaration audit and mismatch correction; bi/tri/bi@/tri@/dip/keep/spread combinator adoption; curry/compose quotation construction; vocabulary namespace refactoring with IN:/USING:; <private> boundary definition; GENERIC:/METHOD: polymorphic dispatch design; TUPLE: class definition with typed slots; SYNTAX: macro word authorship; http.server route/respond handler design; stack checker integration into vocabulary load sequence), the specific word name and the stack effect problem, and the before/after observable metric. Factor retainers are often compared to Clojure developer retainers for functional language platform work, to Haskell developer retainers for purely functional systems where correctness is compiler-verified, and to OCaml developer retainers for practical systems programming in a typed functional language. The distinction from Haskell is that Factor’s correctness guarantees come from the stack checker rather than the type system: a Haskell program where all types check can still have semantic errors; a Factor program where all stack effects are declared and checker-verified has a much smaller class of possible stack-related errors, but the type information is weaker. HourTab’s work log makes that distinction legible to clients: the entry names the word, the stack effect mismatch, and the corruption it caused, so the client understands why 8 hours on word decomposition and checker integration was the highest-leverage work in the engagement.
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: stack effect declaration design (stack effect audit for all exported words; mismatch diagnosis and correction; word decomposition for stack clarity; stack checker clean-build validation; shuffle word elimination); combinator pipeline design (bi/tri/bi@/tri@/dip/keep/spread combinator adoption; curry/compose chain design; map/filter/reduce/each sequence combinators; higher-order word design); vocabulary architecture (IN:/USING: namespace design; <private> boundary definition; vocabulary loading order debugging; tools.test test suite authorship; circular dependency resolution); and Factor object system design (TUPLE: class definition; typed slot annotation; GENERIC: word declaration; METHOD: implementation authorship; PREDICATE: behavioral subclass design; UNION:/MIXIN: intersection type definitions).
What Factor work is most commonly underlogged in a retainer?
Stack effect diagnosis and word decomposition (normalize-config consuming 2 stack items vs declared ( config -- normalized ) consuming 1; stack checker surfaced mismatch immediately; decomposed into 4 focused words; corruptions: 2/day → 0; 10–20 hrs invisible in stack trace analysis and decomposition design), combinator pipeline restructuring (18 shuffle-heavy words replaced with bi/tri/dip/keep combinators; average word length: 14 lines → 5; shuffle-induced bugs: 6 classes eliminated; 14–28 hrs invisible), and vocabulary namespace refactoring (1,200-word vocabulary split into 6 focused vocabularies; 4 circular dependencies resolved; tools.test coverage: 0% → 74%; 20–40 hrs invisible in dependency graph analysis and boundary decisions).
What are typical Factor developer retainer rates?
Entry-level Factor developers (1–2 years, basic word definition and stack effects, shuffle words, basic quotation use, simple TUPLE: class definition) bill at $70–$125/hr. Mid-level Factor engineers (2–4 years, combinator pipeline design with bi/tri/dip/keep/spread/curry/compose, GENERIC:/METHOD: dispatch, vocabulary architecture, tools.test test suite authorship, http.server web service development, io.files stream management) bill at $115–$210/hr. Senior Factor architects (4–8 years, full application architecture with layered vocabularies, SYNTAX: macro authorship for DSL construction, GC tuning for long-running servers, Factor image-based deployment, compiler internals) bill at $170–$310/hr. Monthly retainer ranges: $2,500–$5,800/mo advisory (15–25 hrs), $8,000–$21,000/mo for full concatenative language platform engagements.
What should a Factor developer retainer agreement include?
A Factor developer retainer agreement should specify: stack effect scope (stack effect audit for all exported words; mismatch diagnosis and correction; word decomposition; stack checker clean-build validation; shuffle word elimination); combinator scope (bi/tri/bi@/dip/keep/spread combinator adoption; curry/compose chain design; sequence combinator integration; higher-order word design); vocabulary architecture scope (IN:/USING: namespace design; <private> boundary definition; vocabulary loading order debugging; tools.test test suite authorship; circular dependency resolution); object system scope (TUPLE: class definition with typed slots; GENERIC: word declaration; METHOD: implementation authorship; PREDICATE: behavioral subclass design; UNION:/MIXIN: type definitions); I/O scope (io.files stream management; io.encodings.utf8 encoding; io.sockets TCP server/client design; http.server route/respond handler authorship); parsing word scope (SYNTAX: macro authorship; \\ word quotation literal construction; string>word vocabulary lookup); and hour logging format (word name; stack effect before/after; corruption count before/after; Factor version).
How should Factor developer retainer hours be logged?
Log each Factor retainer session with: advisory category (stack effect declaration audit and mismatch correction; word decomposition for stack clarity; bi/tri/bi@/tri@/dip/keep/spread combinator adoption; curry/compose quotation construction; map/filter/reduce/each sequence combinator adoption; GENERIC:/METHOD: dispatch design; TUPLE: class definition; PREDICATE: subclass authorship; vocabulary namespace refactoring; <private> boundary definition; tools.test test suite authorship; http.server route/respond handler design; SYNTAX: macro authorship), the specific word name and the stack effect problem (normalize-config consuming 2 stack items vs declared ( config -- normalized ); stack checker surfaced mismatch; decomposed into 4 words), and the before/after metric (stack corruptions/day: 2 → 0; shuffle words eliminated: 18; tools.test coverage: 0% → 74%). Include Factor version and image snapshot date.