What are Symbols? Objects? New primitive type?
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
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 whereif (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()
andnew Symbol()
produce different kinds of results. The problem here is the confusion that comes with ToString/ToPropertyKey when called on a Symbol wrapper. What doesobj[new Symbol] = 5
do, for example? It allows footguns likeobj[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]
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.
How would object value types such as int64
work? Should symbols be similar?
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.)
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"
, givensym = Symbol()
. Same forint64
anduint64
in my patch at
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().
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
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.
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 whereSymbol()
andnew Symbol()
produce different kinds of results. The problem here is the confusion that comes withToString
/ToPropertyKey
when called on a Symbol wrapper. What doesobj[new Symbol] = 5
do, for example? It allows footguns likeobj[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.
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.
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 languageI 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())
.
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 languageI 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
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()
andnew Symbol()
produce different kinds of results. The problem here is the confusion that comes with ToString/ToPropertyKey when called on a Symbol wrapper. What doesobj[new Symbol] = 5
do, for example? It allows footguns likeobj[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.
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 languageI 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.
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)
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.).
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 whereSymbol()
andnew Symbol()
produce different kinds of results. The problem here is the confusion that comes withToString
/ToPropertyKey
when called on a Symbol wrapper. What doesobj[new Symbol] = 5
do, for example? It allows footguns likeobj[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
.
Based on recent discussions, it seems that there's some confusion about what exactly Symbols are. So far I've seen three different alternatives:
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".
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 whereif (val != null) val.prop
throws (ignoring accessors and proxies).A new type of primitive along with a new type of wrapper. In this case we use the
String
/Number
/Boolean
precedent whereSymbol()
andnew Symbol()
produce different kinds of results. The problem here is the confusion that comes withToString
/ToPropertyKey
when called on a Symbol wrapper. What doesobj[new Symbol] = 5
do, for example? It allows footguns likeobj[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.