The core idea of dependent types is easiest to see with length-indexed lists (often called Vecn). A Vec3Int is a list of exactly 3 integers — the length is part of the type, not just documentation.
The key property is append: if you concatenate a Vecm with a Vecn, the result is a Vec(m+n). That arithmetic lives in the type signature. The compiler checks it statically — no runtime assertion needed.
<div class="demo-wrap">
<div class="panel" id="panel-a">
<div class="panel-title">Vec A <span class="len-badge" id="badge-a">len=0</span></div>
<div class="vec-display" id="vec-a"></div>
<div class="ops">
<button id="push-a" type="button">Push to A</button>
<button id="pop-a" type="button" class="ghost">Pop from A</button>
</div>
</div>
<div class="panel" id="panel-b">
<div class="panel-title">Vec B <span class="len-badge" id="badge-b">len=0</span></div>
<div class="vec-display" id="vec-b"></div>
<div class="ops">
<button id="push-b" type="button">Push to B</button>
<button id="pop-b" type="button" class="ghost">Pop from B</button>
</div>
</div>
<div class="panel result-panel">
<div class="panel-title">A ++ B <span class="len-badge result-badge" id="badge-r">len=0</span></div>
<div class="vec-display" id="vec-r"></div>
<div class="ops">
<button id="append-btn" type="button" class="accent">Append A ++ B</button>
<button id="head-btn" type="button" class="ghost">Head of (A++B)</button>
</div>
</div>
</div>
<div class="type-log" id="type-log">
<div class="log-title">Type checker output</div>
<div id="log-entries"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; font-size: .92rem; }
.demo-wrap { display: flex; gap: .6rem; flex-wrap: wrap; }
.panel { flex: 1 1 140px; border: 1.5px solid #c8d4de; border-radius: 10px; padding: .6rem .7rem; background: #f4f8fb; }
.result-panel { border-color: #4a7fa5; background: #eaf3fb; }
.panel-title { font-weight: 700; font-size: .85rem; color: #1d3557; margin-bottom: .4rem; display: flex; align-items: center; gap: .4rem; }
.len-badge { background: #1d3557; color: #fff; border-radius: 99px; font-size: .72rem; padding: .1rem .45rem; font-weight: 600; letter-spacing: .02em; }
.result-badge { background: #2a7abf; }
.vec-display { display: flex; flex-wrap: wrap; gap: 4px; min-height: 38px; align-items: center; margin-bottom: .5rem; }
.cell { width: 32px; height: 32px; border-radius: 7px; background: #1d3557; color: #fff; display: flex; align-items: center; justify-content: center; font: 700 .85rem ui-monospace, monospace; transition: transform .12s; }
.cell.new { animation: pop-in .2s ease; }
.cell.from-a { background: #1d6a57; }
.cell.from-b { background: #5a2d8a; }
@keyframes pop-in { from { transform: scale(0.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.ops { display: flex; gap: .4rem; flex-wrap: wrap; }
button { font: 600 .8rem system-ui, sans-serif; padding: .35rem .7rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; transition: opacity .1s; }
button:hover { opacity: .85; }
button.ghost { background: #fff; color: #1d3557; }
button.accent { background: #2a7abf; border-color: #2a7abf; }
.type-log { margin-top: .7rem; border: 1.5px solid #c8d4de; border-radius: 10px; padding: .55rem .7rem; background: #f9fbfc; font-family: ui-monospace, monospace; font-size: .78rem; max-height: 160px; overflow-y: auto; }
.log-title { font-family: system-ui, sans-serif; font-weight: 700; color: #1d3557; font-size: .82rem; margin-bottom: .35rem; }
.log-entry { padding: .15rem 0; border-bottom: 1px solid #e8eef3; color: #333; line-height: 1.4; }
.log-entry:last-child { border-bottom: none; }
.log-ok { color: #0a7d33; }
.log-err { color: #c92f3c; }
.log-info { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simulates a dependent-type checker for length-indexed vectors (Vec n).
// Invariant: vecA.length === lenA, vecB.length === lenB (enforced at every step).
// append(A, B) produces a Vec of length lenA + lenB — checked statically here.
// head is only valid on a Vec whose length > 0 — rejected at type-check time otherwise.
let vecA = [], vecB = [], vecR = [];
let counter = 1;
const $ = id => document.getElementById(id);
const logEl = $('log-entries');
function log(msg, cls) {
const d = document.createElement('div');
d.className = 'log-entry ' + (cls || 'log-info');
d.textContent = msg;
logEl.prepend(d);
}
function renderVec(id, arr, colorClass) {
const el = $(id); el.innerHTML = '';
arr.forEach((v, i) => {
const c = document.createElement('div');
c.className = 'cell ' + (colorClass || '') + (i === arr.length - 1 ? ' new' : '');
c.textContent = v;
el.appendChild(c);
});
}
function updateBadge(id, len) {
$(id).textContent = fmt('len=%len%', { len: len });
}
function renderAll() {
renderVec('vec-a', vecA, 'from-a');
renderVec('vec-b', vecB, 'from-b');
renderVec('vec-r', vecR, vecR.length === vecA.length + vecB.length && vecR.length > 0 ? 'from-a' : '');
updateBadge('badge-a', vecA.length);
updateBadge('badge-b', vecB.length);
updateBadge('badge-r', vecR.length);
}
$('push-a').addEventListener('click', () => {
const v = counter++;
vecA.push(v);
vecR = [];
log('push ' + v + ' → Vec A :: type Vec ' + vecA.length + ' Int ✓', 'log-ok');
renderAll();
});
$('pop-a').addEventListener('click', () => {
if (vecA.length === 0) {
log('TYPE ERROR: pop on Vec 0 — head/tail of empty Vec is not inhabited', 'log-err');
return;
}
const v = vecA.pop(); vecR = [];
log('pop ' + v + ' from A → Vec ' + vecA.length + ' Int ✓', 'log-ok');
renderAll();
});
$('push-b').addEventListener('click', () => {
const v = counter++;
vecB.push(v);
vecR = [];
log('push ' + v + ' → Vec B :: type Vec ' + vecB.length + ' Int ✓', 'log-ok');
renderAll();
});
$('pop-b').addEventListener('click', () => {
if (vecB.length === 0) {
log('TYPE ERROR: pop on Vec 0 — head/tail of empty Vec is not inhabited', 'log-err');
return;
}
const v = vecB.pop(); vecR = [];
log('pop ' + v + ' from B → Vec ' + vecB.length + ' Int ✓', 'log-ok');
renderAll();
});
$('append-btn').addEventListener('click', () => {
const m = vecA.length, n = vecB.length, expected = m + n;
vecR = vecA.concat(vecB);
// Verify the dependent-type invariant: length must equal m + n
console.assert(vecR.length === expected,
fmt('append invariant violated: expected %expected% got %length%', { expected: expected, length: vecR.length }));
log('append :: Vec ' + m + ' ++ Vec ' + n + ' → Vec ' + expected +
' (type-checked: ' + m + ' + ' + n + ' = ' + expected + ') ✓', 'log-ok');
renderAll();
});
$('head-btn').addEventListener('click', () => {
const r = vecR;
if (r.length === 0) {
log('TYPE ERROR: head :: Vec 0 → Int is not valid — Vec 0 has no elements. The type system rejects this before the program runs.', 'log-err');
return;
}
log('head (A++B) = ' + r[0] + ' :: Vec ' + r.length + ' ⊢ head :: Int ✓', 'log-ok');
});
// Seed with 3 elements each so the demo is immediately interesting
function seed() {
for (let i = 0; i < 3; i++) { vecA.push(counter++); }
for (let i = 0; i < 3; i++) { vecB.push(counter++); }
log('Initial state: Vec A (len=3) and Vec B (len=3) ready. Append to get Vec 6.', 'log-info');
renderAll();
}
seed();
<div class="demo-wrap">
<div class="panel" id="panel-a">
<div class="panel-title">Vec A <span class="len-badge" id="badge-a">len=0</span></div>
<div class="vec-display" id="vec-a"></div>
<div class="ops">
<button id="push-a" type="button">Push to A</button>
<button id="pop-a" type="button" class="ghost">Pop from A</button>
</div>
</div>
<div class="panel" id="panel-b">
<div class="panel-title">Vec B <span class="len-badge" id="badge-b">len=0</span></div>
<div class="vec-display" id="vec-b"></div>
<div class="ops">
<button id="push-b" type="button">Push to B</button>
<button id="pop-b" type="button" class="ghost">Pop from B</button>
</div>
</div>
<div class="panel result-panel">
<div class="panel-title">A ++ B <span class="len-badge result-badge" id="badge-r">len=0</span></div>
<div class="vec-display" id="vec-r"></div>
<div class="ops">
<button id="append-btn" type="button" class="accent">Append A ++ B</button>
<button id="head-btn" type="button" class="ghost">Head of (A++B)</button>
</div>
</div>
</div>
<div class="type-log" id="type-log">
<div class="log-title">Type checker output</div>
<div id="log-entries"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; font-size: .92rem; }
.demo-wrap { display: flex; gap: .6rem; flex-wrap: wrap; }
.panel { flex: 1 1 140px; border: 1.5px solid #c8d4de; border-radius: 10px; padding: .6rem .7rem; background: #f4f8fb; }
.result-panel { border-color: #4a7fa5; background: #eaf3fb; }
.panel-title { font-weight: 700; font-size: .85rem; color: #1d3557; margin-bottom: .4rem; display: flex; align-items: center; gap: .4rem; }
.len-badge { background: #1d3557; color: #fff; border-radius: 99px; font-size: .72rem; padding: .1rem .45rem; font-weight: 600; letter-spacing: .02em; }
.result-badge { background: #2a7abf; }
.vec-display { display: flex; flex-wrap: wrap; gap: 4px; min-height: 38px; align-items: center; margin-bottom: .5rem; }
.cell { width: 32px; height: 32px; border-radius: 7px; background: #1d3557; color: #fff; display: flex; align-items: center; justify-content: center; font: 700 .85rem ui-monospace, monospace; transition: transform .12s; }
.cell.new { animation: pop-in .2s ease; }
.cell.from-a { background: #1d6a57; }
.cell.from-b { background: #5a2d8a; }
@keyframes pop-in { from { transform: scale(0.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.ops { display: flex; gap: .4rem; flex-wrap: wrap; }
button { font: 600 .8rem system-ui, sans-serif; padding: .35rem .7rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; transition: opacity .1s; }
button:hover { opacity: .85; }
button.ghost { background: #fff; color: #1d3557; }
button.accent { background: #2a7abf; border-color: #2a7abf; }
.type-log { margin-top: .7rem; border: 1.5px solid #c8d4de; border-radius: 10px; padding: .55rem .7rem; background: #f9fbfc; font-family: ui-monospace, monospace; font-size: .78rem; max-height: 160px; overflow-y: auto; }
.log-title { font-family: system-ui, sans-serif; font-weight: 700; color: #1d3557; font-size: .82rem; margin-bottom: .35rem; }
.log-entry { padding: .15rem 0; border-bottom: 1px solid #e8eef3; color: #333; line-height: 1.4; }
.log-entry:last-child { border-bottom: none; }
.log-ok { color: #0a7d33; }
.log-err { color: #c92f3c; }
.log-info { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simulates a dependent-type checker for length-indexed vectors (Vec n).
// Invariant: vecA.length === lenA, vecB.length === lenB (enforced at every step).
// append(A, B) produces a Vec of length lenA + lenB — checked statically here.
// head is only valid on a Vec whose length > 0 — rejected at type-check time otherwise.
let vecA = [], vecB = [], vecR = [];
let counter = 1;
const $ = id => document.getElementById(id);
const logEl = $('log-entries');
function log(msg, cls) {
const d = document.createElement('div');
d.className = 'log-entry ' + (cls || 'log-info');
d.textContent = msg;
logEl.prepend(d);
}
function renderVec(id, arr, colorClass) {
const el = $(id); el.innerHTML = '';
arr.forEach((v, i) => {
const c = document.createElement('div');
c.className = 'cell ' + (colorClass || '') + (i === arr.length - 1 ? ' new' : '');
c.textContent = v;
el.appendChild(c);
});
}
function updateBadge(id, len) {
$(id).textContent = fmt('len=%len%', { len: len });
}
function renderAll() {
renderVec('vec-a', vecA, 'from-a');
renderVec('vec-b', vecB, 'from-b');
renderVec('vec-r', vecR, vecR.length === vecA.length + vecB.length && vecR.length > 0 ? 'from-a' : '');
updateBadge('badge-a', vecA.length);
updateBadge('badge-b', vecB.length);
updateBadge('badge-r', vecR.length);
}
$('push-a').addEventListener('click', () => {
const v = counter++;
vecA.push(v);
vecR = [];
log('push ' + v + ' → Vec A :: type Vec ' + vecA.length + ' Int ✓', 'log-ok');
renderAll();
});
$('pop-a').addEventListener('click', () => {
if (vecA.length === 0) {
log('TYPE ERROR: pop on Vec 0 — head/tail of empty Vec is not inhabited', 'log-err');
return;
}
const v = vecA.pop(); vecR = [];
log('pop ' + v + ' from A → Vec ' + vecA.length + ' Int ✓', 'log-ok');
renderAll();
});
$('push-b').addEventListener('click', () => {
const v = counter++;
vecB.push(v);
vecR = [];
log('push ' + v + ' → Vec B :: type Vec ' + vecB.length + ' Int ✓', 'log-ok');
renderAll();
});
$('pop-b').addEventListener('click', () => {
if (vecB.length === 0) {
log('TYPE ERROR: pop on Vec 0 — head/tail of empty Vec is not inhabited', 'log-err');
return;
}
const v = vecB.pop(); vecR = [];
log('pop ' + v + ' from B → Vec ' + vecB.length + ' Int ✓', 'log-ok');
renderAll();
});
$('append-btn').addEventListener('click', () => {
const m = vecA.length, n = vecB.length, expected = m + n;
vecR = vecA.concat(vecB);
// Verify the dependent-type invariant: length must equal m + n
console.assert(vecR.length === expected,
fmt('append invariant violated: expected %expected% got %length%', { expected: expected, length: vecR.length }));
log('append :: Vec ' + m + ' ++ Vec ' + n + ' → Vec ' + expected +
' (type-checked: ' + m + ' + ' + n + ' = ' + expected + ') ✓', 'log-ok');
renderAll();
});
$('head-btn').addEventListener('click', () => {
const r = vecR;
if (r.length === 0) {
log('TYPE ERROR: head :: Vec 0 → Int is not valid — Vec 0 has no elements. The type system rejects this before the program runs.', 'log-err');
return;
}
log('head (A++B) = ' + r[0] + ' :: Vec ' + r.length + ' ⊢ head :: Int ✓', 'log-ok');
});
// Seed with 3 elements each so the demo is immediately interesting
function seed() {
for (let i = 0; i < 3; i++) { vecA.push(counter++); }
for (let i = 0; i < 3; i++) { vecB.push(counter++); }
log('Initial state: Vec A (len=3) and Vec B (len=3) ready. Append to get Vec 6.', 'log-info');
renderAll();
}
seed();
Notice what happened: the compiler (simulated here) tracked lengths through every operation. Building a list checks that each push increases the length by one. Appending two lists checks that the result length equals the sum. If you try to take the head of an empty list, the type system rejects it — not at runtime, but at the type-checking stage, before any code runs.
This is the asymmetry that makes dependent types powerful. Unit tests check behavior on specific inputs at runtime. Types check behavior on all possible inputs at compile time. Dependent types extend that compile-time guarantee to properties that mention actual values — lengths, bounds, invariants — not just shapes.
Comments
Loading comments...