Stage 3 Proposal: Array.prototype.at
tc39.es
tc39.es
V8 + dynamic language + better math support = perfect environment for many applications out there
You might miss out on some SIMD operations, but you'd be able to do 90% of what you'd want with scientific calcs. (admittedly, it'd be nice if this were part of the language).
I’ve only worked on probability applications at a surface level for a tiny bit and never really needed it, but I kept wondering whether there were such a numeric types, and if not, what people did if they needed it.
Log odds ratios are also good representations that have this kind of accuracy between zero and one. They're good when you need them. (e.g. 100:1 odds are log(100/1) and 1:100 odds are log(1/100))
The handy functions are more about making sure that cancellations don't happen by providing a few primitive operations that cover lots of the uses. Like the handy log1p which computes log(1+x). If you did this with the log function, you'll compute log(1+1e-200)=log(1)=0. If you use log1p, you get log1p(1e-200)=1e-200. You avoid the loss of info that comes with adding something close to zero to something that isn't close to zero, which is the real trick. And then if you need something close to one, you probable just use the converse: instead of using p=1-eps, you just work with eps itself.
Is there a better way?
Yours is probably more space efficient though.
IEEE 754, BTW, allows for 10-based floats, but IDK if anybody uses it.
Is it just a matter of there being a "blessed" implementation for folks to standardize around?
Support for this I believe is "low level enough" to ve implemented natively, it's not something like protobuf.
const at = (arr, i) => i >= 0 ? arr[i] : arr[arr.length + i];
Why does the core language need to be extended to incorporate this?I had the exact same thoughts when `.includes()` was proposed to be added to the spec - it's just `.indexOf() !== -1`! - but time has proven me wrong, and I suspect it will prove us wrong about `at` as well.
I've written an `.indexOf() !== -1` check hundreds if not thousands of times, and I've gotten the conditional wrong enough times to actually need to revert a change in prod.
I have similar thoughts about .at(). Taking an element from the end of the array is ever-so-slightly error prone. Sure, you won't make a mistake this time, or the next time, but write it a hundred times, or a thousand, and I bet a bug will slip in. It's darn near impossible to get `.at(-1)` wrong, however.
Also: `[1, NaN, "hi"].indexOf(NaN)` is -1 but `.includes(NaN)` returns true as expected.
Brutal.
You need to use a transpiler (like Typescript), but at this point, there no reason to implement that in the language, a transpiler can implement the feature by simply adding the method to the prototype of the object, like this:
Array.prototype.at = function (i) { return i < 0 ? this[this.length + i] : this[i] }Plus you're probably missing a few dozen edge cases and optimizations that the native version would incorporate.
We wouldn't have been in node_modules dependency hell if they incorporated a few more things into the standard library or language.
We shouldn't write stuff in native code for performance... We should write the compiler to detect these and output optimized code.
- frameworks like react or express and extensions for the same
- meaningful libraries like axios, ramda, date-fns or qs
- transpiler/bundler enhancements like webpack loaders or babel presets
I would prefer that the JS committee focused on bringing meaningful new capabilities to node and the browser that are currently lacking, like they did with the various HTML5 libraries, fetch(), ES classes and so on, rather than providing tiny functions that aren't even providing new functionality (array indexing is easy and idiomatic in JS). If there was no opportunity cost then sure, add tiny pointless builtin functions all day long.
Those tiny little libs providing tiny little functions are likely not direct dependencies of your application, rather dependencies of a subdependency of a subdependency of a subdependency of your preferred "meaningful" library.
I... don't really see why it wouldn't? What's your logic here?
Even with all the basic functions one could hope for being brought into JS' core, there will still be tons of bizarre micro-libraries like these in the npm repository being used by developers who rely on libraries first when it comes to any given feature, because doing so is part of JS culture for the reasons I outlined before.
FWIW, I consider `at` to be similar to some of the things you consider "new capabilities". `fetch` vs `XMLHttpRequest` is fairly analogous to `at` vs `arr[i]`, and similar arguments can be made about ES6 classes over prototypal classes, etc.
> `fetch` vs `XMLHttpRequest` is fairly analogous to `at` vs `arr[i]`
If you consider all syntactic sugar to be equivalent, no matter how minor the change, then anything above a basic Turing machine implementation is fairly analogous. The problem with XHR is that the interface was really bad. The interface for arr[i] is not really bad.
Similarly, sure you can do point-free style w/ ramda's `R.add` but realistically, do you really? The fundamental game changer capability is the language's ability to do math in the first place; anything on top is arguably cherry on the cake. `at` seems uncannily similar in that regard.
Wrt something being in a 3rd party library vs stdlib, I'll generally prefer a batteries included approach in JS because module resolution is complex enough to cause difficult problems to troubleshoot (peerDeps hell, complex symlinking semantics, library duplication in bundles, core.js explosion, package manager specific breakages, etc)
const leftpad = (str, len, char = ' ') => str.length >= len ? str : (char.repeat(len - str.length) + str);
Anybody who installs a dependency instead of writing a one line function is just leaving themselves exposed for no real benefit.Providing the at function outside of the prototype chain like I did above will not incur the cost you mentioned, even in the unlikely case that such calls are the bottleneck of your application.
Plus, if array lookup calls are your bottleneck then you probably need a different data structure.
Just look at your at() function, I already spotted a bug, if a numeric string is passed: `([].length + '-1') === '0-1'`
const at = (arr, i) => i >= 0 ? arr[i] : arr[arr.length + +i]; let i = -1n;
let test = +i;
> Uncaught TypeError: can't convert BigInt to number
See this is where standard libraries shine, they consider all the pieces of the puzzle. There's no good reason to disallow bigints here.P.S: Unary plus operator is neat but this TypeError with BigInt is why I am starting to prefer the more verbose `Number(i)`.
Having a standard lib for these sort of functions is a common pattern many languages adopt.
Yes, the full "standard libarary" you can expect to be present on any given JS VM may vary, but there's no plausible reason that e.g. at() should not work on almost any conceivable platform.
True, NodeJS runtime comes with its own «standard library» but npm has nothing to do with it.
const at = (arr, i) => {
if(!(Array.isArray(arr) && typeof i === 'number')) {
throw new Error('invalid arguments');
}
// same as before
};
you wouldn't just commit a single-line function in your code. But there are 10,000 micro-functions that could be in the core of JS. Why bother to add this, especially if it's so easy to implement? There are surely better things the JS committee could be putting their time towards, like providing functionality that is presently lacking in JS (maybe they could finally get around to advancing the pipe operator through the TC stages).This goes to show that it's NOT trivial to implement in a robust way.
To invert your argument, why would you want millions of developers to have to waste their time writing this sort of thing for such a common need?
Yes, it's clear that you could write the function correctly.
Could everyone?
What about bugs that aren't obvious? The runtime errors that result?
Why would we want to waste so much developer time and effort globally, when we can just add .at() to the language?
Auditing the entire dependency tree of every library you choose is extremely arduous without proper tooling, and the tooling here has never been good (and even the more recently available better tooling is CVE-focused so won't highlight tells like package size/maintenance status/whatever other heuristics you might devise as a proxy for quality).
const leftpad = (str, len, char = ' ') => str.padStart(len, char);Unfortunately, I couldn't find any links to articles written at the time about this, but they definitely did consider "use" options or script type="es2021" kinds of options.
Imagine learning JS in 5 years: "you can index arrays with subscripts, but actually, if you want to get negative indexes you need to use .at()"
vs.
"Always add 'use es2021'; at the top of your scripts. You can index arrays with subscripts."
I'm hardly a huge Perl fan, but their approach made a lot more sense IMHO and is a good trade-off between making sure existing code works, and not complicating the language for future use. The "fork" (which seems a bit hyperbolic to me) is a very minor short-term pain at best for significant long-term gains.
I don't know of any other mainstream language that's so conservative as JavaScript in never breaking anything, even optionally with flags.
Besides, compatibility is important but not holy. Some programs probably also rely on "[9] * 2" resulting in Number 18, but we really ought to fix that (with or without flag) IMHO because these sort of gotchas result in people writing bugs every single day where it works for an array length of 1 and then it has 2 values and you get NaN. The pain of breaking backwards compatibility is minor compared to the pain of new bugs being created every single day.
Golden opportunities are being missed here, and it's a shame.
0. It will end up completely unnoticed, because everyone is using any language you like and compiles to web assembly, which is the defacto standard for more than a decade.
1. It's like a new C++, because of the many TC39 stage 3 proposals that has been added over the years without the possibility to ever fix the language in order to not fork the web.
I absolutely agree that some metadata at the top of a module enabling behavior like this would be ideal.
From a spec perspective, ES6 modules were a good milestone to "flip the switch" over to strict mode by default, but even with that being a fairly successful strategy (IMHO), it still left some nasty corner cases around the language (namely, there are now two distinct top level grammars, which led to the whole .mjs bikeshedding rabbit hole)
We need more special contextual comments to change a file or scope to behave better so we can move forward and scrape away all the bad legacy of JS.
I don’t think making the JS world into even more of a “set of subtly different languages that look mostly similar” is necessarily a solution so much as an extra problem.
But perhaps we just don't know enough, and they will add the `at`, and at some point actually do bind `arr[index]` to use the implementation of that function?
Instead, adding .at() allows having the new feature now, and in a way that's possible to polyfill for backwards compatibility.
In the face of disasters like Python 2->3 and Apple M1 macs no longer being able to play Starcraft Remastered, the web is an aspirational beacon.
Let’s keep it going.
The Web probably has a longer history of not breaking things, but also has a smaller feature set to keep track of than Windows.
All in all, very hard to compare. But both undoubtedly epic.
It's not easy, and it certainly leads to annoying platform quirks, but it is possible.
An organization can go through and update its C++, because ultimately they're distributing binaries (or doing everything internally and not distributing anything at all).
Web "pages" aren't called that for no good reason. If in 2005 you bought a novel, or some punk writer–artist's printed pamphlet, and now you can't read it because in the meantime some engineers changed a spec somewhere, then that would be a failure, not just in the small, but on a societal level. Just rev the language is something that people who spend 40+ hours in an IDE or programmer's text editor think up when they're used to dealing in SDKs and perpetually changing interdependencies and fixing them and getting paid handsomely for it. But that's not what the Web is. The Web is the infrastructure for handling humanity's publishing needs indefinitely.
To rely upon another observation:
"[This] is software design on the scale of decades: every detail is intended to promote software longevity and independent evolution. Many of the constraints are directly opposed to short-term efficiency. Unfortunately, people are fairly good at short-term design, and usually awful at long-term design. Most don’t think they need to design past the current release."
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypert...
No Flash... Iframes don't work properly anymore... HTTPS servers from 10 years ago are unsupported by todays browsers... Most of the IE hacks no longer work (remember progid:DXImageTransform?)... Any images/resources hosted elsewhere are likely now nonexistent...
Plenty of web features have been introduced and then dropped just a few years later. Backwards compatibility is great... But if it's practically broken anyway, I think there is a good argument for breaking it further. People who need to read an old page will probably need to use IE6 in a VM anyway.
> hacks
Flash was not standardized. Same with IE's proprietary recommendations (and Mozilla's for that matter—XUL is proprietary, even though people often use "proprietary" as an antonym for "open source"). Most of the "web features" that people have in mind are in the same boat: experimental and draft-level proposals that eventually fall by the wayside for one reason or another. The Web is actually the single most successful attempt at a vendor-neutral, stable platform that exists. It's why we're having this conversation now.
The argument is that, because some people did something hacky or bleeding edge and then bled from it, then there's no real point in any amount of stability, so we should punish everyone. What a double whammy that would make for! First, you spend all your time taking care to do things correctly, so you pay the penalty inherent in that—what with moving more slowly than all those around you—and then someone decides, "ah, nevermind screw the whole thing", doubles back on the original offer and then breaks your shit? I can't say I'm able to abide by that. Imagine all your friends getting drivers licenses and receiving a bunch of speeding tickets for their recklessness, then one day you get pulled over and ticketed, too, regardless of the fact that you weren't speeding.
Can you provide some examples? In my experience broken 10+ year old websites is the exception, not the rule. And most of the exception is because flash (which has a workaround; plus most popular flash websites have been ported).
I dislike the abuse of this word. Lacking some shorthand notation doesn't make the language "broken".
It reminds me of handling support tickets where clients say "X needs to be urgently fixed" even though X has never been possible. (Should it? Often yes, but it doesn't mean it's broken.)
array[-1] = “foo”
This already works in JS but doesn’t do what you expect and just assigns the property. array["foo"] = "bar"
is valid javascript. array["at"] = "lunchtime";
Is also valid javascript... And will break functionality in this proposal!Browser makers start implementing the feature and releasing it in the development and beta versions of their browsers. Then if the users of the experimental features start noticing that webpage break, the proposal will get an update.
If I remember correctly, this exact thing happened to `Array.prototype.flatten` which got renamed to `Array.prototype.flat` after it was realized that the former broke a lot of legacy webpages (and after a long discussion of `Array.prototype.smoosh`[1])
1: https://developers.google.com/web/updates/2018/03/smooshgate
https://github.com/tc39/proposal-relative-indexing-method#we...
In Javascript, arrays are actually dictionaries. But not full dictionaries, rather just dictionaries that can have either a string or numeric key.
It's messy because an array in javascript isn't just "an array" it's this mismesh of features that are unexpected to a new-to-javascript developer.
Unexpected is the enemy of readable code.
So [0, 1, 2, 3].at(-2) === 2
I don't like that very much.
Mind you, I don't like the idea of using [0,1,2,3].at(-0) either...
Or, if it’s more intuitive, you can think of the array index as an unsigned integer where the max is equal to the length of the array. If you try to assign -1 to a uint8, the result will be 255 — the highest possible value (the last index).
[1, 2, 3, 4].at(0) === 1
[1, 2, 3, 4].at(1) === 2
[1, 2, 3, 4].at(-1) === 4
findIndex’s behavior is unchanged.
See: https://tc39.es/ecma262/multipage/notational-conventions.htm...
Quite disappointed so, my parent comment doesn't stand too
I've lost count on how many times I've wrote this function manually.
That works in this case, but eta-reduction is not generally safe in Javascript due to the variadic nature of many functions, so I wouldn't recommend it in production code. For example `indices.forEach(console.log)` does not work as one would expect.
This blog post explains why what you’re proposing is dangerous: https://jakearchibald.com/2021/function-callback-risks/#type...
When using eta-reduction in Javascript both functions and all their (optional) arguments have to be known by the programmer and future programmers, instead of needing to know only the argument-slots being used by (x,y,...) => ...
It also defends / insulates against more parameters being added in the future.
Additionally, the way `this` in javascript works (or doesn't for a lot of callbacks) also pushes against using eta-reduction.
Sure, it's a valid way of logging all the arguments that pass through forEach. I don't see a problem here?
>When using eta-reduction in Javascript both functions and all their (optional) arguments have to be known by the programmer and future programmers, instead of needing to know only the argument-slots being used by (x,y,...) => ...
My VSCode setup shows all arguments of functions automatically. Also, I always avoid optional arguments in my code and writing functions that take in a variable amount of arguments or arguments of different types. I always refactor these out of my codebase.
>Additionally, the way `this` in javascript works (or doesn't for a lot of callbacks) also pushes against using eta-reduction.
`this` is another smell that I always avoid using in my codebase, and refactor code that uses it to work without `this`. I thought `this` being harmful is common knowledge?
["1","2"].map(parseFloat) // works as expected
["1","2"].map(parseInt) // nopeconst arr = ['1','2','3']; arr.map(parseInt) is equivalent to: [ parseInt('1', 0, arr), parseInt('2', 1, arr), parseInt('3', 2, arr) ];
> Uncaught TypeError: Cannot convert undefined or null to object
because `myObjectArr.at` doesn't bind `this` to `myObjectArr`. When the internals of `map` call the `at` function, `this` is just `undefined` instead of the original array and it'll throw.
Luckily Array.prototype.map takes a second argument just for this purpose
indices.map(myObjectArr.at, myObjectArr)There should really be a whole new standard library made from ground up without `this`, mutability and all the other legacy stuff.
It's not a big thing of course with such a simple example, but just try to program without defining a single function and you'll see how tiresome it soon begins to write everything out manually.
const arr = [];
arr[-1] = 'Hello world';
console.log(arr[-1]);
// Hello world
Instead of using the negative index, it stringifies the "-1" and uses it as the key of an object both when writing and reading. Thus, arr.length still remains 0But I guess the intersect between C++/JS developers is too small to care
However I think there is a better way to support that - make `throw` an expression rather than a statement. Then you can do
a[10] ?? throw new Error()
which currently doesn't work.