Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should Records and Tuples be objects, instead of primitives? #201

Closed
rickbutton opened this issue Sep 17, 2020 · 15 comments
Closed

Should Records and Tuples be objects, instead of primitives? #201

rickbutton opened this issue Sep 17, 2020 · 15 comments

Comments

@rickbutton
Copy link
Member

rickbutton commented Sep 17, 2020

Introduction

Records and Tuples are deeply immutable data structures that have "value semantics", i.e. they contain no mutable data, they have no identity, and they are compared by contents rather than by reference. Even though Records and Tuples look like Objects and Arrays, they behave like String or Number primitives, and are currently specified as primitives just like strings and numbers, in order to facilitate this behavior.

For example, when you compare two different objects, they are not equivalent, even though they might contain the same data:

const a = { foo: "123" };
const b = { foo: "123" };
assert(a !== b);

If you compare two primitives, they are compared via their contents. Primitives don't have identity, and there isn't such a thing as an "instance" of a primitive, two primitives are either equal because they are the same, or not equal because they are different:

const a = "foo bar";
const b = "foo bar";
assert(a === b);

Because Records and Tuples have "value semantics", they behave like primitives when compared:

const a = #{ foo: "123" };
const b = #{ foo: "123" };
assert(a === b);

However, there are a few possible downsides to making Records and Tuples primitives.

When object operations are performed on primitives (property access, etc) the primitive is "wrapped" in an exotic wrapper object that implements those object behaviors. This is usually not noticed, because the wrapper isn't observed, for example:

const a = 42;
const b = 42..toString();
//          ^ the "toString" property access happens on an instance of Number, not the "number" primitive itself.

However, if a primitive is wrapped unexpectedly, it can cause problems:

const a = Object(42);
const b = Object(42);
assert(a !== b);

Even though both a and b point to the number 42, they are both instances of the Number exotic wrapper object. Because these are objects, they have identity, and are compared by said identity, not their primitive contents. This also applies to primitive records:

