Procedural/functional programming is in no way weaker than OOP, even
without going into Turing arguments (my language has Turing power and
can do anything another will do), which do not mean much. Actually, object oriented techniques were first experimented in languages that did not have them built-in. In this sense OO programming is only a specific style of procedural programming. But it helps enforcing specific disciplines, such as modularity, abstraction and information hiding that are essential for program understandability and maintenance.
Some programming paradigms evolve from theoretical vision of
computation. A language like Lisp evolved from lambda-calculus and the
idea of meta-circularity of languages (similar to reflexivity in
natural language). Horn clauses fathered Prolog and constraint
programming. The Algol family also owes to lambda-calculus, but
without built-in reflexivity.
Lisp is an interesting example, as it has been the testbed of much
programming language innovation, that is traceable to its double
genetic heritage.
However languages then evolve, often under new names. A major factor
of evolution is programming practice. Users identify programming
practices that improve properties of programs such as readability,
maintainability, provability of correctness. Then they try to add to
the languages features or constraints that will support and sometimes
enforces these practices so as to improve the quality of programs.
What this means is that these practices are already possible in older
programming language, but it takes understanding and discipline to use
them. Incorporating them into new languages as primary concepts with
specific syntax makes these practices easier to use and to readily
understand, particularly for the less sophisticated users (i.e., the
vast majority). It also makes life a bit easier for the sophisticated
users.
In some way, it is to language design what a
subprogram/function/procedure is to a program. Once the useful concept
is identified, it is given a name (possibly) and a syntax, so that it can be
readily used in all programs developed with that language. And when
successful, it will be incorporated in future languages too.
Example: recreating object orientation
I now try to illustrate that on an example (which could certainly be
further polished, given time). The purpose of the example is not to show that an object oriented program can be written in procedural programming style, possibly at the expense of lisibility and maintainability. I will rather try to show that some languages without OO facilities can actually use higher order functions and data structure to actually create the means to mimic effectively
Object Orientation, in order to benefit from its qualities regarding program organization, including modularity, abstraction and information hiding.
As I said, Lisp was the testbed of much language evolution, including
the OO paradigm (though what could be considered the first OO language
was Simula 67, in the Algol family). Lisp is very simple, and the code
for its basic interpreter is less than a page. But you can do OO
programming in Lisp. All you need is higher order functions.
I will not use the esoteric Lisp syntax, but rather pseudo-code, to
simplify the presentation. And I will consider a simple essential
problem: information hiding and modularity. Defining a class of
objects while preventing the user from accessing (most of) the
implementation.
Suppose I want to create a class called vector, representing
2-dimensional vectors, with methods including: vector addition, vector size,
and parallelism.
function vectorrec () {
function createrec(x,y) { return [x,y] }
function xcoordrec(v) { return v[0] }
function ycoordrec(v) { return v[1] }
function plusrec (u,v) { return [u[0]+v[0], u[1]+v[1]] }
function sizerec(v) { return sqrt(v[0]*v[0]+v[1]*v[1]) }
function parallelrec(u,v) { return u[0]*v[1]==u[1]*v[0]] }
return [createrec, xcoordrec, ycoordrec, plusrec, sizerec, parallelrec]
}
Then I can assign the created vector to actual function names to be
used.
[vector, xcoord, ycoord, vplus, vsize, vparallel]= vectorclass ()
Why be so complicated? Because I can define in the function
vectorrec intermediary constructs that I do not want to be visible
to the rest of the program, so as to preserve modularity.
We can make another collection in polar coordinates
function vectorpol () {
...
function pluspol (u,v) { ... }
function sizepol (v) { return v[0] }
...
return [createpol, xcoordpol, ycoordpol, pluspol, sizepol, parallelpol]
}
But I may want to use indifferently both implementations.
One way to do it is to add a type component to all values an define all
the above function in the same environment:
Then I can define each of the returned functions so that it will first
test the type of coordinates, then apply the specific function for it.
function vector () {
...
function plusrec (u,v) { ... }
...
function pluspol (u,v) { ... }
...
function plus (u,v) { if u[2]='rec' and v[2]='rec'
then return plusrec (u,v) ... }
return [ ..., plus, ...]
}
What have I gained: the specific functions remain invisible (because of
of local identifiers scoping), and the rest of the program can only use the
most abstract ones returned by the call to vectorclass.
One objection is that I could directly define each of the abstract
functions in the program, and leave inside the definition of the
coordinate-type dependent functions. Then would be hidden as well.
That is true, but then the code for each coordinate-type would be cut
in small pieces spread over the program, which is less redable and
maintainable.
Actually, i do not even need to give them a name, and I could just
keep the as anonymous functional values in a data structure indexed by
the type and a string representing the function name. This structure
being local to the function vector would be invisible from the rest of
the program.
To simplify the use, instead of returning a list of function, I can
return a single function called apply taking as argument an explicitly
type value and a string, and apply the function with the proper type
and name. This looks very much like calling a method for an OO class.
I will stop here, in this reconstruction of an object oriented
facility.
What I tried to do is to show that it is not too hard to build usable
object orientation in a sufficiently powerful language, including
inheritance and other such features. Metacircularity of the
interpreter can help, but mostly on a syntactic level, which still is
far from negligible.
The first users of object orientation did experiment the concepts that
way. And that is generally true of many improvements to programming
languages. Of course, theoretical analysis also has a role and helped
understand or refine these concepts.
But the idea that languages that do not have OO features are doomed to
fail in some projects is simply unwarranted. If need be, they can
mimic the implementation of these features quite effectively. Many
languages have the syntactic and semantic power to do object
orientation quite effectively, even when it is not built-in. And that
is more than a Turing argument.
OOP does not address limitations of other languages, but it supports
or enforces programming methodologies that helps write better
program, thus helping less experienced users to follow good practices
that more advanced programmers heve been using and developping without
that support.
I believe a good book to understand all this might be Abelson &
Sussman: structure and interpretation of computer programs.