Table of Contents

Abstract Data Types - Intro

A overview into correctness

Consider the following list of position papers:

These papers/blog-posts share a strong view (not necessarily overlapping nor in opposition) regarding how programs should be developed in the right way.

There is still no consensus regarding a correct/healthy way of writing programs, however, as Donald Knuth describes in his farsighted essay:

a well-written program will always be recognised as such and appreciated by a skilful programmer.

Correctness of a program refers to the property that:

Although not obvious right away, there is a strong link between:

This link is emphasised by the following remarks:

We recall another of Dijkstra's quotes:

from Notes on Structured Programming.

Almost 50 years after Dijkstra's essay, an active research area consists in development techniques for programs in such a way that their correctness can be verified automatically.

Correctness at AA

We shall adopt Dijkstra's viewpoint regarding program correctness. To this end, we shall investigate a tool which allows us to structure programs in such a way that we can efficiently reason about their correctness. The reasoning process we shall look at is mental (not automatic, i.e. machine implementable). However, (semi-)automated program reasoning techniques exist.

The tool is Abstract Data Types (ADTs). In a nutshell:

A motivating example

Consider the following program:

#define INDEX_OUT_OF_BOUNDS 1
 
int EXCEPTION = 0;
void throw (int e){
	EXCEPTION = e;
}
 
struct AList {
	int* v;
	int sz, len;
};
 
typedef struct AList AList;
 
AList Empty(){
	AList l;
	l.sz = 0;
	l.len = 0;
	return l;
}
 
void copy (int* src, int s_start, int end, int* dst, int d_start){
	int i;
	for (i = s_start; i<end; i++)
		dst[d_start++] = src[i];
}
 
AList add (AList l, int e){
	if (l.v == 0){                 // este v alocat? (Nu...)
		l.v = malloc(sizeof(int));
		l.sz = 1;
		l.len = 1;
		l.v[0] = e;
		return l;
	}
	if (l.sz == l.len){
		int* vp = malloc(l.len*2*sizeof(int));
		copy(l.v,0,l.len,vp,0);
		l.len *= 2;
		l.v = vp;
	}
	l.v[l.sz] = e;
	l.sz++;
	return l;
}
 
int get (AList l, int pos){
	if (pos >= l.sz || pos < 0){
		throw(INDEX_OUT_OF_BOUNDS);
		return 0;
	}
	return l.v[pos];
}
 
AList ins (AList l, int pos, int e){
	if (pos > l.sz || pos < 0){
		throw(INDEX_OUT_OF_BOUNDS);
		return l;
	}
	if (pos == l.sz)
		return add(l,e);
	else
	{
		int temp = l.v[pos];
		l.v[pos] = e;
		return ins(l,pos+1,temp);
	}
}

It contains the methods:

The AList (short for ArrayList) represents a list as an array. Whenever the array becomes full, the capacity of the array doubles.

Now consider the following code:

#define INDEX_OUT_OF_BOUNDS 1
 
int EXCEPTION = 0;
void throw (int e){
	EXCEPTION = e;
}
 
struct LList {
	struct LList* next;
	int val;
};
 
typedef struct LList* LList;
 
LList Empty(){
	return 0;
}
LList add (LList l, int e){
	LList n = malloc(sizeof(struct LList));
	n->val = e;
 
	n->next = l;
	return n;
}
int get (LList l, int pos){
	if (pos < 0 || l == 0){
		throw(INDEX_OUT_OF_BOUNDS);
		return 0;
	}
	if (pos == 0)
		return l->val;
	else
		return get(l->next,pos-1);
}
void ins (LList l, int pos, int e){
	if (pos < 0 || l == 0){
		throw(INDEX_OUT_OF_BOUNDS);
		return;
	}
 
	if (pos == 0){
		LList n = malloc(sizeof(struct LList));
		n->val = e;
		n->next = l->next;
		l->next = n;
	}
	else
		ins (l->next,pos-1,e);
}

The LList (abreviating LinkedList) contains precisely the same methods as the ArrayList. The sole difference in behaviour here is that add will add an element at the beginning of the list. Of course, implementations are conceptually different. The efficiency of each list implementation is also different. For instance:

Leaving efficiency aside, the behaviour of both lists is the same, and any program is expected to behave in the same way irrespective of the type of list implementation which is deployed.

With this in mind, we develop the following List abstraction. For convenience we (temporarily) use C code to describe this abstraction:

List Empty();
List cons (int e, List l);
int head (List l);
List tail (List l);
int isEmpty(List l);

We can group the above function definitions in two categories:

Furthermore, any operation defined on lists can be expressed using a combination of these functions:

List add (List l, int e){
	return cons(e,l);
}
 
int get (List l, int pos){
	if (pos == 0) 
		return head(l);
	return get(tail(l),pos-1);
}
 
List ins (List l, int pos, int e){
	if (pos == 0)
		return cons(e,l);
	return cons(head(l),ins(tail(l),pos-1,e));
}
 
void show (List l){
	if (isEmpty(l))
		printf("[]\n");
	else{
		printf("%i ",head(l));
		show(tail(l));
	}
}

In the previous code, we have shown implementations of the functions add, ins, get together with the display function show. These latter implementations are independent on how the type of the list:

Note that we have defined list operations without actually implementing our list abstractions. The implementations follow for AList:

struct AList {
	int* v;
	int sz, len;
};
 
typedef struct AList AList;
 
AList Empty(){
	AList l;
	l.sz = 0;
	l.len = 0;
	return l;
}
void copy (int* src, int s_start, int end, int* dst, int d_start){
	int i;
	for (i = s_start; i<end; i++)
		dst[d_start++] = src[i];
}
AList cons (int e, AList l){
	if (l.v == 0){
		l.v = malloc(sizeof(int));
		l.sz = 1;
		l.len = 1;
		l.v[0] = e;
		return l;
	}
	if (l.sz == l.len){
		int* vp = malloc(l.len*2*sizeof(int));
		copy(l.v,0,l.len,vp,0);
		l.len *= 2;
		l.v = vp;
	}
	l.v[l.sz] = e;
	l.sz++;
	return l;
}
int head (AList l){
	return l.v[l.sz-1];
}
AList tail (AList l){
	l.sz --;
	return l; 
}
int isEmpty(AList l){
	return l.sz == 0;
}

as well as for LList:

struct LList {
	struct LList* next;
	int val;
};
 
typedef struct LList* LList;
 
LList Empty(){
	return 0;
}
LList cons(int e, LList l){
	LList n = malloc(sizeof(struct LList));
	n->val = e;
 
	n->next = l;
	return n;
}
int isEmpty(LList l){
	return l==0;
}
int head(LList l){
	return l->val;
}
LList tail(LList l){
	return l->next;
}

Let us recap: