Proposal-pattern-matching: array vs iterable destructuring?

Created on 10 May 2018  Â·  28Comments  Â·  Source: tc39/proposal-pattern-matching

The core document says:

Anyone who has learned to use destructuring binding in either assignment or function arguments should be able to apply that same understanding to match branches, and can benefit from much richer and semantically concise conditionals.

This sounds great!

However, this example: when [1,2] ~> ... // matches ifinputcan do ToObject,input.lengthis 2,input[0]is 1, andinput[1]is 2
seems like it's treating input like it's an arraylike, instead of matching array destructuring syntax, which would imply that it matches if [...input] has a length of 2, [...input][0] is 1, and [...input][1]` is 2.

Is there existing discussion on this difference, if intentional?

Most helpful comment

How does "has exactly two items" work? Do you try to get a third item, and fail the match if it is found? Seems a bit strange that for matching against a two-element pattern you call next() on the iterator three times, but maybe that's just the way to go... I'd be curious what other languages with iterable semantics and pattern matching do.

All 28 comments

I should probably correct that. My original version of this proposal assumed match would support array-likes, but that was mostly because I didn't really know how current destructuring works. I'll take that out and clarify. Thanks for pointing this one out 👍

How does "has exactly two items" work? Do you try to get a third item, and fail the match if it is found? Seems a bit strange that for matching against a two-element pattern you call next() on the iterator three times, but maybe that's just the way to go... I'd be curious what other languages with iterable semantics and pattern matching do.

That’s a really good question; it seems like it maybe should only get the first 2 items, and then assert on those, without caring if the iterator is exhausted at 2 yet or not?

bringing up the length matching is a good point, and it's another leftover from when I was thinking in terms of array-likes (and how this proposal has worked even before I got my hands on it). In this case, though, I believe it's an important feature to keep (and _will_ be significantly more painful if it's loosely matched).

Looking at other languages, F#, Elixir/Erlang, Haskell, and Rust all seem to strictly match on iterable length.

@domenic the JS iterator protocol specifies returning {done: true, value: ...} for the last item in an iterator, so you would only call .next() twice. I assume this is what other things in the language do when working with them.

No, it returns { done: false, value: x } for the last item, and { done: true, value: undefined } if you call it after the last item. There is no way to tell you're looking at the last item, only that you are past the last item.

On the subject of other languages, what does "iterable length" mean in those languages? Concretely, how many times does an infinite iterator get invoked?

ah! right. Then yes, you'd need to call next() again. I'm fine with that. I assume this is just what everything else does?

As far as iterable length?... I'm not sure what other languages even support that. Elixir/Erlang don't allow immutability, and iterator matching is only on lists, so circular lists are impossible (iirc). Haskell ditto? I don't know what the others do.

What does JavaScript do if you do const [a,b] = function*() {while (true) { yield 1 }}?

What if the pattern is [a, b, ...c]?

I think that'd be the right answer to the question.

I'm gonna reopen this in the meantime while we talk through this stuff. Thanks for your responses!

JS will call next() twice for your first example and will infinite loop for your second. There isn't really precedent for "checking" that the length is exactly right by consuming one more than the number of items you need.

Hm.

That's tough. How do you feel about that third iterator call happening in match, but not in regular destructuring? 🤔

Not great, but not terrible... I'd appreciate convincing arguments that we should break from destructuring. To me it seems fine for [a, b] to match [1, 2, 3]; we have a long tradition of allowing harmless extra stuff in JS, e.g. in argument lists or destructuring or similar.

My biggest argument for matching exactly on length is that it can be a serious footgun when used as a conditional, but because of the fast-and-loose style of regular destructuring, things not being "exactly right" is fine.

For example, match fails if you have a matcher that looks like {x} but the object in question does not have an x, whereas const {x} = {} won't error - just leave it undefined.

Meanwhile, const [a, b] = [1,2,3] is benign enough in that use case, but something like this can be dangerous and requires much more verbosity to be achieved if length-matching doesn't happen:

match ([1,2,3]) {
  when [] ~> ... // iterables will never get past this point
  when [a] ~> ...
  when [a, b] ~> ...
  when [a, b, c] ~> ...
}

By which I mean: the match proposal _already_ places extra arbitrary checks when determining whether a match "succeeded". An extra .next() doesn't seem very harmful when treated as "one of the conditional tests".

I’m curious — if we take that last example, reverse it and pass [ 1 ]...

match ([1]) {
  when [a, b, c] ~> ...
  when [a, b] ~> ...
  when [a] ~> ...
}

Is the intent that the iterable be iterated for each when uniquely, or will it only iterate once? There’s no guarantee that an iterable yields the same values on each iteration so it seems like it should iterate only once per match statement to me, but I’m not sure how viable that is.

let i = 0;

const chaoticNeutral = {
  * [Symbol.iterator]() {
    let j = ++i;
    while (j--) yield 'x';
  }
};

match (chaoticNeutral) {
  when [a, b, c] ~> ... // length is 1, fails either way
  when [a, b] ~> ...    // length is 1 or 2 — passes only if iterator runs every time
  when [a] ~> ...       // length is 1 or 3 — passes only if iterator runs only once
}

If it does get iterated only once, then it seems the iterator would need to be held open until a match is found in case it must be resumed, and the values it yielded on the last failed match would need to be held as state during this process. If it gets iterated every time, it’s a bit less predictable and more expensive, but the matching logic would be simpler.

iterators aren’t necessarily idempotent/reusable, so it’d be pretty critical to only call .next the minimum number of times to satisfy the longest iterable match.

Good point, I was thinking about it linearly (one match at a time) but yeah, "longest match needed" is statically discernable.

(If we extend "not necessarily idempotent" to its logical extreme, we would also want to only access properties a single time (when { a }, when { a, b }), but I suppose it’s kind of a good faith thing that accessors _ought_ to be idempotent vis a vis object state?)

I think the idea is you don't use iterators with these; you use iterables. Which are supposed to be idempotent (i.e. give an iterator that behaves the same way each time), in the same way that objects are generally supposed to be idempotent (i.e. calling their [[OwnPropertyKeys]] trap is supposed to give the same list each time).

In both cases idempotency can fail, due to either unusual iterables or to proxies. But it seems reasonable enough of an assumption to build a language feature on.

Fair, but is that worth the potential expense of creating and consuming the iterator multiple times?

Yeah, it seems fine. It'd be similar to several destructurings, or several Object.keys() calls, in my mental model. Creating and consuming an iterator is not supposed to be expensive.

Agreed. Each should grab a fresh iterator off of the object. If you pass an actual iterator, well, that's your problem.

What if i wanted to match against array.entries(), map.values(), or string.matchAll(regex)? All of those return iterators (which are indeed iterables, but they just return this, so they couldn’t be reused).

I think having a builtin language feature not able to work with builtin language iterators is a bit more of a problem than that.

Ah, I thought those returned iterables. Shucks.

Then yeah, I guess try to evaluate longest-match items?

I'm fully assuming a given iterator will only ever be run through once, fwiw.

It's part of the benefit of limiting match to be _static_ patterns: you can statically determine how much an iterator is going to need to consume, at max, across all possible matching patterns.

I guess it might be worth -specifying- this, but I wonder if it's best to treat that as an optimization rather than a semantic requirement.

@zkat due to https://github.com/tc39/proposal-pattern-matching/issues/99#issuecomment-389252505, i think it would probably need to be specified somehow.

@ljharb I think we can differentiate between iterators (requiring they be run _once_) and iterables (which can be allowed to create multiple iterators during a match run).

But I think it's still pretty safe to spec it as "one iterator, ever".

@zkat Both iterable and iterator are just contracts, and the iterators ljharb mentioned (like most iterators) also implement the iterable contract. So I would imagine, like domenic said, that only iterables are honored (that’s how e.g. destructuring works), but some iterables are exhaustable (because they are iterators that just return themselves) and some are not.

In any case the treatment couldn’t be an open-ended / an optimization for engines; doing it one way or the other is observable behavior. (Maybe not important to resolve at this stage, just something worth thinking about?)

This is 100% observable behavior, and definitely needs to be specified. In particular, create an iterator that console.logs all its method calls, and you will get different results depending on which of the strategies discussed here are used.

Sketched an answer out in #102 (it's stage 0 so I'm not doing full-fledged Spec-ese yet)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

xtuc picture xtuc  Â·  4Comments

arcanis picture arcanis  Â·  6Comments

tabatkins picture tabatkins  Â·  6Comments

elijahdorman picture elijahdorman  Â·  6Comments

noppa picture noppa  Â·  3Comments