第2章: ファーストクラス関数
簡単なおさらい
関数が「ファーストクラス」であるとは、他のデータ型と全く同等であることを意味します。つまり通常のクラスと同じように扱えます。関数を他のデータ型と同様に操作可能で、特別な扱いは不要です。配列に格納したり、関数の引数として渡したり、変数に代入したりすることができます。
これはJavaScriptの基礎知識ですが、GitHubでコード検索すると多くの開発者がこの概念を回避している、あるいは無知であることが分かるため再確認が必要です。模擬例を見てみましょう。
const hi = name => `Hi ${name}`;
const greeting = name => hi(name);ここでのgreeting内のhiをラップする関数は完全に冗長です。なぜならJavaScriptの関数は呼び出し可能だからです。hiの後に()があれば実行されて値を返し、なければ変数に格納された関数を返します。実際に確認してみてください:
hi; // name => `Hi ${name}`
hi("jonas"); // "Hi jonas"greetingが単に同じ引数でhiを呼び出しているだけなら、次のように簡略化できます:
const greeting = hi;
greeting("times"); // "Hi times"つまりhiは既に1つの引数を期待する関数なのに、なぜ同じ不要な引数でhiを呼び出す別の関数で包む必要があるのでしょうか?7月の真夏日に分厚いダウンジャケットを着てエアコンをつけながらアイスを求めるような矛盾した行為です。
評価を遅延させるためだけに関数を別の関数で囲むのは冗長的で、保守性の問題を引き起こす悪いプラクティスです(詳細は後述)。
次に進む前にこの概念を完全に理解する必要があるため、npmパッケージから発掘した面白い例をさらに検討しましょう。
// ignorant
const getServerStuff = callback => ajaxCall(json => callback(json));
// enlightened
const getServerStuff = ajaxCall;次のようなAJAXコードは至るところで見かけます。これらが等価である理由は:
// this line
ajaxCall(json => callback(json));
// is the same as this line
ajaxCall(callback);
// so refactor getServerStuff
const getServerStuff = callback => ajaxCall(callback);
// ...which is equivalent to this
const getServerStuff = ajaxCall; // <-- look mum, no ()'sこれが正しい実装方法です。理解を深めるため、もう一度この原則を強調します。
const BlogController = {
index(posts) { return Views.index(posts); },
show(post) { return Views.show(post); },
create(attrs) { return Db.create(attrs); },
update(post, attrs) { return Db.update(post, attrs); },
destroy(post) { return Db.destroy(post); },
};この不必要なコントローラは99%が無駄なコードです。次のように書き換えることが可能です:
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
};...あるいはViewsとDbをまとめているだけなら完全に廃棄しても構いません
なぜファーストクラスが望ましいか?
ファーストクラス関数を推奨する理由を考察しましょう。getServerStuffとBlogControllerの例から分かるように、付加価値のない間接層を追加すると、保守や検索が困難な冗長コードが増加します。
さらに、不要なラッパー関数を変更する必要がある場合、外側の関数も変更しなければなりません。
httpGet('/post/2', json => renderPost(json));httpGetがerrを送信するよう変更された場合、接着剤コードも修正する必要があります:
// go back to every httpGet call in the application and explicitly pass err along.
httpGet('/post/2', (json, err) => renderPost(json, err));ファーストクラス関数で実装すると、変更箇所が大幅に減少します:
// renderPost is called from within httpGet with however many arguments it wants
httpGet('/post/2', renderPost);不要な関数を削除する他に、引数の命名と参照にも注意が必要です。命名はコードベースの経年変化や要件変更に伴い、誤解を招く要因となる可能性があります。
同じ概念に複数の名前を使うとプロジェクトで混乱が生じます。汎用コードの問題もあります。例えば以下の2つの関数は同じ処理ですが、後者の方が汎用性が高いです:
// specific to our current blog
const validArticles = articles =>
articles.filter(article => article !== null && article !== undefined),
// vastly more relevant for future projects
const compact = xs => xs.filter(x => x !== null && x !== undefined);具体的な命名(この場合articles)を使うと、特定のデータに縛られているように見えます。これは繰り返し発生する問題の原因となります。
オブジェクト指向コードと同様、thisが重大な問題を引き起こす可能性に注意が必要です。内部関数がthisを使用している場合、ファーストクラス関数として呼び出すと抽象化漏れの危険にさらされます。
const fs = require('fs');
// scary
fs.readFile('freaky_friday.txt', Db.save);
// less so
fs.readFile('freaky_friday.txt', Db.save.bind(Db));自己バインド後、Dbはプロトタイプチェーンの問題コードに自由にアクセスできます。私はthisを汚いおむつのように避けます。関数型コードでは必要ありませんが、外部ライブラリとの連携時には妥協が必要な場合もあります。
thisが速度最適化に必要と主張する人もいます。マイクロ最適化を追求する方には本書を閉じることをお勧めします。返品できない場合は、より複雑な本と交換するのがよいでしょう。
これで次の章に進む準備が整いました。