Functional images (Henderson, Pan, Elliott) — the seam removed rather than designed
Category: the theoretical extreme. Last reviewed: August 23, 2026.
Two papers, forty years apart, that answer the survey's question by refusing its premise: a picture is a function, so there is no backend contract to state, no command to shape, and no unit to argue about. Henderson's Functional Geometry makes a picture a function of the box it is drawn into; Elliott's Functional Images makes an image a function of a point. Read together they mark the limit the rest of the survey is measured against — and they localise, precisely, which of Q1–Q8 are dissolved and which are merely deferred to an unwritten renderer.
| Field | Value |
|---|---|
| Language | Pure functional pseudocode (Henderson); Haskell as a DSEL (Elliott's Pan) |
| License | Academic papers; the Pan compiler was distributed as a free download — no license statement found |
| Repository | None. Pan was distributed from the Pan home page; the chapter notes it "requires having the Microsoft C++ compiler" |
| Documentation | Functional Geometry (2002 revision); Functional Images chapter page |
| Pinned revision | Publications, not commits: Henderson 1982 (10.1145/800068.802148) and its 2002 revision (10.1023/A:1022986521797, PDF funcgeo2.tex; 16/10/2002; 13:53); Elliott, The Fun of Programming ch. 7, 2003 |
| Target range | None — the papers name no device. Henderson mentions "a printer or a screen"; Pan compiled to C |
| Backends shipped | Henderson: "the implementation used to create the pictures in this paper", unnamed, drawing beziers. Pan: one compiler backend emitting C |
NOTE
Everything below is cited to the two PDFs and to conal.net. dl.acm.org refuses the link checker (HTTP 403), so the 1982 paper is cited through a verified Wayback snapshot per the research-doc guidelines.
Overview
What it solves
Henderson's target is the gap between what a picture is and the sequence of commands a device wants — which is the survey's gap, named in 1982:
A picture is an example of a complex object that can be described in terms of its parts. Yet a picture needs to be rendered on a printer or a screen by a device that expects to be given a sequence of commands. Programming that sequence of commands directly is much harder than having an application generate the commands automatically from the simpler, denotational description. (
funcgeo2.pdf, §1)
Elliott's target is narrower and more radical — not a fixed set of primitives at all.
There have been many libraries for generating images using functional programming, each based on its own fixed set of geometric primitives and combinators. In contrast, this chapter addresses the general notion of images, based on a very simple model: functions over continuous 2D space. (chapter abstract)
Design philosophy
Henderson: abstract away size and position, and let the renderer recover them.
The key conceptual idea behind Functional Geometry is that we have abstracted completely from size and absolute location. Each picture is described in terms of subpictures and their relative locations. The implementation, on rendering the picture, must work out from this description, the exact size and the exact location of each part of the picture. (
funcgeo2.pdf, §5)
Elliott: shift the paradigm "from doing to being" (Functional Image Synthesis abstract), where an image's essential nature is not "muddled with details of how to display it on a computer".
How it works
Henderson: a picture is a function of its locating box
The seam's defining declaration is one sentence of prose plus five equations.
Let us define a picture as a function which takes three arguments, each being two-space vectors and returns a set of graphical objects to be rendered on the output device. … The picture
p(a,b,c)will be drawn in a box bounded bybandc, with its bottom left-hand corner at positiona. (funcgeo2.pdf, §5)
Picture = (Vector, Vector, Vector) -> Set GraphicalObject
blank (a, b, c) = {}
over (p, q) (a, b, c) = p(a, b, c) ∪ q(a, b, c)
beside(p, q) (a, b, c) = p(a, b/2, c) ∪ q(a + b/2, b/2, c)
above (p, q) (a, b, c) = p(a, b, c/2) ∪ q(a + c/2, b, c/2)
rot (p) (a, b, c) = p(a + b, c, b)
flip (p) (a, b, c) = p(a + b, b, c)a is the origin corner, b and c the two edge vectors — a bounding parallelogram, not a rectangle, which is why rot45 can shear the frame and still be a picture. The vocabulary the paper builds on is above, beside, over, rot, flip and the Escher-specific rot45; the composite operators (quartet, cycle, nonet) are defined from those.
Two properties fall out that no other subject here has. First, the combinators obey laws, and the paper treats law quality as a design metric:
rot(rot(rot(rot(p)))) = p
rot(above(p, q)) = beside(rot(p), rot(q))
rot(beside(p, q)) = above(rot(q), rot(p))
flip(beside(p, q)) = beside(flip(q), flip(p))It seems there is a positive correlation between the simplicity of the rules and the quality of the algebra as a description tool. (
funcgeo2.pdf, §6)
Second, the paper applies that metric to its own worst operation and convicts it — rot45 rotates about the top-left corner and shrinks by √2, so rot45(rot45(p)) ≠ rot(p):
This analysis suggests that
rot45is not the kind of operation we would include in a general algebraic language for geometry. Rather, we would define more fundamental operations for scaling, rotation and translation and allow the user to definerot45for this specific application. (funcgeo2.pdf, §6)
Elliott: an image is a function of a point
type Point = (Float, Float)
type Image α = Point -> α -- 'Image = Point -> Colour' specialised
type Region = Image Bool -- characteristic function of a point set
type ImageC = Image Colour
type Colour = (Frac, Frac, Frac, Frac) -- BGRA, premultiplied
Pan's model of images is simply functions from infinite, continuous 2D space to colours with partial opacity. (Although the domain space is infinite, some images are transparent everywhere outside of a bounding region.) (fop-conal.pdf, §7.2)
Every operation is either pointwise lifting or domain composition:
lift2 h f1 f2 p = h (f1 p) (f2 p)
over = lift2 overC -- layering
cond = lift3 (\a b c -> if a then b else c)
crop reg im = cond reg im empty -- clipping is a Boolean image
type Transform = Point -> Point
type Filter α = Image α -> Image α
translate (dx, dy) im = im . translateP (-dx, -dy)
swirl r im = im . swirlP (-r)Clipping is not a stack operation but cond against a Region; layering is not a painter's-algorithm ordering but overC under lift2; a spatial transform is not a matrix but the inverse point mapping pre-composed with the image — "we simply construct the transforms in inverted form" (fop-conal.pdf, §7.5). Sampled data enters the same way: a bitmap becomes a subscripting function, reconstructed by bilinear interpolation into an image of infinite extent.
data Array2 α = Array2 Int Int ((Int, Int) -> α)
reconstruct (Array2 w h sub) =
move (-fromInt w / 2, -fromInt h / 2)
(crop (inBounds w h) (bilerpArray2 sub))Rather than creating and storing an actual array of colours, each represented as a quadruple of floating point numbers, conversion from the file representation … is done on-the-fly during 'subscripting'. The details depend on the particular format. This flexibility is exactly why we chose to use subscripting functions rather than a more concrete representation. (
fop-conal.pdf, §7.10)
Q1 — measurement units, and who answers
Dissolved for geometry; unanswered for text — and the second half is the finding.
Henderson has no measurement because a picture has no size to measure: size is an argument (b, c) supplied at render time, and the same picture value drawn into a different parallelogram is a different set of graphical objects. Pan has no measurement because nothing has extent at all — an image is total over infinite continuous space, and the paper's figures are captioned with the window chosen by the author ("Each figure shows an origin-centred finite window onto an infinite image and is annotated with the width of the window in logical coordinates", fop-conal.pdf, footnote 3).
Neither renders text. Henderson's basic picture is "about 30 bezier curves" (funcgeo2.pdf, §5); Pan's primitives are arithmetic on coordinates. Elliott names text as something a model layer presents "via outline fonts" (§7.1) and then leaves that layer out of scope. So the model is silent on shaping, kerning, fallback and grapheme advance — the entire content of friction §1.
IMPORTANT
This bounds how far F1 in comparison.md generalises. F1 says measurement does not belong on the painter. These two subjects appear to say something stronger — that measurement need not exist — but they only get there by not rendering text. They are evidence for F1's placement claim (measurement is not a painter operation) and no evidence at all for eliminating it.
Q2 — capability declaration
Dissolved, and expensively. The contract is a type, stated once: Picture = (Vector,Vector,Vector) -> Set GraphicalObject and Image α = Point -> α. There are no optional methods to probe, no __traits(compiles) at a call site, no PaintEngineFeature bitmask — a total function either typechecks or does not.
The price is that there is nothing to declare and nothing to degrade. Qt's emulation floor and Notcurses' NCVISUAL_OPTION_NODEGRADE (F5) have no counterpart here, because a device that cannot draw what the function denotes has no way to say so: the function has already produced its answer and the renderer is downstream of it. Henderson has exactly one concession, and it is a global cutoff rather than a capability:
So the implementation will need to implement some rule such as
p(a,b,c) = {}, if|b+c| < ε, whereεis some dimension considered too small to draw. (funcgeo2.pdf, §5)
Q3 — semantic operations, and where they live
Both are primitive at the seam and semantic above it — the split sparkles:ui does not make. Henderson's renderer receives a set of line segments and beziers and knows nothing of fish, quartets or Square Limit; the semantics live entirely in the algebra, where they are named, composable and law-checked. Pan's renderer (such as it is) receives a colour per point; Region, annulus, wedgeAnnulus and crop are all definitions above it.
This is the opposite of Slint's bet (F4), and it works only because nothing degrades: the model has no target that could fail to render an annulus. As soon as a target exists that must approximate, the semantics have to survive to the point of approximation — which is exactly why scrollbar is in our drawing seam, and why the scrollbarThumb formula its lowerings are computed through is one shared definition rather than one per backend. Functional images do not refute friction §3; they show that friction §3 is caused by having heterogeneous targets, not by careless layering.
Henderson's rot45 verdict is nevertheless the transferable part: an operator whose laws are ugly is telling you it does not belong in the vocabulary.RuleEdge and the Scrollbar payload's fourteen fields are our rot45 — application-specific operations sitting in a general algebra, two of them (trackGlyph, thumbGlyph) a cell backend's answer riding past every backend that never reads it.
Q4 — command shape
Dissolved, at the cost of the property we most depend on. There is no command value. Henderson composes pictures by function composition; over(p,q)(a,b,c) = p(a,b,c) ∪ q(a,b,c) is set union of the results, not a concatenation of two op streams. Pan composes by lift2 and (.).
So a functional image cannot be recorded, culled, replayed or compared — RecordingCanvas and the op-stream parity harness have no analogue, because the only way to observe a function is to apply it. DrawOp is the other extreme: a closed sum over eight payloads inside a <= 64-byte budget, dispatched by match!, and comparable as a value — which is what lets a recorded stream serve as the parity oracle instead of a golden image (F12). Henderson's implementation note makes the consequence of giving that up explicit: rather than materialise the denoted set,
we want to draw each basic graphical object … as we construct it and rely upon the fact that it doesn't matter if we draw the same object twice because the rendering engine will take care of that. (
funcgeo2.pdf, §5)
Idempotent overdraw substitutes for a comparable stream. That is a defensible trade for a plotter and an indefensible one for a golden test, which strengthens F3: reification is not an accident of our design, it is the price of observability, and the subjects that dispense with it dispense with observability too.
Q5 — sub-unit placement
Deferred rather than dissolved, and it names the replacement for RuleEdge. Both domains are continuous; Pan's is continuous and infinite, so there is no smallest addressable unit to fall below. What the pair actually demonstrates is F6 from the extreme end: continuity relocates the sub-unit question onto the renderer rather than removing it. Henderson has to write the rule back in as a render-time cutoff, and Pan hands it to whoever chooses the window.
The transferable detail is Henderson's ε. Device resolution enters the system once, as a scalar supplied at render time, and its only effect is to prune: below ε a picture denotes nothing. It is not a coordinate, not an enumerated position, and not a per-operation flag. That is the same shape as Notcurses' blitter ladder — name a fidelity, let the renderer own it — reached independently in 1982, and it is the 1982 form of the named fidelity F6 asks for alongside a queried device unit.
Q6 — resolved or semantic styling
Split, and instructively. Pan carries resolved appearance only: Colour is premultiplied BGRA and overC is Porter–Duff over computed on those numbers. Nothing in an image says why it is that colour; a re-resolving backend (our HTML class names) has nothing to re-resolve.
Henderson carries neither. A picture denotes a set of graphical objects with no appearance attached, and the paper deliberately sidesteps the one place appearance would force ordering:
By avoiding the use of fills, it is not necessary to be concerned with the order in which the graphical objects are rendered. (
funcgeo2.pdf, §5)
So the two hedges available at friction §6 — resolved, semantic, or both — are here reduced to "resolved" and "absent". sparkles:ui takes the third option: an operation carries the numbers its own primitive will paint with and, on six of the eight payloads, the Slot that says what those numbers meant, while a whole Visual is reconstructed through visualOf at the few call sites that want one rather than stored on every operation. That makes the hedge cheap without making it a decision, and F9 records that no surveyed subject makes it either. Neither model here pays for both, and Henderson shows that dropping appearance from the vocabulary buys order-independence, which is a real property and not merely an omission.
Q7 — payload ownership
Dissolved by purity. A payload is a function: immutable, freely shareable, with no frame lifetime and no thread affinity. DrawOp.text (friction §7) has no counterpart, because there is nothing to borrow from: our bytes are copied into a frame arena by CmdBuffer.textRun, and the operation carries a const(char)[] into that arena, valid while the buffer that built it is alive and unreset.
The sharp version is Pan's Array2 Int Int ((Int, Int) -> α): even an imported photograph is stored as its subscripting function, with format conversion performed during subscripting rather than at load. This is the limit case of F8 — no subject borrows a payload across a frame; each of them copies, refcounts or arena-allocates. Here the payload's owner is the closure, and the question of who frees it is the host language's. We arena-allocate, which puts us inside that consensus and leaves the retain boundary, not the copy, as the open question (UI-O4).
Q8 — extent query
The clearest result in this doc, and the two subjects disagree.
Henderson: extent is pushed down, never queried up. p(a,b,c) receives its box; a picture has no extent to be asked for, and every combinator's job is to subdivide the box it was given (beside halves b, above halves c). This is the limit case of F7: extent is three questions — surface, layout and ink — and Henderson collapses all three by making the surface's box an input, so nothing is maintained at construction and nothing is derived by scan. The surface picks the parallelogram and the scene is a function of it.
Pan: extent is undefined in principle. Images are total over infinite space; a window is a viewing decision made outside the model. Asking a Pan image how big it is is a type error, not a missing method.
Together they say friction §8 is the wrong request. Our build runs the other way round: the display list is assembled with no box in hand, and afterwards neither the buffer that built it, nor the list itself, nor the arena behind it can say what box would have contained it — CmdBuffer answers only how many operations it holds and what a single run measures. So skia-canvas-render.d recovers the box the one way left to it, by folding every operation's rect — F7's derived-by-scan arm, and a symptom of building the display list before knowing the box. Henderson's answer is to make the box an input to the build.
Strengths
- The contract is a type, stated once — no probing, no undeclared surface, no divergence between the concept and the real contract (Q2).
- Algebraic laws are a testing oracle.
rot(above(p,q)) = beside(rot p, rot q)is a property test that needs no golden image, and law simplicity doubles as the admission test for new vocabulary. - Extent flows downward, so no participant ever needs to ask (Q8), and resolution independence is free — device fidelity is one render-time scalar (Q5).
- Payloads are values, so ownership, threading and lifetime questions do not arise (Q7).
Weaknesses
- No text. Neither paper has a glyph, a font, a shaper or an advance. For a toolkit whose display list is 90%
textRun, the model is silent on the dominant case (Q1). - No device, and therefore no degradation. Nothing can report that it cannot draw a hairline;
εprunes but does not negotiate (Q2). - No observable command stream — nothing to record, diff, cull or replay (Q4). Henderson explicitly relies on idempotent overdraw instead.
- Sampling cost is unbounded, which is why
Panneeded a whole optimising compiler ("Panis implemented as a compiler … produces C code, which is then given to an optimising compiler",fop-conal.pdf, §7.1). A seam whose performance requires partial evaluation of the user's program is not a seam a library can ship. - No retained identity. Hit-testing, focus, incremental repaint and accessibility all need to know which widget a pixel belongs to; a
Point -> Colourhas thrown that away by construction.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Picture takes its locating box as an argument | Abstracts "completely from size and absolute location"; one value serves every output size | Nothing can be built before the box is known; no scene exists to inspect, cache or diff |
| Composition is function composition, not a command list | Laws hold, so descriptions can be transformed and reasoned about | No reified stream: no recording, culling, replay, or op-stream parity testing |
| Image is total over infinite continuous space | Clipping, layering and masking all become pointwise combinators (cond, overC, lift2) | Extent is undefined; every consumer must supply a window, and sampling has no natural stopping rule |
| Transforms stored pre-inverted | Avoids inverting arbitrary, possibly many-to-one point mappings | The vocabulary reads backwards (uscaleP 2 shrinks); users need the Filter wrappers to stay sane |
Resolution enters as one render-time ε | Device fidelity is the renderer's business, not the description's | A single global cutoff cannot express per-feature fidelity (a hairline vs. a glyph want different answers) |
| No appearance in Henderson's algebra | Order-independence: overdraw is idempotent, so render order is unconstrained | Anything needing fills, blending or colour falls outside the algebra entirely |
Bearing on the proposal
- Pass the box down; delete the extent scan (friction §8, F7). Henderson's
p(a,b,c)answers all three of F7's extent questions from the surface, by construction.buildDisplayListshould take the target rect the way a picture takes its parallelogram, so the offscreen consumer that makesskia-canvas-render.dfold every operation's rect never needs to. This is the single directly copyable idea. ε, not more enumerators (friction §5, F6). A render-time fidelity scalar is the 1982 form of Notcurses' blitter ladder. It corroborates F6's recommendation to replaceRuleEdgewith a named fidelity rather than a seventh compass point — and it comes from a subject with no cells at all, which is what makes the convergence meaningful. Continuity on its own would not have got there; F6's whole point is that it relocates the question, and Henderson is the subject that says where it lands.- Adopt law-checking above the seam, not the model below it.
sparkles:uicannot be a picture algebra, but its layout combinators can have laws worth testing (above/besideassociativity, rotation identities on box-flow). Henderson'srot45verdict gives the admission test for new vocabulary: if an operator has no simple law, it is application-specific and belongs in the application. TheScrollbarpayload's fourteen fields fail that test. - Do not read F1 as licence to delete measurement. These subjects have no
measurebecause they have no text. F1's evidence is about placement; this doc adds no support for eliminating a metrics service, andRND6's monospace cell advance is precisely the property aPoint -> Colourmodel cannot carry. - Reification is the price of observability, and we are right to pay it (F3). The strongest argument against the functional-image seam is that Henderson has to fall back on idempotent overdraw because there is no stream to compare.
RecordingCanvasexists for exactly the property this model gives up, and F12 states the condition attached to it: the stream pays while the value stays inspectable. - Why
sparkles:uicannot take this route, stated plainly. Three blockers, each independent: (a) the toolkit's unit is a Unicode grapheme cell whose advance is a property of the text (RND6), and a function from continuous points has nowhere to put shaping; (b) the widget model is retained and identity-bearing — focus, hit-testing and incremental repaint all ask "which widget", whichPoint -> αerases; (c) the terminal target is a discrete grid of cells that a renderer cannot sample its way onto without deciding what a half-covered cell is, which is the very question the seam exists to answer.
NOTE
Where this contradicts the synthesis. Nothing here overturns F1–F12, but two are narrowed. F1 is narrowed to a placement claim (Q1, above). F4 already lists nobody among the six places a lowering can live; these two subjects are what that position looks like when it is the only one on offer — a seam with a single target class — and are therefore evidence that our semantic operations are forced by our target spread rather than chosen.
Sources
- Peter Henderson, Functional Geometry, ACM Symposium on LISP and Functional Programming, 1982 —
10.1145/800068.802148(ACM DL refuses automated requests; the link is a verified Wayback snapshot). - Peter Henderson, Functional Geometry (revised), Higher-Order and Symbolic Computation 15(4), 2002 —
10.1023/A:1022986521797; author PDFfuncgeo2.pdf(internal timestampfuncgeo2.tex; 16/10/2002; 13:53). - Conal Elliott, Functional Images, in Gibbons & de Moor (eds.), The Fun of Programming, Palgrave, 2003, ch. 7 — chapter page, PDF
fop-conal.pdf. - Conal Elliott, Functional Image Synthesis, Bridges 2001 — abstract.
- The Pan home page — the compiler and gallery.