4 ms·
I think it's kind of bad that we have this trend to use "walls" to enforce modularity. This whole thing about using "walls" to enforce "developer behavior" is,
by throwaway691999 6y ago
I think it's kind of bad that we have this trend to use "walls" to enforce modularity. This whole thing about using "walls" to enforce "developer behavior" is, in my humble opinion, the wrong direction.
If you think about it, almost all lack of modularity comes from shared mutable variables. Segregate mutability away from the core logic of your system and the smallest function in your architecture will become as modular as a microservice.
Really, any function that is stateless can be moved anywhere at anytime and used anywhere without fear of it being creating a permanent foothold in the architectural complexity of the system. So if the code is getting to structured where you become afraid of moving things... do this rather than build classes and walls around all your subroutines.
Remember as long as that add function doesn't mutate shared state you know it has zero impact on any part of the system other than it's output... you can replace it or copy it or use it anywhere.... this is really all you need to do to improve modularity of your system.
>Again and again we pondered: How should components call each other?
I think this is what's tripping most people up. They think DI IOC and OOP patterns are how you improve modularity. It's not. Immutable functions are what improves modularity of your program. The more immutable functions you have and the smaller they are the more modular your program will be. Segregate IO and mutations into tiny auxiliary functions away from your core logic which is composed of pure immutable functions.
>Circular dependencies are situations where for example component A depends on component B but component B also depends on component A.
I've never seen circular dependencies happen with pure functions. It's rare in practice. I think it occurs with objects because when you want one method of an object you have to instantiate that object which has a bunch of other methods and in turn dependencies that could be circular to the current object you're trying to call it from. In essence this kind of thing tends to happen because when you call a method you're actually calling a group of methods and state within a class and upon all those dependencies as well increasing the chances of a circular dependency.
Still I've seen this issue occur with namespacing when you import files. Walls aren't going to segregate this from happening. You need to structure your dependencies as a tree.
- lmm 6y ago> Really, any function that is stateless can be moved anywhere at anytime and used anywhere without fear of it being creating a permanent foothold in the architectural complexity of the system. That's not really true. A pure function can still be coupled to a particular internal data representation. It can still assume particular invariants that you may not want to maintain. 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 - e.g. if your business logic is calling a function that's about the specific mechanics of encoding data for Redis, you're probably using the wrong abstraction. 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.
- leafboi 6y ago>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.