Lectures onType Theory
Chapter 85
Chapter 85Core route

Coinduction, Copatterns, and Bisimulation

A finite list can be inspected until a constructor closes it. A stream has no last constructor to reach. Its correctness question is instead finite: after any requested number of tail observations, can the next head be produced? A definition that answers every such request is productive: every finite sequence of tail observations can be followed by a head observation that returns an element.

Streams are given by observations

Fix A:Ui. The signature Tco extends T0 by one coinductive record and a guarded corecursor.

Definition 85.1 — The guarded stream fragment

A stream over A is an element of Stream(A). It is observed by two destructors: head returns the next element and tail returns the stream after that element.

ΓA:Ui
ΓStream(A):Ui
Stream-form
Γs:Stream(A)
Γhead(s):A
Head
Γs:Stream(A)
Γtail(s):Stream(A)
Tail

For a state type S:Uj, an output h:SA, a transition t:SS, and an initial state x:S, the guarded corecursor has type

ΓS:UjΓh:SAΓt:SSΓx:S
ΓcorecA(S,h,t,x):Stream(A)
Stream-corec

with judgmental observation equations (Corechead)head(corecA(S,h,t,x))h(x),(Corectail)tail(corecA(S,h,t,x))corecA(S,h,t,t(x)). The recursive stream occurs only on the right of a tail observation. This placement is the guarded-call invariant of this fragment.

The rules do not add a constructor AStream(A)Stream(A). Such a constructor would invite dependent pattern matching on coinductive data and a restricted unfolding rule for recursively defined coinductive values. The destructor presentation makes the two equations that compute into the primitive interface.

Definition 85.2 — Finite stream observations

Define nth:NStream(A)A by structural recursion on its natural-number argument: nth(0,s):=head(s),nth(suc(n),s):=nth(n,tail(s)). The observation depth of nth(n,s) is n: it requests n tails and then one head.

Theorem 85.3 — Productivity of guarded corecursion

For S,h,t,x as in definition 85.1 and every n:N, IdA(nth(n,corecA(S,h,t,x)),h(tn(x))) is inhabited, where t0(x):=x and tsuc(n)(x):=tn(t(x)). Hence every finite observation of a guarded corecursive stream is identified with an application of h at a finite state iterate.

Proof of Theorem 85.3 — Productivity of guarded corecursion

Proof. Induct on n with the state x:S quantified in the motive. At zero, the required identification is reflexivity after the judgmental calculation nth(0,corecA(S,h,t,x))definition85.2head(corecA(S,h,t,x))(Corechead)h(x). For a successor, instantiate the induction hypothesis at t(x): nth(suc(n),corecA(S,h,t,x))definition85.2nth(n,tail(corecA(S,h,t,x)))(Corectail)nth(n,corecA(S,h,t,t(x)))=IH(t(x))h(tn(t(x)))iteratedefinitionh(tsuc(n)(x)). The middle relation is the identification obtained from the induction hypothesis. Each closed numeral instance is judgmental by repeated use of (Corec-tail) followed by (Corec-head); the theorem for a neutral n is propositional because structural recursion on n is stuck. ◻

Remark 85.4 — Guardedness is sufficient, not complete

The equation s=tail(s) is not an instance of Stream-corec: no output function produces a head before the recursive transition. More subtle productive programs may also fail to have the corecursor form when productivity depends on the behavior of a higher-order argument. Thus this fragment proves productivity for terms generated by its rule; it does not describe every productive stream program.

Stream programs are state machines

The corecursor first becomes concrete at small state types.

Construction 85.5 — Iteration and stream map

For x:A and f:AA, define iterate(f,x):=corecA(A,λa.a,f,x). Equations (Corec-head) and (Corec-tail) give head(iterate(f,x))x,tail(iterate(f,x))iterate(f,f(x)). For g:AB and s:Stream(A), define map(g,s):=corecB(Stream(A),λu.g(head(u)),tail,s). It computes by observation: (Maphead)head(map(g,s))g(head(s)),(Maptail)tail(map(g,s))map(g,tail(s)).

