What are Symbols? Objects? New primitive type?

# Brandon Benvie (11 years ago)

Based on recent discussions, it seems that there's some confusion about what exactly Symbols are. So far I've seen three different alternatives:

  1. Exotic objects that conform to the most recent ES6 spec, but with a special typeof .That is, they have special operations for all the internal methods. These internal methods make them act as stateless, immutable, prototypeless objects such that they can be treated like primitives. The problem here is introducing new typeof semantics, where something that otherwise acts like an object is not typeof === "object".

  2. A new type of primitive that have no object wrapper version, thus ToObject(symbol) throws. This means either they have to be falsey or a new situation arises where if (val != null) val.prop throws (ignoring accessors and proxies).

  3. A new type of primitive along with a new type of wrapper. In this case we use the String/Number/Boolean precedent where Symbol() and new Symbol() produce different kinds of results. The problem here is the confusion that comes with ToString/ToPropertyKey when called on a Symbol wrapper. What does obj[new Symbol] = 5 do, for example? It allows footguns like obj[key + '_ext'] to silently do the wrong thing.

A relevant gist here demonstrates test cases for option #1. I'll see if I can add comparable sets of tests for the other two options.

# Eddy Bruel (11 years ago)

I don't have much to add to this discussion, but I would like to point out that option 1 would make it much easier to implement symbols in SpiderMonkey, so as an implementer I have a vested interested in option 1 winning out ;-)

In case anybody is interested, I've summarized why implementing symbols as primitive types in SpiderMonkey is hard here

# Allen Wirfs-Brock (11 years ago)

On Apr 12, 2013, at 3:12 PM, Brandon Benvie wrote:

Based on recent discussions, it seems that there's some confusion about what exactly Symbols are. So far I've seen three different alternatives:

1.) Exotic objects that conform to the most recent ES6 spec, but with a special typeof .That is, they have special operations for all the internal methods. These internal methods make them act as stateless, immutable, prototypeless objects such that they can be treated like primitives. The problem here is introducing new typeof semantics, where something that otherwise acts like an object is not typeof === "object".

2.) A new type of primitive that have no object wrapper version, thus ToObject(symbol) throws. This means either they have to be falsey or a new situation arises where if (val != null) val.prop throws (ignoring accessors and proxies).

3.) A new type of primitive along with a new type of wrapper. In this case we use the String/Number/Boolean precedent where Symbol() and new Symbol() produce different kinds of results. The problem here is the confusion that comes with ToString/ToPropertyKey when called on a Symbol wrapper. What does obj[new Symbol] = 5 do, for example? It allows footguns like obj[key + '_ext'] to silently do the wrong thing.

A relevant gist here demonstrates test cases for option #1. I'll see if I can add comparable sets of tests for the other two options. [1]

gist.github.com/Benvie/5375078

there were lots of additional subtle points brought up on the twitter threads

Regarding #1 This is how it was spec. until the last TC39 meeting.

Some implementers think this will be easiest to implement.

Important point was that we probably don't want a new kind of "object" whose typeof value is not "object". typeof is already confusing enough for objects because of its handling of functions and null. We shouldn't make it worse with typeof Symbol() == "symbol" yet symbols behave as objects.

Regarding #2 My comments in gist.github.com/Benvie/5375078#comment-815195 all directly derive from the "normal" handling in the spec. of primitive values or situations involving primitive values that don't have wrappers.

Regarding #3

The biggest footgun is if new Symbol() creates a Symbol wrapper. However, new Symbol() returning a primitive values would be unlike anything we currently have in the language

I favor reverting to solution 1.

# Axel Rauschmayer (11 years ago)

How would object value types such as int64 work? Should symbols be similar?

# Brendan Eich (11 years ago)

Axel Rauschmayer wrote:

How would object value types such as int64 work? Should symbols be similar?

That came up and was an argument for making typeof sym == "symbol", given sym = Symbol(). Same for int64 and uint64 in my patch at

bugzilla.mozilla.org/show_bug.cgi?id=749786

(Note on that bug's patch: it still allows i = new int64(0) but I will change new to throw, per agreement at last TC39 meeting to make new create a reference type instead of a value type for aggregates from binary data, i.e. structs and typed arrays.)

# Axel Rauschmayer (11 years ago)

On Apr 13, 2013, at 1:09 , Brendan Eich <brendan at mozilla.com> wrote:

Axel Rauschmayer wrote:

How would object value types such as int64 work? Should symbols be similar?

That came up and was an argument for making typeof sym == "symbol", given sym = Symbol(). Same for int64 and uint64 in my patch at

bugzilla.mozilla.org/show_bug.cgi?id=749786

Can't wait for those. They might even warrant an update to JSON.

(Note on that bug's patch: it still allows i = new int64(0) but I will change new to throw, per agreement at last TC39 meeting to make new create a reference type instead of a value type for aggregates from binary data, i.e. structs and typed arrays.)

Nice. If symbols mimic this behavior then it probably should be symbol() instead of Symbol().

# Brendan Eich (11 years ago)

Axel Rauschmayer wrote:

On Apr 13, 2013, at 1:09 , Brendan Eich <brendan at mozilla.com <mailto:brendan at mozilla.com>> wrote:

Axel Rauschmayer wrote:

How would object value types such as int64 work? Should symbols be similar?

That came up and was an argument for making typeof sym == "symbol", given sym = Symbol(). Same for int64 and uint64 in my patch at

bugzilla.mozilla.org/show_bug.cgi?id=749786

Can’t wait for those. They might even warrant an update to JSON.

No, JSON has arbitrary precision decimal numeric literals. Good enough for lots of representations. It's up to the schema (where's that?) to say the type.

# Andreas Rossberg (11 years ago)

On 13 April 2013 00:12, Brandon Benvie <bbenvie at mozilla.com> wrote:

3.) A new type of primitive along with a new type of wrapper. In this case we use the String/Number/Boolean precedent where Symbol() and new Symbol() produce different kinds of results. The problem here is the confusion that comes with ToString/ToPropertyKey when called on a Symbol wrapper. What does obj[new Symbol] = 5 do, for example? It allows footguns like obj[key + '_ext'] to silently do the wrong thing.

That was the consensus at the last meeting, and it's already implemented in V8. The decision included that Symbol.prototype.toString is poisoned, i.e., any attempt to implicitly convert a symbol or a wrapped symbol to a string will throw, including both of your examples. So no footgun, which I agree is important. (Object.prototype.toString is not on that path, however, so can treat symbols separately.)

I'm a bit confused about some bits of Allen's comment on your Gist, though, which don't line up with what I thought we decided on. In particular, ToObject(symbol) should be perfectly fine and creates a wrapper object. Similarly, Symbol()["foo"] auto-converts to a wrapper object and returns undefined as usual, and new Symbol() directly creates a wrapper object.

# Andreas Rossberg (11 years ago)

On 13 April 2013 00:35, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

there were lots of additional subtle points brought up on the twitter threads

Regarding #1 This is how it was spec. until the last TC39 meeting.

Some implementers think this will be easiest to implement.

Hm, judging from the most recent replies to the Mozilla bug, the problems they saw were not specific to symbols (lack of tag bit space in SM), and seem to have been resolved already.

Regarding #3

The biggest footgun is if new Symbol() creates a Symbol wrapper. However, new Symbol() returning a primitive values would be unlike anything we currently have in the language

I agree, it has to be the former. But what footgun are you seeing there? Any attempt to use a wrapper as a key throws under the agreed-upon semantics, so I don't see the issue.

# Claude Pache (11 years ago)

Le 13 avr. 2013 ? 09:56, Andreas Rossberg <rossberg at google.com> a ?crit :

On 13 April 2013 00:35, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

Regarding #3

The biggest footgun is if new Symbol() creates a Symbol wrapper. However, new Symbol() returning a primitive values would be unlike anything we currently have in the language

I agree, it has to be the former. But what footgun are you seeing there? Any attempt to use a wrapper as a key throws under the agreed-upon semantics, so I don't see the issue.

As a JS programmer, I will be very tempted to write new Symbol to get a new symbol. The issue is that you get an error in a different line of code from where the problem lies, and you have to be fond of the subtle distinction between value and object wrapper (which is a feature we never use in our daily coding) to understand what is happening without external help.

In order to mitigate the problem without introducing inconsistency with legacy constructors of primitives, I propose to poison the Symbol constructor, so that it throws a TypeError with a useful message when called as a constructor. The error message would include (inter alia) something like "To obtain a new symbol, use Symbol()."

People who have a really good reason for wanting a wrapper Symbol object should already know that you can obtain one with Object(Symbol()).

# Andreas Rossberg (11 years ago)

On 13 April 2013 13:36, Claude Pache <claude.pache at gmail.com> wrote:

Le 13 avr. 2013 ? 09:56, Andreas Rossberg <rossberg at google.com> a ?crit :

On 13 April 2013 00:35, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

Regarding #3

The biggest footgun is if new Symbol() creates a Symbol wrapper. However, new Symbol() returning a primitive values would be unlike anything we currently have in the language

I agree, it has to be the former. But what footgun are you seeing there? Any attempt to use a wrapper as a key throws under the agreed-upon semantics, so I don't see the issue.

As a JS programmer, I will be very tempted to write new Symbol to get a new symbol. The issue is that you get an error in a different line of code from where the problem lies, and you have to be fond of the subtle distinction between value and object wrapper (which is a feature we never use in our daily coding) to understand what is happening without external help.

While that is true, and unfortunate, it also is the case for pretty much every other mistake you can make in JavaScript (or any dynamic language, for that matter) -- and there are far more subtle ones in that class. So I'm not convinced that this particular case justifies special casing.

/Andreas

# Brandon Benvie (11 years ago)

On 4/13/2013 12:31 AM, Andreas Rossberg wrote:

On 13 April 2013 00:12, Brandon Benvie <bbenvie at mozilla.com> wrote:

3.) A new type of primitive along with a new type of wrapper. In this case we use the String/Number/Boolean precedent where Symbol() and new Symbol() produce different kinds of results. The problem here is the confusion that comes with ToString/ToPropertyKey when called on a Symbol wrapper. What does obj[new Symbol] = 5 do, for example? It allows footguns like obj[key + '_ext'] to silently do the wrong thing. That was the consensus at the last meeting, and it's already implemented in V8. The decision included that Symbol.prototype.toString is poisoned, i.e., any attempt to implicitly convert a symbol or a wrapped symbol to a string will throw, including both of your examples. So no footgun, which I agree is important. (Object.prototype.toString is not on that path, however, so can treat symbols separately.)

I'm a bit confused about some bits of Allen's comment on your Gist, though, which don't line up with what I thought we decided on. In particular, ToObject(symbol) should be perfectly fine and creates a wrapper object. Similarly, Symbol()["foo"] auto-converts to a wrapper object and returns undefined as usual, and new Symbol() directly creates a wrapper object.

/Andreas

This would be the reason I created this thread: there was at least two different interpretations of the outcome of the meeting by people who were at the meeting, and a third one (mine) that came from reading the meeting notes. From that I surmised it would be good to, at the very least, figure out which one of the three is an accurate reflection of what was actually to be the final version.

# Claude Pache (11 years ago)

Le 13 avr. 2013 ? 15:25, Andreas Rossberg <rossberg at google.com> a ?crit :

On 13 April 2013 13:36, Claude Pache <claude.pache at gmail.com> wrote:

Le 13 avr. 2013 ? 09:56, Andreas Rossberg <rossberg at google.com> a ?crit :

On 13 April 2013 00:35, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

Regarding #3

The biggest footgun is if new Symbol() creates a Symbol wrapper. However, new Symbol() returning a primitive values would be unlike anything we currently have in the language

I agree, it has to be the former. But what footgun are you seeing there? Any attempt to use a wrapper as a key throws under the agreed-upon semantics, so I don't see the issue.

As a JS programmer, I will be very tempted to write new Symbol to get a new symbol. The issue is that you get an error in a different line of code from where the problem lies, and you have to be fond of the subtle distinction between value and object wrapper (which is a feature we never use in our daily coding) to understand what is happening without external help.

While that is true, and unfortunate, it also is the case for pretty much every other mistake you can make in JavaScript (or any dynamic language, for that matter) -- and there are far more subtle ones in that class. So I'm not convinced that this particular case justifies special casing.

The idea is simply to avoid adding footguns for new features, even if we can't remove the existing ones.

Regarding the issue discussed here, let me elaborate:

Until now, new Primitive, where Primitive is Number, String or Boolean is, in practice, not a problem, because you never need to create a new number, string or boolean that way. (You can use Primitive as a function for typecasting, but it is not felt as the same thing as creating a new value.) Things are different with symbols, so the new Symbol footgun is practically a new type of footgun.

Note also that new Date, new RegExp, new Map, etc., work as intuitively expected, and, if I have correctly followed the last discussions, function* gen() { /* ... */}; myIterator = new gen (meaning: "I want to get a new iterator from that generator function") would also work.

I do agree that special-casing should be avoided. Therefore, I think we should make a rule: new Primitive should throw for primitive types, except for legacy numbers, booleans and strings (Don't Break The Web) for which it is not a real issue.

# Kevin Gadd (11 years ago)

new Number and new String are also functionally useful to a degree - I've had reason to use both in my compiler's runtime library, either to allow returning a value from a constructor or to assign properties to a number.

It sounds like new Symbol will never be functionally useful because it won't actually produce a valid symbol, in the wrapper case? If so, I agree that it makes sense to ensure that developers never try to use that particular construct and expect it to work. I also agree that in JS, my first instinct would be new Symbol to get a symbol, so having that silently do the wrong thing would not be desirable. Poisoning toString sounds like an okay solution too, as long as it is relatively straightforward to figure out why your toString has become poisoned - given no toString, it becomes kind of difficult to figure out (via stock JS debugging tools) that 'oh, I created a symbol object wrapper instead of a symbol' is your problem. (This is, to be fair, also a problem with the other primitive wrapper objects)

# Brendan Eich (11 years ago)

Claude Pache wrote:

Therefore, I think we should make a rule: new Primitive should throw for primitive types

That's the plan for new value objects, e.g. int64 and uint64.

Further, for aggregates created by binary data (structs and typed arrays) we have emerging consensus that new T(...) makes a mutable aggregate while T(...) makes an immutable value object, which can be optimized by VMs (e.g., stack allocation, hashcons'ing, etc.).

# David Bruant (11 years ago)

Le 13/04/2013 09:31, Andreas Rossberg a ?crit :

On 13 April 2013 00:12, Brandon Benvie <bbenvie at mozilla.com> wrote:

3.) A new type of primitive along with a new type of wrapper. In this case we use the String/Number/Boolean precedent where Symbol() and new Symbol() produce different kinds of results. The problem here is the confusion that comes with ToString/ToPropertyKey when called on a Symbol wrapper. What does obj[new Symbol] = 5 do, for example? It allows footguns like obj[key + '_ext'] to silently do the wrong thing.

That was the consensus at the last meeting, and it's already implemented in V8. The decision included that Symbol.prototype.toString is poisoned, i.e., any attempt to implicitly convert a symbol or a wrapped symbol to a string will throw, including both of your examples.

Just to clarify, implicit conversion is the behavior of ToString (ES5.1 - 9.8 with Table 13), not Symbol.prototype.toString. And that's what I read from the notes too. Do you confirm? Apparently, symbols have a [[name]] 1. It would be good if they even had a public "name" property (or rather Symbol.prototype.name getter) at least for debugging purposes. It would make sense if Symbol.prototype.toString (when called explicitly) returned symbol.name.