技術ブログ

関数の引数は一つだけでいい


関数は複数の引数を持つことができる。当たり前のことだ。ぼくたちはプログラミングを学び始めたころから、そう教わってきた。

add(a, b) という関数があれば、ab を足した結果が返ってくる。シンプルで、わかりやすい。便利なので、一般的にもよく使われている。

けれども、ここでひとつ立ち止まって考えてみたい。この「当たり前」は、本当に最善なのだろうか。今回は、その常識を疑って、新しい提案をしてみたいと思う。


複数の引数がもたらす二つの問題

複数の引数を持つ関数には、二つの厄介な問題がある。

ひとつめは、引数の順番に依存してしまうことだ。

たとえば createUser(name, email, age) という関数があるとしよう。ぼくたちは、この順番を覚えていなければならない。もちろん、関数の定義を見に行けば型や変数名が書いてある。間違えることは少ないかもしれない。

しかし、誤った順序で指定すれば、それはバグになる。オプション引数が増えると、話はさらに厄介になる。

わかりやすい例がある。高機能なOSSライブラリでは、利用者の期待に応えるあまり、ひとつの関数に大量のオプショナル引数が並んでいることがある。ありがたい機能だ。けれども、ドキュメントを読み込まないと使い方がわからない。

ふたつめの問題は、仕様変更に弱いということだ。

引数を追加するとき、すべての呼び出し箇所を修正しなければならない。JetBrains製のIDEなら抜け漏れなく直せるかもしれない。けれども、確認は必要だし、影響箇所は少ないに越したことはない。

ちなみに、関数の戻り値を複数返してアンパックする場合も、同じ問題を抱えている。


オブジェクトを渡すという解決策

では、どうすればいいのか。

ぼくの提案はシンプルだ。オブジェクトか辞書を、引数として渡せばいい。

TypeScriptで書くと、こうなる。

interface CreateUserParams {
  name: string;
  email: string;
  age: number;
}

function createUser(params: CreateUserParams) {
  // ...
}

// 呼び出し側
createUser({ name: "田中", email: "tanaka@example.com", age: 30 });

この書き方には、三つのメリットがある。

まず、意図が明確になる。パラメータ名で意味を表現できるので、何を渡しているかが一目でわかる。IDEの補完も効きやすい。

次に、柔軟に拡張できる。新しいプロパティを追加しても、オプショナルにしておけば既存の呼び出しを壊さない。

そして、型安全性が高まる。TypeScriptならインターフェース、PythonならTypedDictで構造を明示できる。

戻り値についても同じことが言える。特別な理由がなければ、オブジェクトで返すべきだとぼくは考えている。


ただし、注意点がふたつある

この提案を実践するとき、気をつけてほしいことがある。

ひとつめ。プロパティだらけの関数は、そもそも分割すべきだ。

オブジェクトを渡せば順番は覚えなくていい。それはわかった。しかし、プロパティが大量に必要な関数には、別の問題がある。

関数がプロパティを受け取るのは、それを使って何かをするためだ。大量のプロパティを受け取る関数は、たいてい多くの責任を抱えすぎている。

引数をオブジェクトに変える前に、まず問うべきだ。この関数の責務はひとつか。分割して可読性を上げられないか。

ふたつめ。伝わるプロパティ名をつけること。

オブジェクトにしたところで、名前がわかりにくければ意味がない。プロパティ名だけで、何を渡すべきかわかるようにしよう。どうしても誤解を招きそうなら、JSDocで補足すればいい。


よくある反論に答える

ここで、読者からの声が聞こえてきそうだ。

「引数が1〜2個なら、従来どおりでいいのでは?」

たしかに、シンプルな関数では過剰かもしれない。けれども、将来の拡張を考えると、最初からオブジェクトにしておく方が安全だ。とくに公開するAPIでは、後からの変更は難しい。

「オブジェクトを作るオーバーヘッドは?」

モダンなJavaScriptエンジンでは、オブジェクトリテラルの生成は極めて高速だ。ゲームのフレームループのような場面を除けば、影響はほとんどない。可読性のメリットの方が、はるかに大きい。


まとめ

関数の引数は、基本的にひとつでいい。

ただ、その前に考えるべきこともある。プロパティが多すぎる関数には、疑いの目を向けよう。引数なしで呼べるシンプルな関数は、それだけで読みやすい。

責務を適切に分割したうえで、それでも複数のパラメータが必要なら、型定義したオブジェクトを渡そう。

ぼくたちの書くコードは、未来の誰かが読む。その誰かには、数ヶ月後の自分も含まれている。そのとき、引数の順番を思い出そうとして時間を使うのは、もったいない。