Blog › ICP guides

Joy developer on retainer: concatenative programming, quotation combinator ordering, higher-order stack functions, and Joy language on monthly retainer

October 1, 2026 · ~15 min read

A Joy developer was implementing a validation pipeline that used the ifte combinator — Joy’s if-then-else for quotations — to branch on a condition. Joy’s ifte takes three quotations in a specific order on the stack at the point of the call: the condition quotation (a program that pushes a Boolean) is the first argument (topmost on the stack when the three quotations are pushed), the then-quotation (executes if the condition is true) is the second, and the else-quotation (executes if the condition is false) is the third. Stated differently, if you write the program left to right, the correct push order before calling ifte is: push the condition quotation, push the then-quotation, push the else-quotation, then call ifte — so at the moment of the call, the else-quotation is on top, the then-quotation is beneath it, and the condition quotation is below that.

The developer wrote two invocations in the wrong order: [then-quotation] [condition] [else-quotation] ifte — placing the condition quotation as the second argument pushed rather than the first, so at call time the else-quotation was on top, [condition] was beneath it, and [then-quotation] was at the bottom of the three. When ifte executed, it took what was in the condition position — which was [then-quotation] — and executed it as the condition test. [then-quotation] was designed to transform a value, not to test one: it did not push a Boolean. ifte inspects the top of the stack after executing the condition quotation and expects to find a Boolean; instead it found whatever [then-quotation] left on the stack, causing a type mismatch. Both invocations failed this way. Fix: reordered all three quotation arguments to [condition] [then-quotation] [else-quotation] ifte (correct push order); confirmed ifte’s parameter ordering from Manfred von Thun’s Joy documentation at La Trobe University; ifte invocation errors: 2 → 0.

The work log said “fixed combinator argument ordering, 5h.” It cannot explain why 5 hours: that Joy does not enforce quotation type at the call site, that ifte takes quotations as data and only discovers the condition quotation’s failure to push a Boolean when it actually executes that quotation and inspects the stack, that diagnosing the failure required understanding ifte’s internal execution model (execute condition quotation; inspect top of stack for Boolean; branch to then or else quotation), that the developer needed to re-read von Thun’s 2001 paper “Purely Functional Programming in Joy” to confirm the parameter order, or that Joy combinators are named as words (ifte is a name, not a syntax structure) in a way that implies nothing about their argument order on the stack. A client reading that entry sees five hours for what sounds like moving code around.

Joy language overview: purely concatenative programming and its mathematical foundations

Joy was designed by Manfred von Thun at La Trobe University in Melbourne, Australia, around 1999. Von Thun’s goal was explicit and ambitious: to establish the rigorous mathematical foundations of concatenative programming, a programming model in which programs are functions from stacks to stacks, and the composition of two programs is their sequential application written as juxtaposition — placing one program immediately after another. Joy is the reference language for this model, created not primarily as a production tool but as a vehicle for demonstrating the mathematical coherence of concatenative programming as a complete programming paradigm.

The key consequence of the purely concatenative model is that Joy has no named parameters. In a language with named parameters — Haskell, ML, Python, almost every language in common use — a function names its arguments: double x = x * 2. The name x refers to the argument inside the function body. In Joy, the equivalent word definition is double == 2 *: the argument is already on the stack when the word begins executing, and the word operates on it positionally, not by name. There is no x. There is a stack, and the word’s program is a function that transforms it.

This makes Joy programs pure function compositions in a category-theoretic sense. Concatenation of programs is composition of their denoted functions: if program P denotes function p and program Q denotes function q, then program P Q (P then Q) denotes q ˆ p (apply p first, then q to its result). The identity program is the empty program (or equivalently, the empty quotation [] executed with i). Von Thun developed formal denotational semantics for Joy: each program is assigned a mathematical function from stacks to stacks; the semantics of the full language is a mapping from programs to such functions; and the composition rule gives the semantics of concatenation. Von Thun demonstrated that Joy is Turing complete using only combinators (higher-order operations that operate on quoted programs) without any named variables.

Key von Thun papers: “Purely Functional Programming in Joy” (2001); “Foundations of Arity” (2003); “A Rewriting System for Joy” (2003). Joy is available as source code from von Thun’s La Trobe page and as a community fork at github.com/nicowillis/joy. The primary Joy implementation is an interpreter written in C.

The concatenative model: programs as stack functions, juxtaposition as composition

Every Joy operation is a function from stacks to stacks. The integer literal 5 is a function that takes a stack and returns a new stack with 5 on top. The word dup is a function that takes a stack with at least one item and returns a stack with that item duplicated at the top. The word + takes a stack with at least two numbers on top and returns a stack with their sum. Programs are built by writing operations one after another: 5 3 + is the composition of three functions, and its result as a function is to push 8 onto any input stack.

The type of a Joy program is a stack type: a description of the input stack shape and the output stack shape. Von Thun’s notation writes this as a stack-effect comment: ( X Y -> Y X ) for swap (takes two items, returns them in reversed order), ( X -> X X ) for dup (duplicates the top), ( X -> ) for pop (discards the top). Crucially, X and Y in these comments are type variables: they range over any type. A program that composes two operations P Q has a type that is the composition of their types: the output of P must be compatible with the input of Q.

The absence of named parameters means that all combination in Joy is through combinators. In a language with named parameters, you can write a function that takes a list and a transformation function and applies the transformation to each element, naming both arguments. In Joy, you write map: a combinator that expects a list and a quotation on the stack (the list below the quotation at the top) and applies the quotation to each element. There is no name for the list or the quotation inside map’s implementation; there is only the stack, and operations that consume and produce items on it.

Quotation is the central data abstraction that makes Joy’s combinator model work. The expression [ P Q R ] in Joy is a list literal: it pushes the list containing the programs P, Q, and R as data onto the stack without executing them. This list is also an anonymous program: the combinator i (immediate execution) takes a quotation from the top of the stack and executes it. So [ P Q R ] i is equivalent to P Q R. Quotations allow programs to be treated as first-class data: they can be pushed, popped, passed to combinators, stored in lists, and inspected.

The contrast with Forth is instructive. Forth is also concatenative and stack-based, and it is the practical predecessor that Joy formalizes. Idiomatic Forth avoids named local variables for the same reasons Joy has none. But Forth provides local variable extensions (VALUE, LOCALS|) for cases where stack juggling becomes too complex, and Forth words are defined imperatively. Joy maintains mathematical purity: there are no local variables, no imperative assignment, and no escape from the purely functional stack model. Forth informally implements the concatenative model as a systems programming language; Joy is the mathematical model that Forth informally implements, expressed as cleanly as possible.

The combinator dip is Joy’s mechanism for reaching below the top of the stack. [ P ] dip takes a quotation and the item below it, temporarily saves the top item, executes the quotation on the remainder of the stack, then restores the saved top item. Its stack type is ( X [P] -> {P-applied-below} X ): after dip, the top item X is back on top, but P has operated on everything below it. dip is the primary mechanism for accessing items below the top when operating on deeper stack positions.

Joy’s combinators: branching, recursion, iteration, and higher-order operations

Joy’s combinators are its control structures. Because Joy has no syntax for if-then-else, for loops, or recursion (no named parameters means no self-reference by name in a definition), all of these are implemented as library combinators that operate on quotations. The set is rich and covers essentially all the control structures a programmer needs.

Branching: ifte is the primary branching combinator. Its parameter order is: push [condition], push [then], push [else], then call ifte. ifte internally executes [condition] on a copy of the current stack (so the stack is not modified by the condition test), inspects the Boolean left on top, and executes either [then] or [else] on the original stack. The condition quotation must push exactly one Boolean; the then and else quotations operate on the original pre-condition stack. This is the parameter order that the developer in our opening scenario violated: the condition quotation must be the deepest of the three on the stack at call time (first pushed in source order), not the second deepest.

Linear recursion: linrec takes four quotations: [if-done] (condition that is true when the base case is reached), [base-case] (what to do when if-done is true), [combine] (what to do with the recursive result), and [recurse-on] (how to reduce the input before recursing). Its application: linrec executes [if-done] on the current stack; if true, executes [base-case]; if false, saves the current state, executes [recurse-on] to reduce the problem, recurses, then executes [combine] to combine the result with the saved state. Classic example: [null] [succ] [dup pred] [+] linrec computes the sum of integers from 1 to N on the top of the stack.

Binary recursion: binrec is for divide-and-conquer problems. Like linrec, it takes [if-done], [base-case], [split] (how to split the input into two subproblems), and [combine] (how to combine two results). binrec is the natural combinator for mergesort, quicksort, and tree-processing algorithms.

General recursion: genrec is Joy’s most powerful recursive combinator. Unlike linrec and binrec, which have fixed recursion shapes built in, genrec takes [if-done], [base-case], [body], and a quotation that is replaced by the recursive call: [body] is executed with the genrec call itself spliced in, allowing the recursive structure to be controlled by [body]’s logic. genrec can express any recursive structure and is used when linrec and binrec do not fit the problem shape.

Iteration and mapping: [f] map applies quotation [f] to each element of the list on the top of the stack, returning a new list of results. [pred] filter returns the sublist of elements for which [pred] is true. [identity] [f] fold accumulates the list using [f] with [identity] as the initial accumulator value. [body] times executes [body] a fixed number of times (the count is taken from below the quotation on the stack). [condition] [body] while executes [body] repeatedly as long as [condition] is true.

Multi-quotation application: cleave applies two quotations to the same top-of-stack value, leaving both results. bi applies two quotations to the same value (like cleave). app2 applies one quotation to two values: takes two values and a quotation, applies the quotation to each value, leaves both results. These combinators are essential when a value needs to participate in two different computations without being duplicated manually with dup and intervening dip calls.

Typical Joy retainer work and what it looks like in a work log

Joy retainer work falls into three recurring categories. The first, and most commonly underlogged, is combinator argument ordering diagnosis. The scenario from the opening is typical: a developer writes a multi-quotation combinator call with quotations in the wrong order. Joy does not validate quotation types at the push site; the program runs, the combinator executes the wrong quotation as the condition (or the if-done predicate, or the base case), and the failure only appears when the combinator inspects the stack after executing that quotation and finds an unexpected type. The diagnostic process requires understanding each combinator’s internal execution model: for ifte, execute condition quotation on a copy of the stack, check top for Boolean, branch; for linrec, execute if-done quotation, check top for Boolean, branch to base case or execute recurse-on then recurse then combine. Tracing the type error back to the wrong quotation in the wrong argument position is 4 to 8 hours of careful stack reasoning that appears in a log as “fixed combinator call.”

Work log entry for the scenario above: “ifte combinator in validation pipeline: condition quotation was in second push position (first from top at call time), not first push position (third from top at call time); ifte executed [then-quotation] as condition test; [then-quotation] transformed a value, did not push Boolean; ifte Boolean check failed; same error in 2 of 4 ifte invocations in the pipeline; confirmed ifte parameter order from von Thun (2001): [condition] pushed first, [then] pushed second, [else] pushed third, then ifte; reordered both invocations; ifte invocation errors: 2 → 0; 5h.” Without the Joy context, the entry reads as five hours to reorder three lines of code.

The second recurring category is quotation composition and stack type analysis. Joy programs built from composed quotations can accumulate subtle type errors: a quotation that is correct in isolation leaves one extra item on the stack when invoked after a particular earlier quotation, or consumes one item that the following quotation expects to find. Verifying the stack type of a composed program requires tracing intermediate stack states through each operation — there are no variable names to anchor reasoning, only stack positions. A complex quotation that is ten words long may have six intermediate stack states to reason about. Work log entry: “quotation [verify-and-route]: traced stack type through all 9 operations; found dup at position 4 left an extra copy that was not consumed before [route-handler] call; [route-handler] expected 2 items, found 3; added pop after dup at position 4 to discard extra copy; added stack-effect comments for all 4 auxiliary words; type violations per test run: 7 → 0; 6h.”

