And, to be brutally honest, as much as I love those functional combinators, first-class functions, streams, etc, they suck to reason about.
Sometimes loops are better!
And, to be brutally honest, as much as I love those functional combinators, first-class functions, streams, etc, they suck to reason about.
Sometimes loops are better!
> Sometimes loops are better!
That I think is backwards. A loop could be doing literally anything - it probably is futzing with global variables - so there's no way to reason about it except by executing the whole thing in your head. A map (or mapA) or a fold (or foldM) or a filter or a scan is much more amenable to reasoning, since it's so much more specific about what it's doing even without looking at the body.
I find this has good readability and by containing the mutation you can reason about it “at a distance” quite simply since further away it’s for-intents-and-purposes pure code.
What you might lose are the compiler-enforced guarantees that a functional language gives you. Some languages give you the best of both worlds - with rust you could put this in a pure function (immutable inputs, owned outputs) and the borrow checker even reasons about things like this within a function body.
And that's what map() basically does.
The opposite is true in my experience. Loops working over variables local to a function is a lot easier to reason about then a series of intricately intertwined lambdas, if they share state. If everything you do is functional then a series of pipelines might be alright, but it might not. If the functions are spread all over the place it can take a larger amount of mental load to keep all the context together.
Code should permit local reasoning, and anytime that is obscured, rather than helped by, abstractions, we incur additional cognitive load.
I mean accessing a mutable variable from a lambda is obviously insane, agreed. The whole point of using the functional combinators is that you don't do that.
When you are forced to use some accumulating global state, that leaves you with writing in a different style--loopy, if you will--which maybe is a good signal to the reader that something is weird or different, but then again maybe it isn't.
One thing that bit me using Java streams recently is that it completely broke down when I had a concurrent data structure that needed to have the coarseness of locking tuned for performance. Laziness and functional streaming operators had to just go out the window to even make it clear where a lock was taken and released. So loops it was.
Well yeah, that's very much expected. If you're even talking about locking, stream transformations aren't a good fit (except maybe if you do the microbatching/orchestration style where your stream transformation steps operate on chunks of a few thousand items - and even then your locks should be scoped to a single transformation step).
(Now I'd happily claim that for equal developer time one can usually outperform a locking-and-mutation implementation with a stream-processing implementation - not because the low-level mechanics are faster but because it's easier to understand what's going on and make algorithmic improvements - but that's a rather different matter)
They're dead simple, there's no abstraction, its just loops and variables (and state, which is the real killer). But they're impossible to reason about as a result.
You can write bad code in any language with any constructs. The more code you do write the worse bad ideas just metastasize like cancer.
”Simple” code is simple to reason about, but it is not as expressive.
”Complex” code is just as simple to reason about, but being more expressive, it requires more intimate knowledge of the language features and abstractions used.
Then there is ”bad” code, which is confusing for reasons other than domain complexity.
Sometimes complexity just gets handed to a developer even if they say no, and that doesn’t make their complex code bad code.
> Then there is ”bad” code, which is confusing for reasons other than domain complexity.
I've previously seen these two summarized as "the difference between complex and complicated".