Syntax of Λs #
Kennedy's Λu attaches units to scalars. Λs distributes them over an index type, so the primitive is a space and scalars are the one-point case.
Representation choices #
Three, all forced by wanting a decidable checker rather than chosen for elegance.
Unit expressions are exponent vectors, not trees. A unit expression with k
unit variables in scope is a Term B (Fin k): the very structure
LambdaS.Unify solves systems over. Units form a free abelian group, so a
normal form is an exponent vector, and equality is vector equality with no
normalization pass to write or verify. m * s / m and s are literally the
same object.
Scope is a type index. Ty B D j k and Tm B D j k carry the numbers of
enclosing ∀δ and ∀u:d binders, so well-scopedness is a typing invariant
rather than a side condition, and substitution has nowhere to go wrong.
A space is the list of units its components carry. This is LambdaS.Space
at a finite index type, written structurally: a space is its unit
assignment. Ordering is the indexing: [m, s] and [s, m] are isomorphic but
not equal, because a linear map's entry (j,i) carries δ_W(j)/δ_V(i) and
identifying them would let a matrix's columns permute silently.
Explicit typing, and why it matters here #
Λu.e and e[μ] are written, not inferred, exactly as in Kennedy's Λu. So
the checker needs substitution and decidable equality but never unification,
types stay unique, and infer is provably complete as well as sound. The
unification development in LambdaS.Unify is for a future inference engine,
where let-generalization forces it; nothing here depends on it.
Unit expressions #
A unit expression with k unit variables in scope.
Equations
- LambdaS.UExp B k = LambdaS.Term B (Fin k)
Instances For
Equations
- s.instDecidableEqOfFintype t = decidable_of_iff (s.base = t.base ∧ s.vars = t.vars) ⋯
The trivial unit.
Equations
- LambdaS.Term.one = { base := fun (x : B) => 0, vars := fun (x : V) => 0 }
Instances For
Equations
- LambdaS.Term.instOne = { one := LambdaS.Term.one }
A base unit as a unit expression.
Equations
Instances For
A unit variable as a unit expression.
Equations
Instances For
Substitute σ for de Bruijn unit variable 0, discharging one binder.
Linear in the exponents, because substitution into an element of a free abelian group is a linear map.
Equations
Instances For
Simultaneous substitution #
Single-variable substitution is not closed under going beneath a binder: pushing
σ into ∀u. τ must shift σ past the new variable and leave the bound one
alone. The standard fix is to define everything from a simultaneous
substitution, and to define single substitution and weakening as instances of it.
Λs gets that cheaply. Simultaneous substitution is a linear map on exponent
vectors, so its identity, composition and homomorphism laws are Finset
arithmetic rather than structural inductions. Only the walk over types is
structural, and there the sole interesting case is the quantifier.
Substitute a unit expression for every unit variable at once.
Equations
Instances For
Extend a substitution under a binder: the bound variable maps to itself and everything else is shifted past it. This clause is the whole content of the de Bruijn discipline.
Equations
- LambdaS.liftU η i = Fin.cases (LambdaS.Term.ofVar 0) (fun (i : Fin k) => (η i).weaken) i
Instances For
Spaces #
A space with k unit variables in scope: the list of units carried by
its components. The index type is Fin V.length, the assignment is V.get.
Equations
- LambdaS.Sp B k = List (LambdaS.UExp B k)
Instances For
The dual space carries reciprocal units.
Equations
Instances For
The bridge to the model #
Hart's classification (LambdaS.Map) is developed over units of the model,
Uom B, and spaces of the model, Space B I. A closed unit expression is a
unit of the model outright (the same exponent vector), a closed space is a
space of the model indexed by position, and the unit the row at codomain unit
w carries at component i under T-MCons is the model's entry.
A closed unit expression as a unit of the model: its exponent vector.
Equations
- t.toUom = LambdaS.Uom.ofExp t.base
Instances For
The embedding is injective: with no unit variables in scope, a unit expression is its exponent vector.
The unit of entry (j, i) of a matrix at Lin V W, read off the two
spaces: W_j / V_i.
Equations
- LambdaS.linEntry V W j i = LambdaS.Term.div (List.get W j) (List.get V i)
Instances For
The calculus's entry units are the model's. The unit Lin V W
assigns entry (j, i) is entry of LambdaS.Map at the two spaces, so
Hart's classification, developed in the model, is about the matrices the
calculus types.
Dimensions #
A dimension expression has exactly the structure a unit expression does: the
dimension group is free abelian on base dimensions, just as the unit group is
free abelian on base units. So DExp is UExp at the dimension alphabet, and
every operation (multiplication, division, rational powers, substitution,
weakening, decidable equality) is reused rather than rebuilt.
dimension Velocity = Length/Time is therefore Term.div, an abbreviation with
nothing left to check. Unit definitions are the interesting case and live in
LambdaS.Declare, because unit yard = 3 foot declares a generator and an
equation, and equations can conflict.
Why dimension variables #
The alternative is two unit quantifiers, ∀u and ∀u:d. It does not work. An
unbounded ∀u still has to say something about u's dimension, and there is
no right answer: reporting the trivial dimension claims u is dimensionless, so
convert x u 1 typechecks under a binder that promised nothing; reporting
"unknown" makes dimOf partial and threads Option through every dimension
lemma.
Abstracting the dimension is the answer, and it collapses the two quantifiers into one:
∀u. τ ≡ ∀δ. ∀u:δ. τ
Unbounded quantification is bounded quantification at a dimension variable.
dimOf stays total, convert under ∀u is rejected because δ matches
nothing, and there is a single quantifier rule to state and prove.
A dimension expression with j dimension variables in scope. Literally a
unit expression over base dimensions: the two are the same free abelian group
construction, so UExp's whole API applies.
Equations
- LambdaS.DExp D j = LambdaS.UExp D j
Instances For
A unit system: the dimension each base unit measures.
dim need not be injective: meter and foot share a dimension, which is what
lets Λs avoid restricting compound units to one named unit per dimension. Nor
need it land on a generator: joule can be declared directly at
Mass·Length²·Time⁻² rather than needing a base dimension of its own, which is
what makes dimension definitions worth having.
Base units have closed dimensions: a generator's dimension cannot mention a dimension variable, since those are bound by types.
- dim : B → DExp D 0
The dimension each base unit measures.
Instances
A dimension context: the declared dimension of each unit variable in
scope. ∀u:d. τ extends it by d.
Equations
- LambdaS.DCtx D j k = (Fin k → LambdaS.DExp D j)
Instances For
The empty context: a closed term has no unit variables in scope.
Equations
- LambdaS.DCtx.nil D i = i.elim0
Instances For
Extend under a dimension binder: every declared dimension is weakened into the larger dimension scope.
Equations
- Δ.weakenDim i = LambdaS.UExp.weaken (Δ i)
Instances For
The dimension of a unit expression, relative to a dimension context.
Linear in the exponent vector: a matrix product, with the naive "each base unit has one base dimension" the special case where every column is a standard basis vector. Base units contribute only to base dimensions; the dimension variables of the result come entirely from the context.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Two units are interchangeable when they have the same dimension.
Note what is not here. Before dimension contexts this also demanded
u.vars = v.vars, a conservative stand-in for not knowing a unit variable's
dimension. With Δ supplying it the clause is gone, and convert works under a
bounded quantifier, which is what makes a dimension-typed quantity usable.
Equations
- LambdaS.SameDim Δ u v = (LambdaS.dimOf Δ u = LambdaS.dimOf Δ v)
Instances For
Equations
Types #
Types of Λs, indexed by the number of dimension variables and the number of unit variables in scope.
Six formers, one unit quantifier. ∀u. τ is not primitive: it is
∀δ. ∀u:δ. τ, and the free theorems that need a genuinely unconstrained unit
get one by abstracting its dimension.
- Q
{B D : Type}
{j k : ℕ}
: UExp B k → Ty B D j k
A scalar quantity carrying a unit. The one-point space.
- arrow {B D : Type} {j k : ℕ} : Ty B D j k → Ty B D j k → Ty B D j k
- vec
{B D : Type}
{j k : ℕ}
: Sp B k → Ty B D j k
A vector over a space.
- lin
{B D : Type}
{j k : ℕ}
: Sp B k → Sp B k → Ty B D j k
A linear map
V ⊸ W. Entry(j,i)carriesδ_W(j) / δ_V(i), which is Hart's rank-one condition holding by construction. - all
{B D : Type}
{j k : ℕ}
: DExp D j → Ty B D j (k + 1) → Ty B D j k
Unit polymorphism bounded by a dimension,
∀u:d. τ. - allDim
{B D : Type}
{j k : ℕ}
: Ty B D (j + 1) k → Ty B D j k
Dimension polymorphism,
∀δ. τ. Together withallthis is unbounded unit quantification.
Instances For
Push a unit substitution and a dimension substitution through a type.
Every other substitution on types is an instance of this one. The quantifier
cases are the only interesting clauses, and they are where the earlier
definitions were wrong: going under ∀u:d must map the bound variable to
itself and shift everything else past it, which is exactly liftU.
Equations
- LambdaS.Ty.ground x✝¹ x✝ (LambdaS.Ty.Q u) = LambdaS.Ty.Q (LambdaS.substU x✝¹ u)
- LambdaS.Ty.ground x✝¹ x✝ (a.arrow b) = (LambdaS.Ty.ground x✝¹ x✝ a).arrow (LambdaS.Ty.ground x✝¹ x✝ b)
- LambdaS.Ty.ground x✝¹ x✝ (LambdaS.Ty.vec V) = LambdaS.Ty.vec (List.map (LambdaS.substU x✝¹) V)
- LambdaS.Ty.ground x✝¹ x✝ (LambdaS.Ty.lin V W) = LambdaS.Ty.lin (List.map (LambdaS.substU x✝¹) V) (List.map (LambdaS.substU x✝¹) W)
- LambdaS.Ty.ground x✝¹ x✝ (LambdaS.Ty.all d τ) = LambdaS.Ty.all (LambdaS.substU x✝ d) (LambdaS.Ty.ground (LambdaS.liftU x✝¹) x✝ τ)
- LambdaS.Ty.ground x✝¹ x✝ τ.allDim = (LambdaS.Ty.ground x✝¹ (LambdaS.liftU x✝) τ).allDim
Instances For
Substitute a unit expression for the outermost unit variable.
Equations
- τ.subst σ = LambdaS.Ty.ground (Fin.cons σ (LambdaS.idU B k)) (LambdaS.idU D j) τ
Instances For
Weaken into a larger unit-variable scope.
Equations
- τ.weaken = LambdaS.Ty.ground (fun (i : Fin k) => LambdaS.Term.ofVar i.succ) (LambdaS.idU D j) τ
Instances For
Substitute a dimension expression for the outermost dimension variable. Units are untouched: a unit expression cannot mention a dimension variable.
Equations
- τ.substDim σ = LambdaS.Ty.ground (LambdaS.idU B k) (Fin.cons σ (LambdaS.idU D j)) τ
Instances For
Weaken into a larger dimension-variable scope.
Equations
- τ.weakenDim = LambdaS.Ty.ground (LambdaS.idU B k) (fun (i : Fin j) => LambdaS.Term.ofVar i.succ) τ
Instances For
Decidable equality, by structural recursion. Written by hand rather than
derived: the deriving handler cannot carry the [Fintype B] constraint that
DecidableEq (UExp B k) needs.
Equations
- (LambdaS.Ty.Q u).decEq (LambdaS.Ty.Q v) = decidable_of_iff (u = v) ⋯
- (LambdaS.Ty.vec V).decEq (LambdaS.Ty.vec W) = decidable_of_iff (V = W) ⋯
- (LambdaS.Ty.lin V W).decEq (LambdaS.Ty.lin V' W') = decidable_of_iff (V = V' ∧ W = W') ⋯
- (a.arrow b).decEq (c.arrow d) = match a.decEq c, b.decEq d with | isTrue h1, isTrue h2 => isTrue ⋯ | isFalse h1, x => isFalse ⋯ | x, isFalse h2 => isFalse ⋯
- a.allDim.decEq b.allDim = match a.decEq b with | isTrue h => isTrue ⋯ | isFalse h => isFalse ⋯
- (LambdaS.Ty.all d a).decEq (LambdaS.Ty.all e b) = if hde : d = e then match a.decEq b with | isTrue h => isTrue ⋯ | isFalse h => isFalse ⋯ else isFalse ⋯
- (LambdaS.Ty.Q a).decEq (a_1.arrow a_2) = isFalse ⋯
- (LambdaS.Ty.Q a).decEq (LambdaS.Ty.vec a_1) = isFalse ⋯
- (LambdaS.Ty.Q a).decEq (LambdaS.Ty.lin a_1 a_2) = isFalse ⋯
- (LambdaS.Ty.Q a).decEq (LambdaS.Ty.all a_1 a_2) = isFalse ⋯
- (LambdaS.Ty.Q a).decEq a_1.allDim = isFalse ⋯
- (a.arrow a_1).decEq (LambdaS.Ty.Q a_2) = isFalse ⋯
- (a.arrow a_1).decEq (LambdaS.Ty.vec a_2) = isFalse ⋯
- (a.arrow a_1).decEq (LambdaS.Ty.lin a_2 a_3) = isFalse ⋯
- (a.arrow a_1).decEq (LambdaS.Ty.all a_2 a_3) = isFalse ⋯
- (a.arrow a_1).decEq a_2.allDim = isFalse ⋯
- (LambdaS.Ty.vec a).decEq (LambdaS.Ty.Q a_1) = isFalse ⋯
- (LambdaS.Ty.vec a).decEq (a_1.arrow a_2) = isFalse ⋯
- (LambdaS.Ty.vec a).decEq (LambdaS.Ty.lin a_1 a_2) = isFalse ⋯
- (LambdaS.Ty.vec a).decEq (LambdaS.Ty.all a_1 a_2) = isFalse ⋯
- (LambdaS.Ty.vec a).decEq a_1.allDim = isFalse ⋯
- (LambdaS.Ty.lin a a_1).decEq (LambdaS.Ty.Q a_2) = isFalse ⋯
- (LambdaS.Ty.lin a a_1).decEq (a_2.arrow a_3) = isFalse ⋯
- (LambdaS.Ty.lin a a_1).decEq (LambdaS.Ty.vec a_2) = isFalse ⋯
- (LambdaS.Ty.lin a a_1).decEq (LambdaS.Ty.all a_2 a_3) = isFalse ⋯
- (LambdaS.Ty.lin a a_1).decEq a_2.allDim = isFalse ⋯
- (LambdaS.Ty.all a a_1).decEq (LambdaS.Ty.Q a_2) = isFalse ⋯
- (LambdaS.Ty.all a a_1).decEq (a_2.arrow a_3) = isFalse ⋯
- (LambdaS.Ty.all a a_1).decEq (LambdaS.Ty.vec a_2) = isFalse ⋯
- (LambdaS.Ty.all a a_1).decEq (LambdaS.Ty.lin a_2 a_3) = isFalse ⋯
- (LambdaS.Ty.all a a_1).decEq a_2.allDim = isFalse ⋯
- a.allDim.decEq (LambdaS.Ty.Q a_1) = isFalse ⋯
- a.allDim.decEq (a_1.arrow a_2) = isFalse ⋯
- a.allDim.decEq (LambdaS.Ty.vec a_1) = isFalse ⋯
- a.allDim.decEq (LambdaS.Ty.lin a_1 a_2) = isFalse ⋯
- a.allDim.decEq (LambdaS.Ty.all a_1 a_2) = isFalse ⋯
Instances For
Weakening then grounding by an extended environment ignores the extension: the weakened type does not mention the new variable. This is what lets a context captured outside a unit binder be reused inside it.
The quantifier case. Grounding under a binder and then substituting the instantiating unit is the same as grounding with the environment extended by it.
This is the lemma the uapp case of soundness turns on, and it is where the
corrected liftU earns its place.
Grounding a single substitution: the same as grounding with the environment
extended by the grounded instantiation. This is what turns uapp's result type
into something the closure's own environment can produce.
The skeleton #
Erase every unit and dimension and a type becomes a plain simple type. Nothing in
the calculus's shape depends on units, which is why substituting one leaves the
skeleton alone. That is what makes the normalization argument in
LambdaS.Normalization Tait's rather than Girard's.
The size of a type's skeleton.
Equations
Instances For
Substituting into a weakened type changes nothing. The type-level statement that a unit binder binds a genuinely fresh variable.
With subst and weaken both built from ground, this is composition plus the
identity law rather than a structural induction of its own.
Terms #
Terms of Λs, with de Bruijn indices for value variables and type indices for the dimension- and unit-variable scopes.
- var {B D : Type} {j k : ℕ} : ℕ → Tm B D j k
- lam {B D : Type} {j k : ℕ} : Ty B D j k → Tm B D j k → Tm B D j k
- app {B D : Type} {j k : ℕ} : Tm B D j k → Tm B D j k → Tm B D j k
- lit
{B D : Type}
{j k : ℕ}
: ℚ → Tm B D j k
Every literal is dimensionless. There is no unitless type, only
1. - ucon
{B D : Type}
{j k : ℕ}
: UExp B k → Tm B D j k
A unit constant, so
m : Q m. This is what makes1.3 mwork. - mul {B D : Type} {j k : ℕ} : Tm B D j k → Tm B D j k → Tm B D j k
- div {B D : Type} {j k : ℕ} : Tm B D j k → Tm B D j k → Tm B D j k
- add
{B D : Type}
{j k : ℕ}
: Tm B D j k → Tm B D j k → Tm B D j k
Addition, where unit errors are caught.
- pow {B D : Type} {j k : ℕ} : ℚ → Tm B D j k → Tm B D j k
- idx {B D : Type} {j k : ℕ} : Tm B D j k → ℕ → Tm B D j k
- mrow {B D : Type} {j k : ℕ} : Tm B D j k → ℕ → Tm B D j k
- ifle
{B D : Type}
{j k : ℕ}
: Tm B D j k → Tm B D j k → Tm B D j k → Tm B D j k → Tm B D j k
Compare and branch, fused so that no
Booltype is needed. The scrutinees are compared at a common unit, which is what keeps the form parametric: a rescaling multiplies both by the same positive factor and the ordering survives (Num.OrderedNum.le_scale). Comparing across units does not typecheck, so the observation that could detect a rescaling is unreachable by construction. - mapp {B D : Type} {j k : ℕ} : Tm B D j k → Tm B D j k → Tm B D j k
- comp {B D : Type} {j k : ℕ} : Tm B D j k → Tm B D j k → Tm B D j k
- vnil
{B D : Type}
{j k : ℕ}
: Tm B D j k
The empty vector, at the empty space.
- vcons
{B D : Type}
{j k : ℕ}
: Tm B D j k → Tm B D j k → Tm B D j k
A scalar consed onto a vector: the vector introduction step. The unit of the new component is read off the scalar's type, so the constructor carries no annotation.
- mnil
{B D : Type}
{j k : ℕ}
: Sp B k → Tm B D j k
The zero-row matrix, carrying its column space. The annotation is the design point: a matrix with no rows still has a width, and nothing else could supply it.
- mcons
{B D : Type}
{j k : ℕ}
: UExp B k → Tm B D j k → Tm B D j k → Tm B D j k
A row consed onto a matrix: output unit
w, a row (a vector term whose components carryw / δ_V(i)), and the rest of the matrix. The annotationwis needed because a row over an empty column space determines no output unit. - log
{B D : Type}
{j k : ℕ}
: Tm B D j k → Tm B D j k
Logarithm. Requires a dimensionless argument, which is what makes the base-measure problem a type error: a probability density is not dimensionless, so
log pdoes not typecheck. - exp
{B D : Type}
{j k : ℕ}
: Tm B D j k → Tm B D j k
Exponential. Also requires a dimensionless argument.
This is what makes
exp (-i·E·t/ħ)a typed statement: the phase of a quantum time evolution must be dimensionless, and the rule enforces it. - ulam
{B D : Type}
{j k : ℕ}
: DExp D j → Tm B D j (k + 1) → Tm B D j k
Unit abstraction,
Λu:d. e. - uapp
{B D : Type}
{j k : ℕ}
: Tm B D j k → UExp B k → Tm B D j k
Unit application,
e[μ]. Checks thatμhas the declared dimension. - dlam
{B D : Type}
{j k : ℕ}
: Tm B D (j + 1) k → Tm B D j k
Dimension abstraction,
Λδ. e. - dapp
{B D : Type}
{j k : ℕ}
: Tm B D j k → DExp D j → Tm B D j k
Dimension application,
e{d}. - convert
{B D : Type}
{j k : ℕ}
: Tm B D j k → UExp B k → UExp B k → Tm B D j k
Conversion,
e in v, writtenconvert e u vwith the source unitucarried explicitly.A term constructor rather than sugar for a multiplication, so that the trusted core checks the dimensions and determines the factor. Under the elaboration alternative a wrong factor would still typecheck, putting the Mars Climate Orbiter failure outside the trusted boundary.
This is the only form that can observe a unit, and so the only form that costs parametricity: a term containing it is scale-invariant for coherent scalings rather than for all of them. See
LambdaS.Conversion.
Instances For
A typing context.
Equations
- LambdaS.Ctx B D j k = List (LambdaS.Ty B D j k)