The third category is recursive combinator selection. Joy’s three main recursive combinators — linrec, binrec, and genrec — have different shapes. linrec is correct for problems where the recursive step reduces the input by a fixed amount and the result is combined linearly (factorial, sum, list reversal). binrec is correct for problems where the input is split into two independent subproblems (sort, tree processing). genrec is necessary when neither shape fits: mutual recursion, recursion with multiple base cases, or recursion where the recursive step depends on intermediate results. Choosing the wrong combinator produces code that works but is significantly harder to read and verify; the mismatch between the problem structure and the combinator’s built-in recursion shape also makes termination harder to reason about. Retainer work in this category: “tree-fold implementation: original used genrec; restructured as binrec after confirming binary tree structure always produces exactly two subproblems; binrec termination is automatic for leaf nodes; genrec version required explicit termination guard; binrec version eliminates guard; stack depth on 1000-node tree: same; code length: 12 words → 8 words; 5h.”

Track Joy developer retainer hours without the status emails

When a 5-hour session traces a wrong branching behavior to a misplaced condition quotation in an ifte call (condition was second instead of first, was executed as the condition test, did not push a Boolean, caused type mismatch), the work log needs to name the combinator, the documented parameter order, the actual argument positions, and the invocation error count before and after. HourTab gives your Joy retainer client a public dashboard URL — no status emails.

See HourTab pricing →

How HourTab tracks Joy developer retainer hours

Joy retainer work is invisible by the same mechanism that makes Joy mathematically elegant: quotations are just values on the stack, and a misplaced quotation is a valid Joy program that compiles and runs without error until a combinator executes it and finds it doesn’t push the expected type. There is no type annotation at the push site, no named-parameter binding that would make the argument order explicit in the source, and no combinator-call syntax that distinguishes the condition from the then from the else. The three quotations are just three list literals pushed in sequence, and Joy’s text gives no hint that the order matters until you read the combinator’s documentation. A client who sees “5h — fixed combinator argument ordering” has no way to assess whether five hours was proportionate to what sounds like moving three pieces of code around.

Compare this with other functional language retainer work. A Haskell developer who makes an argument order error gets a compile-time type error: the Haskell type system checks the types of all arguments at the call site, and passing a transformation function where a Boolean-returning predicate is expected produces a concrete type error message naming the expected type and the actual type. An ML developer has the same protection. A Scheme developer using a named higher-order function like (if-branch condition then else) has named arguments in the interface that make the order at least documentable. Joy’s combinators have no syntax, no named parameters, and no compile-time type checking: the argument order is a convention documented in von Thun’s papers, and the only signal that the convention was violated is a type mismatch deep inside the combinator’s execution, often several operations after the misplaced quotation was consumed.

HourTab gives Joy developers a public retainer-hours URL they send to clients — typically academic computer science researchers using Joy for work on concatenative programming theory, developers working on Forth-family language toolchains who have studied Joy as a formal model, and organizations doing formal verification of stack-based programs. For Joy retainers, each work log entry should name the mechanism at the level of the concatenative model: which combinator, what the documented parameter order is (citing von Thun), what the actual argument positions were on the stack at call time, what type error resulted from the wrong argument in the condition position, and what the error count was before and after the fix. Comparative context for rate discussions: senior Joy expertise — formal denotational semantics depth, Joy interpreter extension in C, advanced recursive combinator design — commands $155 to $270 per hour because the combination of purely concatenative model mastery, combinator design experience, and formal program analysis capability is extremely rare. Joy is not taught in most computer science curricula; most Joy practitioners learned it independently from von Thun’s papers and the Joy interpreter source.

FAQ: Joy developer retainers

What does a Joy developer on retainer typically do?

