JavaScriptに関数型プログラミングでアプローチするメリットは、小さくて理解しやすい個々の関数を用い、複雑な関数を構築できることです。しかし、もっともエレガントなソリューションを見出すには、ときには逆の方向から問題を見ることが必要になります。
本記事ではJavaScriptの関数合成について分析し、それによってなぜ判断が簡単でバグが少なくなるのか、段階的なアプローチで説明します。
ネスト関数
関数合成は、複数の単純な関数を受け取り、指定した論理的順序でサブ関数を実行する1つの複雑な関数にまとめる手法です。
結果を得るには、1つの関数を別の関数にネストし、結果を生成するまで、外部関数の操作を内部関数の結果上で繰り返し実行します。結果は、適用された関数の順序によって異なります。
これは以下のように、JavaScriptでよく見られる関数呼び出しを別の関数への引数として渡すプログラミング手法を用いて簡単に実証できます。
function addOne(x) {
return x + 1;
}
function timesTwo(x) {
return x * 2;
}
console.log(addOne(timesTwo(3))); //7
console.log(timesTwo(addOne(3))); //8
上のケースでは、値に1を追加するaddOne()関数と、値に2をかけるtimesTwo()関数を定義しました。関数の結果をほかの関数の引数として渡すことで関数をネストして、同じ初期値でも異なる結果を生成できます。まず内部関数が実行され、その結果が外部関数に渡されます。
命令型の関数合成
同じ手順の操作を繰り返し実行したい場合、最初の関数を自動的に適用してから別の小さな関数を適用する、新しい関数を定義すると便利です。以下に紹介します。
// ...previous function definitions from above
function addOneTimesTwo(x) {
var holder = x;
holder = addOne(holder);
holder = timesTwo(holder);
return holder;
}
console.log(addOneTimesTwo(3)); //8
console.log(addOneTimesTwo(4)); //10
上のケースでは、2つの関数を特定の順序で、手動で合成しています。まずホルダー変数に渡されている値を割り当て、次に最初の関数を実行して値を更新し、そのあと2番目の関数を実行し、最後にホルダー値を返す新しい関数を作成しました。
注意:渡す値を一時的に保持するholderと呼ばれる変数を使用していることに注意してください。このような単純な関数では追加のローカル変数が冗長に見えますが、命令型JavaScriptでも、関数に渡される引数の値を定数のように扱うことをおすすめします。修正はローカルで実行できますが、関数内の異なる段階で呼び出されたときに、引数の値がわからず混乱を招きます。
同様に、2つの小さな関数を逆の順序で適用する別の新しい関数を作成したい場合は、次のように記述します。
// ...previous function definitions from above
function timesTwoAddOne(x) {
var holder = x;
holder = timesTwo(holder);
holder = addOne(holder);
return holder;
}
console.log(timesTwoAddOne(3)); //7
console.log(timesTwoAddOne(4)); //9
当然、このコードは繰り返しが多いように見えます。合成した2つの新たな関数は、呼び出した2つの小さな関数が実行される順序以外はほぼ同じです。DRYの原則(Don’t Repeat Yourself:「重複を避ける」の意味)を守る必要があります。また、こういった値を変更する一時的な変数の使用は、作成中の合成した関数の内側に隠されているとしても、まったく機能的ではありません。
結論を言うと、もっといい方法があります。
関数合成関数を作成する
既存の関数を受け取り、任意の順序で合成できる関数合成関数を作成します。毎回、内部で遊ぶことなく一貫性のある方法で作成するには、引数として関数に渡す順序を決める必要があります。
選択肢は2つあります。引数はそれぞれ関数となり、左から右、あるいは右から左へ実行します。つまり、これから説明する新しい機能を使用すると、compose(timesTwo, addOne)は引数を右から左へ読み込むtimesTwo(addOne())、あるいは引数を左から右へ読み込むaddOne(timesTwo())を意味します。
引数を左から右へ実行するメリットは、英語の読み方と同じ方向だということです。加算の前に乗算が起こることがわかるように、合成した関数にtimesTwoAddOne()と名づけました。クリーンで読みやすいコードを論理的に名づけることは重要です。
引数を左から右へ実行するデメリットは、実行する値が最初に来る必要があることです。しかし、値を最初に置くと、結果の関数を他の関数と合成する利便性を失います。動画「Hey Underscore, You’re Doing it Wrong」ではこのロジックの背後にある考え方を説明しています(ただし、いまではlodash-fpやRamdaなどの関数型プログラミングのライブラリーと一緒にUnderscoreを使う場合は、Brianが話している関数型プログラミング問題の対処に役立つ、Underscoreのfpオプションがあります)。
ともかく、本当にしなければならないのは、最初にすべてのコンフィギュレーションデータを渡し、最後に実行する値(複数可)を受け渡すことです。つまり引数を右から左へ読んで適用するための関数合成関数を定義することがもっとも理にかないます。
次の基本的なcompose関数を作成します。
function compose(f1, f2) {
return function(value) {
return f1(f2(value));
};
}
上の非常に単純なcompose関数を用いると、以前の複雑な関数が簡単になり、結果は同じであることが分かります。
function addOne(x) {
return x + 1;
}
function timesTwo(x) {
return x * 2;
}
function compose(f1, f2) {
return function(value) {
return f1(f2(value));
};
}
var addOneTimesTwo = compose(timesTwo, addOne);
console.log(addOneTimesTwo(3)); //8
console.log(addOneTimesTwo(4)); //10
var timesTwoAddOne = compose(addOne, timesTwo);
console.log(timesTwoAddOne(3)); //7
console.log(timesTwoAddOne(4)); //9
この単純なcompose関数は便利なものの、柔軟性と適応性に起因する多くの問題は考慮されていません。たとえば、2つ以上の関数を合成したいことがあります。また、進行中にthisを追跡できなくなります。
関数合成がどのように機能するか把握せずとも、これらの問題を解決することは可能です。おそらく、デフォルトで引数の順序を右から左へ構成するRamdaなどの外部の機能的なライブラリーからの、より安定したcomposeを継承する方が、自作するより生産的でしょう。
型に関する責任
次の関数によって正しく取り扱われるように、合成されている各関数から返される型を知ることはプログラマーの責任だと心に留めておくことが重要です。型を厳密に判別する純粋な関数型プログラミング言語とは異なり、JavaScriptは不適切な型の値を返す関数を合成しようとするのを防げません。
受け渡す数にも制限はなく、1つの関数から次の関数へ、同じ型の変数を維持することへの制限もありません。しかし、合成している関数に、以前の関数からどんな値が返されても対処する準備があることを確認する責任があります。
オーディエンスを意識する
将来的に、あなたのコードをほかの誰かが使用したり変更したりする可能性があります。従来のJavaScriptコードの内部構成を使用すると、関数型パラダイムに慣れていないプログラマーには複雑に見えます。目標は、読んだり維持したりすることが簡単でクリーンなコードです。
ES2015構文の出現で、シンプルに合成した関数であれば、次のようにアロー関数を使用した特別なcomposeメソッドを使用せずとも1行での呼び出し作成できます。
function addOne(x) {
return x + 1;
}
function timesTwo(x) {
return x * 2;
}
var addOneTimesTwo = x => timesTwo(addOne(x));
console.log(addOneTimesTwo(3)); //8
console.log(addOneTimesTwo(4)); //10
今日から始めよう
関数型プログラミングの手法と同じく、合成した関数は純粋である必要があると覚えておいてください。手短に言うと、毎回特定の値が関数に渡され、その関数は同じ結果を返し、そして関数自体以外に値を変更する副作用が生じないことを意味します。
データに適用させたい関数のセットがある場合、関数合成におけるネストは大変便利で、その関数のコンポーネントを再利用可能かつ合成しやすい関数に分解できます。
慣れるために、関数型プログラミングの手法と同じく、既存のコードに関数合成を慎重に散りばめることをおすすめします。正しくできれば、結果はよりクリーンで簡潔で読みやすいコードになります。それは、私たちみんなが欲しているものではありませんか?
※本記事はJeff Mott、Dan Prince、Sebastian Seitzが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Function Composition: Building Blocks for Maintainable Code)
[翻訳:柴田理恵/編集:Livit]