whatwg / webidl

Web IDL Standard
https://webidl.spec.whatwg.org/
Other
410 stars 164 forks source link

More convenient way to refer to / use "map iterators" - tuples #324

Open tabatkins opened 7 years ago

tabatkins commented 7 years ago

I'm writing the CSS Typed OM spec, and I want to allow you to pass a Map iterator to the CSSCalcValue constructor (since CSSCalcValue is a maplike already). However, it appears that to do so, I need to define the argument's type as sequence<sequence<(DOMString or double)>>, and then in the algorithm, explicitly iterate the sequence and verify that each entry is a 2-tuple where the first is a DOMString and the second is a double.

This is obviously extremely inconvenient, not to mention the fact that the type is extremely opaque - it's not clear at all that it's intended to take a map iterator.

(URL is also doing this, tho at least they have the convenience of both key and value being DOMStrings there, so their type/code is very slightly simpler.)

Can we have a sequence<> variant that is specialized for Map iterators? Maybe map-sequence<keyType, valueType>? This would also do the type-checking automatically.

tabatkins commented 7 years ago

Domenic points out that it's not immediately clear what would happen if you used map-sequence<> as a return value.

Seems pretty easy to just have it return a Map, same as sequence<> as a return value means you return an Array.

bzbarsky commented 7 years ago

Seems pretty easy to just have it return a Map

A map-sequence (aka pair iterator) can take on values that a Map can't. For example, it can have duplicated keys.

That might not be a problem in practice, of course...

annevk commented 7 years ago

Yeah, for URLSearchParams you can have duplicate keys and the order between those duplicates is significant. For Headers you can have duplicate keys, but there it results in one key with multiple values.

Typically for these constructs you also want to support record<key, value> which can express less (only unique keys), but has more convenient syntax.

(And maybe record<key, sequence<value>>, though we haven't gone that far yet.)

I think using map is a little wrong, something like key-value-sequence seems more accurate, and if we go there it should encompass all those input forms I think so we are somewhat consistent. (And then users need to deal with duplicate keys.)

This also seems like another case where you don't necessarily want it as return value, just as input. But if it needs to be a return value, it should be a nested array or iterator for one I think, since that can encompass the full semantics.

tabatkins commented 7 years ago

Yeah, for URLSearchParams you can have duplicate keys and the order between those duplicates is significant. For Headers you can have duplicate keys, but there it results in one key with multiple values.

That doesn't want to return a Map, it wants a MultiMap. Since that doesn't exist in JS yet, you'll instead be returning an interface of your own, and the question of "what does it mean to return a map-sequence<>" is moot. (I'm not opposed to readying ourselves for such a future, tho.)

Typically for these constructs you also want to support record<key, value> which can express less (only unique keys), but has more convenient syntax.

Yes, I'm handling that via a separate overload. I don't think it's necessary or desirable to tie "takes a record<>" with "takes a map iterator"; for one thing, the former only accepts string types for keys, while the latter is free-form. (Since record<> becomes an Infra ordered map on the IDL side, and maplikes use an ordered map for their map entries, it's super trivial to initialize a maplike from a record, yay!)

(And maybe record<key, sequence<value>>, though we haven't gone that far yet.)

And that seems to be handled reasonably by the existing signature and behavior.

I think using map is a little wrong, something like key-value-sequence seems more accurate, and if we go there it should encompass all those input forms I think so we are somewhat consistent. (And then users need to deal with duplicate keys.)

Sure, kv-sequence works too. It's just that this particular pattern (an iterator of 2-tuples) is only really used by/for Maps, so putting it directly into the name makes sense to me. The structure is really awkward to author by hand, after all; it's nearly always going to be a Map iterator in practice anyway.

This also seems like another case where you don't necessarily want it as return value, just as input. But if it needs to be a return value, it should be a nested array or iterator for one I think, since that can encompass the full semantics.

Yeah, that's fine with me. It's trivial to load it back into a Map by just passing it to the constructor, and it's consistent with MultiMaps when those get reified.


So, let's assume that using map-sequence<> as a return value just returns an Array-of-Arrays; that's more general than using Map directly (and encourages returning maplike interfaces if that's really what you want).

tabatkins commented 6 years ago

Reviving, because @annevk reminded me about the Headers constructor which is still hand-rolling taking a map-sequence, which is super non-obvious when you read it.

As an argument type: map-sequence<Foo, Bar> does the same processing as sequence<>, but ensures that (a) each item in the sequence is an Infra "pair" containing a Foo and a Bar. Your algorithm receives it as an Infra list of pairs.

As a return type: the returned value must be an Infra list, containing only Infra pairs where the first item is of type Foo and the second is of type Bar; it transforms into a JS Array of Arrays.

tabatkins commented 6 years ago

Alternate name: pair-sequence<>, to go along with the "pair iterator" term already used in the spec for objects that iterate in this form. This is probably more understandable anyway.

annevk commented 6 years ago

I like that, due to Headers consistently throwing TypeError such a change would not be observable either.

domenic commented 6 years ago

I'm a little unsure we should use the return type that way, especially given how its counterpart in ES seems to be bespoke iterators (e.g. MapIterator and friends). I might just disallow it as a return type altogether? Although then it's just very unusual since we have no precedent for such asymmetry...

tabatkins commented 6 years ago

I'd be fine with that; I'm only talking about the return type because of symmetry with sequence<>. In any case, we can always start with the one that has justification and add the rest later.

Hm, does anyone besides Map return the equivalent of a pair-sequence?

kainino0x commented 5 years ago

This is likely interesting to WebGPU as well. We don't have anything that uses a pattern like this right now, but there are places where we might like to.

Hm, does anyone besides Map return the equivalent of a pair-sequence?

Object.entries(), I think. Being able to express this also allows a user to explicitly write the pair-sequence, which is the same type accepted by Map's constructor.

kainino0x commented 5 years ago

Oh, and there is a place where we already are trying to define a tuple today, to do an ES6-style multiple-return.

I think if we had typed tuples in WebIDL, it would mostly solve both issues: tuple<GPUBuffer, ArrayBuffer> sequence<tuple<DOMString, Buffer>> sequence<tuple<unsigned int, GPUVertexBufferDescriptor>>

Though it would not express the uniqueness of keys.

Ms2ger commented 5 years ago

Something like this could work for converting tuples; it'd just need to be added everywhere types are mentioned.

<div id="es-to-tuple" algorithm="convert an ECMAScript value to tuple">

    An ECMAScript value |V| is [=converted to an IDL value|converted=] to an IDL
    <a lt="tuple type">tuple&lt;<var ignore>T</var><sub>0</sub>, …, <var ignore>T</var><sub>|n| &minus; 1</sub>&gt;</a>
    value as follows:

    1.  If [$Type$](|V|) is not Object, [=ECMAScript/throw=] a {{ECMAScript/TypeError}}.
    1.  Let |iter| be [=?=] [$GetIterator$](|V|, <emu-const>sync</emu-const>).
    1.  Let |i| be 0.
    1.  While |i| &lt; |n|:
        1.  Let |next| be [=?=] [$IteratorStep$](|iter|).
        1.  If |next| is <emu-val>false</emu-val>, [=ECMAScript/throw=] a {{ECMAScript/TypeError}}.
        1.  Let |nextItem| be [=?=] [$IteratorValue$](|next|).
        1.  Initialize |S|<sub>|i|</sub> to the result of [=converted to an IDL value|converting=]
            |nextItem| to an IDL value of type |T|_|i|.
        1.  Set |i| to |i| + 1.
    1.  Return a tuple where the value of the element at index |j| is |S|<sub>|j|</sub>.
</div>

<div id="tuple-to-es" algorithm="convert a tuple to an ECMAScript value">

    An IDL tuple value |tuple| of type
    <a lt="tuple type">tuple&lt;<var ignore>T</var><sub>0</sub>, …, <var ignore>T</var><sub>|n| &minus; 1</sub>&gt;</a> is
    [=converted to an ECMAScript value|converted=]
    to an ECMAScript Array object as follows:

    1.  Let |A| be [=!=] [$ArrayCreate$](0).
    1.  Let |i| be 0.
    1.  While |i| &lt; |n|:
        1.  Let |V| be the value in |S| at index |i|.
        1.  Let |E| be the result of [=converted to an ECMAScript value|converting=]
            |V| to an ECMAScript value.
        1.  Let |P| be the result of calling [$ToString$](|i|).
        1.  Call [$CreateDataProperty$](|A|, |P|, |E|).
        1.  Set |i| to |i| + 1.
    1.  Return |A|.
</div>