Construction 85.6 — Zip and Fibonacci states

For f:ABC, s:Stream(A), and t:Stream(B), use the state Stream(A)×Stream(B) to define zipWith(f,s,t):=corecC(Stream(A)×Stream(B),λ(u,v).f(head(u))(head(v)),λ(u,v).(tail(u),tail(v)),(s,t)). Its two reusable computation equations are (Ziphead)head(zipWith(f,s,t))f(head(s))(head(t)),(Ziptail)tail(zipWith(f,s,t))zipWith(f,tail(s),tail(t)).

Define the Fibonacci stream by the state transition (a,b)(b,a+b): fibs:=corecN(N×N,λ(a,b).a,λ(a,b).(b,a+b),(0,1)). The first five observations are nth(0,fibs)0,nth(1,fibs)1,nth(2,fibs)1,nth(3,fibs)2,nth(4,fibs)3. Each equality is a judgmental closed calculation. Apply (Corec-tail) repeatedly to the numeral depth. Finish with (Corec-head).

The propositional theorem theorem 85.3 packages the same calculation uniformly for a variable depth.

Lemma 85.7 — Successor iterate swap

For S:Uj, t:SS, x:S, and n:N, IdS(tsuc(n)(x),t(tn(x))) is inhabited.

Proof of Lemma 85.7 — Successor iterate swap

Proof. Induct on n while quantifying over x. At zero both endpoints reduce to t(x). At suc(n), the iterate definition and the induction hypothesis at t(x) give tsuc(suc(n))(x)tsuc(n)(t(x))=IH(t(x))t(tn(t(x)))t(tsuc(n)(x)). ◻

Proposition 85.8 — Fibonacci recurrence

For every n:N, the following identity type is inhabited: IdN(nth(n+2,fibs),nth(n,fibs)+nth(n+1,fibs)).

Proof of Proposition 85.8 — Fibonacci recurrence

Proof. Let (an,bn) be the nth iterate of (a,b)(b,a+b) at (0,1). By theorem 85.3, nth(n,fibs)=an. By lemma 85.7, applying the transition to (an,bn) gives an+1=bn. The same facts give an+2=lemma85.7bn+1=transitionequationan+bn=an+1=bnan+an+1. Substitution of the three observation equations yields the result. This argument holds for arbitrary n; it is not an extrapolation from the five closed observations above. ◻

Exercise 85.1

★☆☆ Use Boolean state and negation to define the alternating stream tt,ff,tt,. Calculate observations at depths zero through four and identify the state iterate used by each calculation.

Copatterns compile observations

A pattern describes how an input was constructed. A copattern describes how a result will be observed. For streams, the primitive copatterns are head() and tail(), where marks the defined result.

Definition 85.9 — Guarded stream copattern definition

A unary stream definition by copatterns has the form head(F(x))=h(x),tail(F(x))=F(t(x)), where x:S, h:SA, and t:SS. It is complete because it gives one clause for each stream destructor. Its compilation is F(x):=corecA(S,h,t,x). The two source clauses become the two judgmental equations (Corec-head) and (Corec-tail). A unary clause system is accepted by the guarded copattern fragment if and only if it contains both displayed clauses for some S,h,t and compiles to this corecursor term. Thus “accepted” names this syntactic class rather than an unstated checking judgment.

For example, the two equations for map in construction 85.5 are a copattern definition. Compilation chooses the input stream itself as state. The right side of the tail clause is a guarded recursive call because a tail observation has been matched before the call is demanded.

Exercise 85.2

★★☆ Give copattern clauses for zipWith(f,s,t) and compile them to the corecursor using the pair state from construction 85.6. Derive both clauses from the compiled term. Then delete the tail clause and state exactly which observation is uncovered.

Bisimulation proves equality of observations

Ordinary induction on a stream cannot begin because there is no stream constructor on which to split. A first attempt can instead induct on finite observation depth. Fix s:Stream(A) and try to prove (Faileddepthinduction)P(n):=IdA(nth(n,map(λx.x,s)),nth(n,s)). The zero case reduces to reflexivity. In the successor case, the two tail equations reduce the goal to IdA(nth(n,map(λx.x,tail(s))),nth(n,tail(s))). This is not P(n): the fixed stream s has changed to tail(s). One repair quantifies over every stream. The reusable repair records instead a relation whose evidence gives equal heads and another piece of evidence after taking tails. Those two obligations determine bisimulation.

Definition 85.10 — Observational stream equality

Streams s,t:Stream(A) are observationally equal, written sAt, when sAt:=n:NIdA(nth(n,s),nth(n,t)). This is an internal family of identifications. It does not add a judgmental equation st and therefore does not make a stuck destructor compute.

A nested copattern, also called a deep copattern, places a finite composite of destructors around the defined result. The customary deep-copattern presentation of Fibonacci consists of head(fibs)=0,head(tail(fibs))=1,tail(tail(fibs))NzipWith(+,fibs,tail(fibs)). The first two clauses are judgmental closed instances of the state-machine equations. The third is only observational; it is proved in corollary 85.17 and is not a primitive unfolding equation. Deep copattern notation therefore does not enlarge definitional equality.

Remark 85.11 — No stream uniqueness rule

The signature Tco has no uniqueness or eta rule saying that a stream is equal to the corecursor determined by its observations. In this fragment, a proof of sAt therefore does not yield an inhabitant of IdStream(A)(s,t). A stream-extensionality principle can close that gap. An observational type theory extended with a stream clause defined by all finite head-and-tail observations can also close it. Neither extension is a rule of Tco.

Definition 85.12 — Stream bisimulation

A relation B:Stream(A)Stream(A)Uk is a stream bisimulation when it is equipped with functions bisimHeadB:s:Stream(A)t:Stream(A)B(s,t)IdA(head(s),head(t)),bisimTailB:s:Stream(A)t:Stream(A)B(s,t)B(tail(s),tail(t)). The first component matches the immediate observations. The second returns the same relation after one tail observation.

Theorem 85.13 — Stream coinduction

Let B be a stream bisimulation. For all s,t:Stream(A) and q:B(s,t), sAt.

Proof of Theorem 85.13 — Stream coinduction

Proof. We must construct an identification at every depth n. Induct on n while quantifying over s,t, and q:B(s,t). At zero, nth(0,s)head(s)andnth(0,t)head(t), so bisimHeadB(s,t,q) has the required type. At suc(n), the tail component gives q:=bisimTailB(s,t,q):B(tail(s),tail(t)). The induction hypothesis at q gives nth(n,tail(s))=Anth(n,tail(t)), which converts along the two successor equations of definition 85.2 to the required identification at depth suc(n). ◻

Remark 85.14 — Why the bisimulation hypotheses are exact

Deleting the head clause allows a relation between streams with different first elements. Deleting closure under tail allows a relation that matches only the first element. Each deletion therefore invalidates the corresponding case of the proof of theorem 85.13.

Exercise 85.3

★☆☆ Let s,t:Stream(A) and let B(s,t) mean only that head(s)=head(t). Give two streams related by B whose depth-one observations differ. Identify the missing premise of definition 85.12.

Theorem 85.15 — Map fusion

For g:AB, f:BC, and s:Stream(A), map(f,map(g,s))Cmap(λx.f(g(x)),s).

Proof of Theorem 85.15 — Map fusion

