- I know all that, have been programming for 15 years, yet I still would have done the mistake.
- Simple unit tests may very well not catch this bug the first time.
- The language design allows your brain to ignore the index parameter because JS accepts superfluous parameters, which is a terrible decision.
- map() is a mapping primitive. It's supposed to adapt a type to another type so that you can pass it to a monoid. JS breaks this convention.
- Even traditionally not functional languages know better than that and separate iteration from indexing. E.G: python map() just maps, and if you want numbering, you use explicitly enumerate(). Ruby has each() and each_with_index(), etc.
Just another of the numerous sucky things in JS. They are not big, but they accumulate very quickly and make it one of the worse language existing. Certainly the worst of all modern stack languages. It's a shame it has a monopoly on the most awesome platform in the world: the web.
In fact, it's such a big problem the most popular JS projects are all things to avoid coding in JS or workaround JS deficiencies: typescript, coffeescript, jsx, babel, webpack, lowdash ...
Without that we would not be able to have variable length argument lists in the past.
Spread has existed in Python forever under the name of "splat operator", default values as well. Same with ruby.
That what I meant when I said "they accumulate". A bad design decision not only affect the user cognitive load and productivity, but it also cascades to the rest of the language and shapes it.
People came at Guido relentlessly for adding new features, debating his decisions, trying to change the philosophy and aesthetic of the language.
The guy stood up to them for 20 years, staying polite but resolute at doing things his, at the time controversial, way.
To me, that's even more impressive than being a good language designer.
The name “splat” is not commonly used in Python. That name comes from Ruby (or Perl?). In Python it is usually called “star” or similar.
Here you can see Steven D'Aprano, one of the dev of the Python stdlib, naming it "splat": https://mail.python.org/archives/list/python-ideas@python.or...
I use "splat" all the time myself.
On the other hand, itertools.starmap() is named this way because it does:
def starmap(func, iterable):
for e in iterable:
yield func(*iterable)
Things rarely have one name in computing. Hell I call curly brackets "mustaches" all the time. It's as hard as cache invalidation apparently.Obviously occasional people are going to pick up terminology from other communities, and the name “splat” has been gaining popularity recently (I had literally never heard that term before a few years ago, and have been writing Python code since 2002). I occasionally hear British expats in the USA calling elevators “lifts” or baby carriages “prams” or lines of people “queues”. Doesn’t mean those have been common American terms.
This was certainly not called the “splat operator” “forever” as claimed in the previous comment.
> I call curly brackets "mustaches" all the time
I have never heard anyone call these “mustaches”. The common terms are “curly brackets” and “braces”. Nobody is going to have any idea what you are talking about.
Mustache is a Ruby library from 2009. Here’s the first commit https://github.com/mustache/mustache/commit/6ee6bcf21d381554...
But more to the point, someone calling their template library “mustache” because a curly brace has a vaguely mustache-like shape doesn’t remotely imply that people regularly call curly braces “mustaches” or would have any idea what “mustache” meant in the context of someone pronouncing their computer code.
I have not done it with parseInt per se, but I have made this precise mistake at least twice with passing a function `f` with an optional second argument in `some_array.map(f)`, and it took a while to figure out what was happening each time.
Now that Javascript has proper iterables and iterators and generators, I have been enjoying using versions of `map` and `spreadmap` which take in a callback function and iterables and produce an iterator. Then I can explicitly use `enumerate` if I want it. https://observablehq.com/@jrus/itertools#map
Even with all of JavaScript's deficiencies, it's still far easier to just deal with them than to switch languages entirely.
This obviously doesn't explain every Javascript fan, but the tendency is strong enough to shift the dynamics of the community.
I can't say I relate to the religious fervor with which people defend programming languages. C++ is probably my favorite language to work in, but I absolutely understand where the hatred for it comes from. I readily admit that it's grotesque in many ways and my preference is likely for lack of sustained exposure to certain other modern languages.
So have you not used PHP, or are you counting it as not-modern? :P
Using multiple parameters for those are so uncommon that it's easy not to realise that it's supported.
_If you know all of that_ it may not be surprising to you, but in most cases there is no need for the average developer to know it, making it very surprising when it crops up like this.
ES5 deprecated that behaviour and it's been widespread enough that code which started in the IE9+ era now looks like the very old buggy code omitting the radix, so anyone getting started relatively recently quite reasonably may never have needed to learn about it:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... https://eslint.org/docs/rules/radix
But TypeScript doesn't catch this specific bug because it's technically correct[1] from an API stand-point, as the second parameter to parseInt is a number and the second argument to map's callback is a number.
For as many situations as TypeScript genuinely helps you, there are just as many situations where it gives you false security, like in this case, and I'm starting to consider not using it anymore. [2]
[1] not actually "the best kind of correct" despite popular television quotes
[2] https://sdegutis.com/2019-06-20-considering-removing-typescr...
This is, of course, why famously machine shops simply put up signs saying “don't touch the whirring metal” rather than using safety guards and similar measures.
I don't disagree that it's useful to have the extra arguments for map but the part which is really deadly is ignoring extra arguments, even if it's obvious why backwards-compatibility made changing it unlikely. I really wish that JavaScript had implemented keyword-arguments when the benefits had become obvious because when features like map were added a decade or two later they could have been defined as passing arguments by keyword and causing an error when given a function which didn't accept those names.
A bunch of confusion points in Javascript (including this one) have been well understood for a long time, best practices have been developed to deal with them, and mature tooling exists to easily enforce them.
Put another way: If you are unsure, go download eslint along with the plugins for your environment, and find/create a ruleset that turns on almost everything.
This may come off as arrogant but.. I'll proceed nonetheless. There is a need for the average developer to know the interfaces of the functions they're choosing to use. They're well documented, and the documentation is neither hard to find, nor difficult to read.
There may be an argument here for strongly typed languages -vs- weakly typed ones, where often some of the ambiguity around optional function parameters is eased, but most languages have optional function parameters, this is not in any way unique to JS. And parseInt and [].map are very common JS methods: this is not some obscure API not all devs would be unfamiliar with, these are built-ins.
You might be a dev who isn't very familiar with Javascript, but you still need to fix it. Or you might be having an off day. Or you're writing a critical fix under a lot of pressure and simply forget about the issue. And I'll bet if this code was being reviewed, most devs would still overlook the issue.
There are many real world scenarios where it isn't so easy or clear cut. We're all human, and we make mistakes. In other fields, we try and reduce how easy it is to make these mistakes. Some things will always be dangerous like table saws. But a programming language?
Anyway, the "deal with it"/"get good" mentality just seems like a lack of empathy. Not to mention the arrogant in-group thing of "oh, you don't know <wart x> of my favourite but flawed language? you must not be a good dev". And the fact that fixing something like this could cumulatively save a huge amount of brainpower for current and future devs.
You can say the same thing to Pythonistas who are fussed by memory management, but the tradeoff of that intuitiveness is performance (which is being somewhat obviated by newer languages like Rust, which have their own challenges to learn). The actual task you're trying to do is more complex, so the knowledge required is too.
In Javascript's case, the thing you're becoming an expert in is the path-dependent history of dumb language decisions by people who never should have been let anywhere near a language. It's beyond me that people don't understand why this would bug some, at the very least because it was so easily avoidable.
But one of the safeguards here is that clean, intuitive code makes it possible for any engineer who's a little more thoughtful to catch little bugs like this. God knows I've done it dozens of times over the course of my career: while reading code for some other purpose, a block catches my eye as having something off about it, and I dig in and find a bug.
The problem with unintuitiveness,especially when it's baked into the language, is that you don't scrutinize every line of code you encounter with your full brain and attention. Without going on the hunt for "map and Javascript in general are horribly designed, parseInt has a rarely-used second param, root out likely bad uses", my brain on another task would skim right over a map-parseInt call like the above. It reads as if it's correct, and in any sane language it would be.
Seems like you'd only want to use parseInt if you expect to need radix changes at some point, e.g. converting between hex strings, decimal values, and binary strings
['1', '7', '11'].map(Math.round)
// => [1, 7, 11]
[["00000001", 2], ["00000111", 2], ["0x0B", 16]].map(x => parseInt(...x))
// => [1, 7, 11] ['1', '7', '11'].map(x => +x)['1', '7', '11'].map(x => ~~x)
['1', '7', '11'].map(Number)> You just have to know this
Hidden, silent, unintuitive behavior that you "just have to know" (and remember each time you might read or write the code) is absolutely strange and surprising in an engineering context. Javascript's deadly combination of unnecessary unintuitiveness (map's ludicrous API) and permissibility (ignoring extra arguments) strikes again.
The permissibility of guessing what malformed code means is actually somewhat defensible in Javascript's case, since the incentive ecosystem of the Web is complex and frontend parsing has long had a tradition of best-effort parsing instead of throwing an exception. But the absurd signature of map here is half the problem, and there's really no good explanation for that than yet another instance of "Javascript is a dumpster fire that should only be used when forced to". Similarly "easy-to-use" languages like Python can afford strong typing and rejecting extra args when not specified because they're not bound by the Web's permissibility. But Python also, for all its flaws, has adults making language decisions and does the sane thing with APIs like map without loss of expressiveness. Adding loop metadata like indices requires an explicit call to enumerate(), trading off an iota of verbosity for intuitiveness, which is one of the most important things in writing code that can remain productive and bug-free.
Surprise! You need to know the basics of how your language works.
The correct error is something along the lines of:
let numbers = input.iter().map(parseInt).collect::<Vec<_>>();
--> src/main.rs:10:32
|
4 | fn parseInt<T>(input: T, radix: u32) -> Option<i32> where T: AsRef<str> {
| ----------------------------------------------------------------------- takes 2 arguments
...
10 | let numbers = input.iter().map(parseInt).collect::<Vec<_>>();
| ^^^ expected function that takes 1 argument
This isn't rocket science, it's basic language design. From time immemorial engineers have been making mistakes as they write software, so compilers evolved not to pretend otherwise and hope for the best, but to help engineers catch them. Except for one. ['1', '7', '11'].enumerate().map(([val, idx]) => parseInt(val, idx))
I understand that this pattern of having extra "helper" arguments is pervasive in JS and might seem less exotic to the community, but I feel it is a problematic approach, as it is foot-gun prone.No.
> expected function that takes 1 argument
`map` expects a function that takes one, two, or three arguments.
array.stride(2).map(|(x,y) ...|)
Both error checking and ergonomics are preserved.0: Going by JS naming conventions anyway; I'd use somthing like map, mapi, no-that's-useless personally.
If you think it should be ok for all devs to believe that parseInt === parseDecimalInt then maybe all languages should be decimal-only. That isn't the case though.
Seems intuitive that if parseInt allows omitting the radix parameter, that it should default to whatever radix a standard integer primitive would default to.
But in this case, the radix is _not_ omitted. The map function passes the index of the current iteration as the second parameter to the function it is passed. It is kinda like this: ``` ['1','7','11'].map((item, index) => parseInt(item, index))
[9, 10, 11].sort()
[ 10, 11, 9 ][...].map(Number)
?
I would never have guessed that it was passing the index as a second parameter, I would have expected a compiler error.