Each colored block below is a primitive operation with a known effect: IO (read/write), State (mutable memory), Throw (exceptions), or Pure (no side effects at all). Click the blocks to toggle them on or off and compose a function body. The checker on the right infers the effect row — the union of all effects — and compares it against the allowed set you choose.
<!-- Effect System demo: compose operations and check the inferred effect row -->
<div class="layout">
<div class="panel left-panel">
<h3 class="panel-title">Function Body</h3>
<p class="panel-desc">Click operations to add them to your function. Each carries an effect tag.</p>
<div id="op-list" class="op-list">
<!-- operations injected by JS -->
</div>
<div class="allowed-row">
<span class="allowed-label">Allowed effects for this function:</span>
<div id="allowed-list" class="allowed-list">
<!-- toggles injected by JS -->
</div>
</div>
</div>
<div class="panel right-panel">
<h3 class="panel-title">Inferred Effect Row</h3>
<div id="effect-row" class="effect-row">—</div>
<div id="verdict" class="verdict"></div>
<p class="panel-desc" id="explain"></p>
<button id="reset-btn" class="reset-btn" type="button">Reset</button>
</div>
</div>
/* global reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; font-size: 14px; }
.layout { display: flex; gap: 12px; padding: 10px; min-height: 360px; }
.panel { flex: 1; background: #f4f7fa; border-radius: 10px; padding: 14px; }
.panel-title { font-size: 1rem; font-weight: 700; margin-bottom: 6px; color: #1d3557; }
.panel-desc { font-size: .82rem; color: #555; margin-bottom: 10px; line-height: 1.4; }
/* operation items */
.op-list { display: flex; flex-direction: column; gap: 7px; margin-bottom: 12px; }
.op-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px;
border-radius: 8px; cursor: pointer; border: 2px solid transparent;
transition: all .15s; user-select: none; font-weight: 600; font-size: .88rem; }
.op-item:hover { filter: brightness(.93); }
.op-item.active { border-color: #1d3557; }
.op-item .badge { width: 28px; height: 28px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: .75rem; font-weight: 800; flex-shrink: 0; }
.op-io { background: #dbeafe; } .op-io .badge { background: #3b82f6; color: #fff; }
.op-state{ background: #dcfce7; } .op-state .badge { background: #16a34a; color: #fff; }
.op-throw{ background: #fee2e2; } .op-throw .badge { background: #dc2626; color: #fff; }
.op-pure { background: #f3f4f6; } .op-pure .badge { background: #6b7280; color: #fff; }
.op-item.active.op-io { border-color: #3b82f6; }
.op-item.active.op-state{ border-color: #16a34a; }
.op-item.active.op-throw{ border-color: #dc2626; }
.op-item.active.op-pure { border-color: #6b7280; }
/* allowed toggles */
.allowed-row { margin-top: 4px; }
.allowed-label { font-size: .8rem; color: #555; display: block; margin-bottom: 6px; font-weight: 600; }
.allowed-list { display: flex; gap: 6px; flex-wrap: wrap; }
.allow-toggle { padding: 4px 10px; border-radius: 20px; border: 2px solid #aaa;
cursor: pointer; font-size: .8rem; font-weight: 600; background: #fff;
transition: all .15s; user-select: none; }
.allow-toggle.on { background: #1d3557; color: #fff; border-color: #1d3557; }
/* right panel */
.effect-row { font-size: 1.1rem; font-family: ui-monospace, monospace;
background: #fff; border-radius: 8px; padding: 10px 14px;
min-height: 44px; margin-bottom: 10px; color: #1d3557;
border: 1px solid #cdd9e3; word-break: break-all; }
.verdict { font-size: .95rem; font-weight: 700; margin-bottom: 8px; min-height: 1.3em; }
.verdict.ok { color: #0a7d33; }
.verdict.bad { color: #c92f3c; }
.reset-btn { margin-top: 10px; padding: 6px 16px; border-radius: 8px;
border: 1px solid #1d3557; background: #fff; color: #1d3557;
font: 600 13px system-ui, sans-serif; cursor: pointer; }
.reset-btn:hover { background: #1d3557; color: #fff; }
@media (max-width: 480px) { .layout { flex-direction: column; } }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Effect System demo — operations have effect tags; the checker infers
// the effect row (union of tags) and tests subset against the allowed set.
const OPS = [
{ id: 'read-file', effect: 'IO', label: 'Read file (IO)', cls: 'op-io' },
{ id: 'write-log', effect: 'IO', label: 'Write log (IO)', cls: 'op-io' },
{ id: 'read-state', effect: 'State', label: 'Read counter (State)', cls: 'op-state' },
{ id: 'write-state', effect: 'State', label: 'Update counter (State)', cls: 'op-state' },
{ id: 'throw-err', effect: 'Throw', label: 'Throw error (Throw)', cls: 'op-throw' },
{ id: 'pure-compute', effect: null, label: 'Pure computation (no effect)', cls: 'op-pure' }
];
// available effect labels for the allowed set
const ALLOW_OPTS = ['IO', 'State', 'Throw'];
const active = new Set(); // currently active operation ids
const allowed = new Set(['IO']); // start with IO allowed
function buildUI() {
// render each operation as a clickable block
const opList = document.getElementById('op-list');
OPS.forEach(op => {
const el = document.createElement('div');
el.className = `op-item ${op.cls}`;
el.dataset.id = op.id;
el.innerHTML = `<span class="badge">${op.effect || '∅'}</span><span>${op.label}</span>`;
el.addEventListener('click', () => toggleOp(op.id));
opList.appendChild(el);
});
// render the allowed-set toggles
const allowList = document.getElementById('allowed-list');
ALLOW_OPTS.forEach(eff => {
const btn = document.createElement('button');
btn.className = `allow-toggle${allowed.has(eff) ? ' on' : ''}`;
btn.dataset.eff = eff;
btn.textContent = eff;
btn.addEventListener('click', () => toggleAllowed(eff));
allowList.appendChild(btn);
});
update();
}
function toggleOp(id) {
// toggle an operation on/off
active.has(id) ? active.delete(id) : active.add(id);
document.querySelectorAll('.op-item').forEach(el => {
el.classList.toggle('active', active.has(el.dataset.id));
});
update();
}
function toggleAllowed(eff) {
// toggle an effect in/out of the allowed set
allowed.has(eff) ? allowed.delete(eff) : allowed.add(eff);
document.querySelectorAll('.allow-toggle').forEach(btn => {
btn.classList.toggle('on', allowed.has(btn.dataset.eff));
});
update();
}
function inferredEffects() {
// collect the union of effects from active operations
const effs = new Set();
active.forEach(id => {
const op = OPS.find(o => o.id === id);
if (op && op.effect) effs.add(op.effect);
});
return effs;
}
function update() {
// recompute and display the effect row and verdict
const inferred = inferredEffects();
const rowEl = document.getElementById('effect-row');
const verdictEl = document.getElementById('verdict');
const explainEl = document.getElementById('explain');
if (inferred.size === 0) {
rowEl.textContent = '∅ (Pure)';
} else {
rowEl.textContent = '{ ' + [...inferred].join(', ') + ' }';
}
// verdict: inferred effects must be a subset of allowed
const bad = [...inferred].filter(e => !allowed.has(e));
if (bad.length === 0) {
verdictEl.textContent = '✓ Effect row fits within allowed set.';
verdictEl.className = 'verdict ok';
if (inferred.size === 0) {
explainEl.textContent = 'No active operations — the function is pure. It can be called from anywhere.';
} else {
explainEl.textContent = 'All inferred effects are permitted. The type checker would accept this function at this call site.';
}
} else {
verdictEl.textContent = fmt('✗ Leaked effects not in allowed set: %bad%', { bad: bad.join(', ') });
verdictEl.className = 'verdict bad';
explainEl.textContent = 'The function performs effects the call site does not permit. Add those effects to the allowed set, or remove the offending operations.';
}
}
document.getElementById('reset-btn').addEventListener('click', () => {
// clear all active operations
active.clear();
document.querySelectorAll('.op-item').forEach(el => el.classList.remove('active'));
update();
});
buildUI();
<!-- Effect System demo: compose operations and check the inferred effect row -->
<div class="layout">
<div class="panel left-panel">
<h3 class="panel-title">Function Body</h3>
<p class="panel-desc">Click operations to add them to your function. Each carries an effect tag.</p>
<div id="op-list" class="op-list">
<!-- operations injected by JS -->
</div>
<div class="allowed-row">
<span class="allowed-label">Allowed effects for this function:</span>
<div id="allowed-list" class="allowed-list">
<!-- toggles injected by JS -->
</div>
</div>
</div>
<div class="panel right-panel">
<h3 class="panel-title">Inferred Effect Row</h3>
<div id="effect-row" class="effect-row">—</div>
<div id="verdict" class="verdict"></div>
<p class="panel-desc" id="explain"></p>
<button id="reset-btn" class="reset-btn" type="button">Reset</button>
</div>
</div>
/* global reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; font-size: 14px; }
.layout { display: flex; gap: 12px; padding: 10px; min-height: 360px; }
.panel { flex: 1; background: #f4f7fa; border-radius: 10px; padding: 14px; }
.panel-title { font-size: 1rem; font-weight: 700; margin-bottom: 6px; color: #1d3557; }
.panel-desc { font-size: .82rem; color: #555; margin-bottom: 10px; line-height: 1.4; }
/* operation items */
.op-list { display: flex; flex-direction: column; gap: 7px; margin-bottom: 12px; }
.op-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px;
border-radius: 8px; cursor: pointer; border: 2px solid transparent;
transition: all .15s; user-select: none; font-weight: 600; font-size: .88rem; }
.op-item:hover { filter: brightness(.93); }
.op-item.active { border-color: #1d3557; }
.op-item .badge { width: 28px; height: 28px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: .75rem; font-weight: 800; flex-shrink: 0; }
.op-io { background: #dbeafe; } .op-io .badge { background: #3b82f6; color: #fff; }
.op-state{ background: #dcfce7; } .op-state .badge { background: #16a34a; color: #fff; }
.op-throw{ background: #fee2e2; } .op-throw .badge { background: #dc2626; color: #fff; }
.op-pure { background: #f3f4f6; } .op-pure .badge { background: #6b7280; color: #fff; }
.op-item.active.op-io { border-color: #3b82f6; }
.op-item.active.op-state{ border-color: #16a34a; }
.op-item.active.op-throw{ border-color: #dc2626; }
.op-item.active.op-pure { border-color: #6b7280; }
/* allowed toggles */
.allowed-row { margin-top: 4px; }
.allowed-label { font-size: .8rem; color: #555; display: block; margin-bottom: 6px; font-weight: 600; }
.allowed-list { display: flex; gap: 6px; flex-wrap: wrap; }
.allow-toggle { padding: 4px 10px; border-radius: 20px; border: 2px solid #aaa;
cursor: pointer; font-size: .8rem; font-weight: 600; background: #fff;
transition: all .15s; user-select: none; }
.allow-toggle.on { background: #1d3557; color: #fff; border-color: #1d3557; }
/* right panel */
.effect-row { font-size: 1.1rem; font-family: ui-monospace, monospace;
background: #fff; border-radius: 8px; padding: 10px 14px;
min-height: 44px; margin-bottom: 10px; color: #1d3557;
border: 1px solid #cdd9e3; word-break: break-all; }
.verdict { font-size: .95rem; font-weight: 700; margin-bottom: 8px; min-height: 1.3em; }
.verdict.ok { color: #0a7d33; }
.verdict.bad { color: #c92f3c; }
.reset-btn { margin-top: 10px; padding: 6px 16px; border-radius: 8px;
border: 1px solid #1d3557; background: #fff; color: #1d3557;
font: 600 13px system-ui, sans-serif; cursor: pointer; }
.reset-btn:hover { background: #1d3557; color: #fff; }
@media (max-width: 480px) { .layout { flex-direction: column; } }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Effect System demo — operations have effect tags; the checker infers
// the effect row (union of tags) and tests subset against the allowed set.
const OPS = [
{ id: 'read-file', effect: 'IO', label: 'Read file (IO)', cls: 'op-io' },
{ id: 'write-log', effect: 'IO', label: 'Write log (IO)', cls: 'op-io' },
{ id: 'read-state', effect: 'State', label: 'Read counter (State)', cls: 'op-state' },
{ id: 'write-state', effect: 'State', label: 'Update counter (State)', cls: 'op-state' },
{ id: 'throw-err', effect: 'Throw', label: 'Throw error (Throw)', cls: 'op-throw' },
{ id: 'pure-compute', effect: null, label: 'Pure computation (no effect)', cls: 'op-pure' }
];
// available effect labels for the allowed set
const ALLOW_OPTS = ['IO', 'State', 'Throw'];
const active = new Set(); // currently active operation ids
const allowed = new Set(['IO']); // start with IO allowed
function buildUI() {
// render each operation as a clickable block
const opList = document.getElementById('op-list');
OPS.forEach(op => {
const el = document.createElement('div');
el.className = `op-item ${op.cls}`;
el.dataset.id = op.id;
el.innerHTML = `<span class="badge">${op.effect || '∅'}</span><span>${op.label}</span>`;
el.addEventListener('click', () => toggleOp(op.id));
opList.appendChild(el);
});
// render the allowed-set toggles
const allowList = document.getElementById('allowed-list');
ALLOW_OPTS.forEach(eff => {
const btn = document.createElement('button');
btn.className = `allow-toggle${allowed.has(eff) ? ' on' : ''}`;
btn.dataset.eff = eff;
btn.textContent = eff;
btn.addEventListener('click', () => toggleAllowed(eff));
allowList.appendChild(btn);
});
update();
}
function toggleOp(id) {
// toggle an operation on/off
active.has(id) ? active.delete(id) : active.add(id);
document.querySelectorAll('.op-item').forEach(el => {
el.classList.toggle('active', active.has(el.dataset.id));
});
update();
}
function toggleAllowed(eff) {
// toggle an effect in/out of the allowed set
allowed.has(eff) ? allowed.delete(eff) : allowed.add(eff);
document.querySelectorAll('.allow-toggle').forEach(btn => {
btn.classList.toggle('on', allowed.has(btn.dataset.eff));
});
update();
}
function inferredEffects() {
// collect the union of effects from active operations
const effs = new Set();
active.forEach(id => {
const op = OPS.find(o => o.id === id);
if (op && op.effect) effs.add(op.effect);
});
return effs;
}
function update() {
// recompute and display the effect row and verdict
const inferred = inferredEffects();
const rowEl = document.getElementById('effect-row');
const verdictEl = document.getElementById('verdict');
const explainEl = document.getElementById('explain');
if (inferred.size === 0) {
rowEl.textContent = '∅ (Pure)';
} else {
rowEl.textContent = '{ ' + [...inferred].join(', ') + ' }';
}
// verdict: inferred effects must be a subset of allowed
const bad = [...inferred].filter(e => !allowed.has(e));
if (bad.length === 0) {
verdictEl.textContent = '✓ Effect row fits within allowed set.';
verdictEl.className = 'verdict ok';
if (inferred.size === 0) {
explainEl.textContent = 'No active operations — the function is pure. It can be called from anywhere.';
} else {
explainEl.textContent = 'All inferred effects are permitted. The type checker would accept this function at this call site.';
}
} else {
verdictEl.textContent = fmt('✗ Leaked effects not in allowed set: %bad%', { bad: bad.join(', ') });
verdictEl.className = 'verdict bad';
explainEl.textContent = 'The function performs effects the call site does not permit. Add those effects to the allowed set, or remove the offending operations.';
}
}
document.getElementById('reset-btn').addEventListener('click', () => {
// clear all active operations
active.clear();
document.querySelectorAll('.op-item').forEach(el => el.classList.remove('active'));
update();
});
buildUI();
Notice the asymmetry: checking that a given effect row is a subset of the allowed set is a simple set-inclusion test — instant. Inferring the full effect row of an arbitrary program, on the other hand, requires solving a system of effect-variable constraints (similar to unification in type inference), and deciding whether two effect-polymorphic functions can be composed safely is the core challenge effect-system designers face.
Comments
Loading comments...