Here I will discuss Functional Programming (FP) and Object Oriented Programming (OOP) in a fairly pure form. Actual languages, however often make compromises to allow older forms as well as multi-paradigm programming.
Both FP and OOP rely in the notion of program "State" but do so in different ways. In fact there are really two different things that go by the term "state" and it is useful to distinguish between them. The version most people focus on when they say "state" is the current set of values of variables in a program, whether held in objects or not. This depends on having modifiable "variables" in the program.
Using Pascal notation, x := 2 gives an integer variable a value 2. You set a state. Later, within the same scope, you are allowed to say x := 3 which "changes the state".
The way most people visualize the above is that the memory of some real or abstract machine has been altered in a specific way. I will refer to this notion of "state" as Explicit State. The Explicit State is the current collection of values of variables.
However, there is a different, independent view of "state" that most programmers are aware of, but don't always focus on in the same way. In this view, the "state of a computation" is represented by the program counter, which represents the current focus of execution in a program. For example now using Java-like syntax:
if(x == 3){
// a
....
} else {
//b
....
}
At some point in the execution the program counter (PC) may be at point (a) or at point (b), but not both simultaneously, of course. If the PC is at point (a) then the "state of the computation" includes the fact that x is 3. If it is at point (b) then the state is that x is not 3. In this view the state of the computation changes as the program executes (and is dynamic). In the former view the program state is often thought of as static, though that isn't quite correct (since things change). I will refer to this second notion of "state" as Implicit State. It is the current location of the PC and all that implies about what has gone before, no matter how implemented.
Now to the question: What is the fundamental difference between (pure)FP and (pure)OOP?
Pure FP relies on implicit state and deemphasizes (or eliminates) explicit state. In Scheme, for example, a let gives a value to an identifier that is valid throughout a scope. It is called a variable in the documentation, but it is really a name given to a constant. Once given a value the name doesn't refer to any other value in that scope, but might have a different value in a different (or larger, or smaller) scope.
One envisions a computation in a pure FP as an expression that has a value. In order to calculate that expression other expressions are evaluated (recursively, so to speak) until some expressions evaluate to themselves (5, and "foo"). The state of the computation is purely implicit, the location of the PC within the chain of recursion. When the chain ends the original expression can yield a value.
There are no "variables" in the C or Pascal sense. Named values (i.e. constants) may be assigned to memory locations or stored on the stack, or in the cloud, or ... It isn't necessarily specified. In a sense, the state is a Stack.
- Object-Oriented Programming
The first OOP was actually Simula (1967), developed in Scandinavia to do simulation programming. Simula was the main inspiration for C++. The first (and maybe only) pure OOP was/is Smalltalk. It became the inspiration for most other modern OOPs; Apple Object Pascal, Java, Python, .... However, most other OOP languages than Smalltalk compromise in a number of ways. To discuss pure OOP more deeply requires an excursion. There are two ways to think about an object in an OOP; its creation and its use.
In pure OOP an object exists (somehow) and is best viewed as a "bundle of behavior". It responds to a set of messages that are valid for its type. The messages may contain parameters. Whether or not it has anything like "state" is immaterial. It "behaves" when sent a message. As part of carrying out its task (as defined by the message and parameters) it may, itself, send messages to other objects. Some messages are functional in nature and return values (objects or primitive values). Some are not. You need non-functional methods to, for example, drive printers and other external devices.
In this view it is not fruitful to think of objects as encapsulating "state". The state is not material. Only the behavior matters. The implementation of an object might rely on saved state, or it might rely on delegating things to other objects through a chain of messages (again, like recursion to some base case).
Objects when used are bundles of behavior, nothing more.
- Object Creation in Pure OOP
OOP languages, however also provide a mechanism for the creation of objects. There are two main mechanisms for this (Classes and Prototypes), which don't affect the discussion here. I will assume a class-based languages since most (non javascript) programmers are most familiar with classes. In creating a class definition the job of the programmer is to define the behavior of its objects. In Java, that means the visible methods (visibility is a bit complex for this discussion, think non-private for a simplification).
An object of the new class must be able to respond to a given message (with parameters, perhaps). That can be made to happen in different ways. For example, the object might defer the action to another object. But it might also define a (classic) variable, and give it a value. Having given that variable a value, it is free to retrieve it later and (perhaps) modify it. But don't be naive about things. Just because the name of a method is "setDisplay" doesn't necessarily mean that there is some variable (anywhere) that will have its value changed. It is a concept, nothing more. The object will somehow (up to the programmer) retain the value given so that the "state of the computation" can move forward.
So, in this "pure"OOP view, the job of creation of objects is the job of defining the behaviors to which the object can respond. You can think of an object (if you like) as a bundle of state + behavior, but that should only work at the definitional stage, not the usage stage.
Note that in this view, a computation is seen as a collection of independent objects that communicate via message passing. The objects may be normally active (actor model) or not, with the latter being more common. An object is, in the non-actor case, only active when it has been sent a message. It is easy to visualize that a PC comes along with any message allowing the object to execute the method. If the object sends a message itself, the PC is passed along as well, letting the receiver of the message act.
- Why this OOP view is important.
One of the problems of learning to program is the problem of how much information needs to be kept in mind as you proceed. If it was necessary to retain every detail of the program, it would be an impossible task. Therefore we use various abstraction facilities (mental facilities) to reduce the task. I can't remember the state of 4gb of values. I can't even really remember the implicit state of an if-statement if nested more than about 4 levels deep. Therefore I don't write such programs and instead create simple objects with simple behaviors. I also create objects that have any explicit state private - no exceptions. I design objects so that I can effectively think of them as nothing but bundles of behavior, though Java certainly has facilities that let me do otherwise. However, if I break my rules, my program becomes harder to understand, use, and modify.
- A pure OOP program is a set of simple objects with simple methods. The complexity of the program is in the interactions of objects, not implementations of the objects.
Yes, I write a lot of classes. But each is simple to conceptualize and simple to build. Each object is a bundle of behavior, nothing more.