You don't want to use this implementation because it uses reflection, which makes it slow and means the compiler can't catch your errors.
Being a systems programmer, wonder if it's easier for him to just use the for loop as a default. Performance demands are always high in systems programming (because there'll be a whole stack of additional software standing on top of your code and depending on it for performance), so one might end up needing to use for loops often enough that it's easier to just use them all the time for consistency's sake.
...they give you the declarative syntax of filter/reduce/etc but can have the same evaluation strategy as for loops, with comparable performance.
There's nothing inherently slow about higher order collection functions, rust has them and they compile to the same machine code as the equivalent loop construct. Its just that most languages implement them on the wrong data structures. Functional languages implement them on lazy lists, which is good, but has overhead. A lot of languages implement them on arrays, which is bad because then it needs to process the whole array at once, and allocate all the memory, even though the next transformation in the pipeline doesn't need all that memory. Rust implements them on iterators, which have all the sane benefits as lazy lists, but fit better into a performance-focused imperative language.
Those two function calls introduce an overhead that may be significant in a "tight loop", and that may not exist in a hand-coded for loop.
Perhaps .NET's optimizer is smart enough to get things down to a for loop equivalent in some cases, but in general I haven't seen much evidence of that happening.
Any optimizing compiler will inline those functions. "Avoid function calls for performance" hasn't been relevant optimization advice for at least a decade.
If the optimizer isn't choosing to inline where it is a performance win, it's not a good optimizer.
Especially for trivial functions like Current() and MoveNext(), which are pretty much the textbook example of functions where inlining really helps (not so much because of the function call overhead, but because it enables so many intraprocedural loop optimizations).
That's a strawman argument- Neither OP nor my comment made any mention of "map". This is a discussion about reduction and filtering. Yes, using transducers in a way that is silly like you suggest will produce disappointing results.
Transducers aren't performance improvers, they're an abstraction over the process of iteration. They actually degrade performance in the absence of a particularly smart inlining compiler.
So it sounds like you're saying that iterators in Rust are faster than transducer-based filter/reduce.
I think it's clear that my original point was that transducer-based filter/reduce is faster than lazy-list based filter/reduce, and comparable in performance to a for loop. (in that lazy-list operations are MUCH MUCH slower in most cases for filter reduce operations than the latter two approaches)
The optimization techniques to make higher-order functions compile down into the same code as a for loop are well-known. All you have to do is inline, constant propagate, and maybe SROA. Every optimizing compiler I know of, even JavaScript, has no problem doing this.
So I'd dare to say he's a bit of a minimalist. Fanboys and fellow travelers aside, one of the few persons with a similar point of view that I can think of would be Niklaus Wirth.
But that's the luxury that research and academia offer to you, if your problems can be approach from a tabula rasa view, you can tailor your tools to be similary pure (his "pure" obviously not being of the mathematical/functional persuasion).
I mean, there's no solid reason why we have several different kinds of screwdrivers. But that doesn't help you when you have to assemble your IKEA Wöbsörwös furniture to earn your living.
There is a reason Haskell's type system has to be so strong :)
They do run side-by-side just fine, too, FWIW.
For loops make the logic occurring on each operator explicit and in the current frame of reference, as opposed to reduce, which splits the logic from the point of operation.
Also, when reduce is used to produce an artifact similar to a list comprehension: the list comprehension is going to be better optimized, more comprehensible for many programmers, and with two character changes can be turned into a generator instead of a memory-heavy list.
var admins = users.Where(user => user.isAdmin()).Select(user => user.Name)
vs var admins []string
for _, user := range users {
if user.IsAdmin() {
admins = append(admins, user.Name)
}
}
So, at the end of the day, these two pieces of code do the exact same thing, and have mostly the exact same meaning. I read them almost identically...Make a list of admins. iterate through the users, if the user is an admin, append its name to the list of admins.
The unrolled version is a lot easier to modify later. What if you decide that each of these admin users should have a star next to their name? For the go code, you just add one line:
var admins []string
for _, user := range users {
if user.IsAdmin() {
admins = append(admins, user.Name)
user.Name = user.Name + "*"
}
}
for the C# code, you either have to write a second LINQ query (and now you're iterating the list twice unnecessarily), or you have to unroll it into a normal loop.So now you've added complexity to the language that is only really good for saving carriage returns, and it makes your code harder to maintain. Sure, it's pretty and interesting from a programming theory POV... but at the end of the day, I don't get paid to write pretty or interesting code. I do get paid to write maintainable code.
As for your query:
``` var admins = users.Where(u => u.IsAdmin()).Select(u => u.Name); var stars = admins.Select(name => name + "*"); ```
Like you said, you don't get paid per line feed - it's ok to have multiple lines and you can keep the nice syntax. It's ok to not like complicated code - no one likes complicated code - which is why I don't like for loops - I don't _care_ how something is iterated I just want to map every object.
Even if the first half were returning the users themselves to the admins list, you're still iterating over the list twice.
This is yet another reason why the for loop is good - it makes it more clear what is actually going on. This is your code in for loops:
var admins []string
for _, u := range users {
if u.IsAdmin() {
admins = append(admins, u.Name)
}
}
for i := range admins {
admins[i] = admins[i] + "*"
}
The problem with the fancy syntax is that it makes it too easy to write suboptimal code that you'd never write if you were writing plain old for loops, like the above.There is nothing about higher-order operations that would require developers to implement xs.map(...).map(...).map(...) or xs.Select(...).Select(...).Select(...) or whatever as multiple traversals over the data structure.
> The problem with the fancy syntax is that it makes it too easy to write suboptimal code that you'd never write if you were writing plain old for loops, like the above.
Maybe Google should just invest in hiring better people? I know it's getting increasingly harder to find people who still haven't evolved from the 1960ies mindset, but still ...
What happens when you need an alphabetical list of admins who have logged in during the last week? The for-loop approach becomes progressively more and more cluttered with implementation cruft, while a declarative approach just adds a filter and a sort.
And, to be frank, even the example you present is clearer to me in LINQ than in Go, and I am much more familiar with Go.