I don't think R&T will ever ship at this point, since the browser vendors are apparently unwilling to absorb the complexity that would be required to add new primitive types with value semantics.
I've been following that proposal closely, and even (unsuccessfully) tried to contribute suggestions to it. I think what's killing it is that the authors of the proposal won't accept arbitrary values as fields of R/T, but all the potential users are saying that they won't use R/T if they can't put arbitrary values in them.
The reluctance of the authors is due to backward compatibility with sandboxed "secure" JavaScript (SES). That said, every other language in existence that has immutable structs and records allows to put arbitrary values in them.
> If you allow arbitrary values, what's the difference between a record and a frozen object?
The behaviour of equality. Frozen objects are already considered to have unique identities, in that `Object.freeze({}) !== Object.freeze({})` even though both objects are otherwise indistinguishable. This behaviour can't be changed and it relates to the fact that `Object.freeze(a) === a`.
> I thought that the whole point is to have guaranteed deep immutability
Not really. The whole point apparently according to most people[0] is to have composite values that don't have unique identities, so they fit in with all the existing comparison operations (eg, `===`, `Map`, `indexOf`, `includes`) just as you can do with strings.
Immutability is a prerequisite for this, since if `a` and `b` are mutable, mutating `a` might be different to mutating `b`. Thinking again about strings, equality works because strings are immutable:
const foo = "foo", bar = "bar";
const a = foo + bar;
const b = foo + bar;
a === b; // true
Implementations will typically use different underlying memory allocations for these strings[1], but at a language level they are considered to be the same value. If it were possible to modify one of the strings (but not the other) using `a[0] = "x";` it would mean `a` and `b` are not equivalent so should not be considered equal.
As explained here[2], deep immutability is not necessary for this behaviour.
In my opinion guaranteed "deep immutability" is not generally useful/meaningful (if you have a particular use case, feel free to share it). In theory it's not possible to enforce "deep immutability" because someone can always refer to something mutable, whether that's an object reference or a number indexing a mutable array.
If you really do want something that guarantees a certain notion of "deep immutability", this concept seems somewhat orthogonal to records/tuples, since there are existing values (eg, strings and numbers) that should be considered deeply immutable, so you'd expect to have a separate predicate[3][4] for detecting this, which would be able to effectively search a given value for object references.
In case you're interested I tried to summarise the logic behind the rejection of this behaviour[5] (which I disagree with), but it's very much a TLDR so further reading of linked issues would be required to understand the points made. Interestingly, this post is on an issue raised by the odd person that actually tried to use the feature and naturally ran into this restriction.
Sorry for this massive wall of text, but I think it's hard to capture the various trains of thought concisely.
Thanks for the history! Reading through the issues, I agree with you that some of the motivations against objects in records seem pretty strange. Mostly they seem to be around existing JS-written 'membranes' (related to the SES stuff mentioned above?) getting confused by primitives-containing-objects, depending on which permutation of typeof checks they use. Out of curiosity, do you think that the Shadow Realms proposal they refer to will ever go anywhere?
Otherwise, there's the argument that "x.y" syntax shan't be used to access a mutable object from an immutable record, but that just feels like the all-too-common motive of "we must ensure that users write morally-correct code (given our weird idiosyncratic idea of moral correctness), or otherwise make them pay the price for their sins".
> Out of curiosity, do you think that the Shadow Realms proposal they refer to will ever go anywhere?
I haven't really been following the Shadow Realm proposal (I'm not part of TC39, so only familiar with certain proposals), but I don't think it should conflict with R/T.
If R/T values are allowed to be passed between realms, they should effectively be "transformed" such that eg, `f(#[v])` is equivalent to `f(#[f(v)])` (where `f` is the transformation that allows values to be passed between realms). For "deeply immutable" values (no object references), `f(v)` will simply return `v` (eg, `#[42]`, f(#[42])` and `f(#[f(42)])` are all the same) and a membrane should be able to trivially optimise this case.
From this comment[0] it sounds like `f({})` in the current Shadow Realm proposal will throw an error, so I'd expect that `f(#[{}])` would also throw an error.
As you were pointing out, I think the only real contention between R/T and realms is in existing JS implementations of membranes, particularly because they might use the following condition to detect if something is "deeply immutable":
v === null || typeof v !== "object" && typeof v !== "function"
If `typeof #[{}] === "tuple"`, then their `f` function will pass that value through without handling the contained object value by throwing or by creating/finding a proxy.
If `typeof #[{}] === "object"`, it should be fine because `f(#[{}])` will either throw or create/find a proxy for the tuple. There might be some unexpected behaviour around equality of R/T values passed through the membrane, but this is pretty obscure and it should be fixed once the membrane library is updated to handle R/T values.
Personally, I'm still not 100% convinced that the assumptions made from the above condition are important enough to cause such a change to the proposal, but I don't see the value of `typeof #[]` as being a usability issue. Code that needs to check the types of things is a bit smelly to me, but in cases where you do need to check the type, `typeof v === "tuple"` and `Tuple.isTuple(v)` both seem usable to me, so just making `typeof #[] === "object"` should be fine and it solves this hypothetical issue. This is similar to array objects, which are also fundamentally special (`Object.create(Array.prototype)` is not an array object) and are detected using `Array.isArray(v)`.
> Otherwise, there's the argument that "x.y" syntax shan't be used to access a mutable object from an immutable record, but that just feels like the all-too-common motive of "we must ensure that users write morally-correct code (given our weird idiosyncratic idea of moral correctness), or otherwise make them pay the price for their sins".
Agreed, and I've pointed out[1] that even the current proposal doesn't address this, since unless you've done some defensive check on `x`, there's nothing stopping someone passing a mutable object for `x` instead of a record. If you do want to perform a dynamic[2] defensive check, perhaps you should be asking "is it deeply immutable?" or even checking its shape rather than "is it a record?".
[2] If you're using a type system like TypeScript, this check should happen statically, because you'll use a type that specifies that it's both a record and the types of the properties within it, so your type will encode whether or not it contains mutable objects