From the client's point of view, objects can be inserted into or deleted from the set, and an object can be tested as to whether it is in the set.
We still have a few implementation decisions to make. Here are some that are visible to the client:
Here, we shall assume that an error must be signaled whenever an attempt is made to insert an element already on the list, and whenever an attempt is made to delete an object which is not on the list. From these two decisions follow that there is no real point in allowing multiple objects on the list.
int member(list l, void *element) { list ll; for(ll = l; ll; ll = ll -> next) { if(equal(ll -> element, element)) return 1; } return 0; }Some people may object that we are using very short identifiers such as l and ll, whereas good software engineering practice favors longer, more explicit, names. See the section on naming conventions for a complete discussion about this topic.
Some other people may object that we are using a pointer expression such as ll rather than ll != NULL, which is more explicit. However, in the C language they are entirely equivalent for the purpose of a test like this one. The first version is shorter and all C programmers know both to use it and to understand it. It can thus be considered an idiom of the C language.
This first version of member is not quite acceptable. The problem is that we can't have the equal function be global, since the definition of equality depends on the domain of the elements. The only solution is to pass the equality as a parameter like this:
int member(list l, void *element, int (*equal)(void *, void *)) { list ll; for(ll = l; ll; ll = ll -> next) { if(equal(ll -> element, element)) return 1; } return 0; }We now have a reasonable version of member. We can improve on it a bit by noticing that l is a local variable just like ll, so that we can use it instead of ll like this:
int member(list l, void *element, int (*equal)(void *, void *)) { for(; l; l = l -> next) { if(equal(l -> element, element)) return 1; } return 0; }which is somewhat shorter and avoids the introduction of another variable. On the other hand, this version might be harder to understand in that it uses a parameter for something it was not intended, namely a loop variable. The previous version makes the distinction explicit. Which version is the best is a matter of taste.
The answer to the second question is "anywhere will do". To make our lives easy, we insert at the beginning of the list.
Before we look at some code, we have solve some minor problems. The insert function will take a list as an argument and possibly modify it. But since parameter passing is by value, modifications inside insert will not necessarily be visible to the caller. For instance, this function:
void stupid(list l) { l = malloc(sizeof(struct cell)); l -> next = NULL; l -> element = NULL; }or using the cons function from the list module:
void stupid(list l) { l = cons(NULL, NULL); }has as its only effect to allocate a cell that then can never be referenced, which gives a memory leak (see need for garbage collection).
We must somehow communicate the updated list to the caller. There are two options, both acceptable. We can either pass a pointer to the list to be modified, or return the modified list as the value of the insert function.
Here is a version of insert using pointer passing:
#include < assert.h> ... void insert(list *lp, void *element, int (*equal)(void *, void *)) { assert(!member(*lp, element, equal)); *lp = cons(element, *lp); }We notice several things. First, we need the line:
#include < assert.h>in order to get access to the assert function (or rather the assert macro). Then, we notice that the insert function must also be passed the equal, so that it can check whether the element to insert is already on the list.
Also notice the change of names. We have used lp for the argument. It is an abbreviation for `list pointer'. We can't really use l since that would be deceiving the reader into thinking that it is a list. It isn't. It is a pointer to a list. Also, using lp frees up the name l for the local variable.
Make sure you understand every use of pointer dereferencing (i.e. * in this example.
Here is a version of insert using return value:
#include < assert.h> ... list insert(list l, void *element, int (*equal)(void *, void *)) { assert(!member(l, element, equal)); return cons(element, l); }Notice that the two versions must be used somewhat differently. Assuming that we want to insert an element e into a list l using the function equal, the first version would be called like this:
insert(&l, e, equal);whereas the second version would be called like this:
l = insert(l, e, equal);
But whereas with insert we could insert the element at the beginning of the list, with delete we have to find the element and remove it. Now, it may seem that we can just do what we did in member, that is:
for(ll = l; ll; ll = ll -> next) { if(equal(ll -> element, element)) /* we found it, delete it */ ... }But that does not really work. The problem is the following: When the statement:
equal(ll -> element, element)is true, then, ll is a pointer to the cell containing the element to be removed, i.e, the cell pointed to by ll must be removed from the list. But removing a cell from the list means modifying either the next field of the previous cell, or (in case the cell to remove is the first one) modifying the initial list. The traditional solution (which we do not recommend in C) is to use a trailing pointer. Here is a version of delete with such a trailing pointer:
void delete(list *lp, void *element, int (*equal)(void *, void *)) { list ll; list trail = NULL; assert(member(*lp, element, equal)); for(ll = *lp; !equal(ll -> element, element); trail = ll, ll = ll -> next) ; if(!trail) *lp = cdr_and_free(*lp); else trail -> next = cdr_and_free(trail -> next); }First a few remarks. Since we checked that element is in the list (in the call to assert), we don't have to check for the end of the list in the loop. We simply know that equal will ultimately return a true value. Also notice that we again allow ourselves to use a pointer value (here trail as a boolean value in a test. Also notice the use of the sequencing operator (the comma) in the last clause of the for loop. The variable trail is assigned the old value of ll before ll is advanced to the next cell. Further notice that the body of the for loop is empty, and that the semicolon that indicates that is on a line by itself. This is considered style. This version of delete has several problems. We already mentioned that we don't recommend the method of trailing pointers. The reason is that it introduces a special case that we then must test for. The special case is when trail is NULL, in which case the first element should be deleted. Notice that we treat the special case first, even though we have to negate the value of trail. As a matter of style, it is better to treat the special case first. Some people might think that doing so would be less efficient, since after all we have to negate the value of trail. This is wrong. Any reasonable compiler will generate the same code for the two cases.
The other main problem with this version of delete is that it scans the list twice, once in the call to the member function, and once to find and delete the element.
Let us first fix the problem with the trailing pointer. To avoid the special case, we can use a feature of the C programming language that lets us create a pointer not only to a block of memory returned by malloc but to any part of memory of the right type. Here is a version of delete using that feature:
void delete(list *lp, void *element, int (*equal)(void *, void *)) { list *llp; assert(member(*lp, element, equal)); for(llp = lp; !equal((*llp) -> element, element); *llp = &((*llp) -> next)) ; *llp = cdr_and_free(*llp); }We immediately notice how the special case disappeared, and the resulting code is much shorter. Some people might object that this code is less readable (some would say unreadable) than the previous one. See using advanced features for a discussion concerning this topic.
Now, let us turn our attention to the second problem (traversing the list twice). We don't have to use the member to test whether the element is a member of the list or not. We can simply use the existing code, something like this:
void delete(list *lp, void *element, int (*equal)(void *, void *)) { list *llp; for(llp = lp; *llp && !equal((*llp) -> element, element); *llp = &((*llp) -> next)) ; assert(*llp); *llp = cdr_and_free(*llp); }We notice several things. Most trivially, the clauses of the for loop are now so complicated that they will not (or just barely) fit on a line. We have therefore split the loop header into three lines, one for each clause. Second, we have re-introduced the test for the end of the list. We must do that since we eliminated the call to member. We simply no longer know whether the element is a member or not when we execute the loop. The result of a loop is now that *llp is NULL if and only if the element is not in the list. The call to assert makes sure the element is indeed on the list.
But now we have one loop in the delete function and one in the member function. In such a situation, the highly skilled programmer usually tries to factor the code, i.e. put the code in one single place. In this case, it doesn't look obvious to do so, since the loops don't look anything alike. Factoring this code requires some imagination. We are going to introduce another function that we will call find. The find function does the work of the loop in delete, i.e. it returns a position (of type list *, called (say) llp) such that if *llp is NULL, then the element is not on the list, and if *llp is not NULL, then the element is on the list. Here it is:
list * find(list *lp, void *e, int (*equal)(void *, void *)) { for(; *lp && !equal((*lp) -> element, e); lp = &((*lp) -> next)) ; return lp; }By re-using the argument lp we don't need a new variable llp. Together with using e as the name for the element to check for, it allows us to cut down the size of the code so that the loop heading again will fit on one line.
Now, we can easily define delete by using find like this:
void delete(list *lp, void *element, int (*equal)(void *, void *)) { list *llp = find(lp, element, equal); assert(*llp); *llp = cdr_and_free(*llp); }If we are willing to re-use lp, we can make it one line shorter like this:
void delete(list *lp, void *element, int (*equal)(void *, void *)) { assert(*(lp = find(lp, element, equal)); *llp = cdr_and_free(*llp); }and if we have a garbage collector (see need for garbage collection), we can even eliminate the call to cdr_and_free like this:
void delete(list *lp, void *element, int (*equal)(void *, void *)) { assert(*(lp = find(lp, element, equal)); *lp = (*lp) -> next; }which should be compared to our initial 13-line version. Sure, we have introduced another function (find), but now we can use it to implement member. Recall our previously best version:
int member(list l, void *element, int (*equal)(void *, void *)) { for(; l; l = l -> next) { if(equal(l -> element, element)) return 1; } return 0; }which we can now simply replace by:
int member(list l, void *element, int (*equal)(void *, void *)) { return *(find(&l, element, equal)); }Similarly, we can modify insert to use find directly (although we don't have to), like this:
insert(list *lp, void *element, int (*equal)(void *, void *)) { lp = find(lp, element, equal); assert(!(*lp)) *lp = cons(element, *lp); }Notice that this version no longer inserts at the beginning, but at the end of the list. The list has to be scanned anyway in order to find out that the element is not a member. This scan stops at the end of the list, which is the point we use for insertion. This trick will be useful when we discuss manipulating sorted lists.
#ifndef ULSET_H #define ULSET_H /* We use unsorted linked lists to implement sets of elements, that can handle only equality */ struct ulset; typedef struct ulset *ulset; /* create an empty ulset */ extern ulset ulset_create(int (*equal)(void *, void *)); /* return a true value iff the element is a member of the ulset */ extern int ulset_member(ulset l, void *element); /* insert an element into an ulset. The element must not already be a member of the ulset. */ extern void ulset_insert(ulset l, void *element); /* delete an element from an ulset. The element must be a member of the ulset. */ extern void ulset_delete(ulset l, void *element); #endifWe have added comments describing briefly what the module does, and describing what each interface function does. The phrases must be and must not be mean that a program that does not respect what these phrases say, is a program with errors in it. This module is therefore free to do whatever it thinks reasonable (including nothing at all) when such a condition is violated. In our implementation, we will call assert and abort the execution of the program.
For the implementation file, we must use a header object as described in uniform reference semantics. The use of this header object will also fix the incompatibility between the interface of insert and delete on the one hand and ulset_insert and ulset_delete on the other hand. Here is the implementation file:
#include "ulset.h" #include "list.h> #include < assert.h> #include < stdlib.h> struct ulset { int (*equal)(void *, void *); list elements; }; ulset ulset_create(int (*equal(void *, void *))) { ulset l = malloc(sizeof(struct ulset)); l -> elements = NULL; l -> equal = equal; return l; } static list * find(list *lp, void *e, int (*equal)(void *, void *)) { for(; *lp && !equal((*lp) -> element, e); lp = &((*lp) -> next)) ; return lp; } int ulset_member(ulset l, void *element) { list *lp = find(&(l -> elements), element, l -> equal); return *lp; } void ulset_insert(ulset l, void *element) { list *lp = find(&(l -> elements), element, l -> equal); assert(!(*lp)) *lp = cons(element, *lp); } void ulset_delete(ulset l, void *element) { list *lp = find(&(l -> elements), element, l -> equal); assert(*lp); *lp = cdr_and_free(*lp); }Notice that for reasons of symmetry, we introduced a variable lp at the beginning of each function of the interface, which is always initialized to the value of a call to find. For symmetry, we also renamed the local list variable in ulsetisert so that it is called temp just as in ulset_delete.
We prefix all interface functions (except the constructor) with ulset_. Such prefixing is neither necessary nor desired for internal functions such as find.