Either way, a very good reason for not using loops is to make code more readable. If I see a loop I have to run it inside my head to figure out what the intent is, and if there's multiple things going on, that can take time. If I see a 'map' I think "right, this is tranforming every element in this list in this way"; if I see a filter, I think "right, this is removing elements in this list that don't satisfy this property", if I see a groupBy, I think "right, this is grouping elements in this list by this property", etc., etc.
Code isn't only more succinct, it's more human friendly.
Choosing to write more verbose, difficult to decipher, difficult to maintain code, under the claim that it will perform better, is not a good thing: "premature optimisation is the root of all evil".
In practice, if this becomes an issue (which you find out through benchmarking once you know there is a perf issue), most modern languages offer an easy way to swap out your data-structure for a lazily evaluated one, which would then perform the operations in one pass. Languages like Haskell or Clojure are lazy to begin with, so do this by default.
Your Big O analysis is correct. However, in a real-world case the list could be an iterator over a log file on disk. Then you really don't want to use chained combinators, repeatedly returning to the disk and iterating over gigabytes of data on a spinning plate.
And yeah, you could benchmark to figure that out, or you could use the right tool for the job in the first place.
Machines in Haskell: https://hackage.haskell.org/package/machines
Scalaz Stream in Scala: https://github.com/scalaz/scalaz-stream
I've no doubt an equivalent exists for Clojure, too, although I'm not familiar enough with the language to point you in the right direction.
One of the most amazing things about writing the IO parts of your program using libraries like these is how easy they become to test. You don't have to do any dependency injection nonsense, as your combinators work the same regardless of whether they're actually connected to a network socket, a file, or whether they're just consuming a data structure you've constructed in your program. So writing unit tests is basically the same as testing any pure function - you just feed a fixture in and test that what comes out is what you expect.
I found this really useful when writing a football goal push notifications service for a newspaper I work for. I realised that what the service was essentially doing was consuming an event stream from one service, converting it into a different kind of stream, and then sinking it into our service that actually sent the notification to APNS & Google. The program and its tests ended up being much more succinct than what I would normally write, and the whole thing was fun, and took hardly any time.
I would say that is the right tool for the job.
This started as a conversation about for loops vs. chained combinators and wound up with specialized libraries for IO. You're not wrong, but that's a lot more work than a for loop to parse a log file efficiently.
The value of using map is precisely that it can't do everything a for loop can. It can only do one simple thing, which makes it easy for the reader to understand what it's doing.
Because a for loop is the most appropriate construct in the given language. That's different than comparing hypothetical constructs that a language doesn't have.
Go has higher-order functions, and you can write all the maps funcs you want, it just doesn't have the ability to create generic combinators with parametric polymorphism. That's a trade-off I accept with Go to get things done. Like a lot of people, Go for me hits a sweet spot of simplicity, productivity, and performance; simply put the benefits outweigh the drawbacks. Whatever, it's a programming language; a tool.
It's like complaining that a functional language doesn't have an easy way to write mutating procedural code. You're always working within the confines of some language, and unless you're directly working on that language's implementation where you can change things, I'd rather be working with the language than fighting it.
> It's like complaining that a functional language doesn't have an easy way to write mutating procedural code.
I do complain about such languages. That's why I use Scala.
The longer the piece of code one has to read, the easier it is to read it incorrectly, miss a bug, or introduce a bug.
x = x + 1
foo[x]
But easier to miss a bug in foo[x++]1) You cherry picked one of the most confusing uses of operators (and sources of bugs) that we have. If it weren't so ingrained in our educations/history (and so useful for old fashioned looping semantics) I think we all would have moved on from ++ syntax a long time ago.
2) As flawed as it is, it is a well studied phenomenon that LoC is nearly the only measurable statistic we have that is predictive of error rates (that is higher LoC solutions tend to have higher bug counts).
3) The bigger point about looping constructs isn't about LoC in any case. It is de-coupling 2 different areas of code responsibility. One is the mechanics of iteration and the other is what to do on each event.
At the end of the day most looping constructs end up being sugar around while loops anyway, so the reductionist argument is all of it is unnecessary complication, just do everything in a while loop.
I don't know why people keep saying this. We write code to solve problems. The best code solves the most problems in the most correct and efficient ways.
Regardless of your measure of efficiency, if someone cannot quickly tell if your code is correct, does what is intended, and does or does not apply to their problem, then the code isn't good.
That's your measure: Can the people who want to solve the problem you are trying to solve efficiently identify, evaluate, and employ your attempted solution?
The reason people keep saying that we don't have any measures that do this, is that it is a bafflingly tricky problem that has been studied for decades with hardly any forward progress, and lies at the heart of most of the big issues in our industry.
>The reason people keep saying that we don't have any measures that do this, is that it is a bafflingly tricky problem that has been studied for decades with hardly any forward progress
And what if I had a solution? How does this comment help anything? What do you think the difference is between unsolved and unsolvable, and how does your reaction not foolishly apply to both? How would you recognize progress in the context of my post?
Here's a Haskell factorial function:
-- Basic factorial function.
fac :: Integer -> Integer
fac n = product [1..n]
Here's an attempt at a factorial function in another language: .r $15265f38a [] (44) ; dup n + offset;
_
Which is better code? Do they both compute a factorial? Do they both terminate? How did you determine this without using any 'measurable' data?What I'm getting at is that determining which of 2 pieces of code are better is one of the biggest open question in the computer science/software industry. Further, it has been widely studied for a long time and across a wide variety of measurements and contexts, yet remains an open question.
If you have a measure that can be systematically reproduced by anyone for any 2 pieces of code that purport to solve the same problem, that would be a huge breakthrough worthy of wide publication and probable riches.
If you can make that machinable (even given caveats around provably impossible things like the halting problem) you could literally transform the entire industry.
If that is your standard, then it is highly impractical, but not theoretically meaningless. However, "determining which of 2 pieces of code are better" is not such a problem. It is one that can be solved by heuristics and approximation, just the same as predicting the outcome of a race can.
So while there is as of yet no perfect algorithm, I would strongly disagree that there is _no_ algorithm or that there is no way to compare different algorithms meaningfully. We have measurements, and just because they are not perfect doesn't mean they cannot be verifiably correct within a useful margin of error.
We can reliably tell which two pieces of code are better. We cannot do it with extreme precision, and we cannot do it quickly enough to make predictions, but this does not mean that the concept of 'better code' is invalid or not understood in a productive sense. It certainly doesn't mean you can't make informal arguments from it.
Unless you need a do/while loop. Then you're SOL.