A Joy developer on monthly retainer covers quotation composition (building programs as concatenations of quotations, where every program is a function from stacks to stacks and juxtaposition is composition); combinator selection and application (ifte for branching, linrec for linear recursion, binrec for binary recursion, genrec for general recursion, map and filter for list operations, dip for reaching below the top, i for immediate execution); point-free transformation (converting pseudocode with named parameters into Joy’s nameless stack-based style); stack type analysis (determining what each program leaves on the stack for each input stack state, written in von Thun’s format as before and after type comments); formal verification (using Joy’s denotational semantics to prove program properties); and Joy interpreter maintenance (building from source, extending with new primitive words, or porting the C interpreter to a new platform).

What Joy work is most commonly underlogged in a retainer?

Combinator argument ordering diagnosis is the most commonly underlogged category: tracing a type error inside a combinator to a misplaced quotation argument — the condition quotation was second instead of first; the then-quotation was executed as the condition test; no Boolean was pushed; combinator failed — takes 4 to 8 hours and appears in a log as “fixed combinator call.” Stack type analysis (manually verifying that a complex quotation composition has the correct type for all input stacks; 5 to 9 hrs invisible) is the second. Quotation decomposition (decomposing a complex inline quotation into named auxiliary words with individual stack-effect comments; 4 to 7 hrs invisible) is the third. Recursion combinator selection (choosing between linrec, binrec, and genrec for a new recursive structure; verifying termination; benchmarking; 5 to 8 hrs invisible) is the fourth. Formal model consultation (re-reading von Thun’s papers to confirm combinator semantics for a specific combinator; 3 to 6 hrs invisible) is the fifth.

What are typical Joy developer retainer rates?

Entry-level Joy developers with 1 to 2 years covering basic combinators, stack manipulation, and simple quotation composition typically bill at $70 to $125 per hour. Mid-level Joy programmers with 2 to 4 years covering recursive combinator design, stack type analysis, point-free transformation, and combinator argument ordering diagnosis typically bill at $105 to $185 per hour. Senior Joy language developers with 4 or more years covering formal denotational semantics, Joy interpreter extension, advanced recursive combinators, and category-theoretic program analysis typically bill at $155 to $270 per hour. Monthly retainer ranges: $2,000 to $3,500 per month for advisory engagements covering architecture reviews, combinator selection, and stack type analysis (15 to 25 hours per month); $4,000 to $12,000 per month for full engagement Joy program development or interpreter maintenance.

What should a Joy developer retainer agreement include?

A Joy developer retainer agreement should specify: combinator scope (which combinators are in scope; whether the engagement covers new combinator design or audit of existing usage; whether it covers only ifte/dip/i or the full recursive combinator set); point-free transformation scope (which functions are candidates for Joy rewrite; named-parameter pseudocode vs. pure Joy style; whether stack-effect documentation is required for all auxiliary words); stack type documentation scope (which words and quotations need stack-effect annotations in the format ( before -> after ); whether types are verified by hand or by tooling); Joy interpreter scope (whether the engagement covers standard language use only or also interpreter extension or port); and hour logging format (combinator name; documented parameter order per von Thun; actual argument positions on the stack; type error produced; fix applied; invocation errors before and after; for stack type work: quotation name, intended type, actual observed type, discrepancy, fix, violations before and after).

How should Joy developer retainer hours be logged?

Log each Joy retainer session with: combinator used (ifte, linrec, binrec, dip, i, etc.); expected argument order per von Thun’s documentation (for ifte: condition quotation pushed first in source order — deepest of the three at call time; then-quotation pushed second; else-quotation pushed third — topmost at call time); actual argument positions on the stack when the combinator was called (first from top at call time: what was there; second: what was there; third: what was there); type error produced (condition quotation did not push Boolean; then-quotation was executed as condition; condition quotation was never executed); fix applied (reordered quotation arguments in source; added swap to reorder at call time; restructured code to push quotations in the correct order); invocation errors before: N; after: 0. For stack type work: quotation literal or word name; intended stack type entering (items, types, count); actual stack type observed after execution (items, types, count); discrepancy (underproduction, overproduction, wrong type); fix applied; type violations per test run before and after. For recursion combinator selection: combinators considered; termination condition for each; selected combinator and rationale; performance or readability improvement measured.