6 ms·
Why [“1”,“2”,“3”].map(parseInt) yields [1, NaN, NaN] in JavaScript (2011)
- xg15 3y agoWith talk about "cross-SoC bugs" on the frontpage, I had to think of this as an example of a "cross-API" bug.
- RKearney 3y agoI get Uncaught SyntaxError: Invalid or unexpected token I'm guessing those quotes were supposed to be " rather than “ and ”.
- ohthatsnotright 3y agoApple "smart quotes" strikes again!
- TechBro8615 3y agoSpoiler: because `parseInt` accepts the second argument of the radix of the number to parse, and `map` calls the provided callback function with a second argument of the index in the array, so this is the result of calling `[parseInt("1",0), parseInt("2",1), parseInt("3",2)]` The article is from 2011, and I'm not even sure arrow functions were in the spec at that time. Nowadays the best practice would be: `["1," "2", "3"].map(maybeNum => parseInt(maybeNum))`
- vortico 3y agoThe reason is surprising but perfectly valid once you remember the spec of map() and parseInt(). Love JS WTFs.
- bluepod4 3y agoYep. I actually recall someone on the team running into a similar issue during the workday.
- triyambakam 3y agoI wouldn't even call it a JS WTF. If you use a tool wrong, why be surprised about wrong results?
- paulddraper 3y agoIt's unexpected. No other language does this.
- dragonwriter 3y agoJS discarding extra positional arguments (and that being leveraged in map and similar methods) seems to be the unique bit. I don’t think any other popular language does that.
- qayxc 3y agoIt's not unique at all: C has this feature as well - just look at printf() and other functions that accept variable argument lists.
- Jtsummers 3y agoThat's a variadic function and those have to be explicitly marked as variadic. You cannot pass in an arbitrary number of arguments to other functions. https://en.cppreference.com/w/c/variadic https://en.cppreference.com/w/c/variadic
- qayxc 3y agoIn JS every function is a variadic function, though. The problem is that many people ignore that fact for whatever reason. function fn() { } is semantically the exact same as function fn(a, b, c) { } in JS.
- Jtsummers 3y agoYou're discussing this as if the critical "gotcha" was with `parseInt`. It's not the critical gotcha here. The critical one is actually with the semantics of `map` which behaves differently in JS compared to just about every other language with a map (or equivalent) which passes only the elements of the sequence (or sequences in some cases) to the function. See Python, Ruby, C++ (transform is the nearest direct match), Rust, Common Lisp, Scheme, etc. Using the example with `parseInt` just demonstrates this strange, and unique, design decision.
- qayxc 3y agoHow on Earth is that a WTF? Ignoring the specs will lead to unexpected results regardless of the language or API used.
- ramesh31 3y agoThe radix param always seemed silly and unnecessary to me. It's a huge footgun that causes a ton of bugs like this. It should have just been another method, "parseIntBase" or whatever.
- jdlshore 3y agoThere are a lot of these poor API design decisions in early JavaScript, which we're stuck with due to the need for backwards compatibility. ("Don't break the web.") Modern JavaScript APIs tend to be pretty good. Fortunately, early JavaScript was also very small, so you mostly memorize the footguns (or use a linter) and you're good. This is one of them.
- twelve40 3y agowould a linter help here though? it seems to be a perfectly valid call here, except doing something unintended.
- dragonwriter 3y agoA linter would help, if you have a lint rule that requires a locally defined (regular or arrow) function and not a variable inside of map, filter, etc.
- philsnow 3y agoThe problem here is not with parseInt but with map. Map is such a basic function and should have been defined more simply. By all means, make an extended mapE function that passes all kinds of things like position in the input iterable, a reference to the iterable, a reference to an at-first empty “memo” object that will be discarded at the end of the map call, etc etc.
- ketzu 3y agoSeems like a very normal parameter choice for such a function. Nearly every one of the "big" programming languages uses this in some way. To me it would seem weird to have a parseInt and parseIntBase when one is just parseInt(x) -> parseIntBase(x, 10) C++: https://en.cppreference.com/w/cpp/string/basic_string/stol https://en.cppreference.com/w/cpp/string/basic_string/stol Java: https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html#parseInt-java.lang.String-int- https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.... C#: https://learn.microsoft.com/en-us/dotnet/api/system.int32.parse?view=net-7.0#system-int32-parse(system-string-system-globalization-numberstyles) https://learn.microsoft.com/en-us/dotnet/api/system.int32.pa... But they don't have implicit null/undefined for unspecified parameters, and afaik the map's in these languages do not provide additoinal parameters.
- tracker1 3y agoBecause parseInt takes multiple parameters, and map sends multiple parameters that don't match to appropriate parsing options, returning NaN.
- seiferteric 3y agoOkay but the whole article could have been summed up in 4 sentences: > Looking at the specification of parseInt you should notice that it is defined as accepting two arguments. The first argument is the string to be parsed and the second specifics the radix of the number to be parsed. >...the function that is passed as the first argument to map as the callbackfn. The specification says, “the callbackfn is called with three arguments: the value of the element, the index of the element, and the object that is being traversed.”
- yieldcrv 3y ago[flagged]
- 4pkjai 3y agoI read your comment, didn't understand your explanation. Then I read the article and understood. I liked the slower break down in the article.
- pleb_nz 3y agoOne paragraph would have been enough, for me, it dragged on and I started skipping sections of text. I am amazed someone wanted to spend their precious hours writing such a long article for something that could have been much less. Different strokes and folks though.
- paulddraper 3y agoOr two: > Array.prototype.map passes the element's index as the second argument to the callback, and parseInt accepts an optional second argument as the radix (base). > Using the index as the radix yields a nonsensical result.
- gabrielsroka 3y agoArticle is from 2011
- deleted 3y ago[deleted]
- dragonwriter 3y agoSo? The behavior it concerns still exists, and given the desire to avoid breaking backward conpatibility in JS, likely will when the article gets reposted in 2111, too.
- gabrielsroka 3y agoHN is a news site. The article posted is not news. If you post old news, it's customary to say so. Eg, from dang, the moderator: https://news.ycombinator.com/item?id=36398290 https://news.ycombinator.com/item?id=36398290 https://news.ycombinator.com/item?id=36366875 https://news.ycombinator.com/item?id=36366875 Etc https://hn.algolia.com/?dateRange=all&page=0&prefix=true&query=by%3Adang%20year%20added&sort=byDate&type=all https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que...
- deleted 3y ago[deleted]
- daveidol 3y agoPersonally I don't see this as confusing/problematic. You just need to know .map passes multiple args (which can be extremely useful in other circumstances). Easy fix too: ["1", "2", "3"].map(i => parseInt(i))
- Jtsummers 3y agoWhat's confusing/surprising is that it is different than map in many (most?) other languages with a map implementation. Like in Python it would be: map(some_fun, iterable, *iterables) `some_fun` would have arity 1 or 1 + len(iterables) and be called only with the values of the passed in iterables, not the enumerated indexes or the original iterables or whatever. To get an equivalent, you'd add in an iterable that corresponded to the indexes explicitly. Same thing with Rust with its `enumerate` which would produce a sequence of pairs `(index, original_item)`. And those aren't atypical when examining `map` in other languages.
- crazygringo 3y agoThis is a great example of why I wish all function parameters except a first non-optional one always had to be named. In other words, you could do: parseInt("1"); And you could do: parseInt("1", radix: 16); But you could never do: parseInt("1", 16); I swear, this is my #1 wish for programming languages in general. Identifying parameters by position was necessary back when memory was at a premium, but it's always been a disaster for readability and clarity. And we're still all paying the price. Edit: And I'm not talking about named arguments as an option (lots of languages provide this, though not JavaScript). I mean required, where the very concept of argument order (except for first) doesn't even exist.
- jvanderbot 3y agoThat's not the problem. The problem is when a( <a list L> ) becomes a( l[0], l[1], l[2] ), which is bananas. you pass one object, you should stuff that into the first parameter.
- crazygringo 3y agoIt would prevent the bug, though. Passing the index and object for .map() can come in handy sometimes, so I understand the motivation for including it. But with named arguments, they could be passed as something like "_index:" and "_obj:", rather than getting slotted in as an arbitrary [1] and [2], where they can be misinterpreted as other parameters (like "radix").
- deleted 3y ago[deleted]
- deleted 3y ago[deleted]
- matt_kantor 3y agoI might be misunderstanding you, but if you're saying this is what's happening with `["1","2","3"].map(parseInt)`, that's not correct. The article describes the situation in detail, but `parseInt` is called three separate times (its first argument in each call is `"1"`, then `"2"`, then `"3"`, as you'd expect with any array-mapping operation). The weirdness arises because `.map` always passes more than one argument to the callback (and `parseInt` accepts more than one parameter). This happens even when the array has only a single element, it's just that the first call (`parseInt("1", 0, ["1"])`) happens to have the same return value as `parseInt("1")`, so you only notice the weirdness with the later elements.
- athenot 3y agoThe article mentions using the Number constructor, which is even more elegant: > ["1","2","3"].map(Number) [ 1, 2, 3 ]
- latexr 3y ago> ["1","2","3"].map(function(value) {return parseInt(value)}) > (…) it is much more verbose and arguably less elegant. Article is from 2011 and referencing ES5. With ES6 arrow functions we can make it shorter and more elegant: ["1","2","3"].map(value => parseInt(value)) Because it’s so short, even `n` should suffice and retain clarity: ["1","2","3"].map(n => parseInt(n))
- flockonus 3y agoThis... I prefer 10x more obvious code than elegant code. Not to mention, altering primitives class prototypes can lead to unexpected side effects.
- ccooffee 3y agoSure, but to update their example using the new format, you'd say: ["1","2","3"].map(parseInt); // returns [1, NaN, NaN] The gotcha trap is still there, and is just as likely to get through a code review.
- latexr 3y agoYes, you still need to know that you have to do this, but my point is that the author started with the quoted alternative and abandoned it because it had two problems (verbose, inelegant) which are eliminated in ES6, making the `Function.prototype.only` solution unnecessary.
- mattlondon 3y agoYep came here to say this. Arrow functions are graceful and preferred precisely because of this. Passing a naked function reference like that to map is frowned upon where I work, and your PR would not get approval because it is risky for the reasons pointed out in this article. TL;DR - use modern JavaScript features to make your code more readable and more reliable.
- cosmiccatnap 3y agoIt's concerning the level of handwaving people do by pointing to a specification and showing how this is in accordance with it. This is not a normal way for a language to behave which is why it gets this attention. Languages shouldn't get people to accept their flaws people should push to resolve the flaws in languages.
- vbezhenar 3y agoI like similar example: ["192", "168", "0", "1"].map(parseInt) which yields [192, NaN, 0, 1]. Makes a great question in the interview.
- a4isms 3y agoHaving written a book about JavaScript, I know the correct answer to this question. Having also written a book about things to look for and abhor in an interview, I consider this question a strong red flag.
- ipnon 3y agoPersonally if this was asked during an interview that didn’t allow looking up references, it would be a signal that the company is not a good place to work. It indicates the company places value on memorizing lots of programming trivia instead of valuing thoughtful research and exploring various solutions. Candidates may feel they are signing up for a brainiac contest.
- vbezhenar 3y agoIt's the question to observe how candidate approaches this problem. It's not about receiving right answer.
- deleted 3y ago[deleted]
- 1-more 3y agoFor better results start at -1 '-1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20'.split(' ').map(parseInt) gets you [ -1, NaN, // could be better 1, 2, 3, 4, 5, 6, 7, 8, 9, // ok a little bumpy at the start but now we're cookin' 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, // not great not terrible 42 // dang ] way closer. Pretty sure the Number constructor was made to get you around this. map(Number) does what you would expect.
- throw_m239339 3y agoBecause you shouldn't try to be clever with Javascript. Map passes 3 parameter to the mapped function, the second one being an index, and parseInt accepts a second optional argument as radix. be explicit: ["1","2","3"].map(function(x){return parseInt(x,10)}) Javascript isn't the problem here.
- jjnoakes 3y agoTypeScript allows this too, unfortunately. https://www.typescriptlang.org/play?#code/MYewdgziA2CmB00QHMAUBtARARkwGgAJMAmfIgZkwF14BbAQwAdVH6AnCWASTABcBKfkA https://www.typescriptlang.org/play?#code/MYewdgziA2CmB00QHM...
- wizofaus 3y agoAh, that semi-answers my question - but why? Surely the signature of parseInt doesn't match what map requires for its function parameter?
- jjnoakes 3y agoI agree that it would be nice if it flagged this kind of thing. But TypeScript disagrees. https://github.com/microsoft/TypeScript/wiki/FAQ#why-are-functions-with-fewer-parameters-assignable-to-functions-that-take-more-parameters https://github.com/microsoft/TypeScript/wiki/FAQ#why-are-fun...
- wizofaus 3y agoInteresting but certainly disagree with the reasoning, that somehow having to explicitly discard the unused parameters would be "burdensome", certainly compared to the need to explicitly declare types for complex arbitrary data structures. If you're not even prepared to write, say, array.map((n, _, _) => parseInt(n)) then why bother using a strictly type checked language...
- jjnoakes 3y agoAgreed on all counts.
- galaxyLogic 3y agoWhat would be a better spec for map() then? I think it would be to say that [...].mapBETTER(argFunk) calls its arg-function with just a single argument. The rest of the useful ("optional") arguments should be passed by binding "this" to the array of them. If bind() fails because the function was already bound, should throw an error. But this brings me to my pet-peeve: Why can't I map over Objects (other than Arrays)?
- qayxc 3y agoIt doesn't have anything to do with the specs of map(). It's part of the core semantics of the language: function f() { } is equivalent to function f(a, b ,c) { } because under the hood the latter is simply function f() { let a = arguments[0] let b = arguments[1] let c = arguments[2] } Also no one stops you from simply extending the Array.prototype to your heart's content ¯\_(ツ)_/¯
- galaxyLogic 3y agoIt has to do with the fact that map() passes 3 arguments to its argument function, right?
- qayxc 3y agoSure, if you want to look at it from that direction. Other languages, however, use a different default semantic for parameter passing and simply wouldn't allow using a function/lambda/delegate that doesn't explicitly take 3 arguments as well. JS interfaces are designed with the flexibility and semantics of the language in mind (no surprise there, really) and thus try to cover most use cases. If you don't need the index or the reference to the array in your function, ignore them (e.g. by using map(function(v) { ... })). Passing a function that actually supports more than one argument is by all accounts a layer 8 error and not a fault in the spec of map() IMO. You could just as well argue that it's bad for parseInt() to accept one or two arguments instead of explicitly requiring both. You'd have to try and convince me why one is perfectly fine while the other is not. You can of course argue that both specs are crap, that'd at least be consistent :)
- wizofaus 3y agoIs this problem avoided in Typescript? I assume it still wouldn't be if the function used with map happened to take the number & types of arguments that matched how map was declared, but that's rather less likely. Actually I'm now curious if Typescript would allow using parseInt as a parameter to a version of 'map' that was declared to only accept functions with a single argument - I'd guess not.