On this page

Map fusion

Specification

Let f:α→βf : \alpha \to \beta and g:β→γg : \beta \to \gamma be pure functions. Write ε\varepsilon for the empty list, a::xa::x for prepending aa to xx, and ∣x∣|x| for the length of xx.

Definition 1 (Output specification).

For finite lists x=[x1,…,xn]∈List⁡(α)x=[x_1,\ldots,x_n]\in\operatorname{List}(\alpha) and y∈List⁡(γ)y\in\operatorname{List}(\gamma), the specification predicate Sf,g(x,y)\mathcal{S}_{f,g}(x,y) means that yy preserves the length and order of xx and replaces each element xix_i with g(f(xi))g(f(x_i)):

Sf,g(x,y)=def(∣y∣=∣x∣  ∧  ∀i∈{1,…,n},  yi=g(f(xi))).\mathcal{S}_{f,g}(x,y) \overset{\mathrm{def}}{=} \Bigl(|y| = |x| \;\land\; \forall i \in \{1,\ldots,n\},\; y_i = g(f(x_i))\Bigr).

In Lean, Specification f g xs ys expresses this relation inductively: nil relates the two empty lists, and cons relates their corresponding heads and tails. Here xs and ys denote xx and yy.

Verification/Specification.lean:6–9
inductive Specification (f : α → β) (g : β → γ) : List α → List γ → Prop
  | nil : Specification f g [] []
  | cons (x : α) (xs : List α) (ys : List γ) :
      Specification f g xs ys → Specification f g (x :: xs) (g (f x) :: ys)

Example

Take α=β=γ=N\alpha=\beta=\gamma=\mathbb{N}, f(t)=t+1f(t)=t+1, g(t)=2tg(t)=2t, and x=[1,2,3]x=[1,2,3]. The required output is y=[4,6,8]y=[4,6,8]. The two implementations compute it as follows:

Version A:[1,2,3]→map⁡(f)[2,3,4]→map⁡(g)[4,6,8],Version B:[1,2,3]→map⁡(g∘f)[4,6,8].\begin{aligned} \text{Version A:}\quad &[1,2,3] \xrightarrow{\operatorname{map}(f)}[2,3,4] \xrightarrow{\operatorname{map}(g)}[4,6,8],\\ \text{Version B:}\quad &[1,2,3] \xrightarrow{\operatorname{map}(g\circ f)}[4,6,8]. \end{aligned}

Implementations and correctness

Version A uses two maps; Version B composes the functions within one map:

Af,g(x)=map⁡(g,map⁡(f,x)),Bf,g(x)=map⁡(g∘f,x).A_{f,g}(x) = \operatorname{map}(g,\operatorname{map}(f,x)), \qquad B_{f,g}(x) = \operatorname{map}(g\circ f,x).
VersionA.lean:6–7
def versionA (f : α → β) (g : β → γ) (xs : List α) : List γ :=
  (xs.map f).map g

Theorem 2 (Correctness and equivalence).

For every x∈List⁡(α)x\in\operatorname{List}(\alpha), both implementations satisfy the specification and produce the same output:

Sf,g(x,Af,g(x))  ∧  Sf,g(x,Bf,g(x)),Af,g(x)=Bf,g(x).\mathcal{S}_{f,g}(x,A_{f,g}(x)) \;\land\; \mathcal{S}_{f,g}(x,B_{f,g}(x)), \qquad A_{f,g}(x)=B_{f,g}(x).

Proof.

Satisfaction. Induction on xx proves versionA_correct and versionB_correct. The empty case uses Specification.nil; the nonempty case uses Specification.cons and the induction hypothesis.

versionA_correct

Uniqueness. The specification determines one output:

(Sf,g(x,y)∧Sf,g(x,z))⇒y=z.\bigl(\mathcal{S}_{f,g}(x,y)\land\mathcal{S}_{f,g}(x,z)\bigr) \Rightarrow y=z.

Specification.unique

Equivalence. Apply uniqueness to the two satisfaction proofs:

Verification/Equivalence.lean:26–28
theorem versionA_eq_versionB (f : α → β) (g : β → γ) (xs : List α) :
    versionA f g xs = versionB f g xs := by
  exact Specification.unique (versionA_correct f g xs) (versionB_correct f g xs)
□

Traversal cost

Definition 3 (Traversal cost).

Assign one unit of cost to each nonempty list cell visited by a map:

Cmap(ε)=def0,Cmap(a::x)=def1+Cmap(x).C_{\mathrm{map}}(\varepsilon)\overset{\mathrm{def}}{=}0,\qquad C_{\mathrm{map}}(a::x)\overset{\mathrm{def}}{=}1+C_{\mathrm{map}}(x).

countedMap f xs returns a pair (output,cost)(\text{output},\text{cost}). Its second component implements this recurrence: zero for [], and cost + 1 for x :: xs.

Verification/Performance.lean:7–11
def countedMap (f : α → β) : List α → List β × Nat
  | [] => ([], 0)
  | x :: xs =>
    let (ys, cost) := countedMap f xs
    (f x :: ys, cost + 1)

The counted executions versionACounted and versionBCounted follow the two implementations. A adds the costs of its two maps; B counts one composed map.

Verification/Performance.lean:21–24
def versionACounted (f : α → β) (g : β → γ) (xs : List α) : List γ × Nat :=
  let (ys, firstCost) := countedMap f xs
  let (zs, secondCost) := countedMap g ys
  (zs, firstCost + secondCost)

Write π2\pi_2 for the second component of a pair (.2 in Lean). The costs below refer precisely to these executions:

CA(x)=defπ2(versionACounted  f  g  x),CB(x)=defπ2(versionBCounted  f  g  x).\begin{aligned} C_A(x)&\overset{\mathrm{def}}{=}\pi_2(\mathtt{versionACounted}\;f\;g\;x),\\ C_B(x)&\overset{\mathrm{def}}{=}\pi_2(\mathtt{versionBCounted}\;f\;g\;x). \end{aligned}

Lemma 4 (One map).

A counted map returns the ordinary map result and visits exactly ∣x∣|x| cells:

countedMap  f  x=(map⁡(f,x),∣x∣).\mathtt{countedMap}\;f\;x=(\operatorname{map}(f,x),|x|).

Proof.

Induction on xx: the empty case has cost zero; the nonempty case adds one visit. This is countedMap_spec:

countedMap_spec

□

Theorem 5 (Exact traversal costs).

The counted executions preserve the original outputs and record their costs:

versionACounted  f  g  x=(Af,g(x),2∣x∣),versionBCounted  f  g  x=(Bf,g(x),∣x∣).\begin{aligned} \mathtt{versionACounted}\;f\;g\;x&=(A_{f,g}(x),2|x|),\\ \mathtt{versionBCounted}\;f\;g\;x&=(B_{f,g}(x),|x|). \end{aligned}

In particular, CA(x)=2∣x∣C_A(x)=2|x| and CB(x)=∣x∣C_B(x)=|x|.

Proof.

Apply countedMap_spec twice for A and once for B. Mapping preserves list length. The two conjuncts of implementation_costs are exactly the two equalities above:

implementation_costs

□

For n=∣x∣n=|x|, these counts imply CA(x)−CB(x)=nC_A(x)-C_B(x)=n and CB(x)≤CA(x)C_B(x)\le C_A(x), with strict inequality exactly when n>0n>0. Both have Θ(n)\Theta(n) traversal cost. The model excludes function evaluation, allocation, and compiler optimizations; it counts list-cell visits, not measured runtime or memory usage.

Map fusion — CloK