Proof. Define the relation by the dependent sum L(r):=map(f,map(g,r)),R(r):=map(λx.f(g(x)),r),B(u,v):=r:Stream(A)(IdStream(C)(u,L(r))×IdStream(C)(v,R(r))). The original pair belongs to B with witness s. For a pair witnessed by r, eliminate the two displayed identities. It remains to treat the canonical representatives umap(f,map(g,r)) and vmap(λx.f(g(x)),r). Their heads calculate as head(u)(Maphead)f(head(map(g,r)))(Maphead)f(g(head(r)))(Maphead)head(v). Their tails satisfy tail(u)(Maptail)map(f,map(g,tail(r))),tail(v)(Maptail)map(λx.f(g(x)),tail(r)). Thus the tail pair belongs to B with witness tail(r) and two reflexivity identifications after the displayed computations. Transporting back along the eliminated identities gives the required head and tail data for the original u,v. The relation is a bisimulation, and theorem 85.13 gives the result. ◻

Exercise 85.4

★★☆ Let s,t:Stream(A). Assume f:AAA and c:x:Ay:AIdA(f(x)(y),f(y)(x)). Put u:=zipWith(f,s,t),v:=zipWith(f,t,s). Construct a bisimulation proving uAv. State the tail witness and use c only in the head component.

Lemma 85.16 — Zip observations

For f:ABC, s:Stream(A), t:Stream(B), and n:N, the identity type IdC(nth(n,zipWith(f,s,t)),f(nth(n,s))(nth(n,t))) is inhabited.

Proof of Lemma 85.16 — Zip observations

Proof. Induct on n while quantifying over s and t. At zero, nth(0,zipWith(f,s,t))definition85.2head(zipWith(f,s,t))(Ziphead)f(head(s))(head(t))definition85.2f(nth(0,s))(nth(0,t)). At suc(n), the calculation is nth(suc(n),zipWith(f,s,t))definition85.2nth(n,tail(zipWith(f,s,t)))(Ziptail)nth(n,zipWith(f,tail(s),tail(t)))=IH(tail(s),tail(t))f(nth(n,tail(s)))(nth(n,tail(t)))definition85.2f(nth(suc(n),s))(nth(suc(n),t)). ◻

Corollary 85.17 — The feedback equation for Fibonacci

The Fibonacci stream satisfies tail(tail(fibs))NzipWith(+,fibs,tail(fibs)).

Proof of Corollary 85.17 — The feedback equation for Fibonacci

Proof. By lemma 85.16, specializing to addition, fibs, and tail(fibs) identifies the right observation at depth n with nth(n,fibs)+nth(n+1,fibs). The left observation is nth(n+2,fibs), and proposition 85.8 gives the final identity for every neutral n. ◻

The selected coinductive-record schema

Streams have one observable field and one successor field. The same proof works for finitely many fields.

Definition 85.18 — The T_ co record schema

Fix observation types O1,,Om. Fix also r recursive successor fields. The schema generates a coinductive record C with destructors oi:COi(1im),dj:CC(1jr). Given a state S, outputs hi:SOi, transitions tj:SS, and x:S, its corecursor satisfies oi(corecC(S,h¯,t¯,x))hi(x),dj(corecC(S,h¯,t¯,x))corecC(S,h¯,t¯,tj(x)). Every recursive occurrence is the complete result of a recursive destructor. Dependent observation types, nested recursive fields, mixed inductive–coinductive declarations, and higher-order guards are not in this schema.

Theorem 85.19 — Finite-observation productivity for the schema

For k:N, indices 1j1,,jkr, and 1im, abbreviate cx:=corecC(S,h¯,t¯,x). Then oi(djk(dj1(cx)))hi(tjk(tj1(x))). For k0, both finite composites are empty and the equation reads oi(cx)hi(x).

Proof of Theorem 85.19 — Finite-observation productivity for the schema

Proof. Induct on k. The empty word reduces by the oi equation. In a nonempty word, the destructor adjacent to the corecursor is dj1; its equation replaces the state x by tj1(x). Apply the induction hypothesis to the remaining word j2,,jk at that state. A finite index list is empty or has this first entry, so these are all cases. ◻

