Well the issue mentioned is that map calls the callback function with up to three arguments[1].
A type check could prevent this because it would require map to take a reference to a function with three parameters, or the compiler would complain inside the map implementation.
Similarly, passing it a function with only one parameter would be a type violation and the compiler would complain.
Now in a language with type checking, you could still potentially run afoul.
Say the map function was overloaded with one variant for one-parameter callbacks, one variant for two-parameter callbacks etc. Then the compiler might figure out it could use the second overload if the "toReadableNumber" function got changed to take the extra "base" parameter.
So again you end up with the numbers getting converted with a variable base.
Though, IMHO, having such an overloaded map function is inviting trouble and is a very poor design.
A proper typesystem for functional types will check at compile-time that in all places where you would assign such a function, the function will always have the right type.
So it's NOT a runtime error, it's a compile-time error, even though you can assign different functions at runtime.
You can assign a reference to a function and pass that to a function that expects a callback, and then change that assignment based on incoming data. And if that’s not enough you can create and modify functions at runtime. And you’d have to content with JavaScript’s spread syntax and rest parameters.
But it would only have to be a runtime error (your words) for the code which is dynamically loaded.
Static code will get checked at compile-time, even though the exact function passed as callback depends on the incoming data via a series of if/case statements say, from a dictionary or whatever.
It would check the arguments and return type of the function. Map takes a function with three arguments, toReadableNumber only takes one, therefore the functions are of a different type. So someNumbers.map(toReadableNumber) would be an error and not execute at all, instead of being a "bad practice" / potential mistake.
You can create custom types to communicate semantics to the type checker. Whether that's always a good idea is arguable though, but the tools are there (e.g. there's a wide field between weak and strong typing, and an overly strong type checker can be quite a hassle to work with while an overly weak type checker isn't much better than duck typing).
A type check would give map a type, say [a] -> (int -> a -> b) -> [b]. If you try to apply it to any function that does not have the type (int -> a -> b), the check would fail.
See also magicalhippo’s comment on overloading. We could introduce real types instead of primitives, so that ‘number_formatting_base_t’ would conflict with ‘array_index_t’, but nobody in the world bothers beyond bare ‘int’.
Yeah, I see the point, in for example C# if you have overloaded methods it tries it best to map it to the accepted type. However you'd have to work really hard to invent a case where a library updates a signature to accept another type, and then you in your code have 2 overloads of a method that you pass and it now choses the wrong version, all while the library change doesn't break any other code.
Scenario: Library changes signature so you always pass functions with more parameters.
Result: This will break almost every codebase, they probably wouldn't do this in a minor patch.
Scenario: Library adds a new signature where you can pass functions with more parameters and keep them overloaded between each other.
Result: Compiler can't identify which of the two signatures to pass your overloaded function and throws a compilation error.
Just to clarify, if you update toReadableNumber to take a second parameter and that parameter is not a number, typescript will complain.*
TypeScript won't catch the original landmine because it ignores the extra parameters; maybe some linter would? Is there a rule that enforces "functions used by map must spell out all the parameters?"
If your code has tests, an API change like the one described in the article would get caught immediately.
And it only works because `Array.prototype.map` callback has one required arguments and two optional ones. What language with optional function arguments protects you from this sort of behavior? Do people in that language not test this sort of behavior? Or at least run through the app?
More than that, the whole hurrdurr-javascript-bad thing is so tired. Lots of work is being done in JavaScript. Sure, it has its quirks, but those quirks come with expressiveness. You can use functional or imperative style, throw lambdas around, and it runs on pretty much all the phones and computers in the world.
If you have a good type system you don't need to write tests like this. I test my algorithms not my function calls.
Also,
> A lot of work is being done in JavaScript
Is that it? It's one of the best funded languages on Earth, what do you expect - with D for example we can do all the things you mentioned and catch errors like this, and we're basically just some guys working on the language not the combined might of the entire Internet sector.
TypeScript doesn't complain when you pass in a function that ignores some of its arguments. Which is totally fine and safe. If you upgrade your function from no second argument to a numeric second argument TypeScript will not complain and your program might break.
It will not crash, because it still perfectly type-safe, but it might not behave like you want to. So in that sense the article has a point.
However, this is just one instance of a larger issue with changing the behavior of a function, while keeping the types compatible.
- Using a function as a callback to .map() with two numeric arguments and then swapping the arguments.
- Returning a tuple of two of the same type and swapping the order.
- Returning a string in a new encoding.
- ...
Basic rule: if it's a type error and the program might crash, TypeScript will complain. If the types are fine and only the behavior changes, TypeScript will (obviously) not complain. Callback functions and optional arguments are not special in this regard.
> If you upgrade your function from no second argument to a numeric second argument [...] It will not crash, because it still perfectly type-safe
It's type-safe using a definition of "type-safe" that is defined relative to the underlying JavaScript model of "corrupt the user's data rather than crash". It wouldn't be type-safe using most other languages' (including Python, for example) model of "functions have a type that includes the number of arguments, and applying them to the wrong number of arguments is meaningless and hence not considered type-safe".
It's fair for TypeScript to use this approach. But it is surprising to many of us, who view type systems as tools for ruling out some dumb functional bugs, not just crashes.
Array.prototype.map = function (func) {
if (func.length !== 3) {
throw new Error(
"bad! this function you've passed must take THREE arguments. Grr!"
);
}
for (let i = 0; i < this.length; i++) {
this[i] = func(this[i], i, this);
}
return this;
};
;)
(making this workable and bug free is left to the reader or multi-billion dollar corporations)
There can be a non-indexed map as well.
I'm most curious as to who uses the last argument in the JS map function!
> I'm most curious as to who uses the last argument in the JS map function!
It's convenient for some things that would otherwise require a reduce (but where reduce isn't particularly more efficient, because you just need lookahead/lookbehind) or an imperative loop, like transforming a list to a set of moving averages over the list.
It's a little more expressive than reduce our imperative lots
loops in those cases, too.
This is really just a wonderful example how javascript is a mental burden to the programmers instead of a useful tool.