-
Notifications
You must be signed in to change notification settings - Fork 165
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
Figure-out what supporting Array-subclassing implies #345
Comments
It might be a good idea. We could hijack On the other hand, it's hard for me personally to start brainstorming new syntax without boiling the ocean, so maybe sticking with existing syntax is better?
Definitely.
We may want to figure out the story for isConcatSpreadable; that was added specifically so that legacy classes could subclass Array: http://stackoverflow.com/a/27024188/3191. E.g. if NodeList became an Array subclass, it would set isConcatSpreadable to false so that it wouldn't break code. However, NodeList can't really subclass Array anyway since e.g. its read-only. It could do a fake-subclass via [LegacyArrayClass] + fixing isConcatSpreadable, maybe. So new subclasses might want to just leave isConcatSpreadable alone. I think our biggest issue is we need a consumer to help us shake out the bugs and edge cases. That used to be DOM's The biggest question surrounds the fact that Array subclasses cannot control what type of items they contain. E.g. Resurrecting that spec might be enough, but having another spec as a second consumer would make me happier. |
(I'd revive Elements if IDL got this by the way.) |
As I see it, we need the following bits here:
NodeList can't become an Array subclass. Between Elements and what CSS typed OM is doing I think we would have two consumers... |
A great list. I agree with all. Some extra thoughts: For (1), to expand on my "boiling the ocean" comment: new syntax makes me think (3) sounds like a pretty disruptive change for implementations and spec-writing, but cool if we could get away with it. (4) sounds like an OK "for now" limitation, but we'd need to check with our consumers... (6) is quite interesting. I guess length + indices would be most consistent with existing array methods. https://tc39.github.io/ecma262/#sec-createlistfromarraylike is convenient but non-idiomatic in most cases; it would be useful if you need to snapshot some state before going "in parallel" though. In most cases you should probably perform work as you iterate, which definitely would benefit from a helper. |
Any Array-like that doesn't let us type-check insertions is really missing the boat. With any other attribute type, I can ensure that the object is of a known type, and the author messing that up will fail early, at the point you try to make a mutation. This makes it much easier to write specification prose, as I'm working with a known type, and reduces the amount of type-checking that has to be done overall in the platform. (In particular, when passing the object to another API, it can depend on the object's attributes all being of the correct type too, and doesn't have to repeat all the checks, recursively until you hit primitives.) A non-type-enforceable Array subclass throws all this away. Spec authors have to be much more careful when writing their prose, which they will get wrong, and the platform has to recursively check that Array subclasses hold the right type every single time it passes into browser-land code. (Again, recursing all the way down to primitives, or at least to types that the engine knows ahead of time can't recursively-contain any array subclasses.) Page authors have a more difficult time too, because inserting a bad value won't fail immediately; instead it'll throw at some later point when the collection is passed to a platform API and fails its recursive typecheck. It's seriously not worth subclassing arrays if we can't get this right. If this requires further ES changes (such as defining the |
(For more detail on this recursive-checking failure mode, see w3c/css-houdini-drafts#239 (comment).) |
I have to admit to being pretty confused at this point. If someone wants typechecking on set, we have a mechanism for that: an interface with an indexed setter. That's a hook that catches sets with integer-named property sets and does something. That is its whole point. That object won't be an Array, obviously, because it has totally different semantics from Array, both in terms of indexed sets and in terms of length-mutation (it probably either disallows length mutation, or allows it but doesn't use Now whether this is what web authors want, I can't tell you. AWB would claim that they want the "typecheck as late as possible" behavior, and that this is faster because you only do the typechecks when you really care about them and not at every API boundary, and further that any typechecks should be duck-typing, not branding. There are obviously philosophical differences about that... But the point is, we have a tool for getting the observable behavior being described. If that's the behavior we want, and we have a tool for it, why not just use the tool? |
Two reasons.
All I want is something that satisfies the following:
Given that this is contrary to the entire philosophy of WebIDL, I don't think AWB's opinion is particularly germane here. (Plus, his old proposal for a If the only way I can do all of this is to make this a Maplike with a k-v iterator that only uses integers for keys (imposing some semantics on what integers you can use), and manually define a mixin that duplicates the Array builtins, then by golly I'll do it. I'll be very unhappy about it, and I'm certain that y'all will be too, but I'm not going to define an API with a broken, slow, or needlessly-weak interface just because no one is willing to actually define something useful. |
I think you are being told wrong.
You don't have to do that if you use [ArrayClass]. Yes, I know that's claimed legacy too. That said, if you use [ArrayClass] and then use slice() or map() you will get an Array, not your thing. If you want to get your thing from slice() or map(), then you really do need an Array subclass, and then you can't control writes to it. Well, mostly. What ES allows you to do is to have your object be a Proxy whose target is an Array, and then you can still control construction of slice/map return values, plus of course you can control writes, because now you have all the proxy machinery at your disposal. I know there are concerns proxy performance being a problem, but we really can't have it both ways as things stand: either we have fast indexed writes that just blit memory and the JIT can optimize all it likes, or we have indexed writes that run some sort of verification code that the JIT knows nothing about conceptually and then we end up taking slow paths, unless the JIT is explicitly taught about this verification code and what its invariants are. That's all independent of whether we have a [] hook; the presence of such a hook would cause the JIT to deopt just as much as being a full-blown proxy does, until special fast paths are added there. So in principle we could spec something like the maplike/setlike in the spec (which delegate to ES Map/Set after doing some sanity checks), by defining the object as a proxy, with an Array target, with Array.prototype on its proto chain. I'm pretty sure that will do the "right thing" when that object is We could also try lobbying TC39 to have more of a carve-out in ArraySpeciesCreate for exotic objects that quack like array subclasses but aren't really, just like they already have for Proxy. As far as your goals go, what do you consider reasonable semantics in this case for map, foreach, filter, slice, etc? Or rather, do you consider the semantics of the corresponding Array.prototype methods to be unreasonable in this case, and if so in what way? Is the point that you want IDL to predefine these methods for you, instead of you defining them yourself, with whatever semantics you want? (I'm not saying this is an unreasonable desire; just trying to figure out what the actual constraints are here.) |
Sorry for the frustration coming thru in this reply, but this topic is getting me increasingly frustrated. I'm being given exactly opposing advice ("just use indexed getters/setters", "nobody should ever use indexed getters/setters") from the exact people I've been trained to trust on these matters, and it feels like there's a collective shrug about the terrible ergonomics this topic has inflicted and will continue to inflict on authors, which is extra-frustrating given the generally good ergonomics that have been granted to Maps and Sets in WebIDL. |
You're not the only one frustrated by the state of this stuff, trust me. :( There is definitely a very longstanding problem here in terms of disagreement about how arraylikes should behave, because different people seem to want very different things out of them. We've literally been arguing back and forth about this for years, and people can't even seem to agree on what "good ergonomics" means for arraylikes. :( One thing we could do is have some way in IDL to just opt into behavior similar to what typed arrays have in ES. Those are not array subclasses, but have various things like slice/map/filter/etc defined on them, with special typed array semantics that are not generic unlike the Array versions. They perform coercion (which can throw) on write. This sounds like more or less what you're asking for, right? |
Presumably it would deopt into the same speed as any normal method call, yes? If that's the case, then I can stop worrying about this, and just use it. I've been given the impression, tho, that it actually de-opts to something substantially slower than an ordinary method call.
The Array.prototype semantics are "unreasonable" only in that they assume the object is an Array (with no arg-checking) and act accordingly. I want a type-restricted Array, with relatively obvious semantics (at least, other languages handle this pretty consistently):
In general, the semantics are "return an object of the same class, and if mutations/additions happen, typecheck and throw if necessary". In keeping with general WebIDL semantics, they should probably also brand-check their
I'd have to study the details, but the surface gloss you're giving me here sounds 👍. |
Normal DOM method call, as opposed to normal JS function call, right? It .. depends. I can only speak for SpiderMonkey at this level of specificity, and there is a bit more overhead here in SpiderMonkey than a normal DOM call, but not a lot more. Note that a lot of things that are DOM calls are not normal DOM calls, in both SpiderMonkey and other engines, in a variety of ways. But even the "slot path" vmcall really isn't that slow. As in, I'd need to see a use case to see whether the performance matters. Chances are it does not. Your description of what you consider reasonable semantics sounds very much like what typed arrays do. @domenic, @annevk thoughts on having a way for things with indexed getters/setters to opt into that sort of behavior? Honestly, we could just have |
I don't particularly care about the difference? People are pretty happy to call JS functions all over the place; if [] get/set was at JS method speed I'd be happy. Put another way, there's zero perf issues with the speed of Map.get/set function calls; if the Array-like's [] speed was similar, it would be just fine.
Yeah, the perf case we're fighting against is "the UA constantly has to parse author-provided strings back into C++ objects" (the current CSSOM), so there's a lot of ceiling to work under.
Yes, looking into the MDN description, it does indeed sound like Typed Arrays have my desired semantics. They coerce rather than throw, but that's done via a pre-write hook anyway, and so can be changed to do whatever I need. |
Right, JS (scripted) function calls are faster, generally, than either DOM calls or proxy hooks.
OK. The performance of those is pretty comparable to what we'd see here. |
Reading over the ES2015 spec, yes, Typed Array semantics are exactly what I want. (Man, it's pretty hard to chase down where, precisely, the coercion semantics are defined. You have go look up Integer Indexed Exotic Objects, which aren't obviously linked, and chase some definitions there before https://www.ecma-international.org/ecma-262/6.0/#sec-integerindexedelementset finally defines it.) |
Circling back: so does this approach (copy the TypedArray spec, plus a few more Array operations that can change the length of the thing (which TypedArray didn't include for obvious reasons)) sound good? If so, I'd like to put it on Tobie's todo list. ^_^ |
I don't think there's agreement on adding more typed lists. We explicitly decided against that quite a few years ago. |
I think we explicitly decided against auto-generating such lists via the old IDL Array mechanism. I don't see why we shouldn't have interfaces that opt into looking like a typed array: more or less API-compatible with Array, but without actually being an Array subclass, so there is control over writes into the array. You can get there right now with indexed getters/setters and some manual definition of various methods; the only question is whether we should have a built-in IDL shorthand for this sort of thing. |
Because when we discussed those kind of approaches with TC39, they told us not to and to just use Array. It wasn't just about And so then we investigated alternatives to current APIs and found out we could use Array and sequences (for input) just fine. You could go back to TC39 I suppose and say you really want to do typed arrays again... |
They don't need a proxy with this approach - that's the entire point of this thread's discussion. TypedArrays have intercepts reads/writes using the |
[] syntax interception is proxy-like in typed arrays as well. There's no way to intercept that syntax without proxies. |
Whatever typed arrays do (and it's not defined Proxy-like in the ES spec), it's clearly Good Enough for the web platform, tc39, and engines for it to be specced, shipped, and used. The plan here is to lean on that exact mechanism. Here are the alternatives:
All of these suck to varying degrees. Copying Typed Arrays is the only option that doesn't have crap ergonomics, as it puts array-likes on the same footing as map-likes and set-likes, and every other interface in the web. |
It is defined proxy-like in the ES spec; they override the various internal methods. |
Yeah, internal definitions can override each other; it's all spec-ese. |
Look. If we want to be able to do typechecking at Now Web IDL already provides a mechanism for doing typecheck-on-set: indexed setters. The same people who think typecheck-on-set is a bad idea also think that indexed setters are a bad idea. This is not surprising.
To be clear, all I'm saying above is that we could have a simple Web IDL syntax for doing exactly this, so spec authors don't have to reinvent it from scratch (as @tabatkins notes we do just that for maplike/setlike). The typed array bit is just about the exact behavior of the mixins involved: we would have them behave like the relevant functions on typed arrays do. |
That's literally what it means to be proxy-like, is to override your internal methods (i.e. be an exotic object which does not use the default internal methods). |
I feel like it needs to be reiterated, because y'all are pretending like this is something exotic and unheard of: Typed Arrays already do [] typechecking. The thing I'm asking for is already present in the platform and widely-implemented, and not, to the best of my knowledge, considered a mistake. What is making y'all so resistant to the idea of reusing that exact same mechanic? |
Typed arrays are considered a mistake of sorts. TC39 was certainly not happy with them, but it was also their own fault because they didn't add byte support in a timely manner. |
My point is that people clearly want "brand check on set" behavior in some cases. The standard TC39 response is "you shouldn't want that", just like the standard TC39 response is "you shouldn't want that" any time brand checking is mentioned... |
And, to be specific, we already very explicitly ignore that particular "you shouldn't want that" advice everywhere else in WebIDL; this one spot isn't specially privileged in that regard. |
All right, finally sent the email to es-discuss to ask for this functionality. |
Capturing for the future: https://lists.w3.org/Archives/Public/public-webapps/2009JulSep/1346.html is a 2009 thread where everyone in tc39 seems pretty uniformly in favor of allowing platform APIs to do "integer catchalls" (that is, Proxies that intercept get/set on integer keys). FileList and DOMTokenList are explicitly brought up as examples in that thread. |
For history’s convenience, the es-discuss email to which @tabatkins referred to above may be found here: “Intercepting sets on array-like objects”, 2017-06. Mark S. Miller, Allen Wirfs-Brock, Adam Klein, and Domenic Denicola participated in the thread. The thread does not seem to have resolved to a conclusion. |
So at this point TypedOM is just using This means that, per the current state of the world, the array-like interfaces still don't have access to any of the array methods - if you want to map over them, you still have to manually do |
To be specific here, the TypedOM use-cases are:
All three of these are "dead" - updating them has no action-at-a-distance on anything else. (You have to actually assign the object to a property manually, at which point it "snapshots" the object into an internal CSS value disconnected from the JS object.) Two of these (CSSUnparsedValue and CSSTransformValue) are mutable; CSSNumericArray is readonly, both in its contents and the bindings that it's assigned to (to prevent cyclic values and some other inconsistencies from occurring). This means it doesn't matter what sort of object is returned from Wrt Boris's questions from earlier in the thread:
Additionally, we probably want to require some consistency in the constructor/setter signatures - the primary constructor should always take a Related to this: what happens if the array-like currently has, say, three entries (for the indexes 0, 1, and 2) and userland code calls |
I think the main problem here is that you are very interested in type-checked arrays, but ECMAScript doesn't provide them and nobody else seems motivated enough to solve this problem. Furthermore, even a normal subclassed array would not help you, as some of these interfaces already inherit. The other differences is that you are interested in having type-checked intermediate values, whereas the rest of the platform mostly performs that checking when values get applied. Though sometimes there are intermediate interfaces available, such as |
Note that my current design, which I'll be hopefully looking to standardize in some "suggested pattern" in WebIDL in the future, is to use indexed getter/setter/creator, all unnamed, with the creator only allowing creating the very next index (in other words, you can do (See https://drafts.css-houdini.org/css-typed-om/#dom-cssunparsedvalue-length and the following algorithm block for an example of this.)
I tried to go thru TC39; currently the responses are "indexed getters/setters don't use proxies in implementations, they seem like the right way to do this" and "new APIs should use Arrays and not typecheck", which disagree with each other, and there was no response to my attempt at follow-up. I'll ping the thread again. |
FWIW, I think unfortunately es-discuss is not representative of TC39 as many members don't participate there. You actually have to put your thing on the agenda and get someone to discuss it for you (or do it yourself, I saw you attended recently). |
#840 might effectively resolve this... |
ES spec is now set up for things to actually be an Array subclass, but this hasn’t been used in the platform yet.
Does this imply new syntax?
New bindings?
Are there specific requirements in terms of defining certain methods etc. that are required?
The text was updated successfully, but these errors were encountered: