*.empty Idea
I'd love to bring a proposal to the committee for this since it seems like there's interest.
I suspect that even though some of the "empties" seem useless to some, somebody somewhere will find a use case, and consistency is useful (that everything that could have a concept of "empty" would have a .empty)
Errata:
- I don't believe
GeneratorFunction
is a global, so we wouldn't need to specify one of those - I wonder if
Promise.empty
asnew Promise()
, ie, a forever pending promise, would make sense? - or
Date.empty
asnew Date(0)
? - We'd definitely want
Map.empty
andSet.empty
assumingObject.freeze
actually froze them - We'd probably want
Object.freeze(Object.seal(Object.preventExtensions(empty)))
, to be extra restrictive.
Does anyone see any problems or have any objections, beyond "I don't think there's a use case"?
quick one to whoever will write the proposal: please bear in mind the empty function must have a frozen prototype too
Andrea, good catch.
On Mon, Feb 23, 2015 at 11:59 AM, Isiah Meadows <isiahmeadows at gmail.com> wrote:
Object.freeze does not freeze them, as far as I know. It might require method overrides.
Object.freeze does not freeze their state. A proposal for a way to either freeze the state of collections, and/or to create frozen snapshots of collections, for future ES would be welcome and appreciated. I encourage any such effort to pay attention to Clojure and React.
Doesn't Object.freeze imply the other two? I thought it did.
It does. Given that all these methods of Object have their original value and that x is not a proxy
Object.freeze(Object.seal(Object.preventExtensions(x)))
must be equivalent to
Object.freeze(x)
Pardon my ignorance, but in what cases does the proxy in this case differ? (although it's not entirely relevant in this particular case)
Only that the proxy can detect each of the operations as a distinct trap, and do something weird. A proxy that intend to emulate anything like a normal object would choose not to, preserving by convention this equivalence. It becomes a matter of informal unchecked contract conformance, rather than a platform guarantee.
On Mon, Feb 23, 2015 at 3:31 PM, Mark S. Miller <erights at google.com> wrote:
Object.freeze does not freeze their state. A proposal for a way to either freeze the state of collections, and/or to create frozen snapshots of collections, for future ES would be welcome and appreciated. I encourage any such effort to pay attention to Clojure and React.
I ran into this today.
As a strawman proposal:
Add "If TestIntegrityLevel(M, "frozen") is true, throw a TypeError exception" between steps 3 and 4 of 23.1.3.1 (Map.prototype.clear), 23.1.3.3 (Map.prototype.delete), 23.1.3.9 (Map.prototype.set), 23.2.3.1 (Set.prototype.add), 23.2.3.2 (Set.prototype.clear), and 23.2.3.4 (Set.prototype.delete).
This wouldn't be some fancy all-singing all-dancing collection snapshot, and would still leave the user responsible for freezing the prototype, etc, but it would bring Map and Set back into parity with object (that is, m.get(f) behaves for the most part like o[f]).
Users would still have to special-case Map/Set when they implement their own deepFreeze() methods, etc, but this ensures that the internal list is actually protected. It is (AFAICT) otherwise impossible in ES6 to maintain object identity when "freezing" an collection.
I really like this - I'm writing up a ".empty" proposal and I'll include this as part of it, thanks!
This must not be hung off of Object.freeze. Object.freeze is about tamper proofing an object's API, not about making its internal state immutable. I regret the term "freeze" for this purpose as it repeatedly suggests this confusion. OTOH, because of the override mistake, Object.freeze is not directly usable as a good tamper proofing operation, so in this sense it was fortuitously good that it did not use up the name tamperProof.
Deep immutability is a crucial concept deserving of good support for many reasons including security patterns. Deep immutability must of course include encapsulated state captured by lexical closures, and it must do so without violating the security properties that this very encapsulation guarantees. The Auditors of E (www.erights.org/elang/kernel/auditors, wiki.erights.org/wiki/Guard-based_auditing) and Joe-E (www.cs.berkeley.edu/~daw/papers/pure-ccs08.pdf, www.eecs.berkeley.edu/Pubs/TechRpts/2012/EECS-2012-244.html) provided solutions to this dilemma in their respective languages. E and Joe-E also support shallower forms of immutability that are specific to collections -- that are distinct from API tamper proofing but that do not extend to considering the mutability of the contents of the collections.
I welcome proposals that would make sense for JavaScript.
Mark: I also agree that Object.freeze
is flawed, and I welcome a proper
proposal for Object.tamperProof
or what have you. But Object.freeze
works just well enough in the short term that people can build useful
mechanisms on top of it.
My proposal just makes Map
/Set
behave consistently with Object
with
respect to Object.freeze
(preserving the consistency between object
fields and the behavior of Map
). I think that consistency is preferable
to the current situation, even though (as I said) I welcome a proper
mechanism for immutability in the future.
Said a different way: one day we will have Object.tamperProof
and it will
be wonderful. But we will always be stuck with Object.freeze
as well,
because it's been released into the while. Let's at least try to make
Object.freeze
a little more consistent, and not let future features
prevent us from improving what we have now.
this would only be confusing. Object.tamperProof is built on and implies Object.freeze. It is like Object.freeze except that it replaces (some :( ) data properties with accessors in order to work around the override mistake.
Object.freeze only operates on properties and the [[Extensible]] bit, nothing more. This is by design. It is the wrong tool for the (very valuable!) job you seek.
Again, my apologies for the terrible name.
It would also not be compatible with ES6 code. SES will be freezing Map, Set, WeakMap, and WeakSet instances in order to tamper proof their API. I expect many others will as well. Having this freeze then cause a non-mutability in ES7 will break all such ES6 code. This is a non-starter all around.
On Thu, Apr 30, 2015 at 2:22 PM, Mark S. Miller <erights at google.com> wrote:
It would also not be compatible with ES6 code. SES will be freezing Map, Set, WeakMap, and WeakSet instances in order to tamper proof their API. I expect many others will as well. Having this freeze then cause a non-mutability in ES7 will break all such ES6 code. This is a non-starter all around.
Couldn't SES use Object.seal/Object.preventExtensions/Object.defineProperty to perform tamper-proofing without flipping the "frozen" bit?
It doesn't matter. Others will do the same thing in ES6 and then break under your proposal in ES7.
Can you make an alternative proposal that still preserves the essential property of Object.freeze on collections -- that is to say, preserves object identity while preventing future writes?
Here's another strawman:
Add "[[Immutable]]" to the arguments of OrdinaryCreateFromConstructor in
step 2 of 23.1.1.1 (Map) and 23.2.1.1 (Set)
Add "If the [[Immutable]] internal slot of M is true, throw a TypeError
exception" between steps 3 and 4 of 23.1.3.1 (Map.prototype.clear), 23.1.3.3 (Map.prototype.delete), 23.1.3.9 (Map.prototype.set), 23.2.3.1 (Set.prototype.add), 23.2.3.2 (Set.prototype.clear), and 23.2.3.4 (Set.prototype.delete).
Add
Map.prototype.makeReadOnly()
andSet.prototype.makeReadOnly()
methods with the definition:
- Let M be the this value.
- If Type(M) is not Object, throw a TypeError exception
- If M does not have a [[Immutable]] internal slot, throw a TypeError
exception
- Set the [[Immutable]] internal slot of M to true.
This is somewhat awkward and ad-hoc, but it won't break ES6 code.
I am concerned about this issue in part because as written is seems to be
impossible to prevent writes to Map. If you subclass it, freeze it,
override set/delete/etc it is still possible to mutate the map using
Map.set.call(m, 'a', 'b');
. And there is no way for use code to get at
the [[MapData]] slot to protect it.
It's fine to propose something to make maps immutable. Just don't call it Object.freeze.
That is indeed the kind of proposal I was looking for, thanks. An issue is that it locks down the Map in place, which is a design issue we can debate but is not necessarily a show stopper. I would be happy to see this enter stage zero to get the conversation started,
Another possibility is to have collections respond to messages like
- .snapshot() -- returns an immutable snapshot of the receiver collection
- .diverge() -- returns a mutable collection whose initial state is the a snapshot of the receiver
- .readOnly() -- returns a readOnly view of a possibly mutable collection.
by returning a new derived collection with the desired mutability. Given that c is a conforming collection, we'd have the following algebraic properties
c.snapshot().snapshot() === c.snapshot()
c.snapshot().readOnly() === c.snapshot()
c.readOnly().readOnly() === c.readOnly()
c.diverge() !== c
Further, for each mutable collection type, it would be nice (by possibly too breaking of a change) to insert a superclass above it whose prototype holds its query method including the three above, leaving only the mutation methods on the existing subtype. Then, .snapshot() and .readOnly() could return an instance of this supertype that is not an instance of the mutable subtype.
I like the idea of snapshot methods, but they can be implemented in user code using subclasses in ES6. I'm particularly interested in the "lock down in place" mechanism because it cannot be implemented in user code.
And yes, if we had it all to do over again, it would have been nice if the Map prototype chain was:
Map instance -> Map.prototype -> ReadOnlyMap.prototype -> null
And clear/set/delete were properties of Map.prototype.
But that would still have required an explicit test in Map.clear/set/delete to ensure that they were operating on an instanceof Map and not just on a ReadOnlyMap. --scott
On Thu, Apr 30, 2015 at 12:46 PM, C. Scott Ananian <ecmascript at cscott.net> wrote:
But that would still have required an explicit test in Map.clear/set/delete to ensure that they were operating on an instanceof Map and not just on a ReadOnlyMap.
Yes, it does not save a test. Rather it only rationalizes the types of objects so that the type of an immutable or readOnly collection does not include useless mutation methods; that's all.
On 4/30/15 3:46 PM, C. Scott Ananian wrote:
I like the idea of snapshot methods, but they can be implemented in user code using subclasses in ES6.
Can they? You can't create immutable snapshots in user code afaict...
But that would still have required an explicit test in Map.clear/set/delete to ensure that they were operating on an instanceof Map and not just on a ReadOnlyMap.
More likely an internal slot check, not an instanceof check.
I don't see how it would be possible in ES6 user code to ever make a
Map/Set or a Map/Set subclass instance immutable, since
Map.prototype.set.call
/Set.prototype.add.call
will operate on any
Map/Set-like object's [[MapData]]
/[[SetData]]
internal slot. The only
thing I can think of would be using a Proxy to a collection rather than a
collection itself.
I'm probably missing something - can anybody confirm or refute?
On Thu, Apr 30, 2015 at 5:57 PM, Jordan Harband <ljharb at gmail.com> wrote:
I don't see how it would be possible in ES6 user code to ever make a Map/Set or a Map/Set subclass instance immutable, since
Map.prototype.set.call
/Set.prototype.add.call
will operate on any Map/Set-like object's[[MapData]]
/[[SetData]]
internal slot. The only thing I can think of would be using a Proxy to a collection rather than a collection itself.
var readOnlySet = function(s) {
s = new Set(s); // if you're paranoid
var ReadOnlySet = function(){};
ReadOnlySet.prototype = Object.create(Set.prototype);
['entries','has','keys','values','toString',Symbol.iterator].forEach(function(f){
ReadOnlySet.prototype[f] = function() { return s[f].apply(s,
arguments); };
});
['add','clear','delete'].forEach(function(f) {
ReadOnlySet.prototype[f] = function() { throw new TypeError("immutable
set"); };
});
ReadOnlySet.prototype.forEach = function(cb, t) {
return s.forEach(function(v,k,_) { return cb.call(t, v, k, this); },
this);
};
Object.defineProperties(ReadOnlySet.prototype, { size: { get: function()
{ return s.size; } } });
return new ReadOnlySet();
};
In English -- hide the real Set object so that nobody can get to it to mutate it.
On 4/30/15 6:20 PM, C. Scott Ananian wrote:
In English -- hide the real Set object so that nobody can get to it to mutate it.
Sure, but then Set.prototype.has.call() doesn't work on the ReadOnlySet. That might be OK, depending on how you're using it. But having a built-in ReadOnlySet would allow that to work.
I really liked Jordan Harband's suggestion esdiscuss.org/topic/array-prototype-change-was-tostringtag-spoofing-for-null-and-undefined#content-12
of adding Array.empty, Function.empty, etc. to ES7. It is relatively easy to polyfill as well.
[Array, ArrayBuffer, Int8Array, Int16Array, Int32Array, Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array, Float32Array, Float64Array] .map(T => [T, new T(0)]) .concat([ [Object, {}], [String, ''], [RegExp, /(?:)/], [Function, function () {}], [GeneratorFunction, function* () {}] ]) .forEach(([root, empty]) => Object.defineProperty(root, 'empty', { value: Object.freeze(empty), configurable: true, enumerable: false, writable: false }));
The code effectively explains what I think would make suitable replacements for each. I don't see the use cases for some of these, though, such as
String.empty
orRegExp.empty
.