One of Flow's original goals was to be able to understand idiomatic JavaScript. In JavaScript, you can call a function with more arguments than the function expects. Therefore, Flow never complained about calling a function with extraneous arguments.
We are changing this behavior.
What is arity?
A function's arity is the number of arguments it expects. Since some functions have optional parameters and some use rest parameters, we can define the minimum arity as the smallest number of arguments it expects and the maximum arity as the largest number of arguments it expects.
function no_args() {} // arity of 0
function two_args(a, b) {} // arity of 2
function optional_args(a, b?) {} // min arity of 1, max arity of 2
function many_args(a, ...rest) {} // min arity of 1, no max arity
Motivation
Consider the following code:
function add(a, b) { return a + b; }
const sum = add(1, 1, 1, 1);
The author apparently thought the add()
function adds up all its
arguments, and that sum
will have the value 4
. However, only the first two
arguments are summed, and sum
actually will have the value 2
. This is
obviously a bug, so why doesn't JavaScript or Flow complain?
And while the error in the above example is easy to see, in real code it's often
a lot harder to notice. For example, what is the value of total
here:
const total = parseInt("10", 2) + parseFloat("10.1", 2);
"10"
in base 2 is 2
in decimal and "10.1"
in base 2 is 2.5
in decimal.
So the author probably thought that total
would be 4.5
. However, the correct
answer is 12.1
. parseInt("10", 2)
does evaluates to 2
, as expected.
However, parseFloat("10.1", 2)
evaluates to 10.1
. parseFloat()
only takes
a single argument. The second argument is ignored!
Why JavaScript allows extraneous arguments
At this point, you might feel like this is just an example of JavaScript making terrible life decisions. However, this behavior is very convenient in a bunch of situations!
Callbacks
If you couldn't call a function with more arguments than it handles, then mapping over an array would look like
const doubled_arr = [1, 2, 3].map((element, index, arr) => element * 2);
When you call Array.prototype.map
, you pass in a callback. For each element in
the array, that callback is invoked and passed 3 arguments:
- The element
- The index of the element
- The array over which you're mapping
However, your callback often only needs to reference the first argument: the element. It's really nice that you can write
const doubled_arr = [1, 2, 3].map(element => element * 2);
Stubbing
Sometimes I come across code like this
let log = () => {};
if (DEBUG) {
log = (message) => console.log(message);
}
log("Hello world");
The idea is that in a development environment, calling log()
will output a
message, but in production it does nothing. Since you can call a
function with more arguments than it expects, it is easy to stub out log()
in
production.
Variadic functions using arguments
A variadic function is a function that can take an indefinite number of
arguments. The old-school way to write variadic functions in JavaScript is by
using arguments
. For example
function sum_all() {
let ret = 0;
for (let i = 0; i < arguments.length; i++) { ret += arguments[i]; }
return ret;
}
const total = sum_all(1, 2, 3); // returns 6
For all intents and purposes, sum_all
appears like it takes no arguments. So
even though it appears to have an arity of 0, it is convenient that we can call
it with more arguments.
Changes to Flow
We think we have found a compromise which catches the motivating bugs without breaking the convenience of JavaScript.
Calling a function
If a function has a maximum arity of N, then Flow will start complaining if you call it with more than N arguments.
test:1
1: const num = parseFloat("10.5", 2);
^ unused function argument
19: declare function parseFloat(string: mixed): number;
^^^^^^^^^^^^^^^^^^^^^^^ function type expects no more than 1 argument. See lib: <BUILTINS>/core.js:19
Function subtyping
Flow will not change its function subtyping behavior. A function with a smaller maximum arity is still a subtype of a function with a larger maximum arity. This allows callbacks to still work as before.
class Array<T> {
...
map<U>(callbackfn: (value: T, index: number, array: Array<T>) => U, thisArg?: any): Array<U>;
...
}
const arr = [1,2,3].map(() => 4); // No error, evaluates to [4,4,4]
In this example, () => number
is a subtype of (number, number, Array<number>) => number
.
Stubbing and variadic functions
This will, unfortunately, cause Flow to complain about stubs and variadic
functions which are written using arguments
. However, you can fix these by
using rest parameters
let log (...rest) => {};
function sum_all(...rest) {
let ret = 0;
for (let i = 0; i < rest.length; i++) { ret += rest[i]; }
return ret;
}
Rollout plan
Flow v0.46.0 will ship with strict function call arity turned off by default. It
can be enabled via your .flowconfig
with the flag
experimental.strict_call_arity=true
Flow v0.47.0 will ship with strict function call arity turned on and the
experimental.strict_call_arity
flag will be removed.
Why turn this on over two releases?
This decouples the switch to strict checking of function call arity from the release.
Why not keep the experimental.strict_call_arity
flag?
This is a pretty core change. If we kept both behaviors, we'd have to test that everything works with and without this change. As we add more flags, the number of combinations grows exponentially, and Flow's behavior gets harder to reason about. For this reason, we're choosing only one behavior: strict checking of function call arity.
What do you think?
This change was motivated by feedback from Flow users. We really appreciate all the members of our community who take the time to share their feedback with us. This feedback is invaluable and helps us make Flow better, so please keep it coming!