Remark 85.20 — Structural coinduction is a separate calculus

Downen and Ariola’s contextual signature has terms v, coterms e, values V, covalues E, commands ve, types generated in part by N, Stream(A), and AB, and three equality judgments under ΓΔ: command equality, value equality v=v:A, and consumer equality e=e÷A. Here e÷A means that the coterm e consumes a value of type A. Its cut congruence is the named rule

ΓΔv=v:AΓΔe=e÷A
ΓΔve=ve
Cut

A productive command property Ψ(α) has base form Vα=Vα, with α absent from V,V, and is closed under the stream rule

Γ,β÷AΔΨ[headβ/α]Γ,α÷Stream(A)Δ,Ψ(α)Ψ[tailα/α]
Γ,α÷Stream(A)ΔΨ(α)
ωStream

For a concrete instance, let u0,u1:Stream(N) have the contextual equations head(ui)=0,tail(ui)=ui(i{0,1}), and put Ψ(α):=u0α=u1α. The head premise of ωStream contracts to 0β=0β. The tail premise contracts back to Ψ(α) and is discharged by the displayed hypothesis in that premise. Thus the rule derives Ψ(α) from one immediate value equality and one guarded reuse.

The head premise of ωStream performs the same role as bisimHeadB, and its tail premise performs the same role as bisimTailB. The contextual rule phrases those obligations as command equality under a reusable hypothesis; definition 85.12 phrases them as destructor observations of a relation witness. Their Theorem 5.21 proves semantic soundness of the extensional logic for both call-by-value and call-by-name. Theorem 5.23 concludes, separately, that derivable command, value, and covalue equalities imply their corresponding observational equivalences. The stronger strategy-specific logics use the separate soundness statement of Theorem 5.28. None of these results proves a guarded-corecursor or copattern-coverage theorem for Tco.

Bounded sized approximants

Fix Size:Ui and a relation <s. The optional signature Tsize describes observations bounded by a supplied descending size chain. Its rules do not postulate a distinguished size with arbitrarily many available tails, so they describe sized approximants rather than asserting an infinite-stream object.

Definition 85.21 — Sized approximant formation and observations

For A:Uj and α:Size,

ΓA:UjΓα:Size
ΓStreamα(A):Uj
Sized-Stream-form
Γs:Streamα(A)
Γheadα(s):A
Sized-head
Γr:β<sαΓs:Streamα(A)
Γtailα,β(r,s):Streamβ(A)
Sized-tail

Observation equality is judgmental in Tsize; no coercion or equality between approximants at different sizes is silently inserted.

Definition 85.22 — Sized approximant builder

Let S:SizeUk. Given h:α:SizeS(α)A,t:α:Sizeβ:Sizeβ<sαS(α)S(β), the corecursor rule is

Γx:S(α)
ΓcorecAα(S,h,t,x):Streamα(A)
Sized-corec

It is governed by (Sizedcorechead)headα(corecAα(S,h,t,x))h(α,x),(Sizedcorectail)tailα,β(r,corecAα(S,h,t,x))corecAβ(S,h,t,t(α,β,r,x)). The only recursively produced approximant in the second equation is indexed by r:β<sα, which is its decrease certificate. Deleting r would define a different, unsupported signature, while adding a distinguished infinity size or size-weakening operation would require additional rules not present here.

Construction 85.23 — Map on sized approximants

For f:AB and s:Streamα(A), take S(γ):=Streamγ(A), h(γ,u):=f(headγ(u)), and t(γ,δ,r,u):=tailγ,δ(r,u). Then mapα(f,s):=corecBα(S,h,t,s):Streamα(B). The two corecursor equations calculate to headα(mapα(f,s))f(headα(s)),tailα,β(r,mapα(f,s))mapβ(f,tailα,β(r,s)). Thus map is constructed from the displayed corecursor rather than postulated as a second recursive operation.

