The primary motivation for a PL is to provide a good notation for problem solving.
What does a program do? A program manipulates data values.
+-----------+ input ->| PROGRAM |-> output (value) +-----------+ (value)So a program has following aspects:
Observation: Not all operations are valid for all values. For example, operation addition is not valid for the name of a person. So values for which a set of operations is valid are grouped together into a type:
A given problem may require handling its own set of data values. So most PLs provide a facility to create problem specific data values ("structs", user-defined types, ADTs) and operations ("functions") on them.
var age: integer; (* Pascal *) int age; // C, JavaA declaration makes the compiler take note of the name (age) and its type (integer). The compiler also creates a reference for the variable. The question now is whether storage is allocated for the variable or not. Some languages allocate storage at compile time whereas others allocate storage at runtime.
An issue related to declarations is that of initialization. In some languages, declared variables are automatically and appropriately initialized. Some languages (Java, for example) complain if a variable is used before it is initialized. Most of the languages provide some facility for initialization at the time of declaration.
int x = 10; // CA declaration is called a binding occurrence; all other occurrences called applied or bound occurrences:
float x, y; // binding occurrence, C, Java x, y: real; // binding occurrence, Pascal procedure foo(x: integer; var y: real); // binding occurrence Pascal x = 0.7; // bound occurrence of x y = sin(x); // bound occurrences of x and y
The values associated with the variables in a program is called the environment of the program. Evaluation of an expression should only produce a value and not change the environment. This requirement is called referential transparency. The following C expression is not referentially transparent:
a[i++] + c // not referentially transparentHaskell, being a pure functional language, is referentially transparent: the evaluation of functions and expressions is side-effect free; we can replace any expression with its value, e.g., in f(exp_1, ..., exp_n) and still will get the same value. Referential transparency is violated by (so-called "destructive") assignment statements to global variables within function bodies, or expressions with side-effects like i++ above.
Note that expressions are not expected to change the state of computation; rather statements or commands change the state of computation. An example of a statement is the assignment statement:
a = b + c; // statement in CMany PLs provide a notation to group statements, called compound statements. The following illustrates compound statements in Pascal and C.
begin y := x; x := 0 end (* Pascal *) { y = x; x = 0; } // C
{ float temp; temp = x; x = y; y = temp; }The term block refers to a construct that can contain declarations, including procedure declarations. Not all languages allow procedure declarations inside blocks (e.g., C)
In real world programs based on traditional languages, recursion is not used that often. However it is a powerful abstraction technique and the way to "iterate/loop" in pure functional lanugages (and logic programming, i.e., Prolog).
Exceptionsmake error checking less cluttered:
try { for (int i=0; i < n; i++) a[i] += b[i]; } catch (IndexOutOfBoundsExecption e) { // handle exception throw new IncompatibleArraysException(); // pass the buck }We can think of exceptions as a mechanism that provides a separate channel for error checking.
normal code error handling code | | | --- throw ----->| | | | |Both parts are interleaved in the program text, but are distinguished by the keywords "try" and "catch".
Logic languages (Prolog) lack explicit control structures and support a declarative programming style (as opposed to procedural). Instead of "commanding" how something is to be computed (like in imperative programming), ideally, one merely specifies (i.e., states) logic facts and rules and lets an inference mechanism derive the truth of a statement:
human(socrates). % fact mortal(X) <- human(X). % rule ?- mortal(X) % query X=socrates % positive answer with variable binding yes ?- mortal(tom) no % negative answer: negation as failure (to prove)Actual Prolog programs are sometimes similar to the functional style as we have seen it with Haskell. Main difference include that standard Prolog does not have a type system like Haskell, there are no functions; instead everything is modeled using predicates/relations. Still there are some similarities:
append([], Xs, Xs). append([X|Xs], Ys, [X|XsYs]) :- append(Xs,Ys, XsYs). append [] xs = xs append x:xs ys = x:(append xs ys)Note how the function result is used as an extra argument in Prolog! The Prolog system uses depth first search to find the solution based on the facts and rules provided by the programmer. (This is only partially true. The ordering of facts and rules influences search. Logic languages also have nonlogical constructs like cut, fail, etc.)
Note that functions, procedures, and methods supported in one way or another in practically all languages, are abstractions of expressions. Other abstraction supported in most languages is the notion of a block: a group of statements which are treated as a single unit and in which variables can be defined.
Abstract data types (ADTs) are the next step in abstraction. An ADT is a collection of type declarations and associated operations (functions). ADTs can make certain aspects of their definitions "private", i.e., visible only to the implementer and not to the user of the ADT. Typically all data is hidden and access to data is achieved through functions. Formally, an ADT consists of a specification (signature + constraints), and one or more implementations (i.e., types + code).
With increasing software development costs, there is a need for software reuse. Object-oriented programming has two key features which aid software reuse: inheritance and polymorphism. Inheritance enables addition of new functionality while reusing of basic functionality. Polymorphism refers to identifying the object type at runtime and using the appropriate method. Polymorphism requires runtime support. Consider the following example.
class Shape { // old code void draw() { /* ... */ } } class GraphicsEngine { // old code void render(Shape s) { s.draw(); } } class Circle extends Shape { // new code void draw() { /* ... */ } // new functionality } Circle c; g.render(c); // old code can be reused
If threads communicate through shared variable, mutual exclusion of accesses is required to ensure consistency of its value.
Another form of communication between threads is message passing. Message passing can be used even if the threads are running on different machines. The overhead associated with message passing is the copying of values from the source to the destination. RMI of Java can be thought of as a form of message passing.
int a; // global void foo() { static int count; // persistent float f, *a; // local a = new float; // dynamic f = a; // free variable }
| /\ | | || | |----------------| | Local Variables| |----------------| | Parameters | |----------------| | Dynamic Link | | | |----------------| | Static Link | |----------------| | Return Address | |----------------|
IMPERATIVE: Computation proceeds by manipulating values in fixed
locations ("boxes") and storing the new values back in the original
locations.
In imperative programming, one defines a variable called
"numPersons". If a new person enters the room, this variable is
incremented, as in "numPersons++".
FUNCTIONAL: Computation proceeds by creating new values
and locations to store them. So the value held by a location is never
changed.
Ideally, there are no variables. There is an expression for the number
of persons already present in the room. We create a new expression
which adds 1 to this and returns the incremented value. A pure
functional program might look like (here: ML style):
fun addPerson(e) = (plus 1 e);where "e" is a recursive data type and "plus" is the tag. A value in this representation looks like
(plus 1 (plus 1 (plus 1 (plus 1 0))))which reflects the fact that initially there was no one and four people entered, one at a time. (This expression can be evaluated to 4 at any time.) Note that old values are not destroyed; only new values are created.
Pure functional style is as elegant but can be inefficient. So modern languages retain only the spirit of functional programming. An important feature of functional languages is that functions are also values. So it is possible to return functions as results, etc. So functions are said to be first class.
LOGIC: Logic programming is like functional programming in the sense that values are only created, never destroyed. For example, there is a predicate or relation called "addPerson(X, Y)" where "X" is the number of persons before and "Y" is the number of persons after an entry. The definition of this relation looks like
addPerson(X, Y) :- Y = X+1;The fact that this is a relation and not a function can be seen from the following behavior.
If we use "addPerson(3, Y)", "Y" gets a value 4.
If we use "addPerson(3, 4)", the evaluation is "true".
If we use "addPerson(3, 5)", the evaluation is "false".
If we use "addPerson(X, 5)", "X" gets the value 4.
So the same relation is used by the system differently in different contexts.
OBJECT-ORIENTED: In OOP, the values are autonomous. Computation proceeds by sending messages to values asking them to change their state.
Here there is an object called "numPersons" and if a new person enters the room, a message is sent to it, like "numPersons.addPerson()". Then the object changes its state in response to the message.
Again this is pure OOP: there is no central control. But most of the practical OOPLs have the notion of a central control flow. There is also the notion of inheritance. The following lists some of the key ideas in these paradigms.
IMPERATIVE (C, Pascal) changing the values of variables FUNCTIONAL (Haskell, ML) expression evaluations producing new values recursion higher order functions lazy evaluation (not supported by ML) OOP (Java) data abstraction autonomous objects, constructors, destructors message passing (not implemented in Java for normal methods) inheritance polymorphism LOGIC (Prolog) declarative program, implicit control (DFS and backtracking) relations: no input-output distinction unification (pattern matching) variable bindings to express constraints/relations between predicates dessert(X) :- sweet(X), calorie(X, Y), Y < 2. /* X is a dessert is X is sweet and if its calorie content is less than 2 */ SCRIPTING (Perl) no type declarations runtime type checking high-level constructs like pattern matching, associative arraysJava is a language designed from the scratch to support OOP. The core language is simple. The main disadvantage of Java now is its slow execution speed.
New languages evolve in response to new applications. Applications which might inspire development of new paradigms in the near future are: networking, security, and multimedia.
Further issues to remember:
Types .. primitive, composite, type constructors vs data constructors in Haskell strong typing ... = (a) compile-time type checking, or (b) type errors can be detected e.g. as in Haskell and Java, polymorphism name and structural equivalence/compatibility static and dynamic type-checking overloading coercion inheritance and overriding
Last modified: Mon Mar 19 18:34:51 PST 2001