8 ms·
I've written in both paradigms and I mostly agree with the Go team. That it blows your mind blows mine, and I suppose it's good that we have these choices so w
by jaekwon 12y ago
I've written in both paradigms and I mostly agree with the Go team. That it blows your mind blows mine, and I suppose it's good that we have these choices so we don't have to agree.
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. :)
- alkonaut 12y agoA for loop is a general tool. It can be used for anything. Because of that, it conveys little or no information. As a reader I have to read the loop in detail to see that what it does is perform a mapping, and not something slightly different (the last item could be skipped, etc etc). 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.
- jaekwon 12y agoOk, that's a good example. Your one-liner is pretty concise and the intent is clear. 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?
- nostrademons 12y agoAnother data point for you (I'm not the grandparent poster): 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).
- NateDad 12y agoIs there a reason you need some built-in map thing and not just user-defined functions? If a loop is too hard to read inline, you slap it in a nicely named function and now it's more clear and more concise. Either way, you have to write the code to do the conversion. select(odd) doesn't work unless you've already written the code behind whatever "odd" is, for example. 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.
- alkonaut 12y agothey are so pervasive/important that it should be included in the core library. I'd include my own collection_utils every single time. 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.
- NateDad 12y agoInstead of map/filter: // 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.
- twtwtaway 12y agoI also agree with the Go team. Although I am a big fan of functional language constructs like map & filter, adding them to Go does not feel right. One of the great things about Go is that it's simple but it does not hide much from you. You still know exactly what is going on (concurrency& channels being an exception). That's why I find it easy to read Go code.
- gruvector 12y agoThe go community reminds me of the java community. Blind faith in the design decisions of the language. Any feature it doesn't have is passionately defended as a good decision because the clumsy old way is subjectively clearer, up until the day it gets added.
- NateDad 12y agomapping and filtering are just functions and/or loops. You can write those in go. Making some generic thing to save you 3 lines of (really simple) code is not an incredibly useful use of the Go author's time.
- grey-area 12y agoThat sort of behaviour is in no way unique to Go or Java, and I think it's unfair to judge a language or community by some posts from an online newsgroup - most people are too busy to post or lurk on lists like golang-nuts.
- masklinn 12y agoI found that attitude much more prevalent in the .Net community: for just about anything added since C# 2.0 (I didn't follow discussions about generics after the 1.0 release though I expect it also happened then), the idea was essentially dismissed as pointless ivory-tower wankery useless to developers in the Real World right until MS announced it for the next version, at which point it became an Obviously Great idea and a good way to trash-talk java.
- deleted 12y ago[deleted]
- jacques_chester 12y agoSemantically, loops impose order, maps do not. 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.
- jaekwon 12y agoThere's really nothing about a typical map operation that makes it any more parallelize-able than a for-loop, in the presence of closures.
- pcwalton 12y agoThe article is about Rust. In Rust, the type of the closure indicates whether it can mutate externally visible data (and therefore race on it).
- jaekwon 12y agoGotcha! So will Rust then auto-parallelize these "pure" closures?
- pcwalton 12y agoWe'd like to have generic APIs in the future that will allow idiomatic automatic data parallelization. Niko Matsakis has been thinking about this for quite a while. Stay tuned :) (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.)
- bjz_ 12y ago> Niko Matsakis has been thinking about this for quite a while. Stay tuned :) Arrg, you have me excited now!
- ufo 12y agoI wouldn't put my hopes very high on this. They tried doing this kind of ubiquitous automatic parallelization in Haskell but they ran into parallelization overhead issues because its very hard to have the computer figure out the correct parallelization granularity all by itself.
- pcwalton 12y agolet 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.)
- jaekwon 12y agoWell this is how I'd write it in Go: descending_squares := []uint{} for x:=4; 0<=x; x-- { descending_squares = append(descending_squares, uint(x*x)) }
- pcwalton 12y agoUse unsigned integers for your index and values. That's what makes it hard to use a for loop. (Sure, you could cast a signed integer loop index to unsigned inside the loop to avoid the underflow problem in this specific case, but I'd argue that the functional style is so much clearer than code that has to work around unsigned underflow gotchas.)
- jaekwon 12y agoAll I did was take your for-loop code and translated into idiomatic Go code. My point is the for-loop in Go isn't as terrible as the loop example you wrote.
- klibertp 12y agoIt's a question of which you prefer: declarative or imperative. Having that in mind, and knowing that "readability" is a tricky concept, here is an example in LiveScript: # 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.
- rybosome 12y agoLet's take the example interview question: given a string, how do you determine if it is an anagram of a palindrome? The answer is that, for any given character, at most 1 should appear in the string an odd number of times. Here is an implementation in Scala, with higher-order functions: 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?
- jaekwon 12y agoIn Go: 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.
- carapace 12y agoI'll bite. In Python: 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 http://web.stanford.edu/class/cs242/readings/backus.pdf
- z0r 12y agoThe discussion here has been good, and the thread is already old, but I can't help myself! Maps, 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.