Theorem 85.24 — Bounded observation calculation

Let αn<s<sα1<sα0 be witnessed by rq:αq+1<sαq. Starting at x0:S(α0), define xq+1:=t(αq,αq+1,rq,xq). Applying the corresponding n tails and then headαn to corecAα0(S,h,t,x0) reduces judgmentally to h(αn,xn).

Proof of Theorem 85.24 — Bounded observation calculation

Proof. Induct on the length of the displayed chain. At zero, the calculation is headα0(corecAα0(S,h,t,x0))(Sizedcorechead)h(α0,x0). A successor chain first uses (Sized-corec-tail). The resulting term is corecAα1(S,h,t,x1). The induction hypothesis applies to its shorter tail. The calculation covers exactly the observations backed by the displayed finite chain. If <s is well founded, accessibility rules out any one infinite sequence of successive tail observations. It does not impose a uniform finite bound on the depths reachable from a fixed size: at an ω-like size, every finite depth may be reachable along its own finite chain. No infinite-stream productivity conclusion follows from the bounded calculation alone. ◻

For finite sizes 0^<s1^<s2^<s3^, let S(γ):=N, h(γ,x):=x, and t(γ,δ,r,x):=x+x. Construct s:=corecN3^(S,h,t,2):Stream3^(N). Its successive heads along the displayed descent are 2,4,8,16. At the first depth, the map calculation is head3^(map3^(suc,s))construction85.23suc(head3^(s))(Sizedcorechead)suc(2)natural-number computation3. One, two, and three applications of (Sized-corec-tail) give the corresponding mapped heads 5, 9, and 17. This is a mathematical sized approximant calculation, not an infinite stream and not a theorem about every implementation named “sized types.”

Abel and Pientka’s calculus combines size quantification, variance, copatterns, and a reducibility interpretation at its published signature; the bounded fragment above deliberately includes neither its infinity size nor its size-weakening structure. Experimental Agda sized-type features have also admitted consistency bugs; no implementation theorem is inferred from the mathematical display above.

Suggested first pass.

None of these problems is a prerequisite. Begin with exercise 85.5, then complete exercise 85.8.

Exercise 85.5

★★☆ Let s:Stream(A). Define a bisimulation and prove map(λx.x,s)As. Write the head calculation and the exact witness relating the tails.

Exercise 85.6

★★★ Specify a stream of triangular numbers first by a pair-state corecursor and then by complete head/tail copattern clauses. Compile the clauses, calculate the first six observations, and prove that the two definitions are observationally equal.

Exercise 85.7

★★☆ Use the accepted clause system head(zeros)0 and tail(zeros)zeros. Let nats:=iterate(suc,0), so that head(nats)0. First prove by induction that nth(n,map(g,s)) is identified with g(nth(n,s)). Use that lemma to prove tail(nats)Nmap(suc,nats)andIdN(nth(n,nats),n). Explain why replacing the observational equality by would not give the unary state-update clause required by definition 85.9; do not claim that the guarded fragment is complete.

Exercise 85.8

★★★ Practical project.guarded-stream-observer Implement in Kappa the state-machine corecursor through a finite observer rather than by constructing an infinite host value. Maintain the invariant that a request of depth n performs exactly n state transitions before applying the output function. On the Fibonacci state machine, print the observations at depths 0 through 9; the exact output must be 0,1,1,2,3,5,8,13,21,34. A mutated transition (a,b)(a,a+b) must fail the acceptance test by printing 0,0 in the first two positions. Report both traces.

Sources. The destructor and copattern presentation follows Abel, Pientka, Thibodeau, and Setzer, especially its stream and Fibonacci examples [APTS13]. The sized boundary follows Abel and Pientka’s journal development [AP16]. The contextual comparison is restricted to the exact soundness results of Downen and Ariola [DA25].

Search the book

Type to search the local edition.