A linear list is essentially the same as a singly-linked list depicted by the dataview slist_v. However, memory allocation and deallocation of list-nodes not handled previously are handled this time. The following declaration introduces a linear datatype list_vt, which forms a boxed type (of the sort viewtype) when applied to a type and an integer:
datavtype list_vt (a:t@ype+, int) = | list_vt_nil (a, 0) of () | {n:nat} list_vt_cons (a, n+1) of (a, list_vt (a, n)) // end of [list_vt]
Assume that a data constructor named foo is associated with a dataviewtype. Then there is a corresponding viewtype construtor of the name foo_unfold that takes n+1 addresses to form a viewtype, where n is the arity of foo. For instance, there is a viewtype constructor list_vt_cons_unfold that takes 3 address L0, L1 and L2 to form a viewtype list_vt_cons_unfold(L0, L1, L2). This viewtype is for a list-node created by a call to list_vt_cons such that the node is located at L0 and the two arguments of list_vt_cons are located at L1 and L2 while the proofs for the at-views associated with L1 and L2 are put in the store for currently available proofs.
The following function template length computes the length of a given linear list:
fn{ a:t@ype } length{n:nat} (xs: !list_vt (a, n)): int n = let fun loop {i,j:nat | i+j==n} .<i>. (xs: !list_vt (a, i), j: int j): int (n) = case+ xs of | list_vt_cons (_, xs1) => loop (xs1, j+1) | list_vt_nil () => j // end of [loop] in loop (xs, 0) end // end of [length]
Suppose that we do want to modify the content stored in a list-node. For instance, we may want to double the value of each integer stores in a linear integer list. The following code implements a function named list_vt_2x that does precisely this:
fun list_vt_2x{n:nat} (xs: !list_vt (int, n) >> _): void = ( case+ xs of | @list_vt_cons (x, xs1) => let val () = x := 2 * x val () = list_vt_2x (xs1) prval () = fold@ (xs) in // nothing end // end of [list_vt_cons] | list_vt_nil () => () ) (* end of [list_vt_2x] *)
Let us now see an example where linear list-nodes are explicitly freed:
fun{ a:t@ype } list_vt_free {n:nat} .<n>. (xs: list_vt (a, n)): void = ( case+ xs of | ~list_vt_cons (x, xs1) => list_vt_free (xs1) | ~list_vt_nil ((*void*)) => () ) (* end of [list_vt_free] *)
Given a linear list, the function list_vt_free frees all the nodes in the list. Let us go over the body of list_vt_free carefully. If xs matches the pattern list_vt_cons(x, xs1), then the names x and xs1 are bound to the head and tail of xs, respectively; the special symbol ~ in front of the pattern indicates that the list-node referred to by xs is freed immediately after xs matches the pattern. If xs matches the pattern list_vt_nil(), no bindings are generated; the special symbol ~ in front of the pattern indicates that the list-node referred to by xs is freed; nothing in this case is actually freed at run-time as list_vt_nil is mapped to the null pointer.
It is also possible to use the special function free@ to explicitly free a node (also called a skeleton) left in a linear variable after the variable matches a pattern formed with a constructor associated with some dataviewtype. For instance, the following code gives another implementation of list_vt_free:
fun{ a:t@ype } list_vt_free {n:nat} .<n>. (xs: list_vt (a, n)): void = case+ xs of | @list_vt_cons (x, xs1) => let val xs1_ = xs1 // [xs1_] is the value stored in [xs1] val ((*void*)) = free@{a}{0}(xs) in list_vt_free (xs1_) end // end of [list_vt_cons] | @list_vt_nil () => free@{a} (xs) // end of [list_vt_free]
As it can be a bit tricky to use free@ in practice, I present more details as follows. First, let us note that the constructors list_vt_nil and list_vt_cons associated with list_vt are assigned the following types:
list_vt_nil : // one quantifier {a:t@ype} () -> list_vt (a, 0) list_vt_cons : // two quantifiers {a:t@ype}{n:nat} (a, list_vt (a, n)) -> list_vt (a, n+1)
If free@ is applied to a node of the type list_vt_nil(), it needs one static argument, which is a type, to instantiate the quantifier in the type of the constructor list_vt_nil. If free@ is applied to a node of the type list_vt_cons_unfold(L0, L1, L2), then it needs two static arguments, which are a type and an integer, to instantiate the two quantifiers in the type of the constructor list_vt_cons. In the case where the type of xs is list_vt_cons_unfold(L0, L1, L2), typechecking the call free@{a}{0}(xs) implicitly consumes a proof of the at-view a?@L1 and another proof of the at-view list_vt(a, 0)?@L2. As there is no difference between list_vt(T, 0)? and list_vt(T, I)? for any T and I, the static argument 0 is chosen in the code. As a matter of fact, any natural number can be used in place of 0 as the second static argument of free@.
The following code implements a function reverse that turns a given linear list into its reverse:
fn{ a:t@ype } reverse{n:nat} ( xs: list_vt (a, n) ) : list_vt (a, n) = let fun revapp {i,j:nat | i+j==n} .<i>. ( xs: list_vt (a, i), ys: list_vt (a, j) ) : list_vt (a, n) = case+ xs of | @list_vt_cons (_, xs1) => let val xs1_ = xs1 val () = xs1 := ys prval () = fold@ (xs) in revapp (xs1_, xs) end // end of [list_vt_cons] | ~list_vt_nil ((*void*)) => ys // end of [revapp] in revapp (xs, list_vt_nil) end // end of [reverse]
The following code implements a function append that concatenates two given linear lists into one:
fn{ a:t@ype } append{m,n:nat} ( xs: list_vt (a, m) , ys: list_vt (a, n) ) : list_vt (a, m+n) = let fun loop {m,n:nat} .<m>. // [loop] is tail-recursive ( xs: &list_vt (a, m) >> list_vt (a, m+n), ys: list_vt (a, n) ) : void = case+ xs of | @list_vt_cons (_, xs1) => let val () = loop (xs1, ys) in fold@ (xs) end // end of [list_vt_cons] | ~list_vt_nil ((*void*)) => xs := ys // end of [loop] var xs: List_vt (a) = xs // creating a left-value for [xs] val () = loop (xs, ys) in xs end // end of [append]
This implementation of list append essentially corresponds to the one presented previously that is based on the dataview slseg_v. Comparing these two, we can easily see that the above one is much simpler and cleaner, demonstrating concretely some advantage of dataviewtypes over dataviews.
This is also a good place for me to mention a closely related issue involving (functional) list construction and tail-recursion. Following is a typical implementation of functioal list concatenation:
fun{ a:t@ype } append1{m,n:nat} ( xs: list(a, m), ys: list(a, n) ) : list (a, m+n) = case+ xs of | list_nil() => ys | list_cons(x, xs) => list_cons(x, append1<a>(xs, ys)) // end of [append1]
fun{ a:t@ype } append2{m,n:nat} ( xs: list (a, m), ys: list (a, n) ) : list (a, m+n) = let // fun loop {m,n:nat} .<m>. ( xs: list (a, m), ys: list (a, n) , res: &(List a)? >> list (a, m+n) ) : void = ( case+ xs of | list_nil() => (res := ys) // end of [list_nil] | list_cons(x, xs) => let val () = res := list_cons{a}{0}(x, _) val+ list_cons (_, res1) = res val () = loop (xs, ys, res1) prval ((*void*)) = fold@ (res) in // nothing end // end of [list_cons] ) (* end of [loop] *) // var res: List(a) val () = loop (xs, ys, res) // in res end // end of [append2]
With dataviewtypes, we can largely retain the convenience of pattern matching associated with datatypes while supporting explicit memory management. Compared to dataviews, dataviewtypes are less general. However, if a dataviewtype can be employed to solve a problem, then the solution is often significantly simpler and cleaner than an alternative dataview-based one.