*.empty Idea

# Isiah Meadows (10 years ago)

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 or RegExp.empty.

# Jordan Harband (10 years ago)

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 as new Promise(), ie, a forever pending promise, would make sense?
  • or Date.empty as new Date(0)?
  • We'd definitely want Map.empty and Set.empty assuming Object.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"?

# Andrea Giammarchi (10 years ago)

quick one to whoever will write the proposal: please bear in mind the empty function must have a frozen prototype too

# Isiah Meadows (10 years ago)

Andrea, good catch.

# Mark S. Miller (10 years ago)

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)
# Isiah Meadows (10 years ago)

Pardon my ignorance, but in what cases does the proxy in this case differ? (although it's not entirely relevant in this particular case)

# Mark S. Miller (10 years ago)

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.

# C. Scott Ananian (10 years ago)

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.

# Jordan Harband (10 years ago)

I really like this - I'm writing up a ".empty" proposal and I'll include this as part of it, thanks!

# Mark S. Miller (10 years ago)

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.

# C. Scott Ananian (10 years ago)

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.

# Mark S. Miller (10 years ago)

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.

# Mark S. Miller (10 years ago)

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.

# C. Scott Ananian (10 years ago)

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?

# Mark S. Miller (10 years ago)

It doesn't matter. Others will do the same thing in ES6 and then break under your proposal in ES7.

# C. Scott Ananian (10 years ago)

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() and Set.prototype.makeReadOnly()

methods with the definition:

  1. Let M be the this value.
  2. If Type(M) is not Object, throw a TypeError exception
  3. If M does not have a [[Immutable]] internal slot, throw a TypeError

exception

  1. 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.

# Domenic Denicola (10 years ago)

It's fine to propose something to make maps immutable. Just don't call it Object.freeze.

# Mark S. Miller (10 years ago)

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.

# C. Scott Ananian (10 years ago)

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 ​

# Mark S. Miller (10 years ago)

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.

# Boris Zbarsky (10 years ago)

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.

# Jordan Harband (10 years ago)

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?

# C. Scott Ananian (10 years ago)

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.

# Boris Zbarsky (10 years ago)

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.