本文へスキップ

関数呼び出しの引数の厳格なチェック

Flowの当初の目標の1つは、慣習的なJavaScriptを理解できることでした。JavaScriptでは、関数が期待するよりも多くの引数で関数を呼び出すことができます。そのため、Flowは余分な引数で関数を呼び出すことについてこれまで警告を出していませんでした。

この動作を変更します。

アリティとは何か

関数のアリティとは、関数が期待する引数の数です。一部の関数はオプションのパラメータを持ち、一部はrestパラメータを使用するため、最小アリティを関数が期待する最小の引数の数として、最大アリティを関数が期待する最大の引数の数として定義できます。

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

動機

次のコードを考えてみましょう

function add(a, b) { return a + b; }
const sum = add(1, 1, 1, 1);

作成者は、add()関数がそのすべての引数を合計し、sumの値が4になると考えていたようです。しかし、最初の2つの引数だけが合計され、sumの実際の値は2になります。これは明らかにバグですが、なぜJavaScriptもFlowも警告を出さないのでしょうか?

そして、上記の例のようなエラーは簡単にわかりますが、実際のコードでは気付くのがはるかに難しいことがよくあります。たとえば、ここのtotalの値は何でしょうか

const total = parseInt("10", 2) + parseFloat("10.1", 2);

2進数の"10"は10進数では2であり、2進数の"10.1"は10進数では2.5です。そのため、作成者はtotal4.5になると考えていたのでしょう。しかし、正しい答えは12.1です。parseInt("10", 2)は期待通り2になります。しかし、parseFloat("10.1", 2)10.1になります。parseFloat()は1つの引数しか受け付けません。2番目の引数は無視されます!

JavaScriptが余分な引数を許容する理由

この時点で、これはJavaScriptがひどい決断をしているだけの例だと感じるかもしれません。しかし、この動作は多くの状況で非常に便利です!

コールバック

処理するよりも多くの引数で関数を呼び出すことができない場合、配列のマップ処理は次のようになります。

const doubled_arr = [1, 2, 3].map((element, index, arr) => element * 2);

Array.prototype.mapを呼び出すと、コールバックを渡します。配列の各要素に対して、そのコールバックが呼び出され、3つの引数が渡されます。

  1. 要素
  2. 要素のインデックス
  3. マップしている配列

しかし、コールバックは多くの場合、最初の引数(要素)を参照するだけで済みます。次のように記述できるのは非常に便利です。

const doubled_arr = [1, 2, 3].map(element => element * 2);

スタブ化

このようなコードに出くわすことがあります。

let log = () => {};
if (DEBUG) {
log = (message) => console.log(message);
}
log("Hello world");

開発環境ではlog()を呼び出すとメッセージが出力されますが、本番環境では何も行いません。関数を期待するよりも多くの引数で呼び出すことができるため、本番環境でlog()を簡単にスタブ化できます。

`arguments`を使用した可変長関数

可変長関数とは、不定数の引数を取ることができる関数です。JavaScriptで可変長関数を記述する従来の方法は、argumentsを使用することです。たとえば

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

あらゆる点において、sum_allは引数を取らないように見えます。そのため、アリティが0のように見えるにもかかわらず、より多くの引数で呼び出せるのは便利です。

Flowへの変更

JavaScriptの利便性を損なうことなく、動機となるバグをキャッチする妥協点を見つけたと思います。

関数の呼び出し

関数の最大アリティがNの場合、Nを超える引数で関数を呼び出すと、Flowは警告を出します。

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

関数のサブタイピング

Flowは関数のサブタイピング動作を変更しません。最大アリティが小さい関数は、最大アリティが大きい関数のサブタイプです。これにより、コールバックは以前どおり動作します。

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]

この例では、() => number(number, number, Array<number>) => numberのサブタイプです。

スタブ化と可変長関数

残念ながら、これにより、`arguments`を使用して記述されたスタブと可変長関数についてFlowが警告を出すようになります。ただし、restパラメータを使用することで修正できます。

let log (...rest) => {};

function sum_all(...rest) {
let ret = 0;
for (let i = 0; i < rest.length; i++) { ret += rest[i]; }
return ret;
}

ロールアウト計画

Flow v0.46.0では、関数の呼び出しの引数の厳格なチェックはデフォルトでオフになっています。これは、.flowconfigで次のフラグを使用して有効にできます。

experimental.strict_call_arity=true

Flow v0.47.0では、関数の呼び出しの引数の厳格なチェックがオンになり、experimental.strict_call_arityフラグは削除されます。

2つのリリースにわたってこれをオンにする理由

これにより、関数の呼び出しの引数の厳格なチェックへの切り替えがリリースから分離されます。

`experimental.strict_call_arity`フラグを保持しない理由

これは非常に重要な変更です。両方の動作を維持した場合、この変更の有無にかかわらずすべてが機能することをテストする必要があります。フラグを追加するにつれて、組み合わせの数は指数関数的に増加し、Flowの動作を推論することが難しくなります。このため、関数の呼び出しの引数の厳格なチェックという1つの動作のみを選択します。

どう思いますか?

この変更は、Flowユーザーからのフィードバックによって動機付けられました。フィードバックを提供してくださったコミュニティの皆様に心から感謝申し上げます。このフィードバックは非常に貴重であり、Flowの改善に役立ちますので、ぜひこれからもご意見をお寄せください!