9 ms·
It's great to see JS getting some of the features of better planned languages. But I'm still very nervous about some of the stuff mentioned here with regard to
by afandian 7y ago
It's great to see JS getting some of the features of better planned languages.
But I'm still very nervous about some of the stuff mentioned here with regard to mutation. Taking Rust and Clojure as references, you always know for sure whether or not a call to e.g. `flat` will result in a mutation.
In JS, because of past experience, I'd never be completely confident that I wasn't mutating something by mistake. I don't know if you could retrofit features like const or mut. But, speaking personally, it might create enough safety-net to consider JS again.
(Maybe I'm missing an obvious feature?)
- wongarsu 7y agoMutation is a real weakness of Javascript. I think the general idea is "methods don't mutate unless they are ancient". For example Array.map (an IE9-era feature) doesn't mutate, Array.sort (an IE5.5-era feature) does. Similarly a "for(let i=0; i<arr.length; i++) arr[i] = b" loop will obviously mutate modified elements while a "for(let e of arr) e = b" won't; the trend is towards less mutability in new features. Proper immutable support (or a stronger concept of const) would also help with this.
- jacobwilliamroy 7y ago> for(let e of arr) e = b Is that just arr.map( e => b ) ?
- dao- 7y agoNo, it's broken code and doesn't effectively do anything.
- amitport 7y agoNope. The first does not do anything. The second makes a new array of length 'arr.length' with 'b' in every cell
- wongarsu 7y agoIt doesn't do anything and was my attempt to give a simple example that is somewhat obvious while glossing over the complexity let arr = [{a: 1, b: ["a", "b"]}, {a: 9, b: ["a","c"]}]; let b = "!" for(let e of arr) e = b; console.log(arr) [{a: 1, b: ["a", "b"]}, {a: 9, b: ["a","c"]}] (unmodified) for(let e of arr) e.a = 2; console.log(arr) [{a: 2, b: ["a", "b"]}, {a: 2, b: ["a","c"]}] (modified) for(let e of arr) {let copy = {...e}; copy.a = 4;} console.log(arr) [{a: 2, b: ["a", "b"]}, {a: 2, b: ["a","c"]}] (unmodified) for(let e of arr) {let copy = {...e}; copy.b[0] = "!";} console.log(arr) [{a: 2, b: ["!", "b"]}, {a: 2, b: ["!","c"]}] (modified) The most frustrating thing about all of this is that the best way to make a deep copy to avoid all unwanted modification is JSON.parse(JSON.stringify(arr))
- Izkata 7y ago...honestly, I don't see much complexity here. Understanding of reference types and the difference between deep copy and shallow copy makes it pretty straightforward - the result is the same as it would be in python or java.
- wongarsu 7y agoI don't think Python or Java are examples of good or simple behavior here. Java at least has a strong culture of "copy everything or make it immutable at class boundaries" while javascript libraries often leave you guessing. Examples where this is easy are C where every copy and allocation is very explicit, C++ which has a good `const` concept that's actually useful, or Rust with it's ownership concept that makes mutability very obvious.
- jacobwilliamroy 7y agoThanks for sharing more context. I see what you were trying to illustrate with your earlier examples now. Why not make a little library so you can do these kinds of things safely without reimplementing them every project? Maybe call it safeCopy or something.
- nullandvoid 7y agoProviding It was syntactically correct the first call would assign e to a variable b for each iteration ( pretty pointless ) The arr.map providing you had a variable on the left side would output the result of each iteration into said array ( so an array containing all b values - however I guess you meant arr.map(e => e)
- wongarsu 7y ago> Providing It was syntactically correct Providing arr is iterable (e.g. an array) it's perfectly valid js. Your linter might scream at you to add braces and a semicolon, but neither is needed for correctness here.
- jacobwilliamroy 7y agoNope. I meant arr.map(e=>b) . I try to reimplement pointless code, I get more pointless code.
- davidjnelson 7y agoTypescript readonly properties and types are amazing for this. ReadonlyMap<T>, ReadonlyArray<T>, Readonly<T>.
- zachrose 7y agoIn other words you’re concerned that some Array methods mutate the array (push, pop) and some don’t (map, concat)? If so then yeah, that can be annoying and/or confusing.
- afandian 7y agoYes, it stems from arrays. But extends from there to any collection, built in or custom. Kind of bridges the built in type system but it's really about the expressivity of the language. It becomes especially important in React where you share objects up and down an immutable structure of objects.
- zachrose 7y agoYep. If you've bitten the apple of JavaScript tooling you can kinda sorta rig up compiler-enforced immutable data structures with TypeScript. But IMO if you're going that far it's much easier/well-documented to just use Elm or something.
- lkschubert8 7y agoFor me the worst is slice/splice.
- Dextro 7y agoYes, this is a real problem and I've been bitten by it more times than I can count. Now I always keep this handy website ready: https://doesitmutate.xyz/ https://doesitmutate.xyz/
- afandian 7y agoThanks for sharing. I think this is the kind of thing you just have to learn when you use any language. But when you're switching between half a dozen, being able to rely on consistent founding design principles really makes things easier. And when there aren't any, this kind of guide helps.
- Latty 7y agoI really like Python's design here, if we are not talking about full on language features to enforce immutability, where functions that mutate never return a value and are named with verbs (e.g. 'sort()'), while functions that don't mutate return their value and are named with adjectives (e.g. 'sorted()'). This feels natural - mutations are actions, while pure functions are descriptions. The only real downside is the lack of return values mean you can't chain mutations, but personally that never bothered me.
- uryga 7y agoi used to like that distinction as well but verbs are too useful to let mutating stuff use them all up! and pure functions are actions as well, they just result in a new thing. also, some verbs sound awkward adjectified: take, drop, show, go, get ...
- mmartinson 7y agoThat sounds pretty reasonable. I can see the case for mutation support, but the unpredictable nature of it is what is frustrating and dangerous.
- EGreg 7y agoComing from PHP, we’re used to it. Half the methods have $haystack, $needle, and the other half use them in the other order.
- felixfbecker 7y agoJS already has immutable objects with `Object.freeze()`. Personally I just use TypeScript which can enforce not mutating at compile time (for the most part).
- afandian 7y agoThanks. But can I then add and remove items from an immutable object to create new objects? Part of the immutable value proposition is being able to work with the objects. Based on [0] Freezing feels more like constant than immutable. And the 'frozenness' isn't communicated through the language - I could be passed a frozen or unfrozen object and I wouldn't know without inspecting it. And freeze isn't recursive against the entire object graph, meaning the nature of freezing is entirely dependent on the implementation of that object. I really like the language-level expression and type checking of Rust. But it does require intentional language design. I'm not criticising JS (though I think there are plenty of far better langauges). Just saying that calling `freeze` 'immutable' isn't the full story. [0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- underwater 7y agoHave you looked at Immer.js? It allows you to express modifications to immutable objects as a series of imperative operations. In my experience most "immutability" in JS is enforced by convention or, at best, static type systems. It's not ideal, but it works.
- dvlsg 7y agoLenses in ramda work well, too, if you don't mind being functional in your js code. I suppose that still only fits in the "immutable by convention" category, though.
- Klathmon 7y agoObject.freeze is kind of constant, but you can still easily copy and work with the objects if you need to, for example, the following is valid for most objects: const foo = Object.freeze({ a: 1, b: 2 }) const fooCopy = { ...foo } And you are right that Object.freeze doesn't work recursively (although making it work recursively is fairly easy to implement yourself if you use it a lot). But like it or not JS isn't a language with a powerful type system, and it doesn't pretend to have one so knocking it for that is like knocking Python for using whitespace, or knocking Rust for needing a compiler. Luckily, Typescript and Flow have most of what you are asking for, and they work pretty damn well across the entire ecosystem. Off the top of my head, I know typescript has the ability to mark things as read-only even at the individual property level. [1] And they have tons of the type checking nice-ness that you can expect from other "well typed" languages like Rust. [1] https://basarat.gitbooks.io/typescript/docs/types/readonly.html https://basarat.gitbooks.io/typescript/docs/types/readonly.h...
- heavenlyblue 7y agoI still don't understand why neither JSs var or let allow you to redefine the variable with the same name. I makes chaining things while debugging so much harder: let a = a.project(); let a = debug(a); let a = a.eject(); vs let a1 = a.project(); let a1d = debug(a1); let a2 = a1d.eject();
- EGreg 7y agoWhat’s wrong with var a = ...; a = a.project(); a = debug(a); a = a.eject();
- pcr910303 7y agoThat can’t show the programmer’s intent whether it is mutation (e.g. a = a + someNum) verses defining a new variable with different types (but has a similar meaning so has a same name) (e.g. someKindOfData = [...someKindOfData]) Rust allows this, and it really clears codes up. I don’t have to make up different identifiers for same data but different representations. (e.g. I would do the above code in JS as... someKindOfDataAsArray = [...someKindOfObectAsNodeList])
- deleted 7y ago[deleted]
- EGreg 7y agoWhat are you talking about, LOL
- shortercode 7y agoI always assumed it was to protect against: accidental naming errors, confusion of what a declaration is and copy/paste issues. When I first started writing rust and saw it was a thing I thought it was a terrible idea. I'm a more open towards it now, the strong static analysis Rust does help and it can improve code quality if used in small amounts. However, it still can be quite confusing. Given that JS doesn't restrict the type of a declaration you can just assign a new value to it, place it in a small scope or use a chain.
- jacobwilliamroy 7y agoI really like the lisp convention of using !'s to indicate mutation. Like set-car! . In javascript you kind of have to reason backwards and declare your variables as immutable (const). Though there are still some bugaboos; object fields can still be overwritten even if the object was declared with const.
- bernawil 7y agoconst only means the variable itself can't be reassigned though, and really the main complain about mutation comes from Array methods. Like Array.pop will mutate the array and you have to do an Array.split for the last item instead if you want to keep your array.
- Jasper_ 7y agoAt the same time, all this copying leads to an immense amount of garbage, which can really slow apps down with GC pauses. I really wished JavaScript had support for true immutable structures (a la Immutable.js) since these things do add up. In my side project, which is a high performance web app, I was able to get an extra ~20fps by virtually removing all garbage created each frame. And there's a lot of ways to accidentally create garbage. Prime example is the Iterator protocol, which creates a new object with two keys for every step of the iteration. Changing one for loop from for...of back to old-style made GC pauses happen about half as much. But you can't iterate over a Map or Set without the Iterator protocol, so now all my data structures are hand-built, or simply Arrays. I would like to see new language features be designed with a GC cost model that isn't "GC is free!" But I doubt that JavaScript is designed for me and my sensibilities....
- pault 7y agoDoes shallow copying have the same issues? For example, `let foo = { x: 1, ...bar }` just makes a new object with references to bar's members.
- Jasper_ 7y agoShallow copying will create a new object, and thus, some (small) amount of GC garbage. Less than a deep copy, for sure, which means less frequent pauses, but still garbage to clean up nonetheless.
- Xavdidtheshadow 7y agoI really love the Ruby naming conventions around this: `!` indicates mutation. 2.4.1 :001 > a = [4,3,5,1,2] => [4, 3, 5, 1, 2] 2.4.1 :002 > a.sort => [1, 2, 3, 4, 5] 2.4.1 :003 > a => [4, 3, 5, 1, 2] 2.4.1 :004 > a.sort! => [1, 2, 3, 4, 5] 2.4.1 :005 > a => [1, 2, 3, 4, 5]
- kbp 7y agoA method named "method!" means that it's a somehow "unsafe" version of the method "method". A lot of the time it means "destructive version," but if there's no non-destructive version, the destructive one won't have a ! (eg, Array#shift), and sometimes ! means something else (eg, Kernel#exit! is like Kernel#exit, but doesn't run any at_exit code).
- sev 7y agoIf this is a major concern for you, you might want to use something like immutableJS[0]. More often than not I’ve found it unnecessary except in very contained parts of a large app, but in case it’s helpful I wanted to point it out. [0] - https://github.com/immutable-js/immutable-js https://github.com/immutable-js/immutable-js
- Accacin 7y agoAnother one that I personally prefer is Immer.js[0] [0] - https://github.com/immerjs/immer https://github.com/immerjs/immer
- z3t4 7y agoAlways try to limit the scope of variables. Use local scope. And also functions within functions. The nice part when limiting scope is that you never have to scroll or look elsewhere in order to understand what a piece of code does.
- filoeleven 7y agoYou could always look at ClojureScript. I know I am. Array.flat() => flatten Array.flatMap() => mapcat String.trimLeft() => triml, trimr Symbols are great but they’re much more useful when you can write them as (optionally namespaced) literals, which are much faster to work with: (= :my-key :your-key) ;; false (= :my-key :my-key) ;; true Object.entries() and Object.fromEntries() are both covered by (into). You can use (map) and other collection-oriented functions directly with a hashmap, it will be converted to a vector of [k v] pairs for you. (into {} your-vector) will turn it back into a new hashmap. And...all of these things were already in clojurescript when it was launched back in 2013! Plus efficient immutability by default, it’ll run on IE6, and the syntax is now way more uniform than JS. I’m itching to use it professionally.