> 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!
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 like to write loops where they follow the functional map/filter/reduce paradigm where they don’t mutate anything except some initial variables you “fold” over (defined immediately prior to the loop) and which are treated immutable (or uniquely owned) after the loop.
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.
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.
> 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.
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.
I think we probably agree in a lot of concrete cases but discussing in the abstract tends to bring out polarizing statements. That's why I qualified most of my remarks, because I'll happily ham it up with a functional style...in tests, and other places where I don't feel strongly about what's going on.
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.
> 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.
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)
> 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.