Could you show me a code snippet of maps and filters used that you believe is more readable than for loops? Maybe I'll get a laugh out of that. :)
The expressiveness of higher order functions comes not from terseness (only) but by expressing intent more clearly.
E.g this expresses (in pseudo) A conversion followed by a filter.
> odd_ints = strings.map(parseInt).select(odd)
This was expressed in the forum, but there was no agreement that this was readable, so I fully expect you to also think the for loop to be more readable. I suspect there is a divide between those who prefer reading how something is done rather than what is intended, in order to understand it.
I think on a higher level with complex code, you definitely want better abstractions to convey intent, rather than say just having one large function that does everything.
I think in many cases map/filter does in fact help with readability of intent, especially if you're not declaring the function body inline, and the functions are commonly available ones like parseInt. But generally this isn't the case, which diminishes the readability such that it's comparable to a for-loop anyways.
I spend most of my time trying to figure out exactly that -- the higher level abstractions that aren't easily conveyed by "map" or "filter", that having map/filter generic functions isn't high on priority. I just want a simple language that I can spew out thoughts onto uncompilable code... tweak the code often as I see fit, and work on fixing type errors later and once I'm done with that it will probably run fine with few bugs. The nice thing about for-loops is that it exposes more control points e.g. breaking out early, using the index, inserting log lines -- that the flexibility helps me mutate the code quickly.
So maybe it's more about coding style, rather than readability. Do you like to spec out your code completely before writing things down, or do you prefer to define the spec as you write and edit the code because it helps you get things done faster?
I prefer to write exploratory code and spec out the design as I go along, and I also prefer map/filter (or list comprehensions) to for-loops.
I suspect it does have to do with coding style, but not in the way you suspect. I like to write very small functions - often one-liners, rarely more than a page - and have each function do one thing and one thing only. I also tend to code mostly bottom-up, figuring out what abstractions I need, writing them, and then writing the functions that use them. So I almost never use an inline lambda for a map, it's usually a function I've already defined.
All of the exploratory scenarios you list are handled by built-in functions in my language of choice (Python). Breaking out early = itertools.takewhile(). Using the index = enumerate(). Inserting the log lines, I'd just insert them in the mapper (although there's also trace).
I can write go code that makes this line legal:
odd_ints := parseInts(myStrings).select(odd)
but I'd probably write code so it looked like this: odd_ints := odds(parseInts(myStrings))
Is either of these harder to read? Does it matter that parseInts is a "map" and odds is a "filter"? Their function is obvious by their names. If anything, the words "map" and "filter" are extraneous.That said, having a library function of course requires it to work in a type safe way for all collections/functions, which might make this argument really be one about generics and not about two collection functions. If this is omitted because generics is, I think it's just another argument why omitting generics is making the language simple to the point of being stupid.
// using some fake filter/map/lambda syntax
names := machines.filter(strings.HasPrefix(m.tag, "ec2")).map(|m| = m.name)
why not just write names := []string{}
for _, m := range machines {
if strings.HasPrefix(m.tag, "ec2") {
names = append(names, m.name)
}
}
What happens when you read that first implementation? Don't you read it as "for each machine, if its tag has the prefix "ec2", then append its name to the list that is returned? Isn't that the exact same thing you'd read the second one as? Except that the second one requires no special knowledge other than loops and if statements. let descending_squares = range(0u, 5u).rev().map(|x| x * x).collect();
versus: let mut descending_squares = Vec::new();
let mut i = 4u;
loop {
descending_squares.push(i * i);
if i == 0 {
break
}
i -= 1
}
(Note that if you try to use a for loop here you will infinite loop due to unsigned underflow.) descending_squares := []uint{}
for x:=4; 0<=x; x-- {
descending_squares = append(descending_squares, uint(x*x))
}There is something that makes me uncomfortable in there:
> let descending_squares = range(0u, 5u).rev().map(|x| x * x).collect();
The overhead. I wonder about it. I am unable to get a sense of what it is. With a simple for-loop, it's rather easy to see it, but with the version above I have no idea. So "much clearer" is not what I see with the piece of code above. I see what it does, but what is not so clear is what code will be generated, something which matters when trying to write efficient code.
I wrote a blog post a while ago that used iterators heavily http://huonw.github.io/blog/2014/06/comparing-knn-in-rust/ , as you can see the performance is good.
See why for loops are tricky? :)
Overflows in general are tricky. How does Rust deal with it?
You can't write that check explicitly in C, but you can in assembler. Available now, across the different hardware.
(Also, I believe it introduces a lot of data dependencies, getting in the way of the out-of-order execution of modern CPUs.)
It can be the problem of the certain compilers if they don't have the infrastructure to reason about overflow flags though. But it's not a hardware problem.
In his "We Need Hardware Traps for Integer Overflow"[0], Regher quotes 5% to 100% overhead for languages such as JS or Racket, and that a "highly tuned" checker would likely be in the 5% range. Playing with arithmetics-heavy programs and Rust's checked_* (which are backed by LLVM's overflow intrinsics[1]) I got anywhere from 5 to 40% performance loss IIRC.
That's not a lot, but at the same time when you're competing with languages specifically not paying those 5%, a 5% hit on all computations is not going to get you much love.
Which is why Rust currently lets you do that (via num::Checked* and num::Saturating) but uses overflowing default semantics.
[0] http://blog.regehr.org/archives/1154
[1] http://llvm.org/docs/LangRef.html#arithmetic-with-overflow-i...
This isn't just a jump, it is a branch. Especially when the body of the loop is 6 instructions, adding an extra branch is going to be noticable.
> Also, modern compilers could optimize the checks away if they aren't used
This is equivalent to the halting problem, and most code will not be able to optimise them away. Suggesting otherwise is invoking "sufficiently smart compiler", which is invalid.
In any case, you haven't addressed the problem of missed optimisations (especially vectorisation) caused by having to maintain semantics.
> It's certainly not the problem of the CPU's.
Yes, it partly is: the data dependencies and linearisation caused by checking the CPU flags is bad.
To me golang is readable while even small rust examples don't feel quite right right.
is a completely different piece of code.
For loops are almost always going to explicitly show their allocations whereas the functional version has allocations that are not nearly as obvious.
Sorry, that's not really fair. I'm sure the compiler would have caught that when you tried to feed it into something that wanted a vector (or would have not cared if you were just iterating over it).
To be honest, I think Rust has a lot going for it. I just think Go has a lot going for it too... they're just different things. Which is fine, because if everything were the same, the world would be a really boring place. :)
The amount of language trickery is at a minimum with Go, so writing something out in plaintext often just works. It's hard to screw up for loops and if statements for people who have been programming for any significant period of time.
It's not that I like that Go doesn't have map, per se. I just don't miss it. At all. And the fact that it's not in the language means I don't have to read someone else's use of it and try to make sure they're not screwing it up somehow. A wise man once said "It's not that I don't want generics, I just don't want you to have generics."
> Go does help prevent errors in untested context-free code
> snippets... by being really really simple, and not trying
> to mash a ton of logic into a single line for no reason.
But there is an example in this very thread of a manually-implemented map-via-a-for-loop from a well-meaning Go user that accidentally underflows into an infinite loop.This isn't an attack on Go (which I respect as a language for having the temerity to be opinionated, a facet that more languages need to emulate), merely bafflement at your claim that implementing everything anew via bespoke loops is somehow effective at reducing errors.
No, it was an example from my experience and a bug I have had to fix multiple times, as I mentioned. I didn't make it up.
Sorry, I don't see the benefit of "preventing errors in code that's never actually executed" as a design goal.
> The amount of language trickery is at a minimum with Go, so writing something out in plaintext often just works. It's hard to screw up for loops and if statements for people who have been programming for any significant period of time.
I just demonstrated a counterexample in this thread.
for (u = 5; u-- > 0; ) {Also, I can count the number of times in my 15 years of professional development experience that I've wanted to count down to zero using an unsigned int as the index... never. Which is not to say that the declarative isn't nice, just saying that the example of the infinite loop is not exactly compelling.
The point here is that iterators can reduce bugs, independent of aesthetic concerns.
> Also, I can count the number of times in my 15 years of professional development experience that I've wanted to count down to zero using an unsigned int as the index... never.
I like to give that example because it was something I actually hit and a bug that I actually had to fix.
In fact I just hit that again yesterday when iterating over a list in reverse order (painting order for CSS box-shadow).
> The point here is that iterators can reduce
> bugs, independent of aesthetic concerns.
Iterators can reduce bugs, really?
This is a very flawed point. Bugs are not caused by lack of language features, they are caused by people. And people make mistakes independent of language features and sometimes because of language features causing cognitive overload or require them to make assumptions.Unless language features prevent the existence of these bugs. Javascript will blindly let you concatenate a number and a string, Go will not. Therefore you can't make the mistake of unknowingly concatenating a number and a string in Go. Thanks to a language feature.
Iterators prevent indexing mistakes (off-by-one errors, index overflow or overflow, wrong-variable use), therefore iterators can indeed reduce bugs.
> Therefore you can't make the mistake of unknowingly
> concatenating a number and a string in Go.
Look at this another way: If you need to do that you now have to think about it and explicitly convert a number into a string. But while you are thinking about it you can make mistake of concatenating a number with a wrong string or make some other screw up, because your thinking power is now reduced.You have to think about it either way, the feature precludes forgetting about it.
> you can make mistake of concatenating a number with a wrong string or make some other screw up
Which you can make in both cases.
> because your thinking power is now reduced.
Your thinking power is not reduced, it's increased: you don't have to wonder whether you should convert something to a string or it already is one, the compiler will tell you, so you can focus better on the actual work at hand.
Having separate concatenation operator with implicit conversion could reduce that overhead:
a := "foo" ~ "bar" ~ 123
Instead of: import "strconv"
a := "foo" + "bar" + strconv.Itoa(123)
But this is not how Golang guys make decisions. And I'm fine with that nowadays as long as they don't claim to be right in that regard. c := a + b
There's no indication from that line of code what a and b are, a type system that doesn't do implicit conversions will tell you "error: adding string and int" and so the programmer can address the problem (e.g. maybe they meant to parse an integer from the string `a`, maybe they meant to format the integer `b` into a string).Using literals is not a useful comparison because there is no confusion about types in that case.
A higher level of abstraction means less flexibility, which means fewer potential things that you can screw up. I don't see how that is a flawed point.
Sorry, if I was rude, I'm just tired of pseudo-scientific language designs.
# utility, normally wouldn't be here
map2 = (f, xs, ys) ->
map (apply f, _), (zip xs, ys)
startsWith = (prefix, str) ->
map2 (==), prefix, str |> and-list
endsWith = (suffix, str) ->
revStr = unchars << reverse << chars
startsWith (revStr suffix), (revStr str)
"<<" is function composition, "|>" is a "pipe" or "reverse application order operator", "_" is partial application. "map", "zip" and "apply" functions are standard and have expected semantics, "chars" transforms a string into an array of chars and "unchars" does the opposite (and they all come from prelude-ls library).This code is both shorter and easier to read (to me, at least) than equivalent for loops and it also composes better. The details of iteration are irrelevant here and so are abstracted, which means they can be trivially changed.
Of course, for "map" to be this useful it needs to be supported by many functionally oriented language constructs. It's also not the best example for anything, it's just a piece of code I wrote in a style I like, in a language I use.
A map can be auto-parallelised.
A loop cannot.
It seems to me that for a language aimed at exploiting concurrent features, easy wins for parallelisation would be a feature.
(Note that Servo has been using this type system feature for a while now to prevent data races in our massively parallel CSS layout code.)
Arrg, you have me excited now!
Going to search for it, but I'd still like to hear your response.
Same reason why even though mergesort is fairly trivially parallelizable there's basically no stdlib running parallel mergesorts by default: you need huge collections before you recoup the synchronization overhead.
My personal brand of bigotry is relational databases, so I am accustomed to thinking of sorted / ordered behaviour as the special case. Thinking in sets is very powerful.
It doesn't have to be either/or. Sometimes you need a loop. But it would be nice to have mapping too.
def isAnagramOfPalindrome(s: String): Boolean =
s.groupBy { c => c }
.map { case(key, value) => value.size() }
.filter { n => n % 2 != 0 }
.size <= 1
And here's a more traditional implementation in JavaScript. function isAnagramOfPalindrome(str) {
var chars = {};
for (var i = 0; i < str.length; i++) {
var char = str.charAt(i);
chars[char] = (chars[char] || 0) + 1;
}
var numOdd = 0;
for (var key in chars) {
if (chars[key] % 2 != 0) numOdd++;
}
return numOdd <= 1;
}
I find the Scala version much more readable, but I'm assuming you'll prefer the JS version? func isAnagramOfPalindrome(str string) bool {
charCounts := map[rune]int{}
for _, c := range str {
charCounts[c]++
}
numOdd := 0
for _, count := range charCounts {
if count%2 == 1 {
numOdd++
}
}
return numOdd <= 1;
}
In this case I think I do prefer the latter. I like how I can name the intermediate objects. :)Also I don't like the practice of groupBy().map(=>_.size()), which hides the performance penalty of creating arrays. I'm sure a better compiler can do better, but I'd have to know the compiler to assume that.
def odd_count(letters):
return lambda ch: letters.count(ch) % 2
def anagram_of_palindrome(letters):
return sum(map(odd_count(letters), set(letters))) <= 1
How's that for some higher-order function action?But really, unless you are deriving programs by doing algebra you are missing the point of things like map() and reduce() (as far as I know no one is actually doing Functional Programing the way Backus described. Am I wrong? I'd love to be wrong.)
Go read Backus' Turing Award paper: http://web.stanford.edu/class/cs242/readings/backus.pdf
from functools import partial
def odd_count(letters, ch):
return letters.count(ch) % 2
def anagram_of_palindrome(letters):
return sum(map(partial(odd_count, letters), set(letters))) <= 1 def anagram_of_palindrome(letters):
return sum(c % 2 for c in Counter(letters).itervalues()) <= 1Maps, filters, folds provide a vocabulary for manipulating entire collections. They allow you to write code for transforming the individual items of a collection without mixing it with code that deals with the shape of your collection. Maps, folds, and filters exist for trees and all kinds of other structures with interesting shapes that are more challenging to traverse and reason about than a simple array or list. Languages with first class support for these operations provide a common vocabulary for processing collections of any shape. This alone is a good enough reason for me to prefer such languages, even without bringing readability into the mix.
I also think that it is usually possible to write code with maps and filters that is at least as readable as its for loop equivalent, especially with the comprehension sugar commonly found in many functional languages.