This is an old revision of the document!


Introduction to Programming Paradigms

In this lecture, we will examine a few interesting notions:

  • programming styles
  • programming paradigms
  • programming languages

and the relation between them. The key insight in this lecture is that each programming style/paradigm introduces constraints in how programs should be written. These constraints may serve to:

  • improve the legibility/usability of the code by other programmers
  • allow for easy improvements/modifications
  • limit the occurrence of bugs, and easing finding and fixing them
  • allow code portability
  • improve performance

In the first part of the lecture, we introduce and discuss four programs solving the sorting problem. Each program is written in a different programming language and in a different style. The choice of style for each solution highlights most of the aspects which will be discussed in detail in the programming paradigms lecture.

We then introduce the concept of programming paradigm and relate it to programming styles and programming languages.

#include <stdio.h>
#include <stdlib.h>
 
#define SWAP(x,y) t = x; x = y; y = t;
 
void insertion_sort (int* v, int sz) {
	int i, j, t;
 
	for (i = 1; i<sz; i++){
		j = i;
		while (j>0 && v[j-1] > v[j]){
			SWAP(v[j-1],v[j])
			j--;
		}
	}
}
 
int main (){
 
	int* v = malloc(sizeof(int)*7);
	v[0] = 1; v[1] = 9; v[2]=4; v[3]= 15; v[4] = 2; v[5] = 6; v[6] = 0;
 
	insertion_sort(v,7);
}

Characteristics

  • procedural: the program uses procedures (e.g. insertion_sort) or macro-definitions (SWAP). While these may be seem like functions, they behave rather more like reusable cookbooks than functions:
    • SWAP(x,y) is textually replaced by t = x; x = y; y = t;
    • insertion_sort does not return a sorted vector, but simply modifies the array in-place
  • stateful: the actual contents of the array is changed after insertion_sort has been called. There are (at least) two states for the array: before being sorted, and after. The programmer needs to take this into account, since sorting erases the initial ordering in the array.
  • type/representation-dependent: the sorting procedure relies on the data representation: an array. For instance, sorting a linked-list would require rewriting the procedure from scratch.
  • fast: is written in C, and does not use recursive function calls, with may be costly.
type List = [Integer]
 
insertion_sort :: List -> List
insertion_sort [] = []
insertion_sort (x:xs) = insert x (insertion_sort xs)
 
insert :: Integer -> List -> List
insert x [] = [x]
insert x (y:ys) = if x > y then y:(insert x ys) else x:y:ys

Characteristics

  • functional: the implementation relies on two functions insertion_sort and insert, which are very similar to functions in the mathematical sense: there receive values as parameter and return values.
  • stateless: there is no concept of state or state-change: the effect of each function call is only to return a value.
  • modular: the implementation is separated in two components: insert is independent from insertion_sort
  • pipeline: the logic of insertion_sort is to recursively-solve a problem of smaller size, and then use insert to assemble the solution.
  • slow: using recursive functions may be costly.
  • type/representation-dependent (similar to Style 1)
swap([X,Y|Rest], [Y,X|Rest]) :- X > Y.
swap([Z|Rest], [Z|Restp]) :- swap(Rest,Restp).
 
bubblesort(L,R) :- swap(L,Lp), !, bubblesort(Lp,R).
bubblesort(L,L).

Characteristics

  • backtracking: bubblesort attempts to swap two values, and if this is possible, it continues execution. In this particular example, we see a controlled backtracking, via the operator !(cut), which prevents exploring the entire execution tree. More details about !(cut) will follow.
  • stateless & stateful: during the execution of the program, goal satisfaction can trigger variables to be bound (instantiated) to values (which can be interpreted as a side-effect), however once a variable is instantiated it cannot be modified.
  • slow: uses guided backtracking which resembles recursive function call.
  • declarative: it is very close to the bubblesort description: bubblesort swaps two variables X,Y such that X<Y, as long as such two variables exist.
  • representation/dependent
trait Sortable[T] {
	def sortWith (comp:(T, T) => Boolean) : Sortable[T] // comparator
}
 
trait LinkedList [T] extends Sortable[T] {
	override def sortWith (comp:(T, T) => Boolean) : LinkedList[T]
}
 
case class Void[T]() extends LinkedList[T]{
	override def sortWith (comp : (T, T) => Boolean) : LinkedList[T] = this
}
 
case class Cons[T] (h: T, t:LinkedList[T]) extends LinkedList[T]{
	override def sortWith (comp : (T, T) => Boolean) : LinkedList[T] = {
		def ins (x:T, rest:LinkedList[T]) : LinkedList[T] = rest match {
			case Void() => Cons(x,Void[T])
			case Cons(h,t) => if (comp(x,h)) Cons(x,rest) else Cons(h,ins(x,t))
		}
		ins(h,t.sortWith(comp))
	}
}
 
 
object Sort {
 
  def main(args: Array[String]): Unit = {
  	val l = Cons(1,Cons(9,Cons(4,Cons(15,Cons(2,Void())))))
  	println(l.sortWith(_ > _))
  }
}

Characteristics

  • type-independence: list sorting is implemented independently of the type of elements contained in the list. In order to sort a list, it is sufficient to have a comparison function, here denoted as: comp : (T, T) ⇒ Boolean. Type independence is technically realised here via:
    • parametric polymorphism or genericity: the ability to define entities (e.g. classes) parameterised with respect to a type T which is established at runtime;
    • ad-hoc polymorphism or function overriding: the actual implementation of sortWith is established at runtime, depending on the actual type of the object on which it is called (Void or Cons).
    • there is another form of type independence: sortWith is designed to work on any sortable entity, not only lists
  • functional: the actual sorting is very similar in implementation to the pipeline style
  • slow: due to recursivity
  • stateless

We introduce three de-facto programming styles, which we call paradigms, by stating some key features. What is the difference between a paradigm and a style (e.g. any of the 4 above ones)? There is no formal definition for paradigm or style. However:

  • paradigms are rather related to programming language design. More precisely, a programming language will (preponderently) implement or adhere to a paradigm. However, nowadays, this no longer happens (Scala is an example).
  • styles are more specific to programmers, and may blend several aspects of a paradigm.

Programming paradigms:

  • imperative: programs specify instructions which iteratively change state. It is very close/similar to the machine architecture. It is generally otherwise unconstrained.
  • object-oriented: programs consist of object definitions (classes). Programming is focused on relations between objects, and their traits (methods they implement). Apart from this enforced structure, programming is similar to the imperative style.
  • functional: programs consist of functions in the mathematical sense. Functions can only return values, no other modifications are allowed. Programs are stateless, and sometimes this is enforced by the programming language (e.g. Haskell).
  • logic programming: programs consist of rules of the form A ⇒ B (if A is true, then B must be true). The input of a program is a goal (expressed in predicate logic). Program execution attempts to satisfy a goal, e.g. it is a proof of the goal. In this sense, a program can be seen as a set of axioms. For this reason, Prolog - an example of a logic programming language is also called a theorem-prover.
  • rule-based: very similar in appearance to logic programming, but with different inference styles.
  • imperative: The cookbook style is definitely imperative, as all its characteristics conform to it;
  • object-oriented: The abstractionist style is object-oriented.
  • functional: The pipeline style is functional. Also, the abstractionist style is functional. This is obvious in the actual sort implementation.
  • logic-programming: The game-of-life style does conform to the paradigm.
  • C/C++: C is definitely imperative. It is free of constraints, and very flexible. C++ is object-oriented - it adds typing as a constraint (this statement will be discussed in more detail in the following lectures), but retains flexibility. It is possible for programmers to use a functional style in C/C++ albeit with some limitations, especially in the form of the resulting code.
  • Java: is definitely object-oriented and still preserves traces of imperativism, especially with regards to primitive types (which are not objects). As with C/C++, a functional style is possible, but, more than C\C++ may seem forced: for instance, we cannot pass functions as parameter, however we can pass objects which implement certain methods (see, for example, the Command design pattern).
  • Scala: is object-oriented and functional at the same time. It is possible to program in an imperative style in Scala (for instance, it allows for stateful programming like function side-effects). It is also possible to write “strictly-object-oriented” programs (a la Java) as well as functional programs. Most programs will however blend the two.
  • Scheme (or Lisp): is functional, however it is possible to program imperatively in such languages. However such programs are clumsy and rare. It is not possible to write object-oriented-style programs in these languages, as they do not support advanced typing (this will be discussed further on).
  • Haskell: is functional and pure. This latter term deserves a more elaborate discussion which will follow. In short, in Haskell it is not possible to program in another style but functional. Haskell also supports advanced typing (reliant on Abstract Datatypes) and ad-hoc and parametric polymorphism.
  • Prolog: is the de-facto logic programming language. It supports some restricted form functional programming (function definition). There is no means for defining user types.
  • CLIPS: is a rule-based programming language designed at NASA. Nowadays CLIPS is no longer widely-used, however elements of CLIPS design are incorporated in many domain-specific languages (i.e. languages which are deployed for solving specific kind of problems) from AI. A good example are rule-based languages used for reasoning on ontologies.