One Wavelength at a Time: Pipelines That Filter Light
George Ellery Hale’s spectroheliograph, built in the 1890s, photographs the sun in one spectral line — usually H-alpha at 656 nm or the calcium K line — and nothing else. The trick is mechanical. A first slit lets a thin strip of sunlight into a prism. The prism spreads that strip into a spectrum. A second slit, downstream, sits over exactly one wavelength and blocks the rest. Then the whole assembly drifts across the solar disc, and a photographic plate drifts in lockstep behind the second slit. Strip by strip, you accumulate an image of the sun as it looks in that one narrow band — prominences, filaments, the churning chromosphere, all invisible in ordinary white light.
Describe that as a data flow and it’s a pipeline: a source emits strips, a filter stage keeps only the strips at your chosen wavelength, and an assembler stitches the survivors into a picture. Each stage does one job and hands off. That’s the shape I reached for.
In JavaScript, async generators make the three stages read almost like the optical bench — scan is the drifting sun, slit is the second slit, and the loop at the bottom is the plate:
async function* scan() { // the sun drifts across the slit
for (let n = 1; n <= 6; n++) yield { line: n, nm: n % 3 ? 656 : 486 };
}
async function* slit(src, keep) { // pass only one wavelength through
for await (const s of src) if (s.nm === keep) yield s.line;
}
(async () => {
const img = [];
for await (const line of slit(scan(), 656)) img.push(line);
console.log("image:", img); // image: [ 1, 2, 4, 5 ]
})();
Erlang makes the stages real — three separate processes wired together with ! and receive, which is closer to the truth, since the actual instrument runs its stages in parallel and synchronized:
run() ->
A = spawn(?MODULE, assemble, [[]]),
F = spawn(?MODULE, slit, [A, 656]),
[F ! {line, N, case N rem 3 of 0 -> 486; _ -> 656 end} || N <- lists:seq(1,6)],
F ! done.
slit(Next, Keep) ->
receive
{line, N, Keep} -> Next ! {keep, N}, slit(Next, Keep);
{line, _, _} -> slit(Next, Keep);
done -> Next ! done
end.
assemble(Acc) ->
receive
{keep, N} -> assemble([N | Acc]);
done -> io:format("image: ~p~n", [lists:reverse(Acc)])
end.
Both print [1, 2, 4, 5] — the four strips that came through at 656 nm, with the 486 nm ones discarded on the way. Erlang guarantees that messages between two processes arrive in the order they were sent, so the strips assemble in scan order and done lands last, which is the whole reason the picture doesn’t come out scrambled. The {line, N, Keep} clause pattern-matches against the bound value of Keep, so the slit only fires for the wavelength you tuned it to — a band-pass filter written as a function head.
Hale had none of this, obviously. He had a driving clock, a lot of patience, and the insight that you could trade a whole-image exposure for a sequence of strips and reassemble the sun afterward. The reassembly happened in silver halide instead of a list accumulator, but the pipeline was already there in the brass.