I agree with everything you said. To address the part we disagree though:

>On one point I disagree: " In both OOP and procedural programming your designs can never have this level of flexibility due in to modules being tied together by shared mutable state."

When I define OOP I define it as mutable state. The main reason is because there's a equivalence when you do "immutable OOP"

object.verb(parameter)

is no different than:

verb(object, parameter)

Because both features are equivalent I would say neither feature is OOP and neither feature is really FP.

The difference is (mostly) syntactic sugar and it won't change the structure of your programs overall.

That being said:

object.setter()

has no equivalence to FP and is unique exclusively to OOP. Even the vocabulary: "setters" is unique to OOP. Hence following this logic, if you're doing OOP your code will have methods and "setters" that mutate or set internal state. If you're doing FP your code should be devoid of such features.

In fact if you do regular procedural C-style programming with immutable variables your code also becomes isomorphic to FP as assignment becomes equivalent to just creating macros for substitution in an otherwise functional expression.

immutable procedural

def add(x, y):
doublex = x * 2
doubley = y * 2
return doubley + doublex

functional:

def add(x, y):
return y * 2 + x * 2

haskell (functional):

add x y = let doublex = x * 2,
doubley = y * 2
in doubley + doublex

also haskell:

add x y = (y * 2) + (x * 2)

Due to the fuzziness in boundaries there's really only a few things that are hard differentiators between the three styles. When you examine the delineation and try to come up with a more formal definition of each programming style you will find that FP is simply defined as immutable programming, OOP is defined as a programming style that uses subroutines to modify scoped external state and procedural programming involves programming that can mutate variables in all scopes from local, external to global. There's really no other way to come up with hard barriers that separates the definitions and stays in line with our intuition.

It's all semantics either way and most people do a bit of a hybrid style of programming without ever thinking about what are the real differences so it's not too important.

The thing that's sort of bad about OOP is that it sort of promotes a style of programming where subroutines modify external state that's scoped. You tend to get several subroutines that modify some member variable and this is what prevents you from ever reusing those methods outside of that shared state. <= This is in fact the primary area of "bad design" that most people encounter when doing OOP programming... shared state glues all the lego bricks together making your code inflexible to counter the inevitable changes and flaws that you can never anticipate. The other issue is people never realize that this is what's wrong with their program. They blame their initial design as incorrect when really it analogous to saying that their initial lego project was incorrect and they should have glued the bricks together a different way. The primary key to fixing this design problem is to NOT glue your bricks together at all!

While even though you're capable of the above antics in procedural programming... in procedural programming most programmers tend to keep the mutations scoped to within the boundaries of the function definition hence keeping the function reusable. The downside of this is that if you have shared state within the scope of your function it becomes harder to decompose your function into smaller functions because shared state glues all your internal instructions together.

It's easy to see this face in functional reduce. Higher order functions like reduce allow you to break out the function that actually reduces a list while a for loop with a mutating accumulator can not be broken into further smaller functions.

compare the two below:

loop decomposed into two reusable functions (reduce and add):

add = (acc, x) => x + acc
m = reduce(add, [1,2,3])

m => 6

not decomposable by virtue of mutating accumulator:

m = [1,2,3]
acc = 0
for(int i = 0; i < m.length; i++){
acc += m[i]
}

acc => 6