const a = Object(#{ a: 1 });
const b = Object(#{ a: 1 });
assert(a !== b);

Even though a and b point to records with the same contents, they are not equal because instances of the Record exotic wrapper have identity.

Identity-less objects?

Instead of defining Records and Tuples as primitives, we could instead define them as "identity-less objects", or an object without identity. An object without identity might be an object with an internal slot marking it as such an object. When identity-less objects are compared, they are compared by contents, not by identity. Records and Tuples as identity-less objects would have these properties:

  • Records and Tuples are not primitives with wrappers, but instead directly objects.
  • Record and Tuple "identity-less objects" are compared by their contents.
  • Record and Tuple "identity-less objects" are always frozen, and never have internal slots or private fields that mutate.
  • Record and Tuple "identity-less objects" cannot contain private fields.
  • Record and Tuple "identity-less objects" cannot be used as a key in a WeakMap, a value in a WeakSet, or a target of a WeakRef, as they do not have identity.

Given the above properties, Records and Tuples as "identity-less" objects behave in the same way as primitive Records and Tuples, except that they don't require wrappers. This simplifies the model for Records and Tuples, and means that engines don't need to optimize them out where possible (like they do for existing primitives).

Open Questions

Is "identity-less" objects the right approach for describing the behavior of Records and Tuples?

How are "identity-less" objects differentiated from regular objects? A special internal slot?

What should typeof return for "identity-less" objects? object?

@ljharb
Copy link
Member

ljharb commented Sep 17, 2020

Having "identity-less objects" would violate fundamental axioms of the language. If they're not primitives, I don't think they can or should ever have === semantics.

@bakkot
Copy link
Contributor

bakkot commented Sep 17, 2020

However, if a primitive is wrapped unexpectedly, it can cause problems:

We could make Object(record) be a TypeError, so that user code couldn't get access to the wrapper. But, I don't think the fact that the wrappers would compare by identity would be a big deal - it's no weirder than Object(42) !== Object(42).

@ljharb
Copy link
Member

ljharb commented Sep 17, 2020

Similarly, Object(primitive) returning an object is also an axiom of the language that a lot of my code on the web depends on.

I do agree though that it's no big deal that boxed primitives are inequal even though the primitives are equal, since that's how everything always works.

@littledan
Copy link
Member

I want to mention a separate effect of using objects instead of primitives, which is that, by not having wrappers, the [[Prototype]] of Tuples (and possibly Records, if they have a non-null prototype) is Realm-specific, not created based on the current Realm.

This use of direct [[Prototype]] is more likely to easily extend to further kinds of values which are compared structurally, as it would be difficult to have a built-in per-Realm mapping to user-defined types (since it would form a communication channel); an alternative would be possible based on lexically scoped mappings (similar to the operator overloading proposal), but this is both unergonomic to use and expensive to implement/optimize.

At first, I thought that Box would be enabled specifically by using objects instead of primitives, but it turns out that that is admissible from an object-capability-security perspective regardless of object vs primitive; see #200 . So I see the extensibility concern as the main reason to consider Records and Tuples as objects, rather than primitives.

@ljharb

Having "identity-less objects" would violate fundamental axioms of the language. If they're not primitives, I don't think they can or should ever have === semantics.

This is definitely a thought that I went into this proposal thinking, but the ideas that @rickbutton noted in this issue seem interesting to me. I think we should go back and think about why only primitives should have value-based equality semantics. To me, the underlying goals around === being reliable and trustable in all circumstances: === returns the same answer on any two JS values at all times, e.g., it won't be true now and false later with the same parameters for any reason. Some pieces of this:

  • No user-defined code is called in ===. Instead, it's completely implemented by the specification, with no hooks/overloading.
  • No mutable state is accessed in the definition of ===. Instead, it's defined only by comparing inherent, unchanging things about the value, either the identity or unchanging contents.

These invariants correspond to only comparing things that have semantics analogous to Records and Tuples or primitives by value, and comparing objects by identity and not their contents, but they don't force a primitive vs object split in particular.

Maybe there are other reasons why we should be considering that there's a strong association here. It'd be helpful to hear more about what kinds of properties lead you to make the association between value-based === and primitives.

@Jack-Works
Copy link
Member

If it is an object, it should allow Proxys on it.

@littledan
Copy link
Member

littledan commented Sep 22, 2020

@Jack-Works Can you open a separate issue around Proxy with Record and Tuple as a target? It's a big area. It would be great to hear why it's important.

@Jack-Works
Copy link
Member

I'm strongly against the idea of the identity-less object, but if it is an object, it should support Proxys cause all object supports Proxy. I think we can discuss this later if it switched to identity-less object.

@acutmore
Copy link
Collaborator

While reading the Concurrent JS Slides, it made me think about this discussion.

It feels like there would be an advantage of Records & Tuples being primitives when it comes to transferring/accessing them across threads/workers/realms/module-blocks.

@rricard rricard added this to the stage 3 milestone Jun 21, 2021
@mhofman
Copy link
Member

mhofman commented Aug 10, 2021

I think of records/tuples as identity-less objects the same way as I think of registered symbols. The engine is creating a unique and stable identity from some forgeable user data. I don't think there is a need for some special internal slot, just for an internal lookup if a record/tuple object already exists in a global registry for a given content.

@acutmore
Copy link
Collaborator

I think of records/tuples as identity-less objects the same way as I think of registered symbols. The engine is creating a unique and stable identity from some forgeable user data. I don't think there is a need for some special internal slot, just for an internal lookup if a record/tuple object already exists in a global registry for a given content.

If R&T use SameValueZero (#65) then #[0] === #[-0] while also having observably different data.

@mhofman
Copy link
Member

mhofman commented Aug 11, 2021

I forgot about that point. As I noted today in the SES meeting, this would mean that unlike other objects, === and Object.is would yield different results, which might be surprising:

const nz = #[-0];
const pz = #[+0];
typeof nz === 'object';
nz === pz; // true
Object.is(nz, pz); // false

@mhofman
Copy link
Member

mhofman commented Sep 22, 2021

I wanted to quickly clarify that with Box, records and tuples would not always be identity-less. Instead they would bear the combined identity of all the identity bearing boxes they contain, if any.

@hax
Copy link
Member

hax commented Jan 24, 2022

FYI, Java have identity-free objects (value objects) now.

@mhofman Could explain more about how Box make record/tuple have identity again? I don't get it.

@mhofman
Copy link
Member

mhofman commented Jan 25, 2022

I think it's easier to explain in terms of forgeable identity, and consider that every value has an identity comparable through ===.

Like strings and numbers, records/tuples have a forgeable identity. They're unlike objects or unique symbols, which have an unforgeable identity. Since records/tuples are a structure which can contain unique symbols (and maybe in the future boxes if they are re-introduced), the identity of the record/tuple really is a composite of the structure/shape and of the unforgeable values it contains.

All that to say, you can't forge the identity of a record/tuple unless you hold all the symbols/boxes that make it.

@rricard
Copy link
Member

rricard commented Jul 8, 2022

Per the current spec and discussions in-committee, Records & Tuples will be primitives. This allows the equality semantics discussed in #65 and will make them work with the Shadow Realms proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants