Circuits is an experiment in bundling very simple functions. Three constructors — Lift a plain function, Compose two circuits, Knot a feedback loop — and the whole thing stays open. You can rearrange, measure, annotate, or run it backwards before it ever executes. Version 0.2 adds Net, the piece we were missing for contravariant tracking: a value that flows backwards through the computation, letting us run circuits in reverse.
This post is a visual audit of what happens when you compile those bundled functions through MicroHs and stare at the shapes.
The contravariant channel
Net is the fourth structural row of Circuit.Net, completing the bimonoid:
| function | role | nodes | iota symbols | pure? |
|---|---|---|---|---|
copy |
Comonoid.copy — fan-out | 5 | 269 | yes |
discard |
Comonoid.discard — delete | 3 | 12 | yes |
add |
Monoid.plus — fan-in | 39 | 835 | yes |
zero |
Monoid.zero — create | 3 | — | no |
These four operations form a bimonoid: they satisfy the laws of a commutative monoid and comonoid, interacting through the usual bialgebra axioms. The dagger — transpose — swaps fan-in and fan-out. copy becomes add, discard becomes zero. This is the one-line reveal of circuits-ad: backpropagation is just the dagger of forward evaluation, and the contravariant channel is what makes the swap structural rather than a hand-rolled reversal.
Here’s what these look like after MicroHs compiles them to combinators, then through the iota reducer into a single-combinator encoding:
copy — fan-out
S P — constructs a pair. Trivial. The pullback of plus under differentiation.
copy — iota mandala
5 combinator nodes expand to 269 iota symbols. The simplest non-trivial tree.
plus — fan-in
Peano addition, recursive via Y fixpoint. 7.8× larger than copy. The pullback of copy under differentiation.
plus — iota mandala
The cost of recursion made visible. Under the dagger, this swaps with copy.
The iota mandalas are radial tree diagrams. The root sits at center, edges encode application (0) or the iota combinator (1), and depth is colour-coded. Every pure function collapses to a single fingerprint — a measurable structural checksum. Change the algorithm, and the mandala shifts.
Fan-in is structurally expensive. Fan-out is trivial. This is as fundamental as it gets: addition requires recursion; copying is just pairing. The dagger tells you that reverse-mode differentiation is structurally the same cost — the expensive forward pass becomes the cheap backward one, and vice versa.
Sharing and the tensor
Circuits’ Knot with the (,) tensor ties a lazy self-referential binding. This is Haskell’s let knot, exposed as a first-class constructor:
let y = add x x in (y, y)
Computes add once and reuses the result. Without sharing, add is inlined twice:
| function | nodes | iota symbols | saving |
|---|---|---|---|
noShare — add computed twice |
91 | 2,073 | — |
share — add computed once |
51 | 1,155 | 44% |
noShare — add inlined twice
Two copies of the add recursion, each 39 nodes.
noShare — iota mandala
Duplication cost at the combinator level — two recursive sub-trees.
share — add computed once
One add call, result shared via let. 44% smaller.
share — iota mandala
The (,) tensor trace achieves the same effect: one binding, shared across the circuit body.
The lazy knot is 44% cheaper at the combinator level. This is the structural analogue of common subexpression elimination, but visible in the type system — Knot with the (,) tensor is the sharing mechanism. No compiler pass, no optimization flag. It’s a constructor.
Iteration and the Either channel
The Either tensor gives you iteration. Left continues, Right exits. A counting loop that tests with Peano le:
| function | nodes | iota symbols | notes |
|---|---|---|---|
le — less-than-or-equal |
39 | 1,129 | invariant to N |
step — count to 10 |
83 | — | has L box |
step — count to 11 |
85 | — | +2 per S |
le — less-than-or-equal
Peano comparison. Recursive with Y. Same tree for comparing against 10 or 10,000.
le — iota mandala
The pure iota encoding of le. Every edge is 0 (apply) or 1 (ι).
Root at center, depth coded by edge colour. This is the computation with everything
stripped away: no data types, no pattern matching, no control flow. Just application
and one combinator.
Algorithmic complexity is invariant to input size. le is 39 nodes whether the threshold is 10 or 10,000. Only the Peano literal grows — linearly, 2 nodes per S. The comparison structure is fixed.
The Circuit GADT overhead
The GADT itself — Lift, Compose, Knot — compiles to opaque boxes around the computation:
Count.step — the loop body
The step function. Nearly pure — only the L (Either's Left injection) blocks full iota encoding.
Knot (Lift step) — the full circuit
4 extra nodes for the Circuit GADT wrapper. Knot and Lift are scaffolding.
reify adds dictionary passing — about 6 more nodes. encode via Hyper is tighter but opaque to the combinator dumper. The GADT overhead is 2–4 nodes: negligible for the structural inspection it enables.
What this shows
-
Every pure function has an iota fingerprint — a measurable structural checksum. Change the algorithm and the mandala shifts. This is a visual hash of what your program is, not what it claims to be.
-
Fan-in is structurally expensive; fan-out is trivial. Addition costs 7.8× what copying costs. Under the dagger, they swap — the circuits-ad reveal, visible at the combinator level.
-
Sharing has measurable structural cost. 44% fewer nodes for
letvs inline duplication. Circuits’(,)tensor trace achieves this via lazy knot-tying, embedded as a constructor. -
The Circuit GADT is 2–4 nodes of overhead.
reifyadds dictionary passing.encodevia Hyper avoids it but hides the structure. You pay for inspection, not much. -
Algorithmic complexity is invariant to input size.
leis the same 39 nodes at any threshold. Only literals grow, linearly.
Generated by circuits-iota · MicroHs (augustss) · iota-render (Tritlo) · circuits (tonyday567) · June 2026