<!-- Demo layout - controls, expression display, inferred type, inference steps -->
<div class="controls">
<label for="preset">Expression:</label>
<select id="preset">
<option value="identity">λx. x (identity)</option>
<option value="const">λx. λy. x (const)</option>
<option value="apply">λf. λx. f x (apply)</option>
<option value="compose">λf. λg. λx. f (g x) (compose)</option>
<option value="twice">λf. λx. f (f x) (twice)</option>
</select>
<button id="btn-infer" type="button">Infer Type</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="expr-row">
<span class="expr-label">Expression:</span>
<code id="expr-display" class="expr-box"></code>
</div>
<div class="result-row" id="result-row" style="display:none">
<span class="type-label">Principal type:</span>
<code id="result-type" class="type-box"></code>
</div>
<div class="steps-header" id="steps-header" style="display:none">Inference steps</div>
<ol id="steps" class="steps"></ol>
<div class="status" id="status"></div>
/* Reset browser defaults and set base typography for the demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .7rem; }
label { font-size: .85rem; color: #555; }
select { font-size: .9rem; padding: .35rem .5rem; border: 1px solid #bbb; border-radius: 6px; background: #fafafa; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.expr-row, .result-row { display: flex; align-items: baseline; gap: .6rem; margin: .35rem 0; flex-wrap: wrap; }
.expr-label, .type-label { font-size: .82rem; color: #666; min-width: 4.5rem; }
.expr-box, .type-box { font: 1rem ui-monospace, monospace; background: #f0f4f8; border: 1px solid #d0d9e3;
border-radius: 6px; padding: .2rem .6rem; color: #1d3557; }
.type-box { background: #e6f4ea; border-color: #9ecfb0; color: #0a5c2d; font-weight: 600; }
.steps-header { font-size: .8rem; font-weight: 600; color: #666; margin: .7rem 0 .2rem; text-transform: uppercase; letter-spacing: .04em; }
.steps { margin: 0; padding-left: 1.4rem; }
.steps li { font-size: .82rem; font-family: ui-monospace, monospace; margin: .15rem 0; color: #333; line-height: 1.5; }
.steps li .action { color: #1d3557; font-weight: 600; }
.steps li .unify { color: #b56800; }
.steps li .result { color: #0a5c2d; font-weight: 600; }
.status { font-size: .95rem; font-weight: 600; margin-top: .5rem; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Algorithm W: Damas-Milner type inference for the simply-typed lambda calculus
// Generates type constraints and solves them by unification to find the principal type.
// Fresh type variable generator (alpha1, alpha2, ...).
let varCounter = 0;
function freshVar() { return 'α' + (++varCounter); }
// A substitution maps type variables to types.
// applySubst replaces type variables in a type according to a substitution.
function applySubst(subst, type) {
if (typeof type === 'string') return subst[type] ? applySubst(subst, subst[type]) : type;
if (type.tag === 'fun') return { tag: 'fun', from: applySubst(subst, type.from), to: applySubst(subst, type.to) };
if (type.tag === 'list') return { tag: 'list', elem: applySubst(subst, type.elem) };
return type;
}
function applySubstToSubst(s1, s2) {
const out = {};
for (const k in s2) out[k] = applySubst(s1, s2[k]);
for (const k in s1) if (!(k in out)) out[k] = s1[k];
return out;
}
// Collect all free type variables in a type.
function freeVars(type) {
if (typeof type === 'string') return new Set([type]);
if (type.tag === 'fun') { const s = freeVars(type.from); freeVars(type.to).forEach(v => s.add(v)); return s; }
if (type.tag === 'list') return freeVars(type.elem);
return new Set();
}
// Unification - find a substitution that makes two types equal.
// Implements Robinson's unification algorithm with occurs-check.
function unify(t1, t2, subst, steps) {
t1 = applySubst(subst, t1); t2 = applySubst(subst, t2);
const s1 = typeStr(t1), s2 = typeStr(t2);
if (s1 === s2) { steps.push({ action: 'already equal', detail: `${s1} = ${s2}` }); return subst; }
if (typeof t1 === 'string') {
if (freeVars(t2).has(t1)) throw new Error('occurs check failed: ' + t1 + ' in ' + s2);
steps.push({ action: 'bind', detail: `${t1} := ${s2}` });
return applySubstToSubst({ [t1]: t2 }, subst);
}
if (typeof t2 === 'string') return unify(t2, t1, subst, steps);
if (t1.tag === 'fun' && t2.tag === 'fun') {
steps.push({ action: 'match →', detail: `(${s1}) ~ (${s2})` });
subst = unify(t1.from, t2.from, subst, steps);
return unify(applySubst(subst, t1.to), applySubst(subst, t2.to), subst, steps);
}
if (t1.tag === 'list' && t2.tag === 'list') {
steps.push({ action: 'match list', detail: `List(${typeStr(t1.elem)}) ~ List(${typeStr(t2.elem)})` });
return unify(t1.elem, t2.elem, subst, steps);
}
throw new Error('type mismatch: ' + s1 + ' vs ' + s2);
}
// Pretty-print a type as a human-readable string.
function typeStr(t) {
if (typeof t === 'string') return t;
if (t.tag === 'fun') {
const from = t.from.tag === 'fun' ? '(' + typeStr(t.from) + ')' : typeStr(t.from);
return from + ' → ' + typeStr(t.to);
}
if (t.tag === 'list') return '[' + typeStr(t.elem) + ']';
return '?';
}
// Infer the type of an expression given an environment of type schemes.
// Returns the inferred type and the updated substitution.
function infer(expr, env, subst, steps) {
if (expr.tag === 'var') {
const t = env[expr.name];
if (!t) throw new Error('unbound variable: ' + expr.name);
// Instantiate a polymorphic type scheme with fresh type variables.
const inst = instantiate(t);
steps.push({ action: 'var', detail: `${expr.name} : ${typeStr(inst)}` });
return { type: inst, subst };
}
if (expr.tag === 'lam') {
const a = freshVar();
const newEnv = Object.assign({}, env, { [expr.param]: { vars: [], body: a } });
steps.push({ action: 'λ-intro', detail: `λ${expr.param}. assume ${expr.param}:${a}` });
const { type: bt, subst: s2 } = infer(expr.body, newEnv, subst, steps);
const lamType = { tag: 'fun', from: applySubst(s2, a), to: bt };
return { type: lamType, subst: s2 };
}
if (expr.tag === 'app') {
const { type: ft, subst: s1 } = infer(expr.fun, env, subst, steps);
const { type: at, subst: s2 } = infer(expr.arg, env, s1, steps);
const r = freshVar();
steps.push({ action: 'app', detail: `${typeStr(applySubst(s2, ft))} applied to ${typeStr(at)}, result ${r}` });
const s3 = unify(applySubst(s2, ft), { tag: 'fun', from: at, to: r }, s2, steps);
return { type: applySubst(s3, r), subst: s3 };
}
throw new Error('unknown expression node');
}
// Generalize a type over variables not free in the environment (let-polymorphism).
function freeVarsEnv(env) {
const s = new Set();
for (const k in env) freeVars(env[k].body).forEach(v => env[k].vars.includes(v) || s.add(v));
return s;
}
function generalize(type, env) {
const envFree = freeVarsEnv(env);
const free = [...freeVars(type)].filter(v => !envFree.has(v));
return { vars: free, body: type };
}
function instantiate(scheme) {
const mapping = {};
scheme.vars.forEach(v => { mapping[v] = freshVar(); });
return applySubst(mapping, scheme.body);
}
// Preset lambda expressions with their AST representations.
const PRESETS = {
identity: {
label: '\\u03bbx. x',
expr: { tag: 'lam', param: 'x', body: { tag: 'var', name: 'x' } }
},
const: {
label: '\\u03bbx. \\u03bby. x',
expr: { tag: 'lam', param: 'x', body: { tag: 'lam', param: 'y', body: { tag: 'var', name: 'x' } } }
},
apply: {
label: '\\u03bbf. \\u03bbx. f x',
expr: {
tag: 'lam', param: 'f', body: {
tag: 'lam', param: 'x', body: {
tag: 'app',
fun: { tag: 'var', name: 'f' },
arg: { tag: 'var', name: 'x' }
}
}
}
},
compose: {
label: '\\u03bbf. \\u03bbg. \\u03bbx. f (g x)',
expr: {
tag: 'lam', param: 'f', body: {
tag: 'lam', param: 'g', body: {
tag: 'lam', param: 'x', body: {
tag: 'app',
fun: { tag: 'var', name: 'f' },
arg: {
tag: 'app',
fun: { tag: 'var', name: 'g' },
arg: { tag: 'var', name: 'x' }
}
}
}
}
}
},
twice: {
label: '\\u03bbf. \\u03bbx. f (f x)',
expr: {
tag: 'lam', param: 'f', body: {
tag: 'lam', param: 'x', body: {
tag: 'app',
fun: { tag: 'var', name: 'f' },
arg: {
tag: 'app',
fun: { tag: 'var', name: 'f' },
arg: { tag: 'var', name: 'x' }
}
}
}
}
}
};
function runInference() {
varCounter = 0;
const key = document.getElementById('preset').value;
const preset = PRESETS[key];
document.getElementById('expr-display').textContent = preset.label;
document.getElementById('result-row').style.display = 'none';
document.getElementById('steps-header').style.display = 'none';
const stepsList = document.getElementById('steps');
stepsList.innerHTML = '';
const statusEl = document.getElementById('status');
statusEl.textContent = '';
statusEl.className = 'status';
try {
const steps = [];
const { type, subst } = infer(preset.expr, {}, {}, steps);
const finalType = applySubst(subst, type);
// Generalize the final type to a polymorphic scheme.
const scheme = generalize(finalType, {});
let typeDisplay = typeStr(finalType);
if (scheme.vars.length > 0) typeDisplay = '∀' + scheme.vars.join(' ') + '. ' + typeDisplay;
document.getElementById('result-type').textContent = typeDisplay;
document.getElementById('result-row').style.display = 'flex';
document.getElementById('steps-header').style.display = 'block';
steps.forEach(s => {
const li = document.createElement('li');
li.innerHTML = '<span class="action">' + s.action + '</span> <span class="unify">' + s.detail + '</span>';
stepsList.appendChild(li);
});
statusEl.textContent = i18nFmt('Done in %n% step(s).', { n: steps.length });
statusEl.className = 'status ok';
} catch (e) {
statusEl.textContent = 'Type error: ' + e.message;
statusEl.className = 'status err';
}
}
function resetAll() {
varCounter = 0;
document.getElementById('expr-display').textContent = '';
document.getElementById('result-row').style.display = 'none';
document.getElementById('steps-header').style.display = 'none';
document.getElementById('steps').innerHTML = '';
const statusEl = document.getElementById('status');
statusEl.textContent = '';
statusEl.className = 'status';
}
document.getElementById('btn-infer').addEventListener('click', runInference);
document.getElementById('btn-reset').addEventListener('click', resetAll);
// Show the identity function expression on load.
document.getElementById('preset').value = 'identity';
document.getElementById('expr-display').textContent = PRESETS['identity'].label;
<!-- Demo layout - controls, expression display, inferred type, inference steps -->
<div class="controls">
<label for="preset">Expression:</label>
<select id="preset">
<option value="identity">λx. x (identity)</option>
<option value="const">λx. λy. x (const)</option>
<option value="apply">λf. λx. f x (apply)</option>
<option value="compose">λf. λg. λx. f (g x) (compose)</option>
<option value="twice">λf. λx. f (f x) (twice)</option>
</select>
<button id="btn-infer" type="button">Infer Type</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="expr-row">
<span class="expr-label">Expression:</span>
<code id="expr-display" class="expr-box"></code>
</div>
<div class="result-row" id="result-row" style="display:none">
<span class="type-label">Principal type:</span>
<code id="result-type" class="type-box"></code>
</div>
<div class="steps-header" id="steps-header" style="display:none">Inference steps</div>
<ol id="steps" class="steps"></ol>
<div class="status" id="status"></div>
/* Reset browser defaults and set base typography for the demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .7rem; }
label { font-size: .85rem; color: #555; }
select { font-size: .9rem; padding: .35rem .5rem; border: 1px solid #bbb; border-radius: 6px; background: #fafafa; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.expr-row, .result-row { display: flex; align-items: baseline; gap: .6rem; margin: .35rem 0; flex-wrap: wrap; }
.expr-label, .type-label { font-size: .82rem; color: #666; min-width: 4.5rem; }
.expr-box, .type-box { font: 1rem ui-monospace, monospace; background: #f0f4f8; border: 1px solid #d0d9e3;
border-radius: 6px; padding: .2rem .6rem; color: #1d3557; }
.type-box { background: #e6f4ea; border-color: #9ecfb0; color: #0a5c2d; font-weight: 600; }
.steps-header { font-size: .8rem; font-weight: 600; color: #666; margin: .7rem 0 .2rem; text-transform: uppercase; letter-spacing: .04em; }
.steps { margin: 0; padding-left: 1.4rem; }
.steps li { font-size: .82rem; font-family: ui-monospace, monospace; margin: .15rem 0; color: #333; line-height: 1.5; }
.steps li .action { color: #1d3557; font-weight: 600; }
.steps li .unify { color: #b56800; }
.steps li .result { color: #0a5c2d; font-weight: 600; }
.status { font-size: .95rem; font-weight: 600; margin-top: .5rem; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Algorithm W: Damas-Milner type inference for the simply-typed lambda calculus
// Generates type constraints and solves them by unification to find the principal type.
// Fresh type variable generator (alpha1, alpha2, ...).
let varCounter = 0;
function freshVar() { return 'α' + (++varCounter); }
// A substitution maps type variables to types.
// applySubst replaces type variables in a type according to a substitution.
function applySubst(subst, type) {
if (typeof type === 'string') return subst[type] ? applySubst(subst, subst[type]) : type;
if (type.tag === 'fun') return { tag: 'fun', from: applySubst(subst, type.from), to: applySubst(subst, type.to) };
if (type.tag === 'list') return { tag: 'list', elem: applySubst(subst, type.elem) };
return type;
}
function applySubstToSubst(s1, s2) {
const out = {};
for (const k in s2) out[k] = applySubst(s1, s2[k]);
for (const k in s1) if (!(k in out)) out[k] = s1[k];
return out;
}
// Collect all free type variables in a type.
function freeVars(type) {
if (typeof type === 'string') return new Set([type]);
if (type.tag === 'fun') { const s = freeVars(type.from); freeVars(type.to).forEach(v => s.add(v)); return s; }
if (type.tag === 'list') return freeVars(type.elem);
return new Set();
}
// Unification - find a substitution that makes two types equal.
// Implements Robinson's unification algorithm with occurs-check.
function unify(t1, t2, subst, steps) {
t1 = applySubst(subst, t1); t2 = applySubst(subst, t2);
const s1 = typeStr(t1), s2 = typeStr(t2);
if (s1 === s2) { steps.push({ action: 'already equal', detail: `${s1} = ${s2}` }); return subst; }
if (typeof t1 === 'string') {
if (freeVars(t2).has(t1)) throw new Error('occurs check failed: ' + t1 + ' in ' + s2);
steps.push({ action: 'bind', detail: `${t1} := ${s2}` });
return applySubstToSubst({ [t1]: t2 }, subst);
}
if (typeof t2 === 'string') return unify(t2, t1, subst, steps);
if (t1.tag === 'fun' && t2.tag === 'fun') {
steps.push({ action: 'match →', detail: `(${s1}) ~ (${s2})` });
subst = unify(t1.from, t2.from, subst, steps);
return unify(applySubst(subst, t1.to), applySubst(subst, t2.to), subst, steps);
}
if (t1.tag === 'list' && t2.tag === 'list') {
steps.push({ action: 'match list', detail: `List(${typeStr(t1.elem)}) ~ List(${typeStr(t2.elem)})` });
return unify(t1.elem, t2.elem, subst, steps);
}
throw new Error('type mismatch: ' + s1 + ' vs ' + s2);
}
// Pretty-print a type as a human-readable string.
function typeStr(t) {
if (typeof t === 'string') return t;
if (t.tag === 'fun') {
const from = t.from.tag === 'fun' ? '(' + typeStr(t.from) + ')' : typeStr(t.from);
return from + ' → ' + typeStr(t.to);
}
if (t.tag === 'list') return '[' + typeStr(t.elem) + ']';
return '?';
}
// Infer the type of an expression given an environment of type schemes.
// Returns the inferred type and the updated substitution.
function infer(expr, env, subst, steps) {
if (expr.tag === 'var') {
const t = env[expr.name];
if (!t) throw new Error('unbound variable: ' + expr.name);
// Instantiate a polymorphic type scheme with fresh type variables.
const inst = instantiate(t);
steps.push({ action: 'var', detail: `${expr.name} : ${typeStr(inst)}` });
return { type: inst, subst };
}
if (expr.tag === 'lam') {
const a = freshVar();
const newEnv = Object.assign({}, env, { [expr.param]: { vars: [], body: a } });
steps.push({ action: 'λ-intro', detail: `λ${expr.param}. assume ${expr.param}:${a}` });
const { type: bt, subst: s2 } = infer(expr.body, newEnv, subst, steps);
const lamType = { tag: 'fun', from: applySubst(s2, a), to: bt };
return { type: lamType, subst: s2 };
}
if (expr.tag === 'app') {
const { type: ft, subst: s1 } = infer(expr.fun, env, subst, steps);
const { type: at, subst: s2 } = infer(expr.arg, env, s1, steps);
const r = freshVar();
steps.push({ action: 'app', detail: `${typeStr(applySubst(s2, ft))} applied to ${typeStr(at)}, result ${r}` });
const s3 = unify(applySubst(s2, ft), { tag: 'fun', from: at, to: r }, s2, steps);
return { type: applySubst(s3, r), subst: s3 };
}
throw new Error('unknown expression node');
}
// Generalize a type over variables not free in the environment (let-polymorphism).
function freeVarsEnv(env) {
const s = new Set();
for (const k in env) freeVars(env[k].body).forEach(v => env[k].vars.includes(v) || s.add(v));
return s;
}
function generalize(type, env) {
const envFree = freeVarsEnv(env);
const free = [...freeVars(type)].filter(v => !envFree.has(v));
return { vars: free, body: type };
}
function instantiate(scheme) {
const mapping = {};
scheme.vars.forEach(v => { mapping[v] = freshVar(); });
return applySubst(mapping, scheme.body);
}
// Preset lambda expressions with their AST representations.
const PRESETS = {
identity: {
label: '\\u03bbx. x',
expr: { tag: 'lam', param: 'x', body: { tag: 'var', name: 'x' } }
},
const: {
label: '\\u03bbx. \\u03bby. x',
expr: { tag: 'lam', param: 'x', body: { tag: 'lam', param: 'y', body: { tag: 'var', name: 'x' } } }
},
apply: {
label: '\\u03bbf. \\u03bbx. f x',
expr: {
tag: 'lam', param: 'f', body: {
tag: 'lam', param: 'x', body: {
tag: 'app',
fun: { tag: 'var', name: 'f' },
arg: { tag: 'var', name: 'x' }
}
}
}
},
compose: {
label: '\\u03bbf. \\u03bbg. \\u03bbx. f (g x)',
expr: {
tag: 'lam', param: 'f', body: {
tag: 'lam', param: 'g', body: {
tag: 'lam', param: 'x', body: {
tag: 'app',
fun: { tag: 'var', name: 'f' },
arg: {
tag: 'app',
fun: { tag: 'var', name: 'g' },
arg: { tag: 'var', name: 'x' }
}
}
}
}
}
},
twice: {
label: '\\u03bbf. \\u03bbx. f (f x)',
expr: {
tag: 'lam', param: 'f', body: {
tag: 'lam', param: 'x', body: {
tag: 'app',
fun: { tag: 'var', name: 'f' },
arg: {
tag: 'app',
fun: { tag: 'var', name: 'f' },
arg: { tag: 'var', name: 'x' }
}
}
}
}
}
};
function runInference() {
varCounter = 0;
const key = document.getElementById('preset').value;
const preset = PRESETS[key];
document.getElementById('expr-display').textContent = preset.label;
document.getElementById('result-row').style.display = 'none';
document.getElementById('steps-header').style.display = 'none';
const stepsList = document.getElementById('steps');
stepsList.innerHTML = '';
const statusEl = document.getElementById('status');
statusEl.textContent = '';
statusEl.className = 'status';
try {
const steps = [];
const { type, subst } = infer(preset.expr, {}, {}, steps);
const finalType = applySubst(subst, type);
// Generalize the final type to a polymorphic scheme.
const scheme = generalize(finalType, {});
let typeDisplay = typeStr(finalType);
if (scheme.vars.length > 0) typeDisplay = '∀' + scheme.vars.join(' ') + '. ' + typeDisplay;
document.getElementById('result-type').textContent = typeDisplay;
document.getElementById('result-row').style.display = 'flex';
document.getElementById('steps-header').style.display = 'block';
steps.forEach(s => {
const li = document.createElement('li');
li.innerHTML = '<span class="action">' + s.action + '</span> <span class="unify">' + s.detail + '</span>';
stepsList.appendChild(li);
});
statusEl.textContent = i18nFmt('Done in %n% step(s).', { n: steps.length });
statusEl.className = 'status ok';
} catch (e) {
statusEl.textContent = 'Type error: ' + e.message;
statusEl.className = 'status err';
}
}
function resetAll() {
varCounter = 0;
document.getElementById('expr-display').textContent = '';
document.getElementById('result-row').style.display = 'none';
document.getElementById('steps-header').style.display = 'none';
document.getElementById('steps').innerHTML = '';
const statusEl = document.getElementById('status');
statusEl.textContent = '';
statusEl.className = 'status';
}
document.getElementById('btn-infer').addEventListener('click', runInference);
document.getElementById('btn-reset').addEventListener('click', resetAll);
// Show the identity function expression on load.
document.getElementById('preset').value = 'identity';
document.getElementById('expr-display').textContent = PRESETS['identity'].label;
Notice how the same function can be used at different types in different contexts — this is parametric polymorphism. The inferred type uses variables like α and β to stand for any concrete type, and a single inference run produces the most general answer, not just one instance of it.
Comments
Loading comments...