JavaScript: Does It Mutate?
doesitmutate.xyz
doesitmutate.xyz
Go's `append` builtin is guilty of exactly that sin. And worse, it does not always mutate its parameter in-place, so things get hellish if you start aliasing/shallow-copying slices because depending on the size of the underlying array, appends could overwrite the other slice's data or make the two slices diverge entirely.
The abstraction leaks a bit. The result of a data() or c_str() call cannot be trusted after the vector is manipulated.
When passing a vector by reference to another function you effectively get a double pointer (abstracted behind a reference and a container) so the inner one can be moved.
I'm not familiar with go, but if it is like C in this respect append would have been clearer with a double pointer and no return value.
This is especially so when considering that such operations can be occurring in the UI thread (and not a web worker) where excessive computation can interfere with the responsiveness of the application.
Python let's you do it, and you get "None" as the result, which makes such mistakes slower to discover.
This would require some way to mark a function as returning or void.
Aside: it is a common misconception that Python has multiple return values: it does not. It can return a tuple, which can be automatically destructured into multiple values:
a, b = (1, 2) # this could have been a function
and it has syntactic sugar for tuples which allows leaving off the parantheses: >>> 1, 2
(1, 2)
Which, combined, makes it look like multiple return values: def foo():
return 1, 2
but, it really isn't: >>> foo()
(1, 2)
>>> type(foo())
<type 'tuple'>
so this would be hard to achieve in Python. :(You'd have to carry that information in the AST somehow. From a call site, a function object is just a an object with certain properties. "Did it have a return or not?" is not information which exists on that object. Only the return value is (when you call it), and if it is None, you don't know if that came from "return None" or from the default behaviour.
Python could enrich the function object to include that information, and it would essentially be "supporting 0 or 1 return values", instead of "n return values". But adding that no-assignment rule would be backwards incompatible. At that point, might as well go all the way and support any number return values :)
That said, a "pure" modifier that ensure that no side-effect code is allowed inside a function would be good indeed.
(There could be purity-levels as well, e.g. absolutely pure (doesn't even log or write something to a file or use a random number generator inside etc) vs "doesn't mutate program state" (which is a different kind of the same purity concept I think).
[1] Not really, though. There's the process dictionary, and there's no way to statically or dynamically mark a function as not using them. The use of it, however, is discouraged to the point that most popular Erlang books only mention it in passing close to the end, or not at all.
For example, console.log(2*(a = 10)) would print 20 while setting a to 10
In Pony, though, this is averted, as the assignment there returns the old value of a variable being assigned:
var a = 10
var b: Int
b = a = 20
a == 20 and b == 10 // very true
It's a very interesting approach I wish I saw explored more often.> For example, console.log(2(a = 10)) would print 20 while setting a to 10*
`=` is the assignment operator, not the equality operator. The equality operator is `==` in C and `===` in JS.
Even the C-like languages usually have some comment by their designers claiming that C made mistake there and they are not adding it to their language.
Also, it's perfectly possible to have a non-mutatative builder that returns an entirely new builder instance with the given configuration at each step.
Have a look at the insert method on array:
insert(index, obj...) -> ary
From the signature and the convention one could think, that it returns a new array with the object(s) added at the specified index. However, insert mutates the array and returns a reference to the just mutated array, probably for chaining.However, a convention that is not completely followed in the standard library like this does more harm than good as it can confuse newcomers. And you can not change it due to backwards compatibility.
Side note: I just tried solving an actual problem in julia for the first time. That language does a lot of stuff right, at least for my definition of right.
foo! for mutating.
foo? for predicates.
foo->bar for type conversations.
I don't believe this was always the case, so it's probably caused a lot of deprecation warnings for some people.
Under "instances" there is a list of mutators, accessors, and iterators that I find more useful since it is grouped and alphabetized.
Another pitfall I have seen Dev's make is to assume .filter() will return the original array even if the callback returns true for every element. In fact it always returns a copy, even if all elements pass the filter
I recommend the npm package deep-freeze in conjunction with unit testing, makes this way easier to debug.
I welcome any effort to make developers aware of immutability.
One simple thing to help here is to have the "mutability: flag marked on every method in MDN.
I'd maybe even throw out all non-mutating functions in the first place.
If it's on that page it mutates, the end.
This can make things very hard to troubleshoot.
All of these pass the original array as the last argument, reduce()'s callback just takes one more argument, hence the confusion in the original codepen. It would indeed be highly surprising if some of these functions passed a copy of the original array.
I did a layer over strings/arrays/objects to get nearly the same behavior everywhere. An example of those functions (in my custom programming language) https://p.sicp.me/h4MJE.js and the compiled code https://p.sicp.me/ctr6M.js.
It's so empowering to be able to answer this question with an instantaneous "no, of course not, it can't be". People that never experienced it have no idea.
In one of them it matters if your computer will stop for 10ms ever couple of minutes, or if you use an extra 20MB of memory. This is the world of C, Rust and assembly, and you'll be stupid to try high level code structures here.
The other world is where mostly everybody lives. Here having your code feature complete and correct is overwhelmingly more relevant than putting it above the 95th percentile in performance, and you would be stupid to keep adding complexity just because it's sub-optimal.
It should be very clear what world you are in, because there are actually very few borderline problems.
Yes, you are going into it wrong. What I can see on that comment is that:
- You expected to write a "mostly pure" program in a pure language. It won't let you do that, you will write a pure program. You also do not need to be careful in doing it. The most obvious thing you get on a strict language is carelessly - you don't have to care about things that the language does not allow. You focus on (re)writing your program.
- As a consequence of the first point, the more strict (on anything, including mutability) a language is, the easier it is to change old code, because you can assume a lot of stuff about it, and because you care less about making mistakes.
- Specifically about mutability, it does not take any control away. Mutable values are exactly as expressive as immutable ones. You can have mutability semantics on an immutable language, and most try to make it as near a first class syntax as possible, but the one change is that you will have to make it explicit, one way or another.
Or Lodash-FP...
I was quite surprised a while ago when I learned that `sort` in Common Lisp can mutate somehow randomly.
> The sorting operation can be destructive in all cases.
CL-USER> (defun abc-without (letter) (delete letter '(a b c)))
ABC-WITHOUT
CL-USER> (abc-without 'a)
(B C)
CL-USER> (abc-without 'b)
(A C)
CL-USER> (abc-without 'c)
(A)
This behaves strangely because it modifies the constant list (A B C) rather
than consing up a new one on each invocation (like calling LIST or COPY-SEQ
would), so by the end the list it passes to DELETE has been reduced to (A). (defun isort (sequence predicate &key (key #'identity))
(sort (copy-seq sequence) predicate :key key))A better approach would be to edit the title here, and any other place linking to it.
For the ones that do, it might be a good idea to provide examples of how to accomplish the same thing but without mutating. For example, show how to use `concat` where you'd use `push`.