9 ms·
Why ['1', '7', '11'].map(parseInt) returns [1, NaN, 3] in JavaScript
- maxxxxx 7y agoStuff like this is what drives me crazy when I work with dynamic languages like JS or PHP. I have seen a lot of code that looked perfectly fine but suffered from unexpected casts or defaults. I much prefer languages like C#, C++ or TypeScript where the compiler warns me of such problems.
- bastawhiz 7y agoThis has nothing to do with casts or defaults. Map passes multiple parameters and parseInt accepts multiple parameters. Being unaware of the functions you're using is the problem, not some sort of weird gotchas of the language. The code in the title makes multiple assumptions, and those assumptions all proved wrong.
- teraflop 7y agoI mean, I get that this particular example isn't really all that mysterious, but I think it's fair to say that "all parameters are optional and extra parameters are ignored" is a Javascript language gotcha.
- gameswithgo 7y agoA typed language would absolutely fail this at compile time.
- deleted 7y ago[deleted]
- zbentley 7y agoOnly because map's third provided argument exists. If map only supplied element and index to the callback, this would pass most type checkers I'm familiar with, and would remain just as confusing.
- tomxor 7y agoYes but with mandatory parameters the programmer would be aware parseInt had a radix in the first place if they had used it even once. That's one advantage of mandatory parameters, but there are of course disadvantages.
- horsawlarway 7y agoI'd argue basically everything about mandatory arguments is worse than having edgecases like this. Honestly, most languages STILL won't catch this, because the typical pattern is to use method overloading to provide multiple signatures if default arguments aren't supported. parseInt(in) parseInt(in, radix)
- ernst_klim 7y ago> I'd argue basically everything about mandatory arguments is worse than having edgecases like this. Any sane language having optional arguments is using keys for opt args. That's what common lisp and OCaml do. In OCaml you would write: let parse_int ?(radix = `Dec) string =... val parse_int : ?radix:[`Bin | `Oct | `Dec | `Hex] -> string -> int and call it like parse_int "42" - 42 parse_int ~radix:`Bin "111" - 7 List.mapi parse_int ["1"] - Static error: this expression has type `string -> int` but expected int -> 'a -> 'b
- happytoexplain 7y agoPrecisely - this is an API design failure, pure and simple. Not a problem with typing, optional arguments, etc.
- wutbrodo 7y agoIt's both. Silently accepting variadic arguments increases the odds of something like this happening. This case wouldn't have happened without it. Though as I said in another comment, best-effort parsing was a decision that arose out of the Web ecosystem and its one of the few things about Javascript's language design that can't br blamed on incompetence.
- bastawhiz 7y agoTyped languages can still have overloaded functions and optional parameters. You could easily encounter a similar issue in many typed languages.
- ajanuary 7y agoI would argue Javascript silently accepting more arguments than the function signature declares is kinda a gotcha. And that's what leads people to not realise map is passing extra arguments, because `list.map(a => f(a))` works.
- tomxor 7y agoI think the parent is talking about how C for instance does not have optional parameters (for better or worse), if this was the case for JS everyone who has ever used parseInt would be aware it takes a radix, but then again you also wouldn't be able to just plug it into map arbitrarily. Not saying one is better than the other, optional parameters can make for much less verbose code, I especially like parameter defaults introduced in ES6.
- notus 7y agoWhy would it drive you crazy? This is an example of someone not even knowing how the map function works.
- nilkn 7y agoI would certainly consider this a "gotcha" because it's unexpected that map both deviates from the norm and the language makes it so easy to misuse and hide that misuse. This reminds me of "array set" in Tcl (a language many probably haven't used in a long time -- or ever). Unlike the normal "set" command in Tcl, which overwrites the full value of the variable, "array set" is actually a merge operation -- it merges in new keys and doesn't get rid of anything. I've seen experienced programmers use "array set" in a loop, leading to awful bugs. This too could be dismissed by saying that they "don't even know how array set works," but actually I place the blame on the language for making it so easy to misuse.
- maxxxxx 7y agoOnce you work with several languages it’s really hard to keep track of all this stuff. I never know what exactly evaluates to true vs false in PHP or JavaScript for example. Add to that sloppy programmers on your team who don't check their assumptions it’s really easy to have a ton of subtle bugs in your code.
- fieryscribe 7y ago> I never know what exactly evaluates to true vs false in PHP or JavaScript for example. You don't have to know if you don't use implicit type-casting. Make it explicit and you won't really have to worry about it. "Explicit is better than implicit" is part of the Zen of Python for a very good reason.
- maxxxxx 7y ago"You don't have to know if you don't use implicit type-casting. Make it explicit and you won't really have to worry about it. " Correct. Unfortunately a lot of people use implicit casting a lot and have no idea that that's what they are doing.
- epidemian 7y agoTypeScript doesn't complain about this[1], as the type of parseInt matches the type of Array.prototype.map parameter. [1]: https://www.typescriptlang.org/play/#src=console.log(%5B'1'%2C%20'7'%2C%20'11'%5D.map(parseInt))%0A https://www.typescriptlang.org/play/#src=console.log(%5B'1'%...
- acdha 7y ago> I much prefer languages like C#, C++ or TypeScript where the compiler warns me of such problems. parseInt is defined as accepting a string and an optional radix value, which is numeric. map is defined as providing the value and its index, which is also numeric. Would any of C#, C++, or TypeScript catch that without redefining either parseInt or map to require a more specific type, breaking compatibility with many millions of lines of code around the web?
- dehrmann 7y ago> Would any of C#, C++, or TypeScript catch that C# would have a compile error with that map and parseInt definition because it can't coerce the types.
- acdha 7y agoparseInt and the map callback signature both declare the second argument as integers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt#Parameters https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#Parameters https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... The thing which would actually catch this would be the mismatch in the number of arguments (modulo someone declaring that third argument as optional) or breaking compatibility to change one of them not to be a basic integer.
- jfengel 7y agoTo quote the tl;dr: ['1', '7', '11'].map(parseInt) doesn’t work as intended because map passes three arguments into parseInt() on each iteration. The second argument index is passed into parseInt as a radix parameter. So, each string in the array is parsed using a different radix. That's hilarious. Everybody loves the syntactic sugar that makes things easy, until the unexpected point where it makes things very hard.
- tntn 7y agoWhat is the syntactic sugar here? I mostly see a map that doesn't behave how anyone would expect.
- JoeNr76 7y agoThe syntactic sugar here is that map and parseInt can be called without specifying all parameters. (But, since JS has no function overloading, it's the same function you're calling.)
- roryrjb 7y agoThis isn't strange or surprising. parseInt takes two arguments, the second one is the radix and map will call with three arguments, the value, the index and the whole array. You just have to know this and it might be different in other languages. [ ... ].map(x => ...) is the right way to do this.
- crossman 7y agoYeah.. Didn't even need to load the article. You're passing the index as the radix. Makes sense
- mpolichette 7y agoYeah, I’m not surprised at all by this. I get that there are a lot of weird things in JS but I don’t think this is a JS oddity. Surprise! You need to know the basics of how your language works.
- arcticbull 7y agoThat's apologist talk pure and simple. The language, silently, does something that's almost certainly wrong. The language has enough information to provide you with a helpful warning or error message that you probably don't want to do this, but instead, it violates the principle of least surprise by just doing the wrong thing instead. 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.
- pennaMan 7y agoThe map function fully expects a function with two arguments, as per documentation. Why would passing a function that takes two arguments to map be an error?
- seszett 7y agoTL;DR, parseInt() takes the number base as a second argument, and map() passes three arguments (value, index, whole array). Using directly like this a function with map() is just incorrect, the correct way to do it is: ['1', '7', '11'].map(x => parseInt(x)) edit I'm getting downvoted, it doesn't matter much but I don't understand it when the most upvoted comment seems to say more or less the same?
- deleted 7y ago[deleted]
- shay_ker 7y agoThis has less to do with "parseInt" than it has to do with "map" in JavaScript. It doesn't work exactly like .map in Java, Ruby, Elixir, etc., because ".map" in JS also passes in the index, and the full array.
- jonfleck 7y agoEasy fix ['1', '7', '11'].map(Number)
- nailer 7y agoAgreed. Number is not only more obvious than parseInt for other coders but also defaults to base 10, whereas parseInt (at least historically) does not (edit: ...if you've got leading zeros, see post below).
- tomxor 7y ago> defaults to base 10, whereas parseInt (at least histortically) does not. To clarify for others (because this probably sounds crazy): The default is implicit like the rest of JavaScript, it will change base depending on presence of the prefixes 0x (16) and 00 (8), it doesn't seem to have included 0b yet. The confusing bit was octal because as you can imagine some sources might have base 10 padded with zeros, ES5 basically removes implicit octals in parseInt to avoid this issue. Arguably this is more of a problem with the ambiguous octal prefix than the concept of using prefixes to determine base.
- weaksauce 7y agoparseInt will use the first few characters in the string as a heuristic to determine the radix. It’s not reliable for some types of common decimal strings(padded zeros will cause erroneous parsing)
- nivertech 7y agoDisclaimer: I don't know Javascript, but that's why: > ['1','7','11'].map(console.log) 1 0 [ '1', '7', '11' ] 7 1 [ '1', '7', '11' ] 11 2 [ '1', '7', '11' ] [ undefined, undefined, undefined ] > parseInt(1,0) 1 > parseInt(7,1) NaN > parseInt(11,2) 3 The correct way is: > ['1','7','11'].map(x => parseInt(x)) [ 1, 7, 11 ] same as: > ['1','7','11'].map(x => parseInt(x, 10)) [ 1, 7, 11 ]
- czr 7y agoThose are unfortunately not the same. The second one (with an explicit radix of 10) is correct; the first is not (or is at least a bit riskier). See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt#Description https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... (ctrl+f for "always").
- notus 7y agoIn most browser implementations (at least all the ones we develop for) it would assume a radix of 10 for those values specifically.
- deleted 7y ago[deleted]
- acdha 7y agoBut see also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt#Octal_interpretations_with_no_radix https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... and especially https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt#Browser_compatibility https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... — that comment is increasingly stale unless you support browsers which are no longer supported by their vendors like IE8.
- czr 7y agoGood point. I've edited my comment to be more qualified.
- martin-adams 7y ago>> "If the radix provided is falsy, then by default, radix is set to 10." The official docs for parseInt says this: >> An integer between 2 and 36 that represents the radix (the base in mathematical numeral systems) of the string. Be careful — this does not default to 10. [1] I just found it confusing whether the author meant the default value is 10, or if a falsy parameter (not undefined) turns out to be 10. [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- drudru11 7y agoAgreed - if you read the description section in the Mozilla docs, there is no mention of falsy. He should eliminate that section.
- kosievdmerwe 7y agoSo looking at the rest of the documentation, the parameter not being set causes the default radix to be 10 unless the number starts with 0 or 0x, in which case they radix is 8 or 16 respectively. Though newer versions of JS no longer support the octal syntax (it likely just caused bugs for people who had initial zeros in their strings sometimes) parseInt("0x10") == 16 parseInt("0x10", 10) == 0
- deleted 7y ago[deleted]
- evolutionxbox 7y agoBecause parseInt isn't a unary function?
- deleted 7y ago[deleted]
- mavci 7y agoIt’s reminded me to this funny talk https://www.destroyallsoftware.com/talks/wat https://www.destroyallsoftware.com/talks/wat
- alexk7 7y agoThis is not one of the weird things about Javascript. When you use a function (like map), always check the documentation to know its behaviour and don't assume it's similar to some similarly named function from another language. End of story.
- cbm-vic-20 7y ago> don't assume it's similar to the same named function from every other language
- lucideer 7y agoNot every language has such a function. Even languages that do have different versions of it on different objects (e.g. Scala's zipWithIndex)
- happytoexplain 7y agoIt's bewildering to me that one can look at the sheer volume of confusion generated by things like this and insist that it's not confusing.
- JulianMorrison 7y agoSo in summary: JS map doesn't behave like anybody else's map. JS argument passing prefers doing the wrong thing to interrupting the programmer. These two things conspire together and poor parseInt does its best with the resulting garble.
- horsawlarway 7y agoI don't really see any issue here. JS map behaves sanely - It adds extra params, but I've had them be useful every now and then, and I've never had them cause a problem. Arguments behave... Like arguments behave in JS. Arguments have ALWAYS been variadic and accessible through the "arguments" variable within a function. That's not the "wrong" thing, it's just a thing. If you want named arguments - put them up top. Otherwise you'll get an array-like with everything passed. This is entirely consistent with the language. Finally - These two things conspire together to do what exactly? This isn't a subtle bug where it works correctly 99% of the time and blows in prod late on a friday. This is an obvious error with even dead simple test cases that clearly show the dev has messed up. Unit test your shit, or hell, just run it once or twice before using it and you're fine. --- Basically - you should know how your std library works. That includes JS. I think this is pretty trivial. Worse, I actually find this behavior far more intuitive than something like ConfigureAwait(false) in C#, for example.
- IloveHN84 7y agoBecause It's JavaScript.. stop using unsafe languages, really
- jmull 7y agoA bit of a BS comment since there are no languages that will prevent you from misunderstanding the values passed into or returned from whatever methods/procedures/jump destinations you're working with. More generally, general purpose languages don't provide safety guarantees in the problem domain. They can provide certain guarantees in the solution domain, but it's left to the programmer to compose the elements of the language into a correct solution. (In my experience, by far the biggest barrier to a correcty solution in the problem domain is that no one actually knows what that is, much less has expressed it. Instead, people express certain specific behaviors they think the system should have, from which the programmer needs to extrapolate the actual requirements -- not straight-forward since to a greater or lessor degree the expressions will be vague, self-contradictory, self-defeating, and/or incoherent. Then they need to compose those requirements in the solution space. BTW, the language is just a part of the solution space and "safe" languages are usually only referring to static checks, which the solution space often consists of distributed components which aren't strictly controlled in lock-step by the same static sources, meaning the static guarantees are useful, but in a limited way.)
- tntn 7y agoIt is extremely awesome that JavaScript has conditioned a ton of the world's programmers to think that needing to compose your function with the identity function to achieve the desired result is not surprising or bad. I will not be convinced that an implementation of map that gives different results when there are identity functions in the middle is doing the right thing.
- kabwj 7y agohttps://imgur.com/EAaJho6.jpg https://imgur.com/EAaJho6.jpg Thanks Medium! Great readability.
- cellis 7y agoRecently I learned that in Javascript, [5 * 5] * 2 // 50 [5 + 5] * 2 // NaN 1 + [5 * 5] * 3 // 76 1 + [5 * 5] - 1 // 124 Interestingly, even Typescript will not ( by default ) catch this class of bugs.
- nullwasamistake 7y agoUse Typescript. Regular JS just has too many gotchas to be usable in large applications. You can crank up the settings in TsLint and never worry about things like this again
- acdha 7y agoDid you try testing that? The TypeScript compiler does't say a thing about that code: https://www.typescriptlang.org/play/index.html#src=alert(%5B'1'%2C%20'7'%2C%20'11'%5D.map(parseInt))%3B https://www.typescriptlang.org/play/index.html#src=alert(%5B... TSLint similarly reports nothing even with the tslint:all ruleset: https://palantir.github.io/tslint-playground/?saved=N4Igxg9gJgpiBcIDaByAjCgNAAhQdi1zQwF0A6AWwEMAHAChqoCcBnGASQDsAXASgG4AOpxCZwETgDMAlgHMEIAPSLsATQgBXbGCqdsFaNMkBPbNwAWMbRJmyNTKt2kTslplYDu0i9mOam2BAenGTCytgAslQA1lYs9lYWjr7+2ABWLC7SLNgAblQANtJQOLpQ2EYpWkwaetI8EGEqEBYwAdnxMCw43EymYJZg0fWyZpbWnJkFVpIQAW1McyyhnMLAwtib2IIgMAAe3DCcUCw78NhIO9wsRTzwhQU7JJgbWzs106cI2MAAvtjhKhQcpAqDeZycQrYD5dbBlMYwaQBSBSOT2RwQ4S-YQgX5AA https://palantir.github.io/tslint-playground/?saved=N4Igxg9g... https://palantir.github.io/tslint-playground/ https://palantir.github.io/tslint-playground/
- nullwasamistake 7y agoDoesn't seem right, are all the strict compiler options enabled?
- acdha 7y agoYes, can you post the configuration you used for your original claim?
- deleted 7y ago[deleted]
- datpuz 7y agotl;dr: parseInt takes an optional second argument, the radix. It defaults to 10 (so the number is parsed in base 10). Since map() passes in each item in the array as the first argument, and the index as the second, you're parsing each string with the radix of whatever the index is. Not too weird IMO.
- ojosilva 7y agoThis article made me curious to why a Base-1 or Unary Numeral base system has not been implemented in parseInt() as noted by the OP, since the second argument radix must be between 2 and 36: parseInt('1', 1) // same error for '0' or '|' > NaN The result is intriguing still as "NaN" seems to indicate a invalid input in the first parameter instead of a invalid second parameter. I've found that apparently there is no consensus [1] on Base-1 notation or parsing, although my primary intuition is correct [2] in that a parser could be written that would parse a "1" as a 1 base10, "11" as a 2 base10, "111" and so on. The parser would probably look a lot like a simple length() function, but that could vary with certain base-1 encodings like the ones used by the Golomb Rice compression algorithms, which have each string end in "0" (unary coding). [1] https://math.stackexchange.com/questions/371972/what-would-base-1-be https://math.stackexchange.com/questions/371972/what-would-b... [2] https://en.wikipedia.org/wiki/Unary_numeral_system https://en.wikipedia.org/wiki/Unary_numeral_system
- pkaye 7y agoI guess JavaScript like to use the "principle of most surprise"?
- femto113 7y agoThe extra args passed by map coupled with the optional extra args in standard methods is the cause of a lot of confusion, but this feels like a missed opportunity for demonstrating functional programming. In the end author suggests ['1', '7', '11'].map(numStr => parseInt(numStr)); I think you'd learn something much more useful with function radixParser(radix) { return numStr => parseInt(numStr, radix); } ['1', '7', '11'].map(radixParser()); > [ 1, 7, 11 ] ['1', '7', '11'].map(radixParser(8)); > [ 1, 7, 9 ] ['1', '7', '11'].map(radixParser(2)); > [ 1, NaN, 3 ]
- fastball 7y agoIf you're gonna use arrow functions, why not go all the way! const radixParser = (radix) => (numStr) => parseInt(numStr, radis);
- femto113 7y agobecause I still think function foo() { ... } is a clearer way of indicating "I'm defining a function named foo". I tend to use arrows only for anonymous methods.
- deleted 7y ago[deleted]
- tonymet 7y agolet's work to bring back brevity the answer is that map is passing extra args just use an arrow func to pass args to parseInt explicitly there saved you a minute scrolling to the bottom
- deleted 7y ago[deleted]
- deleted 7y ago[deleted]
- Skywing 7y agoThis isn't a quirk of JS at all. It's somebody purposefully calling `parseInt` incorrectly.