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 combinator tree

copy — fan-out

5 nodes · top-down · pure

S P — constructs a pair. Trivial. The pullback of plus under differentiation.

copy iota mandala

copy — iota mandala

269 symbols · radial · pure

5 combinator nodes expand to 269 iota symbols. The simplest non-trivial tree.

add combinator tree

plus — fan-in

39 nodes · top-down · pure

Peano addition, recursive via Y fixpoint. 7.8× larger than copy. The pullback of copy under differentiation.

add iota mandala

plus — iota mandala

835 symbols · radial · pure

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 combinator tree

noShare — add inlined twice

91 nodes · top-down

Two copies of the add recursion, each 39 nodes.

noShare iota mandala

noShare — iota mandala

2,073 symbols · radial

Duplication cost at the combinator level — two recursive sub-trees.

share combinator tree

share — add computed once

51 nodes · top-down

One add call, result shared via let. 44% smaller.

share iota mandala

share — iota mandala

1,155 symbols · radial

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 combinator tree

le — less-than-or-equal

39 nodes · top-down · pure

Peano comparison. Recursive with Y. Same tree for comparing against 10 or 10,000.

le iota mandala

le — iota mandala

1,129 symbols · radial · pure

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 combinator tree

Count.step — the loop body

83 nodes · one L box

The step function. Nearly pure — only the L (Either's Left injection) blocks full iota encoding.

Count.counter combinator tree

Knot (Lift step) — the full circuit

87 nodes · Knot + Lift boxes

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

  1. 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.

  2. 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.

  3. Sharing has measurable structural cost. 44% fewer nodes for let vs inline duplication. Circuits’ (,) tensor trace achieves this via lazy knot-tying, embedded as a constructor.

  4. The Circuit GADT is 2–4 nodes of overhead. reify adds dictionary passing. encode via Hyper avoids it but hides the structure. You pay for inspection, not much.

  5. Algorithmic complexity is invariant to input size. le is the same 39 nodes at any threshold. Only literals grow, linearly.


Generated by circuits-iota · MicroHs (augustss) · iota-render (Tritlo) · circuits (tonyday567) · June 2026