|
1 | 1 | # Doubly-linked Lists |
| 2 | + |
| 3 | +<!-- TODO: BCP: The rest of the tutorial (from here to the end) needs to be checked for consistency of naming and capitalization conventions. --> |
| 4 | + |
| 5 | +A doubly linked list is a linked list where each node has a pointer |
| 6 | +to both the next node and the previous node. This allows for O(1) |
| 7 | +operations for adding or removing nodes anywhere in the list. |
| 8 | + |
| 9 | +Because of all the sharing in this data structure, the separation |
| 10 | +reasoning is a bit tricky. We'll give you the core definitions and |
| 11 | +then invite you to help fill in the annotations for some of the |
| 12 | +functions that manipulate doubly linked lists. |
| 13 | + |
| 14 | +First, here is the C type definition: |
| 15 | + |
| 16 | +```c title="exercises/dll/c_types.h" |
| 17 | +--8<-- |
| 18 | +exercises/dll/c_types.h |
| 19 | +--8<-- |
| 20 | +``` |
| 21 | + |
| 22 | +The idea behind the representation of this list is that we don't keep |
| 23 | +track of the front or back, but rather we take any node in the list |
| 24 | +and have a sequence to the left and to the right of that node. The `left` |
| 25 | +and `right` are from the point of view of the node itself, so `left` |
| 26 | +is kept in reverse order. Additionally, similarly to in the |
| 27 | +`Imperative Queues` example, we can reuse the `List` type. |
| 28 | + |
| 29 | +```c title="exercises/dll/cn_types.h" |
| 30 | +--8<-- |
| 31 | +exercises/dll/cn_types.h |
| 32 | +--8<-- |
| 33 | +``` |
| 34 | + |
| 35 | +The predicate for this datatype is a bit complicated. The idea is that |
| 36 | +we first own the node that is passed in. Then we follow all of the |
| 37 | +`prev` pointers to own everything backwards from the node, and finally |
| 38 | +all the `next` pointers to own everything forwards from the node, to |
| 39 | +construct the `left` and `right` fields. |
| 40 | + |
| 41 | +<!-- TODO: BCP: Maybe rethink the Own_Forwards / Backwards naming -- would something like Queue_At_Left and Queue_At_Right be clearer?? --> |
| 42 | + |
| 43 | +```c title="exercises/dll/predicates.h" |
| 44 | +--8<-- |
| 45 | +exercises/dll/predicates.h |
| 46 | +--8<-- |
| 47 | +``` |
| 48 | + |
| 49 | +Note that `Dll_at` takes ownership of the node passed in, and then |
| 50 | +calls `Own_Backwards` and `Own_Forwards`, which recursively take |
| 51 | +ownership of the rest of the list. |
| 52 | + |
| 53 | +Also, notice that `Own_Forwards` and `Own_Backwards` include `ptr_eq` |
| 54 | +assertions for the `prev` and `next` pointers. This is to ensure that |
| 55 | +the nodes in the list are correctly doubly linked. For example, the |
| 56 | +line `assert (ptr_eq(n.prev, prev_pointer));` in `Own_Forwards` |
| 57 | +ensures that the current node correctly points backward to the |
| 58 | +previous node in the list. The line `assert(ptr_eq(prev_node.next, |
| 59 | +p));` ensures that the previous node correctly points forward to the |
| 60 | +current node. |
| 61 | + |
| 62 | +Before we move on to the functions that manipulate doubly linked |
| 63 | +lists, we need to define a few "getter" functions that will allow us |
| 64 | +to access the fields of our `Dll` datatype. This will make the |
| 65 | +specifications easier to write. |
| 66 | + |
| 67 | +```c title="exercises/dll/getters.h" |
| 68 | +--8<-- |
| 69 | +exercises/dll/getters.h |
| 70 | +--8<-- |
| 71 | +``` |
| 72 | + |
| 73 | +We also need some boilerplate for allocation and deallocation. |
| 74 | + |
| 75 | +```c title="exercises/dll/malloc_free.h" |
| 76 | +--8<-- |
| 77 | +exercises/dll/malloc_free.h |
| 78 | +--8<-- |
| 79 | +``` |
| 80 | + |
| 81 | +For convenience, we gather all of these files into a single header file. |
| 82 | + |
| 83 | +```c title="exercises/dll/headers.h" |
| 84 | +--8<-- |
| 85 | +exercises/dll/headers.h |
| 86 | +--8<-- |
| 87 | +``` |
| 88 | + |
| 89 | +<!-- ====================================================================== --> |
| 90 | + |
| 91 | +Now we can move on to an initialization function. Since an empty list |
| 92 | +is represented as a null pointer, we will look at initializing a |
| 93 | +singleton list (or in other words, a list with only one item). |
| 94 | + |
| 95 | +```c title="exercises/dll/singleton.c" |
| 96 | +--8<-- |
| 97 | +exercises/dll/singleton.c |
| 98 | +--8<-- |
| 99 | +``` |
| 100 | + |
| 101 | +<!-- ====================================================================== --> |
| 102 | + |
| 103 | +The `add` and `remove` functions are where it gets a little tricker. |
| 104 | +Let's start with `add`. Here is the unannotated version: |
| 105 | + |
| 106 | +```c title="exercises/dll/add_orig.broken.c" |
| 107 | +--8<-- |
| 108 | +exercises/dll/add_orig.broken.c |
| 109 | +--8<-- |
| 110 | +``` |
| 111 | + |
| 112 | +_Exercise_: Before reading on, see if you can figure out what |
| 113 | +specification is appropriate and what other are needed. |
| 114 | + |
| 115 | +<!-- TODO: BCP: I rather doubt they are going to be able to come up with this specification on their own! We need to set it up earlier with a simpler example (maybe in a whoile earlier section) showing how to use conditionals in specs. --> |
| 116 | + |
| 117 | +Now, here is the annotated version of the `add` operation: |
| 118 | + |
| 119 | +```c title="exercises/dll/add.c" |
| 120 | +--8<-- |
| 121 | +exercises/dll/add.c |
| 122 | +--8<-- |
| 123 | +``` |
| 124 | + |
| 125 | +First, let's look at the pre- and post-conditions. The `requires` |
| 126 | +clause is straightforward. We need to own the list centered around |
| 127 | +the node that `n` points to. `Before` is a `Dll` |
| 128 | +that is either empty, or it has a List to the left, |
| 129 | +the current node that `n` points to, and a List to the right. |
| 130 | +This corresponds to the state of the list when it is passed in. |
| 131 | + |
| 132 | +In the ensures clause, we again establish ownership of the list, but |
| 133 | +this time it is centered around the added node. This means that |
| 134 | +`After` is a `Dll` structure similar to `Before`, except that the node |
| 135 | +`curr` is now the created node. The old `curr` is pushed into the left |
| 136 | +part of the new list. The conditional operator in the `ensures` clause |
| 137 | +is saying that if the list was empty coming in, it now is a singleton |
| 138 | +list. Otherwise, the left left part of the list now has the data from |
| 139 | +the old `curr` node, the new `curr` node is the added node, and the |
| 140 | +right part of the list is the same as before. |
| 141 | + |
| 142 | +Now, let's look at the annotations in the function body. CN can |
| 143 | +figure out the empty list case for itself, but it needs some help with |
| 144 | +the non-empty list case. The `split_case` on `is_null(n->prev)` |
| 145 | +tells CN to unpack the `Own_Backwards` predicate. Without this |
| 146 | +annotation, CN cannot reason that we didn't lose the left half of the |
| 147 | +list before we return, and will claim we are missing a resource for |
| 148 | +returning. The `split_case` on `is_null(n->next->next)` is similar, |
| 149 | +but for unpacking the `Own_Forwards` predicate. Note that we have to |
| 150 | +go one more node forward to make sure that everything past `n->next` |
| 151 | +is still owned at the end of the function. |
| 152 | + |
| 153 | +Now let's look at the `remove` operation. Traditionally, a `remove` |
| 154 | +operation for a list returns the integer that was removed. However we |
| 155 | +also want all of our functions to return a pointer to the |
| 156 | +list. Because of this, we define a `struct` that includes an `int` |
| 157 | +and a `node`. |
| 158 | + |
| 159 | +```c title="exercises/dll/dllist_and_int.h" |
| 160 | +--8<-- |
| 161 | +exercises/dll/dllist_and_int.h |
| 162 | +--8<-- |
| 163 | +``` |
| 164 | + |
| 165 | +Now we can look at the code for the `remove` operation. Here is the un-annotated version: |
| 166 | + |
| 167 | +```c title="exercises/dll/remove_orig.broken.c" |
| 168 | +--8<-- |
| 169 | +exercises/dll/remove_orig.broken.c |
| 170 | +--8<-- |
| 171 | +``` |
| 172 | + |
| 173 | +_Exercise_: Before reading on, see if you can figure out what |
| 174 | +specification is appropriate and what annotations are needed. |
| 175 | + |
| 176 | +<!-- TODO: BCP: Again, unlikely the reader is going to be able to figure this out without help. We need some hints. --> |
| 177 | + |
| 178 | +Now, here is the fully annotated version of the `remove` operation: |
| 179 | + |
| 180 | +```c title="exercises/dll/remove.c" |
| 181 | +--8<-- |
| 182 | +exercises/dll/remove.c |
| 183 | +--8<-- |
| 184 | +``` |
| 185 | + |
| 186 | +First, let's look at the pre- and post-conditions. The `requires` clause says that we cannot remove a node from an empty list, so the pointer passed in must not be null. Then we take ownership of the list, and we |
| 187 | +assign the node of that list to the identifier `del` |
| 188 | +to make our spec more readable. So `Before` refers to the `Dll` when the function is called, and `del` refers to the node that will be deleted. |
| 189 | + |
| 190 | +Then in the `ensures` clause, we must take ownership |
| 191 | +of the `node_and_int` struct as well as the `Dll` that |
| 192 | +the node is part of. Here, `After` refers to the `Dll` |
| 193 | +when the function returns. We ensure that the int that is returned is the value of the deleted node, as intended. Then we have a complicated nested ternary conditional that ensures that `After` is the same as `Before` except for the deleted node. Let's break down this conditional: |
| 194 | + |
| 195 | +- The first guard asks if both `del.prev` and `del.next` are null. In this case, we are removing the only node in the list, so the resulting list will be empty. The `else` branch of this conditional contains its own conditional. |
| 196 | + |
| 197 | +- For the following conditional, the guard checks if 'del.prev' is |
| 198 | + _not_ null. This means that the returned node is `del.next`, |
| 199 | + regardless of whether or not `del.prev` is null. If this is the |
| 200 | + case, `After` is now centered around `del.next`, and the left part |
| 201 | + of the list is the same as before. Since `del.next` was previously |
| 202 | + the head of the right side, the right side loses its head in |
| 203 | + `After`. This is where we get `After == Dll{left: |
| 204 | +Left_Sublist(Before), curr: Node(After), right: Tl(Right(Before))}`. |
| 205 | + |
| 206 | +- The final `else` branch is the case where `del.next` is null, but |
| 207 | + `del.prev` is not null. In this case, the returned node is |
| 208 | + `del.prev`. This branch follows the same logic as the one before it, |
| 209 | + except now we are taking the head of the left side rather than the |
| 210 | + right side. Now the right side is unchanged, and the left side is just |
| 211 | + the tail, as seen shown in `After == Dll{left: |
| 212 | +Tl(Left_Sublist(Before)), curr: Node(After), right: Right(Before)};` |
| 213 | + |
| 214 | +The annotations in the function body are similar to in the `add` |
| 215 | +function. Both of these `split_case` annotations are needed to unpack |
| 216 | +the `Own_Forwards` and `Own_Backwards` predicates. Without them, CN |
| 217 | +will not be able to reason that we didn't lose the left or right half |
| 218 | +of the list before we return and will claim we are missing a resource |
| 219 | +for returning. |
| 220 | + |
| 221 | +<!-- ====================================================================== --> |
| 222 | + |
| 223 | +_Exercise_: There are many other functions that one might want to |
| 224 | +implement for a doubly linked list. For example, one might want to |
| 225 | +implement a function that appends one list to another, or a function |
| 226 | +that reverses a list. Try implementing a few of these functions and |
| 227 | +writing their specifications. |
| 228 | + |
| 229 | +_Exercise_: For extra practice, try coming up with one or two |
| 230 | +variations on the Dll data structure itself (there are many!). |
| 231 | + |
| 232 | + |
0 commit comments