imperative (Java, C/C++)
functional (Scala, Haskell)
logic (Prolog, Datalog)
OO programming is orthogonal to the above.
Imperative languages emulate instruction sequences for a Von Neumann computer. A Von Nuemann computer has a Processor, Memory and a Bus that links them. The Bus is used to read both instructions and data from Memory into the Processor. The "width" of the Bus is one machine-word (32-bits or 64-bits nowadays). There's a strong correspondence between:
Mutable variables ~~ memory cells
Variable dereferences ~~ load instructions
Variable assignments ~~ store instructions
Control structures ~~ jumps
How can we avoid conceptualising programs word by word?
John Backus - "Can Programming Be Liberated from the Von Neumann style?", 1978: imperative programming is limited by the "Von Neumann" bottleneck. We need other ways of defining high-level abstractions (collections, polynomials, shapes, strings, etc) through teories. A theory consists of:
Theories DON'T describe mutations. For instance, the theory of polynomials defines sums of polynomials like this:
(a * x + b) + (c * x + d) = (a + c) * x + (b + d)
but does not define an operator to change a coefficient while keeping the polynomial the same, like in the following imperative program:
class Polynomial{
public double [] coefficient;
}
Polynomial p = ...;
p.coefficient[0] = 42;
Similarly, the theory of strings defines a concatenation operator ++ which is associative:
(a ++ b) ++ c = a ++ (b ++ c)
but doesn't define an operator to change a sequence element while keeping the sequence the same.
Programming languages provide:
The evaluation stops once it results in a value.
The above evaluation schemes describe the substitution model that reduces a given expression to a value. It can be applied to all expressions. The substitution model is formalised in the lambda-calculus (Alonzo Church). The substitution model can only be applied to expression without a side-effect, like for instance:
x++
Here, the expression has a side-effect (the value of x is incremented between calls), so the expression cannot be evaluated by using the substitution model.
Not all expressions reduce to a values in a finite number of steps. For instance:
def loop: Int = loop
Applying the substitution model to the above expression leads to the following:
loop -> loop -> loop -> ...
Function arguments are evaluated prior to applying the function:
sumOfSquares(3, 2 + 2)
sumOfSquares(3, 4)
square(3) + square(4)
3 * 3 + square(4)
9 + square(4)
9 + 4 * 4
9 + 16
25
Function arguments are evaluated after applying the function:
sumOfSquares(3, 2 + 2)
square(3) + square(2 + 2)
3 * 3 + square(2 + 2)
9 + square(2 + 2)
9 + (2 + 2) * (2 + 2)
9 + 4 * (2 + 2)
9 + 4 * 4
9 + 16
25
Both strategies reduce to the same value as long as:
Call-by-value has the advantage that all arguments are evaluated only once.
Call-by-name has the advantage that a function argument is not evaluated if not used.
Scala allows passing parameters as both CBV and CBN (default is CBV, because it is more efficient):
def constOne(x: Int, y: => Int) = 1
Conditional expressions are like in Java, but for expressions, not statements:
def abs(x: Int) = if (x >= 0) x else -x
x >= 0 is a Predicate.