Blog › ICP guides
J developer on retainer: rank adverbs, leading-axis summation, tacit train programming, verb-noun distinction in forks, and array-language idioms on monthly retainer
October 1, 2026 · ~15 min read
A J developer was building a statistical analysis library and needed to compute the sum of each row for a 5×3 matrix of daily sensor measurements — 5 days, 3 sensors per day. They wrote +/ data expecting a 5-element vector, one total per day. Instead they got a 3-element vector — one total per sensor. The issue: +/ without a rank adverb applies at rank infinity, meaning it inserts + along the leading (first) axis of the entire array. For a 5×3 matrix, the leading axis has length 5 (the rows), so +/ sums across rows, producing a vector of length 3 (one element per column). This is the exact opposite of what a programmer with a row-major intuition expects: “sum each row” means “reduce each row to its sum, producing one result per row,” but J’s leading-axis model means “+/ without a rank adverb = sum across all rows, one result per column.” The fix: +/ "1 data applies +/ at rank 1 — for each rank-1 cell (each row) of the data, compute +/. Result: a 5-element vector, one sum per row. Wrong-shape results: 5/5 → 0/5.
The work log said “fixed row sum calculation, 4h.” It cannot explain why +/ produces column sums on a matrix, what “rank infinity” means in J’s array model, or why the leading-axis model is the correct general model despite feeling counterintuitive to programmers from row-major traditions. A programmer reading that log entry sees a four-hour correction of what appears to be a trivial one-character fix (adding "1 after +/). What is invisible is the diagnostic cost: confirming that the matrix was correctly shaped (it was), that the sensor data was loaded in the right orientation (it was), that the summation primitive was the correct choice (it was), and then tracing exactly why J’s leading-axis convention produces a 3-element result from a 5×3 matrix when row-major intuition predicts 5 elements. The leading-axis model is not a bug in J; it is a deliberate, principled design that generalizes more cleanly to N-dimensional arrays than row-major conventions do. But understanding that principle well enough to diagnose a wrong-shape result from first principles, rather than by trial and error, is the work that consumed the four hours.
J array model: leading axis, rank adverbs, and why +/ sums columns by default
J was designed by Roger Hui and Kenneth Iverson, released in 1990, as the successor to APL. Where APL uses a set of special Unicode glyphs — ι, ρ, φ, ω, and dozens more — J uses only ASCII characters, representing the same operations with digraphs and punctuation: i. for iota (integer generation), $ for rho (shape/reshape), |. for reverse, +/ for reduce-with-plus. This makes J programs typeable on any keyboard but requires learning a compact notation where a single symbol like / (insert) or . (outer product) transforms a verb into a derived verb with new behavior. J inherits APL’s core design principle — arrays are the primary data structure and all operations are generalized to work on arrays of any rank — and extends it with the formal rank operator, which provides a systematic mechanism for applying any verb at any array dimension.
J’s leading-axis model is the formalization of a design principle developed by Jonathan Backus and then substantially refined by Ken Iverson: all primitive operations should apply to the leading (first) axis of an array by default. For a rank-1 array (a vector), the leading axis is the only axis, and this is entirely unsurprising. For a rank-2 array (a matrix), the leading axis is the first axis — the one indexed by the row index. The elements along the leading axis of a matrix are the rows: the “items” of a matrix are its rows, not its columns and not its scalars. This matters for +/ (insert-with-plus) because +/ inserts the + verb between the items of its argument: for a vector 1 2 3, the items are 1, 2, and 3, so +/ 1 2 3 = 1 + 2 + 3 = 6; for a matrix, the items are the rows, so +/ matrix inserts + between the rows, which is a row-by-row element-wise addition that folds all rows into one: the result has the same shape as a single row (a single vector), meaning it has one element per column. A 5×3 matrix has 5 items (rows); +/ matrix computes row0 + row1 + row2 + row3 + row4, which is a vector of length 3 — one element per column.
The shape of a J array is returned by the monadic verb $: $ data for a 5×3 matrix returns 5 3, a 2-element vector giving the size of each axis in order. # data (tally) returns the first element of the shape — the length of the leading axis — which is 5 for a 5×3 matrix, representing the number of rows. The relationship between $ and #: # data is equivalent to {. $ data (take the first element of the shape vector). The dyadic $ is reshape: 5 3 $ data reshapes data into a 5×3 array by cycling through the elements of data as needed. The verb i. n generates the integer vector 0 1 2 ... n-1; i. m n generates an m×n integer matrix whose elements are 0 1 2 ... m*n-1 arranged in row-major order. The shape of i. 5 3 is 5 3; it has 5 items (rows), each of which is a 3-element vector.
The rank adverb "r (double-quote followed by an integer) modifies a verb to apply at a specific rank. f "r y applies verb f to each rank-r cell of y. A rank-r cell is a sub-array of rank r: for a 5×3 matrix, rank-1 cells are the rows (5 of them, each a 3-element vector); rank-2 cells are the whole matrix itself (one cell); rank-0 cells are the scalars (15 of them, each a single number). So +/ "1 data applies +/ to each rank-1 cell (each row), computing the sum of each row and collecting results into a vector of length 5. This is the correct computation for “sum each row.” Conversely, +/ "_ data (rank infinity, the default) applies +/ to the whole array as a single rank-infinity cell, which inserts + between the leading-axis items (rows), producing the column sums. The fix for the sensor library was exactly +/ "1 data: add "1 to instruct J to apply +/ at rank 1 rather than at rank infinity.
The standard rank adverbs and their meanings: "0 applies to each scalar element (rank-0 cell); "1 applies to each vector (rank-1 cell, meaning each row of a matrix); "2 applies to each matrix plane (rank-2 cell, meaning each 2D slice of a 3D array); "_ (underscore) is rank infinity, meaning the entire array is treated as a single cell and the verb is applied once to the whole thing. For monadic verbs, the rank adverb controls how the argument is subdivided before the verb is applied to each piece; for dyadic verbs, the rank adverb can specify separate ranks for the left and right arguments using the syntax "l r, where l is the rank of the left-argument cells and r is the rank of the right-argument cells. The result of a rank-adverbed verb is assembled by J from the per-cell results: if +/ "1 applied to a 5×3 matrix produces five 1-element results (scalars), they are assembled into a 5-element vector. If the per-cell results were each a 2-element vector, they would be assembled into a 5×2 matrix. This assembly rule is systematic and follows from J’s array model.
Why is the leading-axis model the correct general model? For 2D arrays (matrices), the row-major convention of “axis=0 means rows, axis=1 means columns” is intuitive, and summation along axis=1 (giving row sums) feels natural. But for 3D arrays, the convention breaks down: a 3D array of shape 5 4 3 has a leading axis of length 5, a second axis of length 4, and a third axis of length 3; “sum each row” is ambiguous because there are now two kinds of “rows” depending on which axis you call the row axis. The leading-axis model resolves this: the items of a 3D array are its 2D slices (each of shape 4 3), and +/ on a 3D array sums along the leading axis, folding the five 2D slices into one 2D slice of shape 4 3. This generalization is uniform across all ranks. NumPy’s np.sum(data, axis=0) is equivalent to J’s +/ data: both sum along the leading axis (axis 0 in NumPy notation). NumPy’s np.sum(data, axis=1) is equivalent to J’s +/ "1 data: both sum along the second axis, which for a 2D matrix means summing each row. Python programmers who know to use axis=1 for row sums are, in effect, applying the equivalent of J’s "1 rank adverb; the J notation makes this explicit and uniform across all possible axes.
A diagnostic session for a wrong-shape result in J proceeds through several layers. First: verify the shape of the input with $ data and confirm it is what is expected. Second: apply the primitive without any rank adverb and check the shape of the result with $ +/ data: if it returns 3 (a scalar, meaning a 3-element vector was produced), confirm that 3 is the size of the non-leading axis, which confirms leading-axis behavior. Third: determine which axis the computation should have applied to by asking what the items of the computation should be: if the answer is “each row,” the rank is 1; if the answer is “each 2D plane of a 3D array,” the rank is 2; if the answer is “each scalar,” the rank is 0. Fourth: apply the verb with the correct rank adverb and verify the shape of the result. The four hours in the sensor library case were not four hours of typing; they were four hours of understanding why the leading axis of a 5×3 matrix has length 5, why that means +/ produces a 3-element result, and why the fix is "1 rather than any of the other possible solutions (transposing the matrix, reshaping after summation, or using a different primitive).
J tacit trains, forks, hooks, and typical retainer work
Tacit (point-free) programming in J means defining functions entirely by composing verbs, without naming the argument. Where an explicit (non-tacit) J definition would write mean =: monad def '(+/ y) % (# y)' naming the argument y, the tacit equivalent is mean =: +/ % #: three verbs arranged in a train. J’s train rules determine how a sequence of verbs behaves when applied to an argument, and those rules are what make tacit programming both powerful and a common source of bugs when the train structure does not match the programmer’s intent.
A three-verb sequence is a fork: (f g h) y = (f y) g (h y). The left verb f and right verb h are each applied monadically to the argument y, and then the middle verb g is applied dyadically to combine their results. The mean fork +/ % # expands to (+/ y) % (# y): the sum of y divided by the count of y. The range fork >:/ - <:/ expands to (>:/ y) - (<:/ y) = maximum minus minimum = range of y. The centering fork ] - +/ % # expands to (] y) - ((+/ % #) y) = the argument itself minus the mean = centered values. Here, ] (right identity verb, also called “same”) returns its argument unchanged: ] y = y; ] is used as the leftmost verb of a fork when you want the argument to pass through on the left side while the right side computes a derived value. In dyadic context, ] returns its right argument; [ returns its left argument. Both are used extensively in trains to route arguments.
A two-verb sequence is a hook: (f g) y = y f (g y). The monad g is applied to y, and then the dyadic f is applied with y as the left argument and g y as the right argument. The normalize hook (% +/) y = y % (+/ y): each element of y divided by the sum of y, normalizing to sum-to-one. The hook semantics are often confused with fork semantics by programmers who expect two verbs to mean “apply first, apply second.” A chain of function applications in J does not compose left-to-right without the explicit composition operator; it forms a hook with the implicit routing of the original argument to the left of the outer verb.
The verb-noun distinction in forks is a major source of bugs. When the leftmost or rightmost position in a three-item train is occupied by a noun (a data value rather than a verb), the fork semantics change. For a noun N in the leftmost position: (N f g) y = N f (g y); the noun N is used as the left argument to the dyadic verb f, and g y is the right argument. For a noun N in the rightmost position: (f g N) y = (f y) g N; the noun is used as the right argument to the dyadic verb g, and f y is the left argument. This is different from the all-verb fork where both f y and h y are computed from the argument. A common bug: a programmer writes 0 , +/ intending a fork that prepends 0 to the running sum, but since 0 is a noun, this is (0 , +/) y = 0 , (+/ y) = a two-element vector containing 0 and the sum, not a running-sum with 0 prepended. Understanding which position holds a verb and which holds a noun — and how that changes train evaluation — is the central skill for reading and constructing tacit J code.
Longer trains extend the fork and hook rules recursively. A 5-verb train (f g h i j) is a fork where the left and right forks are themselves trains: left = f g h (a fork) and right = i j (a hook), with the overall result being ((f g h) y) ? ((i j) y) for some middle verb. More precisely, J parses trains right-to-left: the rightmost two or three verbs form the innermost unit, and each additional verb extends the train toward the left. A four-verb train (f g h i) is parsed as a hook (f (g h i)), where f is the outer hook verb and (g h i) is the inner fork. A five-verb train (f g h i j) is parsed as a fork (f g (h i j)), where f and (h i j) are the fork’s outer verbs and g is the middle. Each additional verb prepends one level of hook or fork to the left. The practical consequence: when debugging a tacit train that produces wrong results, the first step is to parse the train according to these rules and expand it to its explicit form, tracing what y is routed to at each position.
The composition operators @: and @ are frequently confused. f @: g is standard function composition: apply g to the whole argument, then apply f to the whole result. The rank of the composed verb is determined by g’s rank, and then f is applied to whatever g produces. f @ g is rank-aware composition: apply g at g’s natural rank to sub-arrays of the argument, and then apply f to collect the results. The difference matters when g has a non-infinite rank: sum @ mean where mean is defined at rank 1 will apply mean to each row and then sum to the resulting vector of means, whereas sum @: mean will apply mean to the entire argument (at rank infinity, flattening to a single mean before summing). Using @: when @ is intended, or vice versa, produces wrong-rank or wrong-shape results that are silent: no error, just a different shape than expected.
The bond operator & binds one argument of a dyadic verb, producing a monadic verb. 2 &+ = add 2 (monadic: adds 2 to its argument). +& 2 = same (bond is symmetric for commutative verbs). 2 &* = multiply by 2. %&100 = divide by 100 (convert percentage to proportion). The under operator &. is more complex: f &. g means “apply g to the argument, apply f to the result, then apply the inverse of g to that result.” For example, +/ &. >: means “increment each element (apply >:, which is +1), sum the incremented values (apply +/), then decrement the result (apply the inverse of >:, which is <:, i.e., -1).” The under operator requires that g have a defined inverse; not all verbs do, and using &. with a non-invertible verb produces an error. J has a built-in inverse operator ^: _1 that applies a verb’s inverse, and many primitives have known inverses registered in J’s tables; &. is syntactic sugar for the inverse composition.
Box operations in J use < (box) and > (unbox, or “open”). < 42 returns a scalar atom containing 42; this atom has rank 0 and can be stored alongside atoms of any type in a heterogeneous boxed array. > boxed_atom returns the contents of the box, restoring the original type and shape. J does not automatically look inside boxes during arithmetic: +/ boxed_numbers will not sum the contents; the contents must be unboxed first with >. The adverb each (written "0 applied to rank-0 cells, or more idiomatically as the conjunction &"0) applies a verb to each element of a boxed array; > each box_array opens each box. A common retainer pattern: a programmer uses a boxed array to hold variable-length rows (since J arrays are rectangular, heterogeneous-length rows require boxing), and then attempts arithmetic operations on the boxed array without unboxing first, producing domain errors or shape errors that require understanding J’s box model to diagnose.
Sparse matrix support in J is accessed via the $. family of verbs. A dense array is converted to sparse representation with $. array; sparse arithmetic works transparently with J’s normal verbs; the sparse representation stores only non-default (typically non-zero) elements. J’s sparse support is not as widely used as its dense array operations, and retainer work touching sparse matrices often involves diagnosing why a verb that works correctly on dense arrays behaves unexpectedly on sparse ones, since not all J primitives have implemented sparse support and will fall back to dense computation or produce domain errors. Number formatting with “: (default format) and parsing input with ;: (sequential machine, used for tokenizing text) are also common retainer topics: ;: applies a table-driven state machine to a string, tokenizing it according to a supplied transition table, and debugging the state machine table is a specialized skill.
How HourTab tracks J developer retainer hours
J retainer work is particularly invisible because J’s array idioms produce no errors when rank is wrong. The leading-axis model is mathematically consistent: when a J programmer applies +/ to a 5×3 matrix and receives a 3-element vector, J has not made a mistake; it has computed exactly what its model specifies. The shapes are valid. The values are meaningful (they are the column sums). There is no exception, no crash, no assertion failure, and no NaN. The program runs, produces output, and the output is wrong relative to the programmer’s intent. The diagnostic requires understanding J’s leading-axis model at a level that cannot be acquired from the error output alone, because there is no error output.
This silent wrongness is the defining characteristic of J retainer work. A wrong-shape result is often not noticed immediately. The 5-element row-sum vector was expected; the 3-element column-sum vector was returned; downstream computations that expect 5 elements will either produce further wrong-shape results (if the shape mismatch is silent) or produce a length error (if a downstream operation requires shapes to agree). In the sensor library case, the downstream operation was a division by the daily maximum — data % max_per_day where max_per_day was supposed to have 5 elements (one per day) but was a 3-element vector (column maxima) due to the same leading-axis bug. The division produced a length error because J’s dyadic % requires agreement between the shapes of its arguments (or one argument to be a scalar). That error was the first visible symptom. Tracing back from the length error to its root cause — both the sum and the maximum were computed along the wrong axis — was the diagnostic work. Two rank errors, one length error, four hours.
HourTab gives J developers a public retainer-hours URL they send to clients — typically quantitative analysts using J for financial time-series calculations, researchers building statistical analysis tools in J’s array model, language researchers exploring tacit programming and rank-generalization for functional array languages, and engineers maintaining existing J codebases that predate the widespread availability of NumPy and similar array-processing libraries. For J retainers, each work log entry should name the mechanism at the level of the rank analysis: which verb was applied, what rank was used (infinity vs "1 vs "0), what shape was expected, what shape was produced, and which rank adverb corrected the mismatch. Comparative context for scope discussions: J retainer work is closely related to APL developer retainers (APL is J’s predecessor; the same leading-axis model and rank/axis concepts apply; APL uses special glyphs where J uses ASCII digraphs; tacit programming and trains are J features not present in classic APL) and to Futhark developer retainers (Futhark is a data-parallel functional language for GPU computation; Futhark also has a leading-axis model and similar row-vs-column axis ambiguities; Futhark retainer work involves GPU backend shape errors where J retainer work involves CPU array model shape errors; both share the diagnostic pattern of a mathematically correct result in the wrong shape).
HourTab’s work log format for J retainers makes the rank analysis, train tracing, and shape diagnosis visible to clients who would otherwise see only “added "1 to sum, 4h” and not understand why four hours was spent on two characters. A structured log entry covers: rank category (verb: +/; rank applied: infinity; expected shape: 5; actual shape: 3; fix: +/ "1; corrected shape: 5); shape category (downstream length error: % applied to shapes 5 3 and 3; root cause: both +/ and >:/ applied at rank infinity instead of rank 1); and train category (if the fork or hook structure was also involved). Each category makes the diagnostic path visible: the client who funded the work sees not just the result but the chain of inference that connected the visible symptom (a length error in division) to the root cause (rank-infinity application of primitives in a 5×3 sensor data context) and to the fix (rank-1 application via "1). The log entry “added "1 rank adverb to sum and max operations, 4h” is correct as a time record; it is incomplete as a value record. HourTab’s categorized format — rank mismatch diagnosed, leading-axis model traced, downstream shape error connected to root cause, both fixes verified — gives clients a record of what was actually investigated and why four hours was a reasonable duration for a six-character change across two lines.
Track J developer retainer hours without the status emails
HourTab gives J 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 array model audit log — rank adverb diagnosis, leading-axis shape tracing, tacit train expansion, box/unbox operation structuring — becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: J developer retainers
What does a J developer on retainer typically do?
A J developer on monthly retainer covers J’s leading-axis array model (all primitives apply along the leading axis by default; rank adverbs extend this to any axis; +/ on a matrix sums across rows producing column sums; +/ "1 on a matrix applies +/ to each rank-1 cell producing row sums), rank adverbs ("0 applies to each scalar; "1 applies to each vector/row; "2 applies to each matrix plane; "_ applies to the entire array; rank is how J generalizes operations from 1D to N-D), tacit trains (forks: (f g h) y = (f y) g (h y); hooks: (f g) y = y f (g y); noun-in-fork: (N f g) y = N f (g y); trains enable point-free function composition), shape and reshape operators ($ returns shape; m n $ x reshapes; # returns tally/first-dimension; i. generates integer sequences and integer matrices), boxed arrays (< boxes a value into an atom; > unboxes; boxed arrays are heterogeneous containers; operations do not pass through box boundaries without explicit unboxing), composition operators (@: for rank-agnostic composition; @ for rank-aware composition; & for bonding/partial application; &. for under/inverse), and common diagnostics including wrong-rank errors, domain errors on non-numeric inputs, and shape-mismatch errors from dyadic operations.
What J work is most commonly underlogged in a retainer?
Leading-axis model diagnosis (determining why +/ produced 3 elements instead of 5 on a 5×3 matrix; tracing the difference between rank-infinity application and rank-1 application; explaining why J’s model is correct for N-dimensional arrays while row-major intuition fails above 2D; 4–7 hrs invisible); rank adverb restructuring (adding "1, "0, or "2 to correct operations; identifying which verb in a tacit train has wrong rank; propagating rank corrections through composed trains; 3–6 hrs invisible); tacit train debugging (identifying whether a three-verb sequence is a fork or a hook; determining whether a noun in fork position is changing fork semantics; tracing (f g h) y = (f y) g (h y) vs (N f g) y = N f (g y) to find wrong-value results; 4–8 hrs invisible); composition operator selection (@: vs @; distinguishing rank-agnostic from rank-aware composition; determining whether the rank of f or g controls cell selection; 3–5 hrs invisible); box/unbox operation structuring (deciding which data should be boxed; applying > to unbox before arithmetic; using each adverb with boxed arrays; 3–6 hrs invisible).
What are typical J developer retainer rates?
Entry-level J developers (1–2 years, basic array operations, rank adverbs, scalar arithmetic) bill at $65–$120/hr. Mid-level J programmers (2–4 years, tacit train construction, rank-aware composition, boxed array operations, leading-axis model debugging) bill at $100–$180/hr. Senior J array language developers (4+ years, advanced tacit library design, performance optimization with rank-aware primitives, J/C integration, sparse matrix operations, deep leading-axis model work for N-dimensional array systems) bill at $150–$265/hr. Monthly retainer ranges: $2,000–$3,800/mo advisory (15–25 hrs), $4,500–$12,000/mo for full J array systems engineering.
What should a J developer retainer agreement include?
A J developer retainer agreement should specify: array model scope (which operations apply at rank infinity by default; which require explicit rank adverbs; which axes operations should apply along; whether the engagement covers 2D matrix work, 3D array work, or N-dimensional general arrays); tacit train scope (which functions are defined tacitly; whether forks, hooks, or both are used; whether noun-in-fork position is used; which trains involve @: vs @ composition; whether &. under-inverse operations are in scope); rank adverb scope (which verbs need "0, "1, "2, or "_ rank qualifiers; whether dyadic rank is used; whether rank errors vs shape errors need to be distinguished); boxed array scope (which data structures use boxed arrays; which operations need explicit < boxing or > unboxing; whether each adverb is used for mapped operations over boxed arrays); and hour logging format (rank: verb name, expected rank, actual rank applied, fix applied; shape: expected shape, actual shape, which axis was wrong; train: train type (fork/hook), verb positions, trace of (f g h) y expansion, where result diverged from intent; composition: which @ variant was used, whether rank was the issue, fix applied).
How should J developer retainer hours be logged?
Log each J retainer session with: rank category (verb: which primitive or derived verb; rank applied: rank infinity vs "0 vs "1 vs "2; expected result shape: what shape the programmer expected; actual result shape: what J produced; fix: which rank adverb was added or changed; before/after wrong-shape rate per operation); shape category (operation: which dyadic or monadic verb; left shape and right shape for dyadic; expected output shape; actual output shape; whether shape mismatch was a rank error, an agreement error, or a leading-axis model misunderstanding; fix applied); train category (train type: fork (f g h) vs hook (f g) vs noun-fork (N f g); y value: what argument was passed; expected result: what (f y) g (h y) or N f (g y) should produce; actual result: what J computed; which position in the train produced the wrong value; fix applied); composition category (composition operator: @: vs @ vs &; left verb rank and right verb rank; whether @ was needed instead of @: to control cell selection by rank; whether & was used for partial application vs bond; fix applied); and before/after wrong-shape or wrong-value rate per function for each fixed definition.