In this post, I make no claim to an authoritative definition of the terms and paradigms I discuss. I do hope to convey my personal understanding of each of these terms, in order that it might provide some clarity and context for you to form your understanding.
One final thing to note before we dive in is that few, if any, languages fall strictly within a single paradigm. A language may be primarily procedural but still support some features of a functional language, for instance. The programming paradigms are really more styles of code than language characteristics, but some language features encourage and facilitate certain paradigms.
First up, procedural programming. Procedural programming is any programming style which is based around procedures. A procedural language is a language which works best when used procedurally. These are not to be confused with (and are in many ways the opposite of) functional languages, discussed later. The question then becomes, what is a procedure as opposed to a function? In the context of describing a paradigm, as I do now, a procedure is a set of instructions for how to modify data, and these instructions are usually as independent of the data as possible. Therefore, this modification is usually done in-place. For example, increment might be a procedure taking one symbol and consisting of the instruction “add 1 to the value the symbol refers to”. Then we might call
increment(a) in order to increase a’s value by 1. This is as opposed to a function increment’ which returns the value of
a + 1 without changing a itself. The epitome of the procedural paradigm is the C programming language. Much code within the standard libraries are identifiable as procedures by the fact that they return little to no output, and instead do work on the value of a pointer passed in as one of their parameters. Procedural code often has the advantage of being very memory-efficient, since it tends to modify data in-place. However, purely procedural code can also be incredibly difficult to maintain and reason about, since data is managed independently of the instructions that modify that data, giving them plenty of opportunity to get out of sync and cause unexpected bugs.
As mentioned, the opposite (in some sense) of procedural programming is functional programming. The functional style does work in units of functions. Functions are just like mathematical functions: they take some input and give some output. They don’t have any side effects. They don’t modify data in place, read from the keyboard, write to the network, or wait on a clock. Functions (and as a result, functional programming) are therefore extremely predictable and easy to reason about / follow. However, because real-world programs necessarily do something (write text to a console or read from a file or whatever else) functional languages must have some way of encapsulating these impure “side effects”. Managing these side effects is done differently in every language, but is the greatest source of confusion and intimidation on the part of functional languages. A great example of an extremely pure functional language is Haskell, whose side effects are captured within a construct called a monad (the
IO monad), explained in another article. However, as evidenced by the fact that explaining the IO system requires an entire other post, these languages can be less intuitive to start working in than others. In spite of this, once you adjust to the common patterns, functional code is both quick to write and quick to read, due to the restriction that functions can’t do anything other than return a value (meaning you don’t have to go chasing for what variables get modified by what, when).
Many language features naturally follow from the basic rule of functional programming - functional languages usually support functions as first-class values, allowing them to be returned from other functions or passed as arguments. They often entirely exclude mutable data structures, and use strong and/or static type systems. These are not, in my opinion, necessary to use the functional paradigm (though I would concur that functions as first-class values are a necessity to be considered a functional language). However, these features do make programming within the functional paradigm much easier.
Object-oriented programming is the style of programming in which data is closely associated with the methods that effect it. This doesn’t necessarily mean the methods must be functions, nor does it mean they must be procedures. It just means that there’s an additional label on every method that says “this method is designed to work on [something]” where [something] is a class of objects. In this way, object-oriented programming is a somewhat orthagonal paradigm to procedural and functional. That is, you can have a procedural and object-oriented program (as you may expect from C++ or Java), or a functional and object-oriented program (as may be expected from Scala or O’Caml), but probably not a functional procedural program.
As with functional programming, there are some common language features you can expect when using object-oriented programming. Classes are typically the unit of encapsulation for (data plus associated methods). Interfaces provide groupings of methods without defining the structure of the data or the implementations of the methods. Inheritance allows one class to implement another, or an interface, indicating that that class will have at least the methods defined on its parent, possibly (if it’s a class implementing a parent class) providing a default implementation. Polymorphism allows a child to be passed in to something that expects its parent, due to the guaranteed similarities. Again, these features are not strictly necessary (in my opinion) to program in an object-oriented style, but you certainly don’t have much of an object-oriented language without them.
Imperative syntax is the style of syntax or programming where you as a programmer are telling the computer what changes to make. Procedural languages tend to have very imperative syntax, with calls like
getcount(counter) and our earlier
increment(a). These instructions specify what steps should be taken.
In contrast, declarative syntax is the style of syntax or programming where you as a programmer are telling the computer what should be done. That is, your instructions specify what the result should be. The extreme manifestation of declarative syntax are languages like XML or HTML, languages which are little more than formats for data. HTML has no knowledge of, nor does it care, how your browser renders a
<table>. It just asserts that a
<table> is there. A good example of a benefit of declarative syntax in what would be more commonly considered “programming” is referential transparency. That is, the property of many functional languages asserting that
(doThing(x) + doThing(x)) will have exactly the same result as
y = doThing(x); return (y + y). This may seem intuitive, but consider, as an example, the case where
doThing(x) is a random number generator. Declarative syntax can make it clearer what will happen as a result of your programs, but imperative syntax provides much finer control over how your program runs.
All-in-all, programming is, always has been, and always will be a mess of confusing, overlapping, and even contradicting terminology, but I hope you now have at least a slightly better idea of what each of these more common terms means. There are some other paradigms I didn’t discuss: logic programming, structured programming, etc, but these are mostly obsolete or no longer referred to in industry to the extent that I have never actually heard the phrases said aloud, and therefore excluded them from this discussion.