Oh come on. The edge functions do mutations, but the interior functions are just functions. Somehow mathematics manages to get things done in this manner.
And then you need to cache something to disk, at which point you either plumb a callback all the way into your onion or give up.
Mathematics doesn't manage to get things done in this manner. One of the key distinctions between programming and mathematics is that programs can do things and math just is.
No, I am not. Perhaps I am not communicating my meaning well though...
A model (class) can represent a certain kind of data, and an instance of that model (object) can represent the details of one instance of that kind of data. On this I imagine we can all agree.
OOP attempted to organize systems by bringing the data together with the operations that could be performed on it (class/instance methods). And with the goal of hiding the implementation details, the instance methods would work on Self, mutating the data members of a given object.
There are many problems with this approach, and one is this: You often have business rules which involve greater than one model, such as how a shopping cart checkout will touch a payment record, a cart, product inventory, etc. The tidy models suddenly get wrapped up in a bigger system where some operation needs to carry and pass around multiple objects and push each object to mutate itself in various ways (or worse, just reach in and directly mutate the objects from outside).
So in a real system (complex), you can find yourself at a point where some object has changed, but it's not clear why or where. And adequately writing tests for a system like this is very difficult, because the combinations of possible scenarios is almost beyond reason.
Now imagine an alternate approach where you can still have a data model, but the operations you might perform are separate and do not mutate anything; instead, they take data (some representation of the model, whether a thin OOP object or a simple data structure such as a hash), they perform whatever operation they need to perform, and they return a new representation of that data (where presumably the details are different from the original data that was passed in).
Writing tests for this is very easy now, since the only setup required is whatever is minimally necessary for that small (single responsibility) operation. And it is more observable, because now a series of functions which make up a bigger operation have a history of outputs which can be compared or saved.
I have demonstrated this approach in a real world situation where my team built a greenfield system to replace a legacy system. Both were Rails, and the original used the typical fat model Rails approach as you would learn from reading Rails books and guides. The new replacement had minimally thin models and "service" modules which had the logic. Test coverage in the replacement system was much higher, but lines of code were much lower. Also tests ran multiple times faster in the new system. Code was easier to reason about, and debugging was easier.
The only downside, as such, was that there were now many smaller functions. So choosing good function names took more work and typically resulted in longer names (to express the intent of the function).
There was initial resistance to this approach, but that resistance later became promotion of the approach to other teams.
Ultimately this approach takes most of the business logic out of the Rails framework, which also makes it easier to transition to separate service processes/systems when things grow too big. Really it's not so unlike the intention and benefits of separating data from actions from display as the model-view-controller paradigm does.
Hey, sorry about this but… I meant to reply to that other “this is how it’s done in math” comment, not you. I agree with you in that Rails‘ Fat Models isn’t great, and I think coupling business logic with persistence was a mistake. In general I would say that ActiveRecord is not great. It’s “adequate for some usecases”.
Going full “pure functions all the way”, is where I think we’re hitting diminishing returns - the the gap between what the language says and what the machine does needs some sort of bridge. In Haskell, the bridge is a whole host of abstractions: Monads, endofunctors etc. That’s… too much stuff. I can’t say it better, I’m afraid.