A counterpoint: Option and Result are hard to read and unpleasant to work with. They are so annoying, that the language designers extended the language itself to make them tolerable - do notation in Haskell, '?' operator in Rust, guard-let in Swift.
A plain old for loop has so much to recommend it. You get powerful control flow constructs (break/continue/return) and obvious performance characteristics.
Can you tweak your function to stop filling the first time a zero is encountered? That's a simple one-line change with the for loop, but a puzzle for the functional iteration.
> A counterpoint: Option and Result are hard to read and unpleasant to work with. They are so annoying, that the language designers extended the language itself to make them tolerable - do notation in Haskell, '?' operator in Rust, guard-let in Swift.
Option and Result are tolerable without language extensions (as long as your language has first-class functions and parameterized types, which you want anyway) - https://fsharpforfunandprofit.com/posts/recipe-part2/ . do notation is a small, purely-syntactic piece of sugar that's usable for many different cases, not just error handling.
> A plain old for loop has so much to recommend it. You get powerful control flow constructs (break/continue/return) and obvious performance characteristics.
Those are all language extensions! You're talking about adding four keywords to the language, none of which are anywhere near as reusable as do notation.
> Can you tweak your function to stop filling the first time a zero is encountered? That's a simple one-line change with the for loop, but a puzzle for the functional iteration.
Different code should look different. map, reduce, fold, traverse, foldM are all different functions that do different things, but they're easy to work with because they're all normal functions that obey the rules of functions (and if you're ever confused you can just click through to the implementation in plain old code). Languages don't want to offer several different variants of "for" because it's a language keyword that has to be supported at the language level, but the result is that the "for" loop is far from simple - it does several different things depending on how exactly it's used, and you can't tell which except by going through the details every time.
Fair points - Haskell is all-in on these ideas, and agreed that do-notation has power well beyond Result and Option. I regret including do-notation in my critique, maybe list comprehensions instead.
I think we disagree on what "different code" ought to mean. I have a C function which multiplies a list of numbers; I make it immediately `return 0` if it hits zero. In C that's the same function, just optimized; in Haskell it's a breaking API change due to laziness. I suppose the languages reflect that difference.
I don't think it's about laziness; 99% of the time bailing out of a list operation early vs processing the entire list is a semantic difference that I want to be able to see when I'm reading the code. If what you want to do is really and truly just a performance optimization then the language runtime should be able to do it.
A for loop's greatest strength is its greatest weakness: you can do anything, including in most languages mutating the index variables (god forbid..!), deeply nesting with mutable data, and so on. I usually find I can understand a map quicker than a loop, because a map requires that the code be simpler in most cases.
Obviously in the trivial case though a loop is very easy to understand. The trivial case usually, in my experience, involves iterating through the entirety of an array though anyway, in which case the map is usually more concise.
I like your strength/weakness observation. But if you are mutating index variables, you have a hard case, and it probably will be easier to express with a manual loop than trying to shoehorn it into awkward functional constructs.
An example is the "discard elements by a predicate" function, aka Vec::retain in Rust, std::remove in C++. Rust implements this using a for loop. Maybe it can be done with functional constructs, but it would be harder to write and to understand.
But Vec::retain is pretty close to one of the fundamental functional constructs for containers (it's the in-place version of filter). The argument here is that others should use functions like retain instead of re-implementing that nasty indexing logic. But I don't think it should be surprising that array-like containers are going to have to involve some indexing logic at some level.
Those look like Java loops, and I don't recall them being much more than sugar over a for loop (although presumably they're implementing some Iterable class and apply to more than just arrays).
Either way, like most things in functional programming, it's often about restricting your functions so that they're easier to reason about. There's nothing special about the map function really in any pragmatic sense except when used in conjunction with the other features a good language affords.
I'm also curious if you think something like:
listOfListsmap : List (List Int) -> List (List Int)
listOfListsmap =
(List.map << List.map) (\n -> n + 1)
is easier to understand at a quick glance than a rough equivalent using those loops above:
collections.forEach(x -> {
// anything could happen here to x before the inner loop processes it
// and since we're probably dealing with mutable variables, the inner loop can
presumably access things I put here (which may or may not be a problem, but is something you have to think about)
x.forEach(y -> {
// do something with y, we can do anything with x *and* y here
y = y + 1;
}
}
Can you tweak your function to stop filling the first time a zero is encountered? That's a simple one-line change with the for loop, but a puzzle for the functional iteration.
Yes, and that's exactly why you should use constructs like map when possible. With a manual loop you have to scan more code to verify that it's not doing something more complicated.
To drive parent's point home a bit, I skimmed both examples above when reading and without looking again I am very certain that the map example does not have any weird iteration semantics (eg "stop filling the first time a zero is encountered"), but I'm not sure that the for loop example is similarly 'normal' -- I'd have to check the condition again more carefully.
> Can you tweak your function to stop filling the first time a zero is encountered? That's a simple one-line change with the for loop, but a puzzle for the functional iteration.
A counter-counterpoint: classic for loops are hard to read and unpleasant to work with. They are so annoying, that the language designers extended the language/standard libraries to make them tolerable - array.map in ECMAScript 5, for (x : y) in Java 5 and C++11, foreach in PHP 4, LINQ extensions in C# 3.
A plain old functor/monad has so much to recommend it. You get a uniform way to iterate/transform all kinds of collections, even those which don't support indexing. They can also be used without mutation and are easy to typecheck. In languages with higher-kinded types they can be abstracted over without knowing anything about how the underlying data is structured.
> A plain old for loop has so much to recommend it. You get powerful control flow constructs (break/continue/return) and obvious performance characteristics.
I spent 3.5 hours with a junior team member yesterday refactoring break and continue out of her loops. The code was so much more readable afterwards.
A plain old for loop has so much to recommend it. You get powerful control flow constructs (break/continue/return) and obvious performance characteristics.
Can you tweak your function to stop filling the first time a zero is encountered? That's a simple one-line change with the for loop, but a puzzle for the functional iteration.