>That's not really true. A pure function can still be coupled to a particular internal data representation.

By stateless I mean unchanging state. The existence of a function definition itself is "state" so of course a function must be coupled to internal state.

If you're referring to a function that relies on a mutable variable outside of the scope of the function, that is not what I'm talking about. That is still a function that relies on shared mutable state and is not modular at all. However a function relying on a global constant is isomorphic to a function that relies on the same internal variable. For example imagine a compiler that in the first pass rewrites all global constants used in functions from this:

const x = 1
def g(n):
return n + x

into this:

def g(n):
return n + 1

Because a compiler can do this rewrite in the first pass and still produce programs with equivalent output the two snippets of code above are actually isomorphic or just different aspects of the same concept. You can even just call it syntactic sugar.

>Namespacing functions together with the data structures they operate on is still a good idea, and helps with keeping a coherent model at each level

Sure but this is just an organizational scheme for human understanding. Structurally whether there is a namespace or no namespace doesn't really matter. Shared mutable state does in fact change modularity structurally.

Again with the compiler example if all functions were pure it can rewrite this:

namespace myNameSpace {
def add(x, y):
return x + y
}

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

print(myNameSpace.add(1,2))
print(add(1, 2))

into:

def myNameSpaceadd(x, y):
return x + y

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

print(myNameSpaceadd(x, y))
print(add(1,2))

As you can see in a first pass a compiler can get rid of all the namespaces and still produce a working program. Which goes to show another isomorphism. Namespaces and functions are the same thing as just a bunch of global functions with the namespace prefixed onto the function name.

Namespacing contributes nothing to concrete modularity. It only contributes in assisting humans psychologically when organizing things, which is still very important. In reality it is a type system that actually indicates required coupling. If you want functions to operate only on a certain data type, use the type system to enforce the restriction.

>Pushing mutability to the edges is good and useful but it's not the be-all and end-all of decoupling. Enforced walls are a much better idea than spending your discipline budget on maintaining decoupling by hand. A lot of the time a pure function can actually be decoupled completely from the datatypes it's operating on by using parametricity (and maybe a standard typeclass that the datatype it operates on conforms to), but you may not notice that unless you've got some module boundaries that nudge you to think about that kind of thing.

Again you're kind of referring to behavioral nudges to get people to reason differently about the program. In terms of pure structure pushing mutability to the edges is the end-all of decoupling. There's no other way of doing it other than creating artificial constructs. Most other technique including API walls is really just psychological barriers to herd programmers into behaving in a certain way.

Most of these walls are ust illusory barriers that just affect programmers by adjusting behavior by making certain things harder to do. The function name and type signature is a wall itself around the function and an actual barrier as the type system and the internals of the pure function cannot actually be touched by anything else. If your function is not pure well, then that's a different story.

The only other actual wall I know about outside of the pure function signature itself is the `private` keyword or something similar. You will note that the private keyword rarely protects static stateless functions in practice. Really the private keyword exists to protect people from misusing shared state. Get rid of shared state and you get rid of the need for the private keyword.

If the function is immutable and stateless there is zero risk for module A to use a function from module B. If there are hundreds of different function calls from module A calling functions in module B yes you can create a spiderweb of dependencies. People gawk at the complexity of the spider web but calling a pure function from module B in module A has literally zero effect on module B itself. the complexity of this web can therefore be ignored[].

You talk discipline and psychology but those areas are debatable as everyone reacts differently to stimuli. Sure these things are important because overall people tend to react similarly to the same stimuli. But in terms of raw structure coupling and decoupling really relies on the existence of shared mutable state. People aren't able to see this and they build walls while building objects and impure functions.

[]Where problems can occur with module A and B is if someone decides to edit the function in module B so that it still works in module B but breaks in module A. But the complexity of this problem is not too bad to deal with. Copying the original function or creating a new one and doing a refactor on all the places the function needs to be replaced is trivial with modern development tools. If all your functions are pure then this really amounts to just a find and replace.

There's actually a more elegant way to deal with this problem by decomposing a function into smaller functions and recomposing the function into the edited version you need while keeping the the old function as a composition of your decomposed functions. This method can only be achieved if the internals of your function are immutable.

See example:

module B{
def g(x):
return (x + 1) * 2
}

module A{
def c(x):
return A.g(x)*4
}

what if we want to edit g in B without effecting c in module A? Let's say instead of multiplying the output by 2 I want to multiply it by 3. Solution decompose g. Then recompose:

module B{
def g(x):
return h(f(x))

//requires find and place refactor for all usages in module B.
def new_g(x):
return z(f(x))

//decomposition of g
def f(x):
return x + 1

def h(x):
return x * 2

//new behavior
def z(x):
return x * 3

}

module A{
def c(x):
return A.g(x)*4
}

Such decompositions can only be done if your program was constructed from pure immutable expressions.