Implicit coercion of Symbols

# Rick Waldron (10 years ago)

Kyle Simpson brought this up on Twitter today and I think it deserves one last look. Here's an example of the issue:

var sym = Symbol("description");
sym + ""; // Throws

Meanwhile...

var sym = Symbol("description");
String(sym); // "Symbol(description)" *

(* appears to be the convention that implementors have converged on)

This is the only time that a "thing" in JavaScript throws when it encounters an implicit coercion operation. This detail appears to be problematic in that it's an unnecessary divergence from the language's normal behaviour.

Ref: people.mozilla.org/~jorendorff/es6-draft.html#sec-addition-operator-plus-runtime-semantics-evaluation 11.a

# Caitlin Potter (10 years ago)

One reason it might make sense to throw, is people converting values to string names for use as object properties. Reason you'd want to throw would be to prevent accidentally making the key useless (different from its original Symbol value).

Haven't paid attention to the rationale, but that doesn't seem like a bad one.

# Brendan Eich (10 years ago)

Caitlin Potter wrote:

One reason it might make sense to throw, is people converting values to string names for use as object properties. Reason you'd want to throw would be to prevent accidentally making the key useless (different from its original Symbol value).

This is exactly the reason.

Of course, having String(x) and '' + x diverge is funky, but not novel:

js> o = {valueOf(){return 42}, toString(){return 'haha'}}
({valueOf:function valueOf(){return 42}, toString:function 
toString(){return 'haha'}})
js> String(o)
"haha"
js> ''+o
"42"
# Rick Waldron (10 years ago)

On Fri Jan 02 2015 at 7:53:22 PM Brendan Eich <brendan at mozilla.org> wrote:

This is exactly the reason.

Yep, I just wanted to make sure the subject got some last minute airtime to make sure this is really the way to go. I'll play the opposition here: is the hazard as compelling now as it was when it was first discussed? Now that implementors have had some time to work with Symbol, do proponents of "throw" still feel strongly?

Of course, having String(x) and '' + x diverge is funky, but not novel:

Sure, but the argument was re: the implicit coercion of built-ins.

# Axel Rauschmayer (10 years ago)

Playing devil’s advocate: How realistic a danger is this? Do people ever compose a property key for an object out of several pieces?

It does add a fair amount of complexity for something that doesn’t seem that common.

# Boris Zbarsky (10 years ago)

On 1/2/15 9:33 PM, Axel Rauschmayer wrote:

Do people ever compose a property key for an object out of several pieces?

On the web? All the time.

# Axel Rauschmayer (10 years ago)

Can you give an example?

# Boris Zbarsky (10 years ago)

On 1/2/15 9:40 PM, Axel Rauschmayer wrote:

Can you give an example?

	get: function( num ) {
		return num != null ?

			// Return just the one element from the set
			( num < 0 ? this[ num + this.length ] : this[ num ] ) :

			// Return all the elements in a clean array
			slice.call( this );
	},

That's from jQuery 2.1.3.

And from the same place:

	function cache( key, value ) {
		// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
		if ( keys.push( key + " " ) > Expr.cacheLength ) {
			// Only keep the most recent entries
			delete cache[ keys.shift() ];
		}
		return (cache[ key + " " ] = value);
	}

That's after looking through about 1/10 of the library. I'll bet there are more. I'll also bet this sort of thing appears in every single major library out there.

# Axel Rauschmayer (10 years ago)

Arrays are a good point, this is where I’d think accidental coercions are most likely. The other use case is object-as-dictionary, which will slowly be replaced by Object.create(null) (no need to escape in ES6+) and Map.

I don’t feel strongly either way, I just feel that the added spec complexity is not ideal. Especially ToBoolean() not throwing an exception, while ToString() and ToNumber() do.

# Brendan Eich (10 years ago)

None of the objects in the examples bz cited are Arrays -- what did you mean?

Symbol must not implicitly convert in any object computed-property access to string.

# Axel Rauschmayer (10 years ago)

On 03 Jan 2015, at 19:52, Brendan Eich <brendan at mozilla.org> wrote:

None of the objects in the examples bz cited are Arrays -- what did you mean?

When I though of + being used inside square brackets, I only thought of strings, not of numbers (first example).

In the first example, this is at least array-like. Otherwise, slice wouldn’t work.

# Brendan Eich (10 years ago)

Sure, but you wrote "Arrays", not array-likes. Also bracketing for computed property access works with any object, not just array-likes.

Sorry to be nit-picky but precision required here ;-).

# Rick Waldron (10 years ago)

Right, but regardless of how the potential footgun is loaded, the exception is the divergence. That specifically is what I want us to look at—is this the right thing to do here? Currently, user code can do things like this:

  var a = {};
  //... sometime later...
  a[o + "_idfoobarwhatever"] = "my important stuff";

And even if o isn't a what the author expected, execution proceeds. I still think that the exception is a good path forward, but I may not be right about that.

The exception...

A. Requires user code to be more thoroughly tested—which is a good thing, but inexperienced programmers won't think so. B. Diverges from the well known implicit coercion behaviors that the language has had for 20 years (@Brendan: sorry for the reminder ;). This is arguably both a good thing and a bad thing (C, D, E, F) C. Prevents user code from doing the wrong thing (assuming that A is adhered to). D. Produces a surprising behavior (in comparison to all other implicit coercion operations) E. W/r to A and C, most code will need some kind of guard, just to avoid the exception, ie. either try/catch or if (typeof s !== "symbol") ... F. Not directly related, but querySelectorAll(...) throws if the selector is invalid, which is widely considered a poor design choice for that API. This is something that all DOM-centric libraries paper over, because authors would prefer that an invalid selector simply produce the equivalent of "no found elements", than throwing an exception.

Subjectively: I think it's nice in theory, but bad in practice.

# Brendan Eich (10 years ago)

Rick Waldron wrote:

Subjectively: I think it's nice in theory, but bad in practice.

Compared to what? Converting a symbol to asilent-but-deadly string?

I thought you were gonna raise the other way to restore consistency: make String(sym) throw too. Then we'd need a new Reflect op to get a string from a symbol.

# Rick Waldron (10 years ago)

Sorry, that was poorly delivered—I was editorializing on the benefit of exception vs consistency vs expectation. I still agree that silent-but-deadly string issue is problematic, but it seems like an edge case for Symbols. Yes, plenty of code concatenates strings and/or numbers to make an object key or an array index; but just like "[object Foo]42" or "undefined42", "Symbol(description)42" is a obvious mistake and has the same bad outcome as the preceding two. The exception sucks and silent strings suck. I don't know which sucks less ;)

# Andrea Giammarchi (10 years ago)

Agreed with Brendan, and I've thought the same.

It's been also years we have problems using unknonw objects in the wild, i.e.

var n = {__proto__:null};
var s = String(n); // error, No default value
var s = '' + n; // error, No default value

Same if used as [property accessor] so I am not sure why String(o) when o is Symbol should not throw.

I'd expect it to throw all the time and let developers be a bit more careful on their explicit or implicit casts.

The toString and valueOf are great examples that could compromise a lot of real-world code.

Being Symbols new we should probably/hopefully get them in the right way.

I feel like throwing either cases is the right way.

Just my thoughts, Best

# Rick Waldron (10 years ago)

That example above is pretty compelling for throw always consistency.

# Brendan Eich (10 years ago)

With a new Reflect.* API for converting a symbol to its diagnostic/debugging string?

If you agree, please file a bugs.ecmascript.org ticket.

# Tab Atkins Jr. (10 years ago)

On Jan 3, 2015 3:38 PM, "Rick Waldron" <waldron.rick at gmail.com> wrote:

E. W/r to A and C, most code will need some kind of guard, just to avoid

the exception, ie. either try/catch or if (typeof s !== "symbol") ...

This isn't a cost introduced by the throwing behavior, it's one that'll have to happen in code regardless of what we decide, or else the code will be incorrect. You can never construct modified keys from a Symbol by concatenation, even if it stringifies.

So your code either needs to be sure it won't be passed a Symbol, or else it needs to branch its behavior.

# Alex Kocharin (10 years ago)

An HTML attachment was scrubbed... URL: esdiscuss/attachments/20150104/0ad1ec00/attachment

# Axel Rauschmayer (10 years ago)

Does it have to be a Reflect.* method? It could be Symbol.prototype.getDescription() or a getter.

# Rick Waldron (10 years ago)
# Caitlin Potter (10 years ago)

Given that there is currently no way to get a string representation of a value in a guaranteed side-effect-free fashion, I think something generalized for all primitive and object types would be ideal.

# Alex Kocharin (10 years ago)

An HTML attachment was scrubbed... URL: esdiscuss/attachments/20150104/a9ead304/attachment-0001

# Rick Waldron (10 years ago)

On Sat Jan 03 2015 at 9:41:57 PM Alex Kocharin <alex at kocharin.ru> wrote:

Also, if you want to prevent mistakes like object['blah' + symbol], linters could be changed to forbid/warn about concatenation inside property names.

How would a linter know that symbol was actually a Symbol?

# Boris Zbarsky (10 years ago)

On 1/3/15 9:41 PM, Alex Kocharin wrote:

function log(anything) {
   process.stdout.write(new Date().toJSON() + ' - ' + anything + '\n')
}

Right now it'll never throw. Well, unless you override .toString() to throw, which I've never seen to be done intentionally.

Just to nitpick, try:

console.log("" + HTMLAnchorElement.prototype);

in your favorite browser and watch it throw precisely for this reason.

Or load

data:text/html,<iframe src="http://www.ecma-international.org/"></iframe>

and try:

console.log("" + frames[0]);

With throwing Symbols we'll have innocuous-looking code which will cause an exception.

The innocuous-looking code will totally cause exceptions in the wild today depending on what you pass to it.

# Alex Kocharin (10 years ago)

An HTML attachment was scrubbed... URL: esdiscuss/attachments/20150104/dca15205/attachment

# Brendan Eich (10 years ago)

Alex Kocharin wrote:

04.01.2015, 05:44, "Rick Waldron" <waldron.rick at gmail.com>:

How would a linter know that symbol was actually a Symbol?

It wouldn't. But if it warns about string concatenation inside square braces (means, string + variable, but not number + variable), it should be good enough. This is the error the spec is trying to prevent, isn't it?

No. See the JQuery examples that bz provided.

My point is: concatenating Symbols with other strings have legitimate uses.

Name one.

And javascript shouldn't require any explicit type casting in order to do this, it isn't a statically typed language.

Static types have nothing to do with getting an error on implicit conversion.

# Alex Kocharin (10 years ago)

An HTML attachment was scrubbed... URL: esdiscuss/attachments/20150104/ce1d7479/attachment-0001

# Brendan Eich (10 years ago)

Alex Kocharin wrote:

Name one.

I did name one in another message. Logging.

That's a use-case for some way (could be concatenation, but as noted the downside risk is huge; could be a new Reflect method) to convert symbol to string. Explicit is better than implicit. Saying "Logging" does not say "allow implicit symbol to string conversion".

I agree that String(sym) working where ''+sym throws is funky (my word in this thread). We could make both throw, with a Reflect.symbolToString or whatever it might be called. ES6 is about out of time, this may not fly, but if it is possible it has to be done quickly.

# Andrea Giammarchi (10 years ago)

Alex, like I've said:

log({__proto__:null}) and welcome crashes of the server, if that's a real concern in this form.

Is it that wrong to assume that if console.log({__proto__:null}) is capable of handling non Objects and print a meaningful output, so should any other logger implemented on user-land?

'cause logging a part there's still no case where implicit or explicit Symbol cast makes any sense.

It's like object['undefined'] where undefined is implicitly created by some missed property which has been somehow problematic for long time.

A generic description-aware Reflect.describe(genericObject)like approach seems way more useful for logging annd debugging purpose than a footgun in the wild.

# Claude Pache (10 years ago)

Le 4 janv. 2015 à 03:18, Rick Waldron <waldron.rick at gmail.com> a écrit :

ecmascript#3509

I think that it is asking for the wrong consistency. There is another consistency to be wanted, namely:

String(sym) === sym.toString()

(assuming that Symbol.prototype.toString isn't sabotaged by user code, of course). Indeed:

  • String(sym) and sym.toString() are explicit coercions to string; while
  • sym + "" triggers an implicit coercion to string (in that very case, preceded by a coercion to primitive, but it's not the subject).

Now, it will be argued that it would be a precedent to make such a distinction between implicit and explicit coercion, for, until ES5, there is none. But, precisely, pervasive implicit coercion is often thought to be a mistake in the design of JavaScript (it hides bugs), while coercion in general is indeed useful (e.g., for debugging purpose, as pointed Alex in this thread). Now, as we are evolving the language, it is good to limit the scope of the bad implicit coercion behaviour (such as the abstract operation ToString() of the spec), but to consolidate the functionality of the existing explicit coercion functions (such as String()).

If there is a need to directly expose the implicit coercion to string operation to user code, Reflect.toString() is a natural fit for that, together with Reflect.toBoolean(), Reflect.toPropertyKey(), Reflect.toPrimitive(_, hint), etc.

# Brendan Eich (10 years ago)

Claude Pache wrote:

Now, it will be argued that it would be a precedent to make such a distinction between implicit and explicit coercion, for, until ES5, there is none.

Kind of there all along, as noted up-thread:

js> o = {valueOf(){return 42}, toString(){return 'haha'}}
({valueOf:function valueOf(){return 42}, toString:function 
toString(){return 'haha'}})
js> String(o)
"haha"
js> ''+o
"42"

But I take your point.

But, precisely, pervasive implicit coercion is often thought to be a mistake in the design of JavaScript (it hides bugs), while coercion in general is indeed useful (e.g., for debugging purpose, as pointed Alex in this thread).

Explicit coercion in general is useful. What's "explicit"? It could be console.log is an explicit-enough gesture. It does more than just ToString on its parameters today, IINM (at least in some browsers).

Now, as we are evolving the language, it is good to limit the scope of the bad implicit coercion behaviour (such as the abstract operation ToString() of the spec), but to consolidate the functionality of the existing explicit coercion functions (such as String()).

Yes, this is the rationale for ES6's symbol handling today.

If there is a need to directly expose the implicit coercion to string operation to user code, Reflect.toString() is a natural fit for that, together with Reflect.toBoolean(), Reflect.toPropertyKey(), Reflect.toPrimitive(_, hint), etc.

Yup; ES7 fodder at this stage.

I think it's very unlikely anyone will try to patch ES6 over the trade-offs among consistencies that this thread has illuminated. Thanks,

# Isiah Meadows (10 years ago)

A good name for such a getter would be, IMO, Symbol.prototype.description. It would also seem to fit with some other common JS idioms, such as Array.prototype.length, etc. It also feels clearer and cleaner as a getter than an instance method or function.

# Domenic Denicola (10 years ago)

I re-read through this whole thread and realized nobody brought up the fact that this specific change, of making String(symbol) work while symbol+"" throws, was discussed and agreed upon previously:

I realize people are presumably having second thoughts, but I thought it'd be worth linking to the previous thread for anyone who hasn't seen it and thinks this is a new debate.

# Brendan Eich (10 years ago)

Indeed, in my first reply on-thread, I wrote

"This is exactly the reason."

to acknowledge existing consensus. Thanks for linking!

# Isiah Meadows (10 years ago)

From: Domenic Denicola <d at domenic.me> To: Rick Waldron <waldron.rick at gmail.com>, es-discuss <es-discuss at mozilla.org> Cc: Date: Mon, 12 Jan 2015 18:02:17 +0000 Subject: RE: Implicit coercion of Symbols I re-read through this whole thread and realized nobody brought up the fact that this specific change, of making String(symbol) work while symbol+"" throws, was discussed and agreed upon previously:

I realize people are presumably having second thoughts, but I thought it'd be worth linking to the previous thread for anyone who hasn't seen it and thinks this is a new debate.

Good catch. I wouldn't have been surprised if nobody even thought of it until now (in this discussion, anyways).