Essentially, you add a preprocessing stage to the compiler that can either enforce rules or alter the code
It could quietly transform all object like types into having read-only semantics. This would then make any mutation error out, with a message like you were attempting to violate field properties.
You would need to decide what to do about Proxies though. Maybe you just tolerate that as an escape hatch (like eval or calling plain JS)
This is tangential but one thing that bothers me about C# is that you can declare a `readonly struct` but not a `readonly class`. You can also declare an `in` param to specify a passed-in `struct` can’t be mutated but again there’s nothing for `class`.
It may be beside the point. In my experience, the best developers in corporate environments care about things like this but for the masses it’s mutable code and global state all the way down. Delivering features quickly with poor practices is often easier to reward than late but robust projects.
It’s interesting to watch other languages discover the benefits of immutability. Once you’ve worked in an environment where it’s the norm, it’s difficult to move back. I’d note that Clojure delivered default immutability in 2009 and it’s one of the keys to its programming model.
We shouldn't forget that there are trade-offs, however. And it depends on the language's runtime in question.
As we all know, TypeScript is a super-set of JavaScript so at the end of the day your code is running in V8, JSCore or SpiderMonkey - depending on what browser the end user is using, as an interpreted language. It is also a loosely typed language with zero concept of immutability at the native runtime level.
And immutability in JavaScript, without native support that we could hopefully see in some hypothetical future version of EcmaScript, has the potential to impact runtime performance.
I work for a SaaS company that makes a B2B web application that has over 4 million lines of TypeScript code. It shouldn't surprise anyone to learn that we are pushing the browser to its limits and are learning a lot about scalability. One of my team-mates is a performance engineer who has code checked into Chrome and will often show us what our JavaScript code is doing in the V8 source code.
One expensive operation in JavaScript is cloning objects, which includes arrays in JavaScript. If you do that a lot.. if, say, you're using something like Redux or ngrx where immutability is a design goal and so you're cloning your application's runtime state object with each and every single state change, you are extremely de-optimized for performance depending on how much state you are holding onto.
And, for better or worse, there is a push towards making web applications as stateful as native desktop applications. Gone are the days where your servers can own your state and your clients just be "dumb" presentation and views. Businesses want full "offline mode." The relationship is shifting to one where your backends are becoming leaner .. in some cases being reduced to storage engines, while the bulk of your application's implementation happens in the client. Not because we engineers want to, but because the business goal necessitates it.
Then consider the spread operator, and how much you might see it in TypeScript code:
const foo = {
...bar, // clones bar, so your N-value of this simple expression is pegged to how large this object is
newPropertyValue,
};
// same thing, clones original array in order to push a single item, because "immutability is good, because I was told it is"
const foo = [...array, newItem];
And then consider all of the "immutable" Array functions like .reduce(), .map(), .filter()
They're nice, syntactically ... I love them from a code maintenance and
readability point of view. But I'm coming across "intermediate" web developers who don't know how to write a classic for-loop and will make an O(N) operation into an O(N^3) because they're chaining these together with no consideration for the performance impact.
And of course you can write performant code or non-performant code in any language. And I am the first to preach that you should write clean, easy to maintain code and then profile to discover your bottlenecks and optimize accordingly. But that doesn't change the fact that JavaScript has no native immutability and the way to write immutable JavaScript will put you in a position where performance is going to be worse overall because the tools you are forced to reach for, as matter of course, are themselves inherently de-optimized.
Like @drob518 noted already - the only benefit of mutation is performance. That's all. That's the only, distinct, single, valid point for it. Everything else is nothing but problems. Mutable shared state is the root of many bugs, especially in concurrent programs.
"One of the most difficult elements of program design is reasoning about the possible states of complex objects. Reasoning about the state of immutable objects, on the other hand, is trivial." - Brian Goetz.
So, if immutable, persistent collections are so good, and the only problem is that they are slower, then we just need to make them faster, yes?
That's the only problem that needs to be solved in the runtime to gain countless benefits, almost for free, which you are acknowledging.
But, please, don't call it a "trade-off" - that implies that you're getting some positive benefits on both sides, which is inaccurate and misleading - you should be framing mutation as "safety price for necessary performance" - just like Rust describes unsafe blocks.
It's redundant in single thread environment. Everyone moved to mobile while pages are getting slower and slower, using more and more memory. This is not the way. Immutability has its uses, but it's not good for most web pages.
Yes, js runs in a single-threaded environment for user code, but immutability still provides an immense value: predictability, simpler debugging, time-travel debugging, react/framework optimizations.
Modern js engines are optimized for short-lived objects, and creating new objects instead of mutating uses more memory only temporarily. Performance impact of immutability is so absolutely negligible compared to so many other factors (large bundles, unoptimized images, excessive DOM manipulation)
You're blaming the wrong thing for overblown memory. I don't know a single website that is bloated and slow only because the makers decided to use immutable datastructures. In fact, you might be exactly incorrect - maybe web pages getting slower and slower because we're now trying to have more logic in them, building more sophisticated programs into them, and the problem is exactly that - we are reaching the point that is no longer simple to reason about them? Reasoning about the code in an immutable-first PL is so much simpler, you probably have no idea, otherwise you wouldn't be saying "this is not the way"
This has really irrationally interested me now, Im sure there is something there with the internal setters on TS but damn I need to test now. My thinking is that overriding the setter to evaluate if its mutable or not, the obvious approach.
> That should make arr[1] possible but arr[1] = 9 impossible.
I believe you want `=`, `push`, etc. to return a new object rather than just disallow it. Then you can make it efficient by using functional data structures.
Without persistent data structures (structural sharing) - every change requires copying the entire data structure, memory usage explodes, time complexity suffers, GC pressure increases dramatically.
With persistent data structures - only the changed parts are new; unchanged parts are shared between versions; adding to a list might only create a few new nodes while reusing most of the structure; it's memory efficient, time efficient, multiple versions can coexist cheaply. And you get countless benefits - fearless concurrency, easier reasoning, elimination of whole class of bugs.
Aside: Why do we use the terms "mutable" and "immutable" to describe those concepts? I feel they are needlessly hard to say and too easily confused when reading and writing.
I say "read-write" or "writable" and "writability" for "mutable" and "mutability", and "read-only" and "read-only-ness" for "immutable" and "immutability". Typically, I make exceptions only when the language has multiple similar immutability-like concepts for which the precise terms are the only real option to avoid confusion.
Same reason doors say PUSH and PULL instead of PUSH and YANK. We enjoy watching people faceplant into doors... er... it's not a sufficiently real problem to compel people to start doing something differently.
Read only does not carry (to me) the fact that something cannot change, just that I cannot make it change. For example you could make a read only facade to a mutable object, that would not make it immutable.
> Why do we use the terms "mutable" and "immutable" to describe those concepts?
Mutable is from Latin 'mutabilis' - (changeable), which derives from 'mutare' (to change)
You can't call them read-only/writable/etc. without confusing them with access permissions. 'Read-only' typically means something read-only to local scope, but the underlying object might still be mutable and changed elsewhere - like a const pointer in C++ or a read-only db view that prevents you from writing, but the underlying data can still be changed by others. In contrast, an immutable string (in java, c#) cannot be changed by anyone, ever.
Computer science is a branch of mathematics, you can't just use whatever words you think more comfortable to you - names have implications, they are a form of theorem-stating. It's like not letting kids call multiplication a "stick-piling". We don't do that for reasons.
I love this idea so so much. I have maybe 100k lines of code that's almost all immutable, which is mostly run on the honor system. Because if you use `readonly` or `ReadOnlyDeep` or whatnot, they tend to proliferate like a virus through your codebase (unless I'm doing it wrong...)
I am a fan of immutability. I was toying around with javascript making copies of arguments (even when they are complex arrays or objects). But, strangely, when I made a comment about it, it just got voted down.
There's something in here for sure, switch over to TS with strict typing and you've got generics to help you out more, at least for validation.
A deep clone isn't a bad approach but given TS' typing, I don't know if they allow a pure 'eval' by default.. Still playing with this in my free time though and it's still tricky.
28 comments
[ 1.5 ms ] story [ 44.3 ms ] threadI think you want to use a TypeScript compiler extension / ts-patch
This is a bit difficult as it's not very well documented, but take a look at the examples in https://github.com/nonara/ts-patch
Essentially, you add a preprocessing stage to the compiler that can either enforce rules or alter the code
It could quietly transform all object like types into having read-only semantics. This would then make any mutation error out, with a message like you were attempting to violate field properties.
You would need to decide what to do about Proxies though. Maybe you just tolerate that as an escape hatch (like eval or calling plain JS)
Could be a fun project!
It may be beside the point. In my experience, the best developers in corporate environments care about things like this but for the masses it’s mutable code and global state all the way down. Delivering features quickly with poor practices is often easier to reward than late but robust projects.
As we all know, TypeScript is a super-set of JavaScript so at the end of the day your code is running in V8, JSCore or SpiderMonkey - depending on what browser the end user is using, as an interpreted language. It is also a loosely typed language with zero concept of immutability at the native runtime level.
And immutability in JavaScript, without native support that we could hopefully see in some hypothetical future version of EcmaScript, has the potential to impact runtime performance.
I work for a SaaS company that makes a B2B web application that has over 4 million lines of TypeScript code. It shouldn't surprise anyone to learn that we are pushing the browser to its limits and are learning a lot about scalability. One of my team-mates is a performance engineer who has code checked into Chrome and will often show us what our JavaScript code is doing in the V8 source code.
One expensive operation in JavaScript is cloning objects, which includes arrays in JavaScript. If you do that a lot.. if, say, you're using something like Redux or ngrx where immutability is a design goal and so you're cloning your application's runtime state object with each and every single state change, you are extremely de-optimized for performance depending on how much state you are holding onto.
And, for better or worse, there is a push towards making web applications as stateful as native desktop applications. Gone are the days where your servers can own your state and your clients just be "dumb" presentation and views. Businesses want full "offline mode." The relationship is shifting to one where your backends are becoming leaner .. in some cases being reduced to storage engines, while the bulk of your application's implementation happens in the client. Not because we engineers want to, but because the business goal necessitates it.
Then consider the spread operator, and how much you might see it in TypeScript code:
const foo = {
};// same thing, clones original array in order to push a single item, because "immutability is good, because I was told it is"
const foo = [...array, newItem];
And then consider all of the "immutable" Array functions like .reduce(), .map(), .filter()
They're nice, syntactically ... I love them from a code maintenance and readability point of view. But I'm coming across "intermediate" web developers who don't know how to write a classic for-loop and will make an O(N) operation into an O(N^3) because they're chaining these together with no consideration for the performance impact.
And of course you can write performant code or non-performant code in any language. And I am the first to preach that you should write clean, easy to maintain code and then profile to discover your bottlenecks and optimize accordingly. But that doesn't change the fact that JavaScript has no native immutability and the way to write immutable JavaScript will put you in a position where performance is going to be worse overall because the tools you are forced to reach for, as matter of course, are themselves inherently de-optimized.
Like @drob518 noted already - the only benefit of mutation is performance. That's all. That's the only, distinct, single, valid point for it. Everything else is nothing but problems. Mutable shared state is the root of many bugs, especially in concurrent programs.
"One of the most difficult elements of program design is reasoning about the possible states of complex objects. Reasoning about the state of immutable objects, on the other hand, is trivial." - Brian Goetz.
So, if immutable, persistent collections are so good, and the only problem is that they are slower, then we just need to make them faster, yes?
That's the only problem that needs to be solved in the runtime to gain countless benefits, almost for free, which you are acknowledging.
But, please, don't call it a "trade-off" - that implies that you're getting some positive benefits on both sides, which is inaccurate and misleading - you should be framing mutation as "safety price for necessary performance" - just like Rust describes unsafe blocks.
Yes, js runs in a single-threaded environment for user code, but immutability still provides an immense value: predictability, simpler debugging, time-travel debugging, react/framework optimizations.
Modern js engines are optimized for short-lived objects, and creating new objects instead of mutating uses more memory only temporarily. Performance impact of immutability is so absolutely negligible compared to so many other factors (large bundles, unoptimized images, excessive DOM manipulation)
You're blaming the wrong thing for overblown memory. I don't know a single website that is bloated and slow only because the makers decided to use immutable datastructures. In fact, you might be exactly incorrect - maybe web pages getting slower and slower because we're now trying to have more logic in them, building more sophisticated programs into them, and the problem is exactly that - we are reaching the point that is no longer simple to reason about them? Reasoning about the code in an immutable-first PL is so much simpler, you probably have no idea, otherwise you wouldn't be saying "this is not the way"
The Purely Functional Data Structures book, that Clojure data structures are based on, is from 1996.
This is how far back we're behind the times.
I believe you want `=`, `push`, etc. to return a new object rather than just disallow it. Then you can make it efficient by using functional data structures.
https://www.cs.cmu.edu/~rwh/students/okasaki.pdf
With persistent data structures - only the changed parts are new; unchanged parts are shared between versions; adding to a list might only create a few new nodes while reusing most of the structure; it's memory efficient, time efficient, multiple versions can coexist cheaply. And you get countless benefits - fearless concurrency, easier reasoning, elimination of whole class of bugs.
I say "read-write" or "writable" and "writability" for "mutable" and "mutability", and "read-only" and "read-only-ness" for "immutable" and "immutability". Typically, I make exceptions only when the language has multiple similar immutability-like concepts for which the precise terms are the only real option to avoid confusion.
Generally immutability is also a programming style that comes with language constructs and efficient data structures.
Whereas 'read-only' (to me) is just a way of describing a variable or object.
Mutable is from Latin 'mutabilis' - (changeable), which derives from 'mutare' (to change)
You can't call them read-only/writable/etc. without confusing them with access permissions. 'Read-only' typically means something read-only to local scope, but the underlying object might still be mutable and changed elsewhere - like a const pointer in C++ or a read-only db view that prevents you from writing, but the underlying data can still be changed by others. In contrast, an immutable string (in java, c#) cannot be changed by anyone, ever.
Computer science is a branch of mathematics, you can't just use whatever words you think more comfortable to you - names have implications, they are a form of theorem-stating. It's like not letting kids call multiplication a "stick-piling". We don't do that for reasons.
https://news.ycombinator.com/item?id=45771794
I made a little function to do deep copies but am still experimenting with it.
A deep clone isn't a bad approach but given TS' typing, I don't know if they allow a pure 'eval' by default.. Still playing with this in my free time though and it's still tricky.