In how many different ways can we reverse a sequence of integers?

We start the lecture by looking at three different Java programs which reverse a sequence of integers.

The Cook-book style:

public class Rev {
	public static void main (String[] args) {
		Integer[] v = new Integer[] {1,2,3,4,5,6,7,8,9};
		int i=0;
		while (i < v.length/2){
			int t = v[i];
			v[i] = v[v.length-1-i];
			v[v.length-1-i] = t;
			i++;
		} 
		show(v);
	}
}

Features:

  • the reversal is implemented over an array, and performed in-place by swapping the first half of the array with the second.
  • the code is compact and efficient ($ n/2$ swaps over an array of size $ n$ ).
  • the solution modifies the sequence to a reversed one.

Possible usages:

  • when development speed is critical
  • when minimising the number of computation steps is critical

The Undergrad-math-teacher-style style (short Undergrad style):

interface List {
	public Integer head ();
	public List tail ();
}
 
class Cons implements List {
	Integer val;
	List next;
	public Cons (Integer val, List next){
		this.val = val;
		this.next = next;
	}
	@Override
	public Integer head() {return val;}
	@Override
	public List tail() {return next;}
}
 
class Empty implements List {
	@Override
	public Integer head() {return -1;}
	@Override
	public List tail() {return null;}
}
 
public class V2 {
 
	private static List rev (List x, List y){
		if (x instanceof Empty)
			return y;
		return rev(x.tail(), new Cons(x.head(),y));
	}
 
	public static List reverse (List l){
		return rev(l,new Empty());
	}
 
	public static void main (String[] args){
		List v = new Cons(1, new Cons(2, new Cons(3, new Empty())));
		List r = reverse(v);
	}
 
}

Features:

  • This solution first attempts to separate the sequence representation from the reversal algorithm.
  • The sequence is represented as a list in the ADT-style.
  • The in the reversal, the internal list representation is abstracted by using constructors (Empty and Cons) together with observers (head and tail).
  • The code focuses more on input-output: reversal takes a list (instead of an array), and produces another list. The strategy relies on an auxiliary function rev which uses an accumulator to reverse the list. Both rev and the display function are recursive. However, rev is tail-recursive hence efficient as long as the programming language supports tail-end optimisation (which Java 8 does not support).

Possible usages:

  • When it is necessary to separate the (reversal) algorithm from the sequence representation (e.g. when we want a single reversal algorithm for several types of lists)
  • After calling reverse we have two list objects, the original and reversed list. This could be useful to test if the sequence is a palindrome. This programming style is called purely-functional: objects are never modified. Instead, new objects are created from existing ones.

The Industry style:

import java.util.Iterator;
 
class RevView<T> implements Iterable<T> {
	private T[] array;
 
	public RevView(T[] array){
		this.array = array;
	}
 
	@Override
	public Iterator<T> iterator() {
		return new Iterator<T>(){
			private int crtIndex = array.length - 1;
 
			@Override
			public boolean hasNext(){
				return crtIndex >= 0;
			}
 
			@Override
			public T next(){
				return array[crtIndex--];
			} 
 
			@Override
			public void remove () {}
		};
	}
}
 
public class OORev {
 
	public static void main (String[] args) {
		String[] s = new String[]{"1", "2", "3", "4", "5", "6"};
 
		Iterator<String> r = (new RevView<String>(s)).iterator();
		while (r.hasNext()){
			System.out.println(r.next());
		}
	}
}

Features:

  • The solution here focuses on views. The sequence is represented as an array of strings, but it could be basically any kind of array. The object RevView is a view over the array:
    • Note that RevView only holds a reference to the array, not the array itself. This means that the array could be modified from outside the RevView class;
    • RevView returns an iterator over the array, which allows traversing the array in reverse order;
  • The solution also separates the sequence representation (and the separation could be improved), and focuses on viewing or traversing the list rather than constructing another list from the original one;

Possible usages:

  • Such a solution could be used when different threads may want to read the same sequence - views allow inspecting the sequence without modifying it (in our example).
  • Also, it could be used to check properties of the sequence (e.g. palindrome) without duplicating data, using different views over the same sequence.

The Uni-math-teacher style (short. Uni style):

interface Op <A,B> {
	public B call (A a, B b);
}
 
interface Foldable <A,B> {
	public B fold (Op<A,B> op, B init);
}
 
class List implements Foldable<Integer,List>{
 
	Integer val;
	List next;
	public List (Integer val, List next){
		this.val = val;
		this.next = next;
	}
 
	public List fold (Op<Integer,List> op, List init){
		if (this.next == null)
			return op.call(this.val,init);
		return this.next.fold(op,op.call(this.val, init));
	}
}
 
public class V4 {
 
	public static void main (String[] args){
 
		List v = new List(1, new List(2, new List(3, null)));
		List r = v.fold(new Op<Integer,List>(){
				@Override
				public List call (Integer i, List l){
					return new List(i,l);
				}
		},null);
	}
}

Features:

  • The solution relies on the observation that reversing a list is a particular type of folding operation. Consider the sequence 1,2,3, the + operation and the initial value 0. Folding the sequence with + and 0, amounts to summing up the numbers from the sequence:
fold({1,2,3},+,0) =
 fold({2,3},+,1+0) =
   fold({3},+,2+1+0) =
    fold({},+,3+2+1+0) =
     3+2+1+0
  • However, the folding operation need not be arithmetic (nor the initial value - a number). Suppose we replace + by cons (:) and 0 by the empty list []. The result of the folding operation is 3:2:1:[], which is precisely the reversed list. In this solution, the fold operation is implemented in an abstract fashion, with respect to a generic binary operation op and a generic initial value init.
  • List reversal is a particular case of a fold operation

Possible usages:

  • This solution is also characteristic to the functional programming style, more specifically, programming with higher-order functions. A higher-order function (like fold) takes other functions (like op) and implements some functionality in terms of it. In a functional programming language, implementing (and using) a fold is much simpler and elegant than in an imperative language (like Java).
  • Using higher-order functions is very useful when writing programs over large data (for processing like that done in Machine Learning). Such processing is done using a combination of map-reduce functions: map transforms data uniformly, and reduce computes a new (reduced) value from it. Map-reduce programs are easily to parallelise (however, reversal is not a demonstration for that).

Why so many reversals?

Note that reversal is an algorithmically trivial task: take the sequence of elements of the collection at hand, be it array of list (or anything else), in their reverse order.

Our point is that, apart from mastering algorithms, a skilled programmer needs solid knowledge on programming concepts and on the way programming languages (and the hardware they employ) are designed. In some cases, these concepts may be subtle (e.g. programming with Monads in Haskell) and may supersede the algorithm at hand in complexity. However, they are crucial in developing a efficient, secure and correct applications.

This lecture will focus on different ways of writing code for specific algorithms, in different programming languages, with an emphasis on Functional Languages (Haskell) and Logic-based Languages (Prolog).

We have clear metrics for choosing algorithms. Do we also have metrics for code writing? For instance:

  • legibility
  • compactness (number of code lines)
  • ease of use (can other programmers use the code easily)
  • extensibility (can other programmers add modifications to the code easily)
  • good documentation

is a plausible list. While some of these criteria overlap and are difficult to assess objectively, programmers more often than not agree that some programs are well-written while others are poor (w.r.t. some criteria).

How many other ways?

There are many possible variations (and combinations) to our examples, but a few elements do stand out:

  • the functional style for representing a list - very akin to Abstract Datatypes
  • the functional style for reversal - relying on recursion, relying on folding
  • the Object Oriented style for traversing a list in the third example - which although is tightly linked to Java Collections, can be migrated to any other object-oriented language.

Such elements of style are may be called design patterns, i.e. generic ways of writing code, which can be deployed for different implementations.

Some elements of style may have some common ground, or require certain traits from the programming language. These latter are called programming paradigms. For instance, writing programs as recursive functions, using higher-order functions and representing data as ADTs (recall that constructors are functions) are both styles of the functional paradigm.

In this lecture, we shall go over the following paradigms:

  • imperative
  • object oriented
  • functional
  • logical (or logic programming)
  • associative (or rule-based)
  • Is there a right programming language? How to people choose programming languages?
  • Who invented paradigms? (A history between Lisp vs C)
  • Relationship between paradigms and programs (one-to-one, one-to-many, many-to-many)
  • How extensible can programs be?