proxies: stratifying toString, valueOf

# David Herman (13 years ago)

If you want to create a clean-slate proxy object -- for example, a dictionary -- then you can't predefine toString or valueOf. But this means your object will always fail at the semantic operations [[ToString]] and [[ToPrimitive]]. For example:

> var obj = Proxy.create(myEmptyHandler, proto);
> String(obj)
TypeError: can't convert obj to string
> obj + ""
TypeError: can't convert obj to primitive type

If you actually instrument the proxy to watch which operations it's trying, you'll see:

> var obj = Proxy.create(myEmptyHandler, proto);
> String(obj)
trying: toString
trying: valueOf
TypeError: can't convert obj to string
> obj + ""
trying: valueOf
trying: toString
TypeError: can't convert obj to primitive type

Should we not offer derived traps for toString and valueOf (which, if not defined, default to looking up the "toString" and "valueOf" properties, respectively, on the receiver), to allow for stratified implementation of this reflective behavior, i.e., without polluting the object's properties?

Dave

PS SpiderMonkey also has the unstratified Object.prototype.toSource, which is used for the non-standard uneval function, for printing out values at the console. This is kind of unfortunate since it suggests a need for another proxy trap, but this time it's not for a standard functionality.

# David Herman (13 years ago)

Ugh, that formatted poorly, at least in my mail client. Here's the example again:

js> var obj = Proxy.create(myEmptyHandler, proto);
js> String(obj)
trying: toString
trying: valueOf
TypeError: can't convert obj to string
js> obj + ""
trying: valueOf
trying: toString
TypeError: can't convert obj to primitive type
# Andreas Gal (13 years ago)

I don't think this makes sense. This has nothing to do with proxies:

js> var o = Object.create(null)

js> o + ""

typein:5: TypeError: can't convert o to primitive type

If there is no toString property, toString() on the object will fail.

This is not a new problem, even before ES5 this could break:

js> delete Object.prototype.toString

true js> ({}) + ""

typein:7: TypeError: can't convert ({}) to primitive type

Fixing this just for proxies by stratifying toString, valueOf seems undesirable. We would grant extra powers to proxies, and suddenly this following two ways to invoke toString would produce different results (potentially):

Proxy.create(myEmptyHandler, null) + ""

and

Proxy.create(myEmptyHandler, null).toString() + ""

If you want to stratify toString/valueOf in general and for all objects, I would very much support that.

# Brendan Eich (13 years ago)

On Oct 16, 2011, at 6:29 PM, Andreas Gal wrote:

I don't think this makes sense. This has nothing to do with proxies:

I agree with Andreas. The implicitly-called base level methods are not meta-methods or (spec language) "internal methods". They do not need their own traps. They are base-level property accesses.

Allowing proxies to trap implicit calls to base-level methods but not explicit calls seems weird. If a Dict should not pollute its pseudo-property namespace with 'toString' and 'valueOf', then it can still delegate those to standard methods on its prototype chain. And if someone does enter 'toString' or 'valueOf' into a Dict, let the chips fall where they may.

Anything else is more like a new type (typeof type, data type) that's not an object. That was proposed separately:

wiki.ecmascript.org/doku.php?id=strawman:dicts

# David Herman (13 years ago)

If you want to stratify toString/valueOf in general and for all objects, I would very much support that.

I'm not sure I understand what you mean. Do you mean something like:

js> var obj = Object.create(null, {});
js> String(obj)
TypeError: can't convert obj to string
js> Object.setMetaToStringBehavior(obj, function() { return "hello world" });
js> String(obj)
"hello world"

for normal objects?

# David Herman (13 years ago)

I agree with Andreas. The implicitly-called base level methods are not meta-methods or (spec language) "internal methods". They do not need their own traps. They are base-level property accesses.

Well, certainly that's the way the language currently works. But the way it currently works is problematic, because you can't define an object that is convertible to string without using the toString name.

Allowing proxies to trap implicit calls to base-level methods but not explicit calls seems weird. If a Dict should not pollute its pseudo-property namespace with 'toString' and 'valueOf', then it can still delegate those to standard methods on its prototype chain.

This doesn't work. It's a pigeonhole problem. If you allow toString to be inherited from the prototype, then it pollutes dictionary lookup. If you don't, then string conversion fails. It's not possible to have both.

And if someone does enter 'toString' or 'valueOf' into a Dict, let the chips fall where they may.

Well of course someone should be allowed to enter 'toString' into a Dict. But why shouldn't it be possible to define robust string conversion for an object without having to pollute the object's namespace?

Anything else is more like a new type (typeof type, data type) that's not an object.

I disagree. If you inherit from Object.prototype, then yes, 'toString' has a reserved meaning and a prescribed behavior. But since ES5 it's possible to create an object that does not inherit from Object.prototype (via Object.create), and I see no reason why we should insist on pre-reserving meaning for any properties on such an object. If you define your own prototype hierarchy root, you should be able to give whatever meaning you want to whatever names, or even no meaning to any names whatsoever.

That was proposed separately:

wiki.ecmascript.org/doku.php?id=strawman:dicts

Yup, I remember writing it. ;-) But ES5 pretty much already gives you the ability to define such an object, with the exception of the result of typeof and the fact that [[ToString]] and [[ToPrimitive]] break. What I'm proposing is that we clean up these places in the semantics that currently look for concrete names in favor of something stratified.

One possibility might be to create some private names, à la |iterate|, which you could use in place to 'toString' and 'valueOf':

js> import { toString, valueOf } from "@meta";
js> var obj = Object.create(null, {});
js> String(obj)
TypeError: cannot convert obj to string
js> obj[toString] = function() { return "hello world" };
js> String(obj)
"hello world"

This isn't exactly stratified like proxies, but it at least leaves room to unreserve the meaning of 'toString' and 'valueOf'.

# Axel Rauschmayer (13 years ago)

One possibility might be to create some private names, à la |iterate|, which you could use in place to 'toString' and 'valueOf':

js> import { toString, valueOf } from "@meta"; js> var obj = Object.create(null, {}); js> String(obj) TypeError: cannot convert obj to string js> obj[toString] = function() { return "hello world" }; js> String(obj) "hello world"

This isn't exactly stratified like proxies, but it at least leaves room to unreserve the meaning of 'toString' and 'valueOf'.

Is this only an issue, because objects are also sometimes used as dictionaries? Wouldn’t it make sense to instead untangle these two uses?

# David Bruant (13 years ago)

Le 17/10/2011 03:29, Andreas Gal a écrit :

(...)

Fixing this just for proxies by stratifying toString, valueOf seems undesirable. We would grant extra powers to proxies, and suddenly this following two ways to invoke toString would produce different results (potentially):

Proxy.create(myEmptyHandler, null) + ""

and

Proxy.create(myEmptyHandler, null).toString() + ""

By many aspects, proxies break usual expectations regarding objects. This one doesn't seem to be the worst expectation to break. I agree that both expression are "culturally" expected to return the same value, but i don't remember seeing a constraint in the ES spec imposing them to be the same. Host objects could break this expectation. (Do they?)

# David Bruant (13 years ago)

Le 17/10/2011 07:19, David Herman a écrit :

I agree with Andreas. The implicitly-called base level methods are not meta-methods or (spec language) "internal methods". They do not need their own traps. They are base-level property accesses. Well, certainly that's the way the language currently works. But the way it currently works is problematic, because you can't define an object that is convertible to string without using the toString name.

Indeed, harmony:proxies_semantics [1] says that proxies' [[DefaultValue]] has the semantic of default [[DefaultValue]] (8.12.8) . This means that a proxy is expected to either show a "toString" or "valueOf" property or a TypeError is thrown. 2 internal [[Get]] are performed. This seems quite arbitrary and may lead to complicated trap implementation if there are side effects.

David

[1] harmony:proxies_semantics

# Dean Landolt (13 years ago)

On Mon, Oct 17, 2011 at 1:19 AM, David Herman <dherman at mozilla.com> wrote:

I agree with Andreas. The implicitly-called base level methods are not meta-methods or (spec language) "internal methods". They do not need their own traps. They are base-level property accesses.

Well, certainly that's the way the language currently works. But the way it currently works is problematic, because you can't define an object that is convertible to string without using the toString name.

Allowing proxies to trap implicit calls to base-level methods but not explicit calls seems weird. If a Dict should not pollute its pseudo-property namespace with 'toString' and 'valueOf', then it can still delegate those to standard methods on its prototype chain.

This doesn't work. It's a pigeonhole problem. If you allow toString to be inherited from the prototype, then it pollutes dictionary lookup. If you don't, then string conversion fails. It's not possible to have both.

And if someone does enter 'toString' or 'valueOf' into a Dict, let the chips fall where they may.

Well of course someone should be allowed to enter 'toString' into a Dict. But why shouldn't it be possible to define robust string conversion for an object without having to pollute the object's namespace?

Anything else is more like a new type (typeof type, data type) that's not an object.

I disagree. If you inherit from Object.prototype, then yes, 'toString' has a reserved meaning and a prescribed behavior. But since ES5 it's possible to create an object that does not inherit from Object.prototype (via Object.create), and I see no reason why we should insist on pre-reserving meaning for any properties on such an object. If you define your own prototype hierarchy root, you should be able to give whatever meaning you want to whatever names, or even no meaning to any names whatsoever.

That was proposed separately:

wiki.ecmascript.org/doku.php?id=strawman:dicts

Yup, I remember writing it. ;-) But ES5 pretty much already gives you the ability to define such an object, with the exception of the result of typeof and the fact that [[ToString]] and [[ToPrimitive]] break. What I'm proposing is that we clean up these places in the semantics that currently look for concrete names in favor of something stratified.

One possibility might be to create some private names, à la |iterate|, which you could use in place to 'toString' and 'valueOf':

js> import { toString, valueOf } from "@meta"; js> var obj = Object.create(null, {}); js> String(obj) TypeError: cannot convert obj to string js> obj[toString] = function() { return "hello world" }; js> String(obj) "hello world"

This isn't exactly stratified like proxies, but it at least leaves room to unreserve the meaning of 'toString' and 'valueOf'.

This goes past "leaves room" -- while it may not be possible to drop these two special method names from the spec, by making them subordinate to more stratified private names this effectively unreserves them, at least where the more stratified private name exists. As I've tried to point out in a few threads now -- this strategy could be employed across the whole language to create a kind of opt-in stratification that doesn't necessarily break current programming styles.

Of course, it could be a little confusing when someone is trying to wire up a custom toString on an object that's already been @meta-toStringed, so the custom toString has no effect. This could also be a problem when running new @meta-typed objects through old toString-calling libs.

# Brendan Eich (13 years ago)

On Oct 17, 2011, at 12:43 AM, David Bruant wrote:

Le 17/10/2011 07:19, David Herman a écrit :

I agree with Andreas. The implicitly-called base level methods are not meta-methods or (spec language) "internal methods". They do not need their own traps. They are base-level property accesses. Well, certainly that's the way the language currently works. But the way it currently works is problematic, because you can't define an object that is convertible to string without using the toString name. Indeed, harmony:proxies_semantics [1] says that proxies' [[DefaultValue]] has the semantic of default [[DefaultValue]] (8.12.8) . This means that a proxy is expected to either show a "toString" or "valueOf" property or a TypeError is thrown. 2 internal [[Get]] are performed. This seems quite arbitrary and may lead to complicated trap implementation if there are side effects.

This does seem like a missing trap. Spec and implementations have both tried to "de-meta" [[DefaultValue]] in terms of base-level toString/valueOf calls. I agree it's ugly. Would a defaultValue trap be worth its weight?

# David Bruant (13 years ago)

Le 17/10/2011 17:31, Brendan Eich a écrit :

On Oct 17, 2011, at 12:43 AM, David Bruant wrote:

Le 17/10/2011 07:19, David Herman a écrit :

I agree with Andreas. The implicitly-called base level methods are not meta-methods or (spec language) "internal methods". They do not need their own traps. They are base-level property accesses. Well, certainly that's the way the language currently works. But the way it currently works is problematic, because you can't define an object that is convertible to string without using the toString name. Indeed, harmony:proxies_semantics [1] says that proxies' [[DefaultValue]] has the semantic of default [[DefaultValue]] (8.12.8) . This means that a proxy is expected to either show a "toString" or "valueOf" property or a TypeError is thrown. 2 internal [[Get]] are performed. This seems quite arbitrary and may lead to complicated trap implementation if there are side effects. This does seem like a missing trap. Spec and implementations have both tried to "de-meta" [[DefaultValue]] in terms of base-level toString/valueOf calls. I agree it's ugly. Would a defaultValue trap be worth its weight?

What is the weight exactly? I think this trap could be defined as a derived trap with ES5.1 - 8.12.8 as default implementation (replacin [[Get]] by calls to the get trap). It wouldn't be such a burden for proxy programmers. It may be one more annoyance for implementors though, I agree if that's what you're referring to.

# Brendan Eich (13 years ago)

On Oct 17, 2011, at 8:45 AM, David Bruant wrote:

Le 17/10/2011 17:31, Brendan Eich a écrit :

On Oct 17, 2011, at 12:43 AM, David Bruant wrote:

Le 17/10/2011 07:19, David Herman a écrit :

I agree with Andreas. The implicitly-called base level methods are not meta-methods or (spec language) "internal methods". They do not need their own traps. They are base-level property accesses. Well, certainly that's the way the language currently works. But the way it currently works is problematic, because you can't define an object that is convertible to string without using the toString name. Indeed, harmony:proxies_semantics [1] says that proxies' [[DefaultValue]] has the semantic of default [[DefaultValue]] (8.12.8) . This means that a proxy is expected to either show a "toString" or "valueOf" property or a TypeError is thrown. 2 internal [[Get]] are performed. This seems quite arbitrary and may lead to complicated trap implementation if there are side effects. This does seem like a missing trap. Spec and implementations have both tried to "de-meta" [[DefaultValue]] in terms of base-level toString/valueOf calls. I agree it's ugly. Would a defaultValue trap be worth its weight? What is the weight exactly? I think this trap could be defined as a derived trap with ES5.1 - 8.12.8 as default implementation (replacin [[Get]] by calls to the get trap). It wouldn't be such a burden for proxy programmers. It may be one more annoyance for implementors though, I agree if that's what you're referring to.

Yes, that is the weight.

Dave referenced the private iterate name object you can use to extend an extensible object with a base-level property used by the iteration protocol, and Dean wondered if we can't do more like that. The problem is that makes implementations specialize on a list of private names, instead of on the object as a whole being a proxy (or despecialize, I mean -- the slow path).

The fast paths in current VMs want no few magic private names. The iteration case is ok because it affects only for-of (new syntax), not for-in. Also I claim it can be optimized to noise, based on experience with our meta-programmable for-in in SpiderMonkey. But going a bridge farther is trouble.

Back to a Proxy defaultValue trap. It should be derived as you say. It imposes no overhead on base-level objects. It seems unproblematic, but I can't for the life of me recall why it was left out. Mark or Tom would know.

# Brendan Eich (13 years ago)

On Oct 16, 2011, at 10:19 PM, David Herman wrote:

I agree with Andreas. The implicitly-called base level methods are not meta-methods or (spec language) "internal methods". They do not need their own traps. They are base-level property accesses.

Well, certainly that's the way the language currently works. But the way it currently works is problematic, because you can't define an object that is convertible to string without using the toString name.

Yes, you're right. I didn't mean to make that necessity a virtue.

And if someone does enter 'toString' or 'valueOf' into a Dict, let the chips fall where they may.

Well of course someone should be allowed to enter 'toString' into a Dict. But why shouldn't it be possible to define robust string conversion for an object without having to pollute the object's namespace?

After you and David sorted me out, I think the main thing is to avoid more magic unstratified traps in all objects. A proxy can afford a derived defaultValue trap. One might argue that for implicit conversion, base-level objects could afford a private-named defaultValue trap too. I suspect implementors will complain and this will hit the stupids (SunSpider in particular). But I could be wrong.

That was proposed separately:

wiki.ecmascript.org/doku.php?id=strawman:dicts

Yup, I remember writing it. ;-) But ES5 pretty much already gives you the ability to define such an object, with the exception of the result of typeof and the fact that [[ToString]] and [[ToPrimitive]] break. What I'm proposing is that we clean up these places in the semantics that currently look for concrete names in favor of something stratified.

If dicts must be proxies the economics are wrong, as you noted on twitter.

If dicts are objects, they don't behave like other objects. We could then (a) extend object (as you sketch below) with optional unstratified, private-named traps. Or we could (b) make dicts a new typeof-type as your strawman (I was teasing, I remembered you wrote it ;-) proposes.

I think we're on the slippery slope doing the latter. Unstratified traps for iterate and defaultValue (let's say -- not toString and valueOf separately), won't cut it. All the other proxy traps will be wanted, e.g. as in the wiki.ecmascript.org/doku.php?id=strawman:observe strawman.

Python goes all-in with ugly trap names. We've studied these for wiki.ecmascript.org/doku.php?id=strawman:value_proxies but the proxy (non-native) nature of those saves us, we think. The problem is base-level private-named traps triggering novel control flow in real engines. Currently these will mean a performance hit. With enough implementation work, perhaps they won't -- but then there's the implementation work to count.

I'm not convinced we want to take door (a), not just for dicts. At the May TC39 meeting, for observe-like watchpoints, we talked about marrying proxies and base-level objects without deoptimization using becomes. I think Arv has news on this front, so I'll let him speak.

# Allen Wirfs-Brock (13 years ago)

On Oct 17, 2011, at 9:20 AM, Brendan Eich wrote:

Back to a Proxy defaultValue trap. It should be derived as you say. It imposes no overhead on base-level objects. It seems unproblematic, but I can't for the life of me recall why it was left out. Mark or Tom would know.

The only polymorphic use of [[DefaultValue]] in the ES spec. is with Date and the specification of this usage is very subtle in that 15.9.6 doesn't say that Date instance have a unique implementation of [[DefaultValue]]. However, that would be the most straight forward way to implement the specified "no hint" [[DefaultValue]] behavior of Date instances specified in 8.12.8.

It would seem that to use Proxy to implement a full fidelity implementation of the ES5 Date object requires a [[DefaultValue]] trap.

However, that would screwup my plans for enabling the "subclassing" of the built-in date. Remember, my general approach to subclassing built-ins with special behaviors is to use <| with a literal for the built-in on the RHS. However, Date does not have a literal form, so that doesn't work for it. My work around for Date was based upon the observation that the only primitive property over-ride for Date instances is [[PrimitiveValue]]. State holding internal properties like [[PrimitiveValue]] can easily be replaced by a normal property with a system provided private name key. If that is done, then Date could could be subclassed like any other class:

var JulianDate = Date <| function(...args) {...}; /* plus appropriate instance method over-rides */\

However, if Date instance need a special [[DefaultValue]] such a subclass would not follow the special Date [[DefaultValue]] behavior in the ES spec.

For situations like this, rather than adding additional proxy traps, I would prefer to see more use of private named method properties with system provided keys.

For example, the only use of [[DefaultValue]] in the ES spec. is by the ToPrimitive abstract operation. ToPrimitive could be redefined approximately as follows:

input Type: Object

  1. Let hasMethod = result of calling [[HasProperty]] internal method of input with argument defaultValuePrivateName.
  2. If hasMethod is true a. let hasMethodFunction = result of [[Get]] internal method of input with argument defaultValuePrivateName. b. if hasMethod is callable i. standard boiler plate for calling a method of input with with hint as the arguemnt ii. return result of above step
  3. perform the [[DefaultValue]] behavior currently specified in 8.12.18

(normal Proxy traps would occur in all the appropriate places above.)

What this does is "demotes" [[DefaultValue]] from being an internal method to being a normal method with a well known private name.

Such private named properties are preferable to internal properties primarily because they are inheritable. In general, internal method usage screws up subclassing so their usage should be as minimal as possible.

# Tom Van Cutsem (13 years ago)

2011/10/17 Brendan Eich <brendan at mozilla.com>

Back to a Proxy defaultValue trap. It should be derived as you say. It imposes no overhead on base-level objects. It seems unproblematic, but I can't for the life of me recall why it was left out. Mark or Tom would know.

My guess is we left out [[DefaultValue]] since proxies could deal with it via get("toString") and get("valueOf"), so it kept the handler API minimal. I don't think we ever noticed the opportunity that defaultValue could be the place to introduce stratified toString/valueOf support. I don't see any particular problem in making defaultValue a derived trap if need be.

# Brendan Eich (13 years ago)

Maybe we can hold the line at private-named iterate and defaultValue unstratified traps. I'm looking for two things:

  1. Feedback from implementors on not trapping non-proxy objects based on private names being mapped.

  2. A principled approach to holding the line.

# Erik Arvidsson (13 years ago)

On Mon, Oct 17, 2011 at 13:42, Brendan Eich <brendan at mozilla.com> wrote:

Maybe we can hold the line at private-named iterate and defaultValue unstratified traps. I'm looking for two things:

  1. Feedback from implementors on not trapping non-proxy objects based on private names being mapped.

  2. A principled approach to holding the line.

I still have my hope we can get the Array magic as well as DOM collection lookup behavior behind that line.