In the previous SICP post, I argued that building a language is the right move when a problem domain has clear compositional structure. Apertures is what happens when you take that seriously for distributed coordination.
The problem: multiple parties need to share computation structure while controlling when and where their data enters. The server has optimization expertise. The client has private data. Neither wants to send what they have to the other.
The SICP answer: build a language where this is expressible. Add one primitive — a hole — to a Lisp, and you get pausable, resumable evaluation as a natural consequence.
Primitives
Every language needs atomic elements. Apertures has the usual Lisp primitives — numbers, strings, booleans, symbols — plus one addition:
?x ;; a hole named x
?client.x ;; a namespaced hole (who owns it)
A hole is an unknown value. It sits in an expression and says: someone will fill this in later, but not yet.
$ aperture eval "(+ 3 5)"
8
$ aperture eval "((lambda (x) (* x x)) 5)"
25
So far, standard Lisp. The interesting part is what happens when holes appear.
Combination That Tolerates Unknowns
In most languages, an unknown value is an error. You cannot add 3 to something that does not exist yet. Apertures handles this through partial evaluation: evaluate what you can, preserve what you cannot.
$ aperture partial template.apt # contains: (+ 3 ?x 5)
(+ 8 ?x)
The evaluator added 3 and 5, but left ?x alone. The result is still an expression — not a value, not an error. This is the key move. Partial evaluation is combination that tolerates unknowns.
It goes further. Algebraic rules apply even with holes present:
$ aperture partial zero.apt # contains: (* 0 ?anything)
0
$ aperture partial identity.apt # contains: (* 1 ?x)
?x
$ aperture partial branch.apt # contains: (if true ?x ?y)
?x
Zero times anything is zero, even if you don’t know what “anything” is. One times ?x is just ?x. If the predicate is known, the branch is eliminated even when the branches contain holes.
These simplifications are useful. They also leak information — if (* ?secret ?known) reduces to 0, an observer knows the secret is zero. More on that later.
Abstraction: Named Holes as Deferred Values
Lambda, let, and define work as you would expect:
$ aperture eval "(let ((x 10) (y 20)) (+ x y))"
30
But the interesting abstraction is the hole itself. A hole is a named deferral. It says what kind of value is missing and who should provide it:
(+ ?client.x ?server.y 10)
Client owns ?client.x. Server owns ?server.y. The names create a protocol — an implicit contract between parties about who fills what.
Filling holes is substitution:
$ aperture fill --hole client.x=5 template.apt
(+ 15 ?server.y)
Client filled their hole. The constants folded (5 + 10 = 15). Server’s hole remains. When server fills theirs, the expression becomes ground and evaluates to a final value.
The Closure Property
This is where apertures connects to the SICP thesis most directly.
The closure property (in the SICP sense, not the lambda sense) says: combining things should yield the same kind of thing. In a well-designed language, the result of combination is always something you can combine again.
Apertures has this property at every level:
- Partial evaluation of an expression yields an expression
- Filling holes in an expression yields an expression
- Evaluating a ground expression yields a value
The cycle composes:
# Start with an expression containing two holes
$ cat multi.apt
(+ ?a (* 2 ?b) 10)
# Partial eval: constants fold
$ aperture partial multi.apt
(+ 10 ?a (* 2 ?b))
# Fill one hole: more constants fold
$ aperture fill --hole a=5 multi.apt
(+ 15 (* 2 ?b))
# Fill the other: fully ground, evaluates to a value
$ aperture fill --hole a=5 --hole b=3 multi.apt
21
At every step, you have an expression. You can inspect it, optimize it, serialize it, send it over the network, and continue later. The closure property is what makes multi-party coordination possible — each party’s contribution produces something the next party can work with.
The Problem: Coordinated Computation
Now the payoff. The language exists to solve a specific coordination problem.
A server has CPU and optimization expertise. A client has private data. The client wants the server’s optimization without revealing their data. The server wants to help without seeing data or results.
With apertures, the client sends structure, not data:
;; Client sends this template to the server
(+ (* ?weight (score ?query)) ?bias)
The server partially evaluates — simplifies, rewrites, optimizes — without seeing ?weight, ?query, or ?bias. Then sends the optimized template back. The client fills the holes locally and evaluates locally. Data never leaves the client. Results never leave the client.
The demo/query/ directory in the repo implements this as an HTTP server and client:
# Server: accepts expressions, partially evaluates, returns optimized form
$ go run ./demo/query/server
# Client: sends template, fills holes locally, evaluates locally
$ go run ./demo/query/client '(+ 3 ?x 5)' x=10
Multiple parties can fill holes incrementally — Party A fills ?table and ?columns, Party B fills ?threshold and ?limit. Each party contributes their knowledge. The expression accumulates information until it is ground.
Honest Limitations
Apertures are a coordination mechanism, not a security mechanism. They leak information through program structure (the server sees the query shape), evaluation patterns (which simplifications apply reveals constraints on hole values), and algebraic relationships (zero-product tells you a factor is zero). Do not use apertures when the computation structure itself is sensitive, when parties are adversarial rather than honest-but-curious, or when regulatory compliance (GDPR, HIPAA) requires cryptographic guarantees.
The Point
The SICP insight is that when a problem has compositional structure, you should build a language for it. Distributed coordination has compositional structure: expressions compose, partial evaluation preserves composability, hole filling is incremental, and the closure property means every intermediate state is a valid expression.
Adding one primitive — a hole — to a Lisp gave us pausable, resumable evaluation. No special protocols. No cryptographic machinery. Just a language where unknowns are first-class and evaluation tolerates them.
The language made the coordination problem thinkable. And once it was thinkable, the solution was straightforward.
This post is part of a series on SICP, exploring how the ideas from Structure and Interpretation of Computer Programs appear in modern programming practice.
Resources: GitHub repo (Go implementation) | Workshop paper (PDF) | Language specification
Discussion