// ----- Stream codatatype Stream = Nil | Cons(head: T, tail: Stream); function append(M: Stream, N: Stream): Stream { match M case Nil => N case Cons(t, M') => Cons(t, append(M', N)) } // ----- f, g, and maps type X; function f(x: X): X function g(x: X): X function map_f(M: Stream): Stream { match M case Nil => Nil case Cons(x, N) => Cons(f(x), map_f(N)) } function map_g(M: Stream): Stream { match M case Nil => Nil case Cons(x, N) => Cons(g(x), map_g(N)) } function map_fg(M: Stream): Stream { match M case Nil => Nil case Cons(x, N) => Cons(f(g(x)), map_fg(N)) } // ----- Theorems // map (f * g) M = map f (map g M) comethod Theorem0(M: Stream) ensures map_fg(M) == map_f(map_g(M)); { match (M) { case Nil => case Cons(x, N) => Theorem0(N); } } comethod Theorem0_Alt(M: Stream) ensures map_fg(M) == map_f(map_g(M)); { if (M.Cons?) { Theorem0_Alt(M.tail); } } ghost method Theorem0_Par(M: Stream) ensures map_fg(M) == map_f(map_g(M)); { forall k: nat { Theorem0_Ind(k, M); } } ghost method Theorem0_Ind(k: nat, M: Stream) ensures map_fg(M) ==#[k] map_f(map_g(M)); { if (k != 0) { match (M) { case Nil => case Cons(x, N) => Theorem0_Ind(k-1, N); } } } ghost method Theorem0_AutoInd(k: nat, M: Stream) ensures map_fg(M) ==#[k] map_f(map_g(M)); // { // TODO: this is not working yet, apparently // } // map f (append M N) = append (map f M) (map f N) comethod Theorem1(M: Stream, N: Stream) ensures map_f(append(M, N)) == append(map_f(M), map_f(N)); { match (M) { case Nil => case Cons(x, M') => Theorem1(M', N); } } comethod Theorem1_Alt(M: Stream, N: Stream) ensures map_f(append(M, N)) == append(map_f(M), map_f(N)); { if (M.Cons?) { Theorem1_Alt(M.tail, N); } } ghost method Theorem1_Par(M: Stream, N: Stream) ensures map_f(append(M, N)) == append(map_f(M), map_f(N)); { forall k: nat { Theorem1_Ind(k, M, N); } } ghost method Theorem1_Ind(k: nat, M: Stream, N: Stream) ensures map_f(append(M, N)) ==#[k] append(map_f(M), map_f(N)); { // this time, try doing the 'if' inside the 'match' (instead of the other way around) match (M) { case Nil => case Cons(x, M') => if (k != 0) { Theorem1_Ind(k-1, M', N); } } } ghost method Theorem1_AutoInd(k: nat, M: Stream, N: Stream) ensures map_f(append(M, N)) ==#[k] append(map_f(M), map_f(N)); // { // TODO: this is not working yet, apparently // } ghost method Theorem1_AutoForall() { // assert forall k: nat, M, N :: map_f(append(M, N)) ==#[k] append(map_f(M), map_f(N)); // TODO: this is not working yet, apparently } // append NIL M = M ghost method Theorem2(M: Stream) ensures append(Nil, M) == M; { // trivial } // append M NIL = M comethod Theorem3(M: Stream) ensures append(M, Nil) == M; { match (M) { case Nil => case Cons(x, N) => Theorem3(N); } } comethod Theorem3_Alt(M: Stream) ensures append(M, Nil) == M; { if (M.Cons?) { Theorem3_Alt(M.tail); } } // append M (append N P) = append (append M N) P comethod Theorem4(M: Stream, N: Stream, P: Stream) ensures append(M, append(N, P)) == append(append(M, N), P); { match (M) { case Nil => case Cons(x, M') => Theorem4(M', N, P); } } comethod Theorem4_Alt(M: Stream, N: Stream, P: Stream) ensures append(M, append(N, P)) == append(append(M, N), P); { if (M.Cons?) { Theorem4_Alt(M.tail, N, P); } } // ----- Flatten // Flatten can't be written as just: // // function SimpleFlatten(M: Stream): Stream // { // match M // case Nil => Nil // case Cons(s, N) => append(s, SimpleFlatten(N)) // } // // because this function fails to be productive given an infinite stream of Nil's. // Instead, here are two variations of SimpleFlatten. The first variation (FlattenStartMarker) // prepends a "startMarker" to each of the streams in "M". The other (FlattenNonEmpties) // insists that "M" contain no empty streams. One can prove a theorem that relates these // two versions. // This first variation of Flatten returns a stream of the streams in M, each preceded with // "startMarker". function FlattenStartMarker(M: Stream, startMarker: T): Stream { PrependThenFlattenStartMarker(Nil, M, startMarker) } function PrependThenFlattenStartMarker(prefix: Stream, M: Stream, startMarker: T): Stream { match prefix case Cons(hd, tl) => Cons(hd, PrependThenFlattenStartMarker(tl, M, startMarker)) case Nil => match M case Nil => Nil case Cons(s, N) => Cons(startMarker, PrependThenFlattenStartMarker(s, N, startMarker)) } // The next variation of Flatten requires M to contain no empty streams. copredicate StreamOfNonEmpties(M: Stream) { match M case Nil => true case Cons(s, N) => s.Cons? && StreamOfNonEmpties(N) } function FlattenNonEmpties(M: Stream): Stream requires StreamOfNonEmpties(M); { PrependThenFlattenNonEmpties(Nil, M) } function PrependThenFlattenNonEmpties(prefix: Stream, M: Stream): Stream requires StreamOfNonEmpties(M); { match prefix case Cons(hd, tl) => Cons(hd, PrependThenFlattenNonEmpties(tl, M)) case Nil => match M case Nil => Nil case Cons(s, N) => Cons(s.head, PrependThenFlattenNonEmpties(s.tail, N)) } // We can prove a theorem that links the previous two variations of flatten. To // do that, we first define a function that prepends an element to each stream // of a given stream of streams. function Prepend(x: T, M: Stream): Stream { match M case Nil => Nil case Cons(s, N) => Cons(Cons(x, s), Prepend(x, N)) } comethod Prepend_Lemma(x: T, M: Stream) ensures StreamOfNonEmpties(Prepend(x, M)); { match M { case Nil => case Cons(s, N) => Prepend_Lemma(x, N); } } ghost method Theorem_Flatten(M: Stream, startMarker: T) ensures StreamOfNonEmpties(Prepend(startMarker, M)) ==> // always holds, on account of Prepend_Lemma; // but until (co-)method can be called from functions, // this condition is used as an antecedent here FlattenStartMarker(M, startMarker) == FlattenNonEmpties(Prepend(startMarker, M)); { Prepend_Lemma(startMarker, M); Lemma_Flatten(Nil, M, startMarker); } comethod Lemma_Flatten(prefix: Stream, M: Stream, startMarker: T) ensures StreamOfNonEmpties(Prepend(startMarker, M)) ==> // always holds, on account of Prepend_Lemma; // but until (co-)method can be called from functions, // this condition is used as an antecedent here PrependThenFlattenStartMarker(prefix, M, startMarker) == PrependThenFlattenNonEmpties(prefix, Prepend(startMarker, M)); { Prepend_Lemma(startMarker, M); match (prefix) { case Cons(hd, tl) => Lemma_Flatten(tl, M, startMarker); case Nil => match (M) { case Nil => case Cons(s, N) => if (*) { // This is all that's needed for the proof Lemma_Flatten(s, N, startMarker); } else { // ...but here are some calculations that try to show more of what's going on // (It would be nice to have ==#[...] available as an operator in calculations.) // massage the LHS: calc { PrependThenFlattenStartMarker(prefix, M, startMarker); == // def. PrependThenFlattenStartMarker Cons(startMarker, PrependThenFlattenStartMarker(s, N, startMarker)); } // massage the RHS: calc { PrependThenFlattenNonEmpties(prefix, Prepend(startMarker, M)); == // M == Cons(s, N) PrependThenFlattenNonEmpties(prefix, Prepend(startMarker, Cons(s, N))); == // def. Prepend PrependThenFlattenNonEmpties(prefix, Cons(Cons(startMarker, s), Prepend(startMarker, N))); == // def. PrependThenFlattenNonEmpties Cons(Cons(startMarker, s).head, PrependThenFlattenNonEmpties(Cons(startMarker, s).tail, Prepend(startMarker, N))); == // Cons, head, tail Cons(startMarker, PrependThenFlattenNonEmpties(s, Prepend(startMarker, N))); } // all together now: calc { PrependThenFlattenStartMarker(prefix, M, startMarker) ==#[_k] PrependThenFlattenNonEmpties(prefix, Prepend(startMarker, M)); { // by the calculation above, we have: assert PrependThenFlattenStartMarker(prefix, M, startMarker) == Cons(startMarker, PrependThenFlattenStartMarker(s, N, startMarker)); } Cons(startMarker, PrependThenFlattenStartMarker(s, N, startMarker)) ==#[_k] PrependThenFlattenNonEmpties(prefix, Prepend(startMarker, M)); { // and by the other calculation above, we have: assert PrependThenFlattenNonEmpties(prefix, Prepend(startMarker, M)) == Cons(startMarker, PrependThenFlattenNonEmpties(s, Prepend(startMarker, N))); } Cons(startMarker, PrependThenFlattenStartMarker(s, N, startMarker)) ==#[_k] Cons(startMarker, PrependThenFlattenNonEmpties(s, Prepend(startMarker, N))); == // def. of ==#[_k] for _k != 0 startMarker == startMarker && PrependThenFlattenStartMarker(s, N, startMarker) ==#[_k-1] PrependThenFlattenNonEmpties(s, Prepend(startMarker, N)); { Lemma_Flatten(s, N, startMarker); // the postcondition of the call we just made (which invokes the co-induction hypothesis) is: assert PrependThenFlattenStartMarker(s, N, startMarker) ==#[_k-1] PrependThenFlattenNonEmpties(s, Prepend(startMarker, N)); } true; } } } } } comethod Lemma_FlattenAppend0(s: Stream, M: Stream, startMarker: T) ensures PrependThenFlattenStartMarker(s, M, startMarker) == append(s, PrependThenFlattenStartMarker(Nil, M, startMarker)); { match (s) { case Nil => case Cons(hd, tl) => Lemma_FlattenAppend0(tl, M, startMarker); } } comethod Lemma_FlattenAppend1(s: Stream, M: Stream) requires StreamOfNonEmpties(M); ensures PrependThenFlattenNonEmpties(s, M) == append(s, PrependThenFlattenNonEmpties(Nil, M)); { match (s) { case Nil => case Cons(hd, tl) => Lemma_FlattenAppend1(tl, M); } }