A discussion of object-oriented, procedural, scripting and other classes of programming languages; what they're good for, what they're not...
Procedural programming
Modular programming
Object-oriented programming
Comparison between procedural and object-oriented languages.
Generic programming
Aspect Oriented programming
Scripting
Functional programming (in Haskell)
In this course we have used languages of three fundamentally different types:
Object Oriented languages: Java
Procedural languages: C (well, you might have)
Scripting languages: Bash
There are other, radically different, programming paradigms.
|
Decide which procedures you want; use the best algorithms you can find. |
|---|
In this paradigm the focus is on processing — the algorithm needed for performing the desired computation. Languages support it by providing facilities for passing arguments to functions and returning values from functions. The necessity to distinguish between the functions results in a primitive type system etc.
The need to construct larger programs, reusable components, and so on (the needs of constructing larger scale software) lead to primitive forms of modules (separate files, libraries, packages).
Procedural Languages Include:
C (can be considered a modular programming language)
Pascal
FORTRAN
COBOL
Characteristic features of Procedural languages:
Data structures may be defined, and commands and queries defined on them. But they are not combined into a single conceptual object.
Data is passed to commands or queries for operation. For example in C: reset(&car_count); vs Eiffel: car_count.reset or Java: carCount.reset();
Data structures and related commands and queries may be grouped in some languages, called modular languages. These include Modula 2 and Ada.
Header: counter.h
typedef int counter; /* reset to zero */ extern void reset(counter *c); /* count another one */ extern void increment (counter *c); |
Implementation: counter.c
#include "counter.h"
/* reset to zero */
void reset(counter *c) {
*c = 0;
}
/* count another one */
void increment(counter *c) {
*c = *c + 1;
}
|
If data is to be modified in the subroutine, a pointer must be passed as an argument.
The module and its signature can be maintained in separate files, and allow separate compilation.
|
Decide which modules you want; partition the program so that data is hidden within modules. |
|---|
This is the data hiding paradigm. In addition to qualities of procedural languages, it groups together procedures with related data (one step away from the class concept). Techniques for designing procedures now have modularization constraint.
Like procedural definitions, data definition capability should also be available.
|
Decide which types you want; provide a full set of operations for each user-defined type. |
|---|
When interface and implementation are decoupled we get the Data Abstraction.
Modular Programming Languages include:
Modula 2
Ada
Important feature: separate compilation. This is partly a language
issue (separate headers and bodies of procedures, for example) but it
is mainly an issue of taking advantage of a particular language
implementation. This is of great practical importance to the
programmer, and a challenge to the program designer and compiler
writer.
Separate compilation motivates the creation and use of build
tools like make and Ant.
The module definition is rigid, cannot be adopted to a changeable surrounding (rest of the program which uses it). This flexibility (commonality) arises if to replace an abstract module with a class hierarchy.
|
Decide which classes you want; provide a full set of operations for each class; make commonality explicit via inheritance. |
|---|
Object Oriented Languages Include:
Simula
Java
Eiffel
C++ (and Objective C)
Object Pascal (Delphi)
Smalltalk
Modula 3
Characteristic Features of OO languages:
A class is used to describe a data-structure, and the queries and commands used to inspect and update it.
Multiple objects then can be created as instances of a class.
Classes are organized into a hierarchy.
A new class can be defined by describing how it extends one (or more) existing classes from which it inherits.
class
COUNTER
creation
reset
feature {ALL}
number: INTEGER
-- number counted
increment is
-- count another one
do
number := number + 1
end
reset is
-- reset to zero
do
number := 0
end
end -- class COUNTER
|
public class Counter {
private int num;
public Counter() {
num = 0;
}
/** number counted */
public int number() {
return num;
}
/** count another one */
public void increment() {
num++;
}
public void reset() {
num = 0;
}
}
|
Major feature comparison:
| Feature | Eiffel | Java |
| Multiple Inheritance | + | - |
| Generic Types | + | + |
| Design by Contract | + | - |
| Overloading | - | + |
Java makes a distinction between the concepts of subtyping (using the implements keyword) and inheritance (using the extends keyword). A class can be a subtype of another class, without inheriting its implementation. Eiffel doesn't have this distinction.
Minor feature comparison:
In Java, features may be associated with a class as well as with an object.
In Java, inner classes may be defined inside a class to hold private data. (Coming versions of Eiffel will have this too.)
In Java, private features are hidden from subclasses, in Eiffel they are visible.
| Feature | Object Oriented | Procedural |
| Abstraction through subtyping | + | - |
| Reuse through Inheritance | + | - |
| Encapsulation of data | + | -* |
| Automatic Memory Management | +** | - |
*: is supported by modular languages
**: not supported by all OO languages
But the news is not all good for OO languages:
Subtyping brings a loss of performance. Consider the call target.feature. The type of the object target, and therefore the appropriate implementation of feature, may have to be resolved at runtime. (This is called dynamic despatch, and trying to remove it is a major goal of compiler writers for object-oriented languages.)
Automatic memory management also brings a loss of performance (of course with a corresponding elimination of many horrible defects: segmentation faults, memory leaks, etc).
The conception of everything as an object is not always appropriate.
Procedural languages remain the more appropriate choice in application areas where:
the data abstractions are simple
the need for speed is paramount
the developers can make use of a large infrastructure investment (extensive libraries and trained staff)
For example:
FORTRAN remains the dominant language for scientific computation.
C remains the dominant language for embedded programs and operating systems.
New business applications continue to be written in COBOL.
|
Decide which algorithms you want; parametrize them so they work for a variety of types and data structures. |
|---|
Generic Programming is a programming method that is based on finding the most abstract representation of efficient algorithms without compromising its efficiency. (Ideologically close to mathematical phenomenon that an abstract set of axioms, eg, for geometry, can have multiple realisations resulting in different mathematical theories). Generic programming assumes that there are fundamental computational laws that govern the behaviour of software components and that it's possible to design interoperable modules based on such laws. These laws can also guide the software design.
Generic programming emphasizes the difference between types (primitive and user defined — modules, ADT and classes) and data structures (collections, like stacks or lists). Just as types are generalised in ADT and classes, the data structures are generalised as containers. A container (container class) is a class holding a collection (a set in the general mathematical sense) of elements of some type. (This can be defined using a language feature known as templates, or generics).
Algorithms can be parametrised by containers (e.g. algorithms that will sort, copy or search each concrete type of container without presuming is it a list or an array); such algorithms are known as generic algorithms, and the style of programming is known as generic programming.
C++, Ada, and Java allow for efficient implementation of this paradigm. In Java Java 1.5 Generics and Ada the feature is type safe, and does not require unsafe type-casting, but each generic class is implemented as a single class. In C++ the implementation may require a new class to be generated at compile time. More specialised generic programming languages are yet to be developed (although Alexander Stepanov promised this ten years ago). The presence of templates feature in a language (C++, Eiffel, Java) does not guarantee that the generic programming can be effectively realised using the language syntax. When implemented (eg, as C++'s Standard Template Library), it allows k generic algorithms which operate on j data structures built from i different data types to be presented as only (j+k) software components (instead of i*j*k), which can be as useful as ordinary API, and their performance will not be compromised by their generality.
One should spend at least a semester on learning the generic programming. See the Java Generics Tutorial .
This is a relatively new paradigm.
OO, being a very useful concept, has limitations. Often requirements do not decompose into behaviour which can be captured by a single entity — class. Global constraints, behavioural features which permeate the entire system, distinguishing and segregating different behavioural properties (concerns), application of domain specific solutions — these and other problems cannot be effectively addressed by OOP.
AOP is based on the idea that computer systems are better programmed by separately specifying the various concerns (here, a goal, concept, property, or area of interest) of a system and some description of their relationships, and then using the mechanism (which can be implemented as the language feature, or through code-generating tools) of weaving (or, composing) those aspects together in a coherent program. Aspects can be security, persistence, synchronization, transaction, caching/buffering, and so on. OOP strives to find commonality between classes and reorganise them into a hierarchy. AOP seeks to organise the scattered concerns as first-level programming concepts (like classes are in OOP), and remove them horizontally from the module/object structure.
One implementation of AOP — AspectJ programming language. As a language it contains two specific constructs allowing it to realise the AOP tasks: Implementation of concerns — mapping individual requirements into code so that a compiler can translate it into executable code (this can be emulated by standard languages, like Java and C++; but it will defeat the aspired goal of AOP to improve the technology etc)), and Weaving rules specification — how to compose independently implemented concerns to form the final system (has no prototype feature, should be added as extension, or otherwise).
For implementing the first goal AspectJ introduced the constructs of aspect (which look like a class). To implement the second goal, the construct pointcut is used.
I am very new to AOP and AspectJ. First impression, this is a very large and far-reaching solution of the problem for which the Visitor DP was used. Interested? There are already several books on the subject. You can start by checking this tutorial. Much more stuff is available on Eclipse's AspectJ web site. One should spend at least a semester on learning the AO programming.
Scripting languages include:
C Shell
Bourne Shell
Perl
Tcl
Characteristic features of scripting languages:
Variables do not need to be declared before they are used.
All data is represented as strings.
Literal strings do not need to enclosed in special markers (quotes). Almost everything else does (for example in Bash: ${x} for variables, $[1+2] for arithmetic expressions etc).
Few (or primitive) features for structuring programs or data.
Programs are interpreted rather than compiled.
When the task
is not logically complex;
requires a great deal of string and text manipulation;
can be accomplished largely by running existing programs if you can control their options, input and output;
requires the manipulation of many files.
Scripting languages (particularly Perl) are now used extensively to generate dynamic web pages.
There are other, radically different, ways to program:
Stack Languages
FORTH
PostScript
Database Query Languages
SQL
Logic Programming Languages
Prolog
Functional Programming Languages
Haskell
Miranda
ML
Lisp
Programming languages such as C/C++/Java/Python are called imperative programming languages because they consist of sequences of actions. The programmer tells the computer how to perform a task, step-by-step. Functional programming languages work differently. Rather than performing actions in a sequence, they evaluate expressions.
Functional programs only have functions that return results to their callers. There is no “flow of control” in the sense we're used to from Java, C or Bash.
There is no sense of “this happens first, then that”.
Everything is just a function.
Functions can act on and return complex data types.
There are no variables or assignment statements in functional programs. No values are ever modified.
There are no loops in functional programs. Recursion is used instead.
A functional program code (main function) is a composition of multiple smaller codes (functions): h = (f · g) input; program g computes its output which is used as an input for program f. The two program are run together in strict synchronisation. g only starts if f needs to read input, and g runs only for as long as f requires. Once f stops, g aborts even if not all its input is read in. This is the lazy evaluation — a powerful tool for modularization in functional programming.
Data-types are defined separately from the functions that manipulate them. A generic stack type is defined as follows:
data Stack t = Empty | Push t (Stack t)
A stack is defined to be either the constant stack Empty, or the result of the constructor Push that takes an element and a stack. The following are all valid stacks:
Empty
Push 1 Empty
Push 2 (Push 1 Empty)
Push False (Push True Empty)
Functions can now be defined by pattern matching on the different kinds of stack:
is_empty :: (Stack t) -> Bool is_empty Empty = True is_empty (Push x s) = False depth :: (Stack t) -> Int depth Empty = 0 depth (Push x s) = 1 + depth s
Function definitions need not be exhaustive: top and pop are not defined for empty stacks. (This avoids having to state preconditions like in Eiffel, write error-handling code or special cases...)
top :: (Stack t) -> t top (Push x s) = x pop :: (Stack t) -> (Stack t) pop (Push x s) = s
Note that pop is not a procedure that modifies an existing stack, instead it is a function that makes a new stack from an old one. (We did something like this in Stack Version 3.)
data Expression = Constant Int |
Sum Expression Expression |
Product Expression Expression
value :: Expression -> Int
value (Constant n) = n
value (Sum a b) = (value a) + (value b)
value (Product a b) = (value a) * (value b)
toStringInfix :: Expression -> String
toStringInfix (Constant n) = show n
toStringInfix (Sum a b) = "(" ++ toStringInfix a ++
"+" ++ toStringInfix b ++ ")"
toStringInfix (Product a b) = "(" ++ toStringInfix a ++
"*" ++ toStringInfix b ++ ")"
toStringPostfix :: Expression -> String
toStringPostfix (Constant n) = show n
toStringPostfix (Sum a b) = toStringPostfix a ++ " " ++
toStringPostfix b ++ " +"
toStringPostfix (Product a b) = toStringPostfix a ++ " " ++
toStringPostfix b ++ " *"
-- Definition of the addition function in the Curried form add :: Num a => a -> a -> a add n m = n + m -- Partial application: (add 3) is a function of type Int -> Int -- which adds 3 to its argument -- function definition with guards fib :: Int -> Int fib n | n<2 = 1 | otherwise = fib(n-1) + fib(n-2) -- this is a definition of the function "map" map :: (a -> b) -> [a] -> [b] map f (x:xs) = f x : map f xs -- ':' is the list constructor -- an example of usage of the map code :: String -> [Int] code x = map ord x -- here ord is the function ord :: Char -> Int
The expression tree presented above offers equivalent power to a Visitor pattern style implementation of expression trees in OO language.
+ The Haskell code is much shorter than the equivalent OO code.
+ The Haskell code is significantly easier to follow (especially if you have a modicum mathematical background)
- Few problems suit functional programming as well as recursive data-types do
- Functional programs are usually relatively slow (interpreted)
+ Functional programs are easier to reason about mathematically (types and sets and recursions are key concepts)
Strong features of functional programming:
Operates only on its arguments (no side-effects)
Hierarchical, complex functions being built from simpler ones
Employs functions that are generally useful in other programs (can be grouped into Haskell's modules)
Static and non-repetitive (variables do not vary)
Quite general (can be defined over a class of types, a la templates)
Does not name its arguments (instead defined as types)
Modularity is very important for successful programming. Languages which aim to improve productivity must support modular programming well. But new scope rules and mechanisms for separate compilation are not enough — modularity means more than modules. The ability to decompose a problem into parts depends directly on the ability to glue solutions together. To assist modular programming, a language must provide good glue. Functional programming languages provide two new kinds of glue — higher-order functions and lazy evaluation.
No programming language is perfect. Fortunately, a programming language does not have to be perfect to be a good tool for building great systems. In fact, a general-purpose programming language cannot be perfect for all of the many tasks to which it is put. What is perfect for one task is often seriously flawed for another because perfection in one area implies specialization...
Not everything can be expressed directly using the built-in features of a language. In fact, that isn't even the ideal. Language features exist to support a variety of programming styles and techniques. Consequently, the task of learning a language should focus on mastering the native and natural styles for that language — not on the understanding of every little detail of all the language features.
In practical programming, there is little advantage in knowing the most obscure language features or for using the largest number of features. A single language feature in isolation is of little interest. Only in the context provided by techniques and by other features does the feature acquire meaning and interest.
(from B. Stroustrup's "The C++ Programming language".)
There are a variety of significantly different programming paradigms:
Languages within a family are broadly similar: you should now be able to teach yourself the basics of any object-oriented, procedural, or scripting language within a few weeks!
The differences between language families can be significant.
By the time you graduate you will have had more exposure to other language families. By that time you should be able to consider what programming paradigm best suits a given problem.
Copyright © 2006, 2008 Jim Grundy, Ian Barnes & Chris
Johnson, The Australian National University
$Revision: 1.1 $ $Date: 2008/05/28 04:42:55 $ $Author: cwj $
Feedback & Queries to
comp2100@cs.anu.edu.au