But what you're doing is building small pieces that communicate with each other (via pipes, files, databases, or something similar). That looks almost like an OO design (pieces that communicate with each other over defined interfaces, hiding their internals from each other), except that the inter-object communication channel is both more inefficient and more impoverished in what it can express.
Unix pipes work so well in part precisely because the medium of exchange is so unstructured, with every "module" speaking the same language. You may need to massage the medium between two modules, but guess what, we have other modules like cut and sed and awk, that are not only able to transform the medium so that modules can be attached to one another, but themselves only had to be written once.
I think the Unix pipe pattern of architecture works very well in the large, and you see things very close to it elsewhere. C#'s Linq is fundamentally based on transforming iterator streams - little different, architecturally, than Unix pipes. The Rack middleware stack in Rails has a similar structure - every module has a single method, and recurses into the next step in the pipeline, and gets a chance to modify input on the way in and output on the way out. Both get their power by using fundamentally the same "type" on all the boundaries between modules, rather than module-specific types. It's the very antithesis of a language like Java, which even wants you to wrap your exceptions in a module-specific type.
"That looks almost like an OO design"
Yes. Yes it does. You can only move data so many ways. I've got pipes, you've got messages. Life is good.
"except that the inter-object communication channel is both more inefficient and more impoverished in what it can express"
Really wanted to call bullshit on you here. If it's working, then somehow the efficiencies and paradigm of construction has overcome all these limitations, no? Lot of loaded words here. Are OO paradigms richer in terms of expressiveness? Gee, I don't know. You could say so. But in my mind it's an uniformed opinion. It's all pretty much the same.
Many times OO folks get really frustrated when they start learning FP. I know I did. The sample code did silly things like sort integers. Everything was simple, trivial, academic. Where's the real code? I would wonder. I'd read three books and we'd never get around to building a system.
Looking back, what I missed was that I was already looking at the real code. It was my mindest of wanting all of this expressivness, efficiency, and richness of expression that was preventing me from seeing a very important thing: we were solving the important problem!
Instead, I had a very fine-tuned idea of how things should look: this goes here, that goes there. This is obviously an interface, we should always use SOLID, and on and on and on and on. I had a feel for what good OO looks like. It's a beautiful, rich thing. Love it.
But this kind of thinking not only was not useful in solving FP problems, it consistently led me down the wrong path in structuring FP solutions, which was weird. I would look at things as all being the same -- when I should have been looking at the data and the functions.
Guy I know asked online the other day "What's the difference between microservices and components?" My reply "Everything is the same, but there's a difference in how you think about them. A component plugs in, usually through interfaces. A service moves things, usually through pipes."
If you're looking at a service as being another version of a set of objects passing messages, you're thinking about system construction wrong. Wish I could describe it better than that. It was something I struggled with for a long time.
I think you succeeded.
And I think you're right that OO thinking is probably not going to lead you to a good FP design. Why should we expect it to? (And you're probably also right that OO programmers, unthinkingly, do expect it to.)
Perhaps what I should have said is this: The architecture you're coming up with looks somewhat like Object-Oriented Analysis and Design (OOAD), even if it's implemented with FP rather than OOP.
On to this line: "except that the inter-object communication channel is both more inefficient and more impoverished in what it can express."
There's two kinds of efficiency in play here: programmer efficiency and machine efficiency. In many cases, it makes more sense (now) to worry about programmer efficiency - we're not pushing the machines all that hard. But if I do care about machine efficiency, I can get more of it with a single app than I can with a series of apps connected by pipes, because I don't have to keep serializing and de-serializing the data. Should you care? Depends on your situation. So that's the efficiency argument.
Expressiveness: This chunk of code is expecting a floating-point number here. If it gets that via a (typed) function call, it can demand that the caller supply it with a floating-point number. If it gets it via a pipe, it can't. All it can do is spit out an error message at runtime.
[Edit: fixed typo.]
Can you elaborate please? What exactly was the important problem? Was the important problem turning huge codebases into trivial problems?