Skip to content
This repository has been archived by the owner on Mar 10, 2024. It is now read-only.

Should lastIndex return 0 if there are no items in the array? #21

Open
egeriis opened this issue Aug 2, 2018 · 13 comments
Open

Should lastIndex return 0 if there are no items in the array? #21

egeriis opened this issue Aug 2, 2018 · 13 comments

Comments

@egeriis
Copy link

egeriis commented Aug 2, 2018

I'm wondering if it would be better to follow the same pattern as indexOf and return -1 for lastIndex, when there are no items in the array. Returning 0 would mean that it's not possible to distinguish an array with 0 and an array with 1 element.

@lezsakdomi
Copy link

I would suggest returning undefined, as lastItem does.

Mathematical operations with undefined result in NaN, which most probably causes an error if not handled carefully.

It's downside is that one should not use loose comparsion in lastIndex === 0.

@adfriedm
Copy link

adfriedm commented Jan 29, 2019

I just read the proposal and I came to raise the same issue. I think most people would expect ([].lastIndex + arr.length) === arr.lastIndex, which requires [].lastIndex === -1.

Also, as far as mathematical conventions go, the last index of an empty list is often defined as -1 for instance in the empty sum:

image.

@getify
Copy link

getify commented Jun 13, 2020

Came here also to raise the same concern. As a teacher and author on JS, I am quite confident that having lastIndex return the same 0 for both an empty array and a single element array is a footgun that will trip up new learners and populate future "wtf js" lists/blog posts for decades.

Unfortunately, lastItem can't distinguish between "found actual 'undefined' in the last slot" and "didn't find anything because the array is empty, so here's a conflated empty-slot 'undefined' value instead" -- this sucks, btw, but find(..) regrettably already has the same weakness. So we really need lastIndex to let us distinguish empty arrays (similar to how findIndex(..) distinguishes).

Otherwise, we'll often have to combine with checks of length anyway, in which case a decent part of the reason for this feature is moot.

-1 is a fairly precedented value to return, but I honestly think the more appropriate value would be NaN. The semantic meaning of NaN is often misattributed to its acronym-expansion of "not a number", but in more accurate and helpful terms it's the "iNvalid Number", or as I like to say, the "Not-Available Number". :)

This lastIndex operation clearly implies it will return a number, so undefined is a violation of expectation. -1 is a suitable number for the purpose and familiar to experienced devs, but often looks weird/arbitrary to new learners. They often just have to be taught to overlook its arbitrary'ness as an artifact of older languages that didn't have another suitable sentinel value -- "somebody decades ago just pseudo-randomly picked -1 (as opposed to -42, etc)". They roll their eyes and file that away in their brain.

NaN by contrast is already a built-in sentinel number with no usable/valid numeric value, which IMO is exactly the semantic to fit this empty-array condition: "sorry, there is no valid/available last-index right now".

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jun 17, 2020

[].lastIndex === -1 is consistent with findIndex, indexOf, and lastIndexOf in ECMAScript and several other languages (e.g. Java and C#).

These desirable identities suggest [].lastIndex === -1

array.lastIndex === array.length - 1;
array.lastIndex === array.lastIndexOf(array.lastItem);

-1 is a suitable number for the purpose and familiar to experienced devs, but often looks weird/arbitrary to new learners. They often just have to be taught to overlook its arbitrary'ness as an artifact of older languages that didn't have another suitable sentinel value -- "somebody decades ago just pseudo-randomly picked -1 (as opposed to -42, etc)".

New learners should be taught -1 is not arbitrary and was chosen as it simplifies some algorithms. In particular, it is convenient that indexOf(item) + 1 will give either the index of the slot following item or the base index when item is not present.

var afterDelimiter = maybeDelimited.slice(maybeDelimited.indexOf(delimiter) + 1);

@getify
Copy link

getify commented Jun 17, 2020

It's obviously mathematically convenient in some cases, but not in others:

// run backwards through the array for some reason
for (let i = arr.lastIndex; i >= 0; i--) {
   // ..
}
// oops, will run forever if array is empty

[Edit: this example was a mistake. I intended different logic but typed it out too quickly.]


"These identities require..." Nah, nothing is required here. In math, there's all sorts of examples where identities hold except for special base condition definitions, like "0!" and such. When talking about an operation that is only validly definable for an array that has contents, an empty array is exactly the sort of special base condition that deserves/supports a special case exception.

I agree that making it -1 will be more familiar and consistent. But I also think the choice to keep doubling down on -1 with the addition of the findIndex(..) and lastIndexOf(..) utils (in ES6) was a mistake -- I would have preferred indexOf(..) to have been left the only -1-returning util in JS, a relic of JS's original roots. I still think we could and should course-correct. We broke with consistency on several items in ES6/ES2015 where there were things that were more semantically correct. I think this is another such case.

For a "find" algorithm, there's a valid definition for finding something (or not!) in an empty array: we don't need to do any work in that case, and -1 simply signals "sorry, didn't find it". There's no such valid definition of "lastItem" or "lastIndex" algorithms for an empty array, because the accurate semantic descriptor is more "can't even try to give you an answer" than "didn't find anything". So we have to pick some arbitrary defined behaviors, and "identity" cases don't really matter there.

For lastItem, we'll define some arbitrary (and confusing) sentinel value for that case, such as undefined -- as I said above, that's troublesome because there's no way to tell if that meant "there is no value" vs. "there is an affirmative undefined in that last slot". We have the chance to do the more semantically correct thing for lastIndex and return NaN, clearly signally "couldn't give you an answer because the condition is invalid". Choosing -1 won't be bad, it's just not as proper as I think it could/should be.

I realize it's highly likely my argument will be rejected here. I just think we should consider it instead of dismissing the idea outright because it's less familiar.

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jun 17, 2020

I don't follow why for (let i = arr.lastIndex; i >= 0; i--) will run forever if array is empty. -1 is less than 0, so the loop condition is immediately false and the loop exits without ever running the body or the decrement.


I take your point regarding "These identities require..." and have edited the wording.

In math, there's all sorts of examples where identities hold except for special base condition definitions, like "0!" and such.

Actually, one motivation for defining 0! = 1 was to avoid special cases: "It makes many identities in combinatorics valid for all applicable sizes". It's the same motivation for defining [].lastIndex === -1.


[off topic]

I realize it's highly likely my argument will be rejected here. I just think we should consider it instead of dismissing the idea outright because it's less familiar.

For my part, I believe the aim is to raise and explore different perspectives, ideas, & approaches; and — perhaps only sometimes — to reach consensus. I don't think anyone would dismiss a valid idea outright and I certainly gave your well reasoned idea consideration while writing up my alternative point of view.

@getify
Copy link

getify commented Jun 17, 2020

I don't follow why ... will run forever

You're right, my bad, I was writing quickly and messed up the example logic.

@hax
Copy link
Member

hax commented Jul 20, 2020

[].lastIndex() return 0 is surprising ... I would expect it -1 which consistent with other methods.

@getify I think NaN is not good , even we treat NaN not "not-a-number" but "not-avail-number", it only make sense in float, index is actually unsigned integer (the valid range is 0 to length-1, which make -1 play the role for "not-a-index" or "not-avail-index" just like NaN for float). Actually the committee seems don't like NaN as the return value, for example, codePointAt(outOfRange) return undefined which not follow charCodeAt(outOfRange) which return NaN.

Even we don't consider type theory, in practice, return NaN just bring much trouble if u consider the further operation, for example test whether we out of range: const i = a.lastIndex(); if (i === -1) ... vs if (i === NaN) ... (oh it's wrong! u should use if (isNaN(i)) ... (no, still bad, u should use if (Number.isNaN(i))...))


Though I believe -1 follow the convention, we should also notice that -1 not work well with frequently used slice() method which treat -1 as length-1 not "not-a-index".

So personally I would also accept return undefined. Though it not follow the type number/integer/index , but it could be seen as return type is number?. It also convenient to write a.lastIndex() ?? fallbackValue (note u can not use ?? for 0, -1 or NaN) which may solve many use cases.

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jul 20, 2020

we should also notice that -1 not work well with frequently used slice() method which treat -1 as length-1 not "not-a-index".

The spec for GetLastArrayIndex currently has

Let O be ? ToObject(this value).
Let len be ? ToLength(? Get(O, "length")).
If len > 0, then
    Return len-1.
Return 0.

If lastIndex is defined to be equal to length - 1 in all cases, the spec would change to

Let O be ? ToObject(this value).
Let len be ? ToLength(? Get(O, "length")).
Return len-1.

Then the slice method treating -1 as length - 1 is perfectly & absolutely consistent with [].lastIndex being equal to -1.

[].lastIndex === -1 produces the expected results and works well in

var empty = []; // length is 0 and lastIndex is -1, which is the same as (length - 1)
empty.slice(            empty.lastIndex ); // another empty array
empty.fill(        "-", empty.lastIndex ); // no-op
empty.includes(    "-", empty.lastIndex ); // false
empty.indexOf(     "-", empty.lastIndex ); // -1
empty.lastIndexOf( "-", empty.lastIndex ); // -1

@stiff
Copy link

stiff commented Jan 21, 2021

Personally I find using indexOf somewhat annoying because of -1 in case nothing found. So undefined would be much better fit for it, especially since nullish coalescing is already part of language . Even though -1 would probably be easier to use in generic arithmetic, I'd prefer readability to magic numbers.

@doasync
Copy link

doasync commented Jan 21, 2021

@stiff,

So undefined would be much better fit for it, especially since nullish coalescing is already part of language.

You return an array index, and arrays must use integers as element indexes (link).

I also think, .lastIndex should not be used to check for emptiness with nullish coalescing.

@stiff
Copy link

stiff commented Jan 21, 2021

@doasync , sure since -1 is already used a lot, it of course makes some sense to stick to it.

But it's a very poor design in the first place. Forget for a minute all your great programmer knowledge and just read as plain english based on common human knowledge, which one is more obvious and reveals whats going on: indexOf x = -1, or indexOf x = undefined.

In Julia for example there is OffsetArray in which indexes do not start from 0, and -1 is a perfectly valid index there. And it's also much more logical to expect index to be a valid index, or undefined, than to endow one arbitrary number to be more divine than others.

@Zarel
Copy link

Zarel commented Jan 21, 2021

I think you're discussing all this in the wrong place. This is an abandoned proposal; look at the README; it's been superseded by https://github.com/tc39/proposal-relative-indexing-method

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants