The demo below shows both styles side by side. A small pipeline computes a value through three steps: double, add ten, and square. In direct style each step returns to the caller. In CPS each step receives a continuation and calls it instead of returning.
<!-- Two panels: direct style (returns) vs CPS (continuations). -->
<div class="panel" id="direct-panel">
<div class="panel-title">Direct style — functions return</div>
<div class="pipeline" id="direct-pipeline"></div>
<div class="result" id="direct-result"></div>
</div>
<div class="panel" id="cps-panel">
<div class="panel-title">CPS — functions call their continuation</div>
<div class="pipeline" id="cps-pipeline"></div>
<div class="result" id="cps-result"></div>
</div>
<div class="controls">
<label for="input-val">Input:</label>
<input id="input-val" type="number" value="3" min="1" max="9" title="Pick a starting number (1–9)">
<button id="btn-run" type="button">Run</button>
<button id="btn-trace" type="button">Trace</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="log" id="log" aria-label="Execution trace log"></div>
/* Panel layout for the CPS demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.panel { background: #f3f6f9; border-radius: 10px; padding: .8rem 1rem; margin-bottom: .7rem; }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .08em; color: #5a7088; margin-bottom: .5rem; }
.pipeline { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; }
.step { display: flex; align-items: center; gap: .3rem; }
.box { padding: .3rem .65rem; border-radius: 7px; font: 600 .85rem ui-monospace, monospace;
background: #dde4ec; border: 1.5px solid #b8c5d0; color: #1d3557;
transition: background .25s, border-color .25s; }
.box.active { background: #1d3557; border-color: #1d3557; color: #fff; }
.box.done { background: #0a7d33; border-color: #0a7d33; color: #fff; }
.arrow { color: #8fa4b8; font-size: 1.1rem; user-select: none; }
.result { font: 600 .95rem ui-monospace, monospace; color: #0a7d33; margin-top: .5rem;
min-height: 1.3em; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
label { font-size: .9rem; font-weight: 600; }
input[type=number] { width: 3.5rem; padding: .35rem .5rem; border: 1.5px solid #b8c5d0;
border-radius: 7px; font: 600 1rem ui-monospace, monospace;
text-align: center; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1.5px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.log { background: #1a1e26; color: #a8d8a8; border-radius: 8px; padding: .6rem .9rem;
font: .82rem ui-monospace, monospace; min-height: 3rem; white-space: pre-wrap;
max-height: 8rem; overflow-y: auto; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pipeline: three steps — double, add 10, square.
const STEPS = [
{ id: 'double', label: '× 2', fn: x => x * 2 },
{ id: 'add10', label: '+ 10', fn: x => x + 10 },
{ id: 'square', label: '²', fn: x => x * x }
];
// Direct style: each step returns its result.
function runDirect(x) {
const a = STEPS[0].fn(x);
const b = STEPS[1].fn(a);
const c = STEPS[2].fn(b);
return c;
}
// CPS: each step calls a continuation instead of returning.
function doubleCPS(x, k) { k(x * 2); }
function add10CPS(x, k) { k(x + 10); }
function squareCPS(x, k) { k(x * x); }
// Trace mode logs each continuation call.
let traceMode = false;
let traceLog = [];
function runCPS(x, finalK) {
doubleCPS(x, a => {
add10CPS(a, b => {
squareCPS(b, c => {
finalK(c);
});
});
});
}
function traceCPS(x, finalK) {
doubleCPS(x, a => {
traceLog.push(i18nFmt('k1 called: double(%x%) → %a%', { x: x, a: a }));
add10CPS(a, b => {
traceLog.push(i18nFmt('k2 called: add10(%a%) → %b%', { a: a, b: b }));
squareCPS(b, c => {
traceLog.push(i18nFmt('k3 called: square(%b%) → %c%', { b: b, c: c }));
finalK(c);
});
});
});
}
// Build the visual pipeline boxes.
function buildPipeline(containerId, steps) {
const el = document.getElementById(containerId);
el.innerHTML = '';
steps.forEach((s, i) => {
const step = document.createElement('div');
step.className = 'step';
const box = document.createElement('div');
box.className = 'box';
box.id = containerId + '-' + s.id;
box.textContent = s.label;
step.appendChild(box);
if (i < steps.length - 1) {
const arr = document.createElement('span');
arr.className = 'arrow';
arr.textContent = '→';
step.appendChild(arr);
}
el.appendChild(step);
});
}
function setBox(containerId, id, cls) {
const el = document.getElementById(containerId + '-' + id);
if (el) el.className = 'box ' + cls;
}
function setResult(id, text) {
document.getElementById(id).textContent = text;
}
function appendLog(msg) {
const log = document.getElementById('log');
log.textContent += msg + '\n';
log.scrollTop = log.scrollHeight;
}
function clearLog() {
document.getElementById('log').textContent = '';
}
function resetBoxes() {
STEPS.forEach(s => {
setBox('direct-pipeline', s.id, '');
setBox('cps-pipeline', s.id, '');
});
setResult('direct-result', '');
setResult('cps-result', '');
}
// Animate direct-style pipeline boxes left to right.
function animateDirect(x, delay) {
let val = x;
STEPS.forEach((s, i) => {
setTimeout(() => {
setBox('direct-pipeline', s.id, 'active');
val = s.fn(val);
setTimeout(() => {
setBox('direct-pipeline', s.id, 'done');
if (i === STEPS.length - 1) {
setResult('direct-result', i18nFmt('Result: %v%', { v: val }));
}
}, delay * 0.6);
}, i * delay);
});
}
// Animate CPS pipeline boxes left to right.
function animateCPS(x, delay) {
const vals = [x];
STEPS.forEach(s => vals.push(s.fn(vals[vals.length - 1])));
STEPS.forEach((s, i) => {
setTimeout(() => {
setBox('cps-pipeline', s.id, 'active');
setTimeout(() => {
setBox('cps-pipeline', s.id, 'done');
if (i === STEPS.length - 1) {
setResult('cps-result', i18nFmt('Result: %v%', { v: vals[vals.length - 1] }));
}
}, delay * 0.6);
}, i * delay);
});
}
function getInput() {
const v = parseInt(document.getElementById('input-val').value, 10);
return isNaN(v) || v < 1 ? 3 : Math.min(v, 9);
}
document.getElementById('btn-run').addEventListener('click', () => {
traceMode = false;
clearLog();
resetBoxes();
const x = getInput();
const direct = runDirect(x);
animateDirect(x, 500);
animateCPS(x, 500);
appendLog(i18nFmt('input=%x% → direct=%r% (CPS gives the same answer)', { x: x, r: direct }));
});
document.getElementById('btn-trace').addEventListener('click', () => {
traceMode = true;
clearLog();
resetBoxes();
const x = getInput();
traceLog = [];
traceCPS(x, result => {
animateDirect(x, 400);
animateCPS(x, 400);
traceLog.forEach(line => appendLog(line));
appendLog(i18nFmt('Final continuation called with result: %r%', { r: result }));
});
});
document.getElementById('btn-reset').addEventListener('click', () => {
resetBoxes();
clearLog();
});
// Initialize both pipelines on load.
buildPipeline('direct-pipeline', STEPS);
buildPipeline('cps-pipeline', STEPS);
appendLog('Press Run or Trace to start.');
<!-- Two panels: direct style (returns) vs CPS (continuations). -->
<div class="panel" id="direct-panel">
<div class="panel-title">Direct style — functions return</div>
<div class="pipeline" id="direct-pipeline"></div>
<div class="result" id="direct-result"></div>
</div>
<div class="panel" id="cps-panel">
<div class="panel-title">CPS — functions call their continuation</div>
<div class="pipeline" id="cps-pipeline"></div>
<div class="result" id="cps-result"></div>
</div>
<div class="controls">
<label for="input-val">Input:</label>
<input id="input-val" type="number" value="3" min="1" max="9" title="Pick a starting number (1–9)">
<button id="btn-run" type="button">Run</button>
<button id="btn-trace" type="button">Trace</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="log" id="log" aria-label="Execution trace log"></div>
/* Panel layout for the CPS demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.panel { background: #f3f6f9; border-radius: 10px; padding: .8rem 1rem; margin-bottom: .7rem; }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .08em; color: #5a7088; margin-bottom: .5rem; }
.pipeline { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; }
.step { display: flex; align-items: center; gap: .3rem; }
.box { padding: .3rem .65rem; border-radius: 7px; font: 600 .85rem ui-monospace, monospace;
background: #dde4ec; border: 1.5px solid #b8c5d0; color: #1d3557;
transition: background .25s, border-color .25s; }
.box.active { background: #1d3557; border-color: #1d3557; color: #fff; }
.box.done { background: #0a7d33; border-color: #0a7d33; color: #fff; }
.arrow { color: #8fa4b8; font-size: 1.1rem; user-select: none; }
.result { font: 600 .95rem ui-monospace, monospace; color: #0a7d33; margin-top: .5rem;
min-height: 1.3em; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
label { font-size: .9rem; font-weight: 600; }
input[type=number] { width: 3.5rem; padding: .35rem .5rem; border: 1.5px solid #b8c5d0;
border-radius: 7px; font: 600 1rem ui-monospace, monospace;
text-align: center; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1.5px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.log { background: #1a1e26; color: #a8d8a8; border-radius: 8px; padding: .6rem .9rem;
font: .82rem ui-monospace, monospace; min-height: 3rem; white-space: pre-wrap;
max-height: 8rem; overflow-y: auto; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pipeline: three steps — double, add 10, square.
const STEPS = [
{ id: 'double', label: '× 2', fn: x => x * 2 },
{ id: 'add10', label: '+ 10', fn: x => x + 10 },
{ id: 'square', label: '²', fn: x => x * x }
];
// Direct style: each step returns its result.
function runDirect(x) {
const a = STEPS[0].fn(x);
const b = STEPS[1].fn(a);
const c = STEPS[2].fn(b);
return c;
}
// CPS: each step calls a continuation instead of returning.
function doubleCPS(x, k) { k(x * 2); }
function add10CPS(x, k) { k(x + 10); }
function squareCPS(x, k) { k(x * x); }
// Trace mode logs each continuation call.
let traceMode = false;
let traceLog = [];
function runCPS(x, finalK) {
doubleCPS(x, a => {
add10CPS(a, b => {
squareCPS(b, c => {
finalK(c);
});
});
});
}
function traceCPS(x, finalK) {
doubleCPS(x, a => {
traceLog.push(i18nFmt('k1 called: double(%x%) → %a%', { x: x, a: a }));
add10CPS(a, b => {
traceLog.push(i18nFmt('k2 called: add10(%a%) → %b%', { a: a, b: b }));
squareCPS(b, c => {
traceLog.push(i18nFmt('k3 called: square(%b%) → %c%', { b: b, c: c }));
finalK(c);
});
});
});
}
// Build the visual pipeline boxes.
function buildPipeline(containerId, steps) {
const el = document.getElementById(containerId);
el.innerHTML = '';
steps.forEach((s, i) => {
const step = document.createElement('div');
step.className = 'step';
const box = document.createElement('div');
box.className = 'box';
box.id = containerId + '-' + s.id;
box.textContent = s.label;
step.appendChild(box);
if (i < steps.length - 1) {
const arr = document.createElement('span');
arr.className = 'arrow';
arr.textContent = '→';
step.appendChild(arr);
}
el.appendChild(step);
});
}
function setBox(containerId, id, cls) {
const el = document.getElementById(containerId + '-' + id);
if (el) el.className = 'box ' + cls;
}
function setResult(id, text) {
document.getElementById(id).textContent = text;
}
function appendLog(msg) {
const log = document.getElementById('log');
log.textContent += msg + '\n';
log.scrollTop = log.scrollHeight;
}
function clearLog() {
document.getElementById('log').textContent = '';
}
function resetBoxes() {
STEPS.forEach(s => {
setBox('direct-pipeline', s.id, '');
setBox('cps-pipeline', s.id, '');
});
setResult('direct-result', '');
setResult('cps-result', '');
}
// Animate direct-style pipeline boxes left to right.
function animateDirect(x, delay) {
let val = x;
STEPS.forEach((s, i) => {
setTimeout(() => {
setBox('direct-pipeline', s.id, 'active');
val = s.fn(val);
setTimeout(() => {
setBox('direct-pipeline', s.id, 'done');
if (i === STEPS.length - 1) {
setResult('direct-result', i18nFmt('Result: %v%', { v: val }));
}
}, delay * 0.6);
}, i * delay);
});
}
// Animate CPS pipeline boxes left to right.
function animateCPS(x, delay) {
const vals = [x];
STEPS.forEach(s => vals.push(s.fn(vals[vals.length - 1])));
STEPS.forEach((s, i) => {
setTimeout(() => {
setBox('cps-pipeline', s.id, 'active');
setTimeout(() => {
setBox('cps-pipeline', s.id, 'done');
if (i === STEPS.length - 1) {
setResult('cps-result', i18nFmt('Result: %v%', { v: vals[vals.length - 1] }));
}
}, delay * 0.6);
}, i * delay);
});
}
function getInput() {
const v = parseInt(document.getElementById('input-val').value, 10);
return isNaN(v) || v < 1 ? 3 : Math.min(v, 9);
}
document.getElementById('btn-run').addEventListener('click', () => {
traceMode = false;
clearLog();
resetBoxes();
const x = getInput();
const direct = runDirect(x);
animateDirect(x, 500);
animateCPS(x, 500);
appendLog(i18nFmt('input=%x% → direct=%r% (CPS gives the same answer)', { x: x, r: direct }));
});
document.getElementById('btn-trace').addEventListener('click', () => {
traceMode = true;
clearLog();
resetBoxes();
const x = getInput();
traceLog = [];
traceCPS(x, result => {
animateDirect(x, 400);
animateCPS(x, 400);
traceLog.forEach(line => appendLog(line));
appendLog(i18nFmt('Final continuation called with result: %r%', { r: result }));
});
});
document.getElementById('btn-reset').addEventListener('click', () => {
resetBoxes();
clearLog();
});
// Initialize both pipelines on load.
buildPipeline('direct-pipeline', STEPS);
buildPipeline('cps-pipeline', STEPS);
appendLog('Press Run or Trace to start.');
Notice that in CPS the nesting depth grows with each step — the continuation of step 2 wraps the continuation of step 3. This is the famous callback pyramid. The computation is identical; only the shape of the code changes. When you press Trace you can watch the chain of continuation calls unroll left-to-right instead of returning right-to-left.
Comments
Loading comments...