acc = "";
for (var i = 0; i < strings.length; i++) {
acc += strings[i]
}
The new version is something like this: strings.join("")
The second version can pre-allocate a string and copy characters into that string under the hood. The first version has to allocate a new string and recopy the characters on every iteration.If your array only has one future---ie there are no references to the unchanged array around---you can re-use the old array. That means you get to mutate in place but still pretend you have immutability.
That is not how modern persistent data structures are implemented. Please do not talk about immutability as if it necessarily means having a naive implementation like this.
Egregious mischaracterization.
Said "tricks" are an entire branch of research in CS.
Now, I realize that in my original post, I might have given the wrong impression. I thought that by my second post I was being clear enough, but perhaps I wasn't. Let's try take three:
Immutable data structures do not necessarily guarantee less copying, or necessarily imply a performance gain. A data structure which does not lend itself well to immutability, such as a C-style array, can lead to very inefficient code when used in an immutable fashion. The C-style array or a variation thereof is also the default in most current languages, including Java, Python, C++, Ruby, and many others, so this is hardly a thing of the past. It's important to be aware of the performance characteristics of the data structures one is using, respective to the way in which they are used.
Only in a naive implementation. Clojure, for example, has a persistent vector that only requires O(log32 n) copying, which grows so slowly as to be effectively O(1).
See: http://hypirion.com/musings/understanding-persistent-vector-...
In the C++ world, libstdc++ strings have copy-on-write semantics, which (as far as I heard) turned out to be terrible because you have to do reference counting instead, and with multithreading it requires atomic operations, which is slower than copying for small strings.
There's alternative representations with different trade offs, such as Data.Text
buf_append bufOps = (++)
buf_concat bufOps = concat
Since (++) has a running time that's linear in the length of its left argument, you want to treat it as a right-associative operator to prevent quadratic runtime when joining a bunch of strings.But, going back to the patch in question, the original code was effectively this:
foldr (flip (++)) [] strs
Since flip had been applied to (++), the resulting operator you now want to treat as left associative to avoid quadratic blow-up. The code, however, uses the right-associative fold, foldr, to join the list of strings str.The patch fixes this problem by replacing the code with the equivalent code
concat (reverse strs)
And how is concat defined in the libraries? Looking at the source [1], it's concat = foldr (++) []
Thus the fix is basically foldr (++) [] (reverse strs).
This version reverses strs, at linear-time cost, to be able to apply the normal, unflipped (++) with a right-associative fold and thus avoid the dreaded quadratic blow-up.[1] http://hackage.haskell.org/package/base-4.6.0.1/docs/src/GHC...
EDITED TO ADD: Also, if you knew the buffers were vanilla Strings, you could even eliminate the reverse overhead since
foldr op z xs == foldl (flip op) z (reverse xs)
for all finite lists xs. Thus you could flip (++) and use foldl instead of foldr to get an efficient implementation like this: foldl (flip (++)) [] strsYou can append in O(1) time using difference lists. They don't have all the niceties of Prolog difference lists, but they are still great if you only have to append: