「関数型プログラミングとは?」& 重要ワードの用語解説
関数型プログラミングとは?
関数型プログラミングとは、プログラムを関数の集まりとして捉えるプログラミングスタイルのことです。
関数型プログラミングとよく対比されるのが、オブジェクト指向プログラミングですが、
- 「これからは関数型プログラミングが主流になる」「これだけやっておけば大丈夫」
- 「オブジェクト指向こそ現実をシステム化する唯一の手法」
のような極論を気にせず、自身で考えてそれぞれのメリットを享受していけると良いでしょう。
なぜ関数型プログラミングを学ぶのか?
基本的な概念を知りコードの質を上げるということが一つの理由になると思うのですが、 特段フロントエンドでは、「関数型の思想にそった実装をする機会が多い」のも関数型を学ぶ理由の一つになります。
現在、フロントエンドを語る上で React は外せない存在になっていますが、React は関数型プログラミングの考え方に影響を受けた設計がされています。 React の中心には 「状態」 という概念があるのですが、この状態は イミュータブル である必要があり、 「状態」 に対して破壊的変更を加えることは許されません。
現時点ではこれらの文が何を言っているのか理解できないかもしれませんが、重要なキーワードは以降で説明しますので、この辺りを理解して実際に各コードを書く際に意識できると良いでしょう。 ここに書いた以外にも、関数型プログラミングを学ぶことで以下のようなメリットがあります。
見通しの良いコードがかけるようになる
まず、関数型プログラミングを学ぶ時に一番重要な概念が 「参照透過性」 なのですが、これは
- 関数に対しては「同じ引数を渡すと同じ結果が返ってくる」
- 変数に対しては「最初に定義した値は常に同じである」
という性質のことです。
要は一度宣言したらそのままで、何度参照しても同じ結果が返ってくるということがわかっているので見通しの良いコードとなります。
テストのしやすいコードになる
関数型プログラミングは、「同じ引数を渡すと同じ結果が返ってくる」という性質があるため、テストがしやすいコードを書くことができます。
プログラミングにおける関数は、引数が入力で返り値が出力となるため、それ以外に影響を及ぼす要素がないため、テストがしやすいのです。
これ以外の参照透過性のない関数は、引数と返り値以外に「実行する環境」など関数外部の要素を気にしながらテストを書かないといけないのでテストがしづらいです。
並列処理において強みを発揮する
関数型プログラミングは、変数の参照透過性を守りながらプログラミングを行うので並列処理でも問題が起きづらいです。
JavaScript はシングルスレッドで動作する言語なので恩恵を受けることが少ないですが並列処理が可能な他言語では、関数型プログラミングの恩恵を享受できます。
並列処理時にそぞれのスレッドで同じ変数を参照していると、変数の値が書き変わってしまっていて予期せぬ結果が返ってくるといったことがあるのですが、 関数型プログラミングでは一度宣言した変数を変更せずに処理するので、このような問題が起きません。
関数型プログラミングを学ぶ上で押さえておきたいキーワード
関数型プログラミングを理解するにはいくつかのキーワードを押さえておく必要があるので、以下で紹介します。
データと振る舞いの分離
関数型プログラミングする前に、データと振る舞いの分離という概念を理解して置く必要があります。
まず「データ」というのは、プログラミングの中では変数や定数などの値のことを指します。 一方で「振る舞い」というのは、関数等のロジックのことを指します。
関数型プログラミングでは、パイプライン処理と言ったりもしますが、データを関数に次々と渡して最終的に欲しいデータを得ます。
ショートケーキを作る例で説明すると、スポンジ、クリーム、イチゴ、葉っぱを順番に重ねていくという処理は以下の様になります。
const sponge = ['sponge']
const shortcake = pipeline(sponge)
.process(put('cream'))
.process(put('strawberry'))
.process(put('leaf'))
console.log(shortcake.payload) // ['sponge', 'cream', 'strawberry', 'leaf']
後で関数の中身は紹介しますが、実行部分だけみるとこの様になります。(*1)
最初のデータはスポンジだけですが、それを次々と関数に渡していき、最終的にショートケーキができあがります。 (put 関数は「関数を返す関数」で、process関数でそれぞれの素材をスポンジに追加していっています。 )
この様に、この様に、関数型プログラミングでは次々とデータを関数に渡していくことで、最終的なデータを得るので「データ」と「振る舞い」を分離してコードを作っていきます。
参照透過性
繰り返しにはなりますが、 参照透過性 とは
- 関数に対しては「同じ引数を渡すと同じ結果が返ってくる」
- 変数に対しては「最初に定義した値は常に同じである」
という性質のことです。
関数に関しては、例えばその関数が現在日付を返すような関数では、それは「いつ実行したか」「どこで実行したか」というような関数以外の部分で結果が異なります。
const getDateTime = () => {
return new Date();
}
この関数は、実行するたびに返す結果が異なりますし、実行するタイムゾーンでも結果が異なります。
また、関数型プログラミングは、「再代入」 を行わないことが求められます。 代入という処理は一度宣言した変数を変更する処理ですが、これは変数の参照透過性を破壊します。
let a = 1;
a = 2;
例のプログラムは、短いので変数の中身が 2 であることは自明ですが、プログラムが大きくなると、どこで変数が変更されたのかがわからなくなります。 そのほか for 文でのループ処理ではインクリメント変数が変わるため、参照透過性が保たれない例の一つになります。
for (let i = 0; i < 10; i++) {
console.log(i);
}
これらのプログラムは常に、変数の中身がどうなっているか気にしながらプログラムしたり、コードを読まないといけないので認知負荷が高くなってしまいます。
副作用
関数は基本的に入力と出力から成り立つもので、返り値を返すということが主目的になりますがそれ以外の操作は 副作用 と呼ばれます。
具体的には、
console.log
での出力document.getElementById
での DOM 操作- インスタンスの初期化
- ファイルの読み込み・書き込み
- データベースの読み書き
などが副作用になります。
副作用は関数の参照透過性を破壊するので、関数型プログラミングでは副作用を最小限に抑えることが求められます。 とはいえ、アプリから完全に副作用を排除することは不可能なので、副作用のある部分と純粋な関数を分離して整理することが重要です。
破壊的変更
JavaScript にはレシーバーの値を変更してしまうメソッドがいくつか存在します。
const a = [1, 2, 3]
a.push(4)
console.log(a) // [1, 2, 3, 4]
レシーバーとは、メソッドを呼び出すオブジェクトのことで、上記のコードでは a.push
のドットの前の部分 a がレシーバーに当たります。
この例では、console.log
での出力でわかるように a 変数自体が変更されてしまっており、これを 破壊的変更 と呼ばれます。
関数型プログラミングでは、破壊的変更 を避けることが求められるので、このようなメソッドを使うことは避けるべきです。
push メソッド以外にも JavaScript にはいくつか破壊的変更を行うメソッドが存在するので注意しましょう。
配列に対しては下記が 破壊的変更 を行うメソッドなので関数型プログラミングを実践する際には気をつけましょう。
- push
- pop
- shift
- splice
- sort
イミュータブル
イミュータブル は、immutable という英単語の名前の通り、不変なという意味です。 言い換えると参照透過性を保つということになります。
例えば、破壊的変更のパート部分では、 [1, 2, 3] という配列から [1, 2, 3, 4] の様な配列を得たいユースケースでした。 これをイミュータブルに保ちながら、元の配列を変更せずに新しい配列を受け取る場合は、下記のようにスプレッド構文で実現できます。
const a = [1, 2, 3]
const b = [...a, 4]
console.log(b) // [1, 2, 3, 4]
そのほか concat メソッドを使うことでも同じことが実現できます。 オブジェクトに関してもスプレッド構文を使うことでイミュータブルな操作が可能なので、同様に覚えておけると良いでしょう。
純粋関数
純粋関数とは、文字通り副作用のない関数のことで、
「同じ引数を渡すと同じ結果が返ってくる」
関数のことです。
二つの引数の合計を返却する関数は純粋関数です。
const add = (a, b) => {
return a + b;
}
一方、
- 返り値のない関数
- 引数のない関数
- 標準入出力を行う関数
- データベースアクセスを伴う関数
- グローバル変数を参照する関数
- HTTPリクエストを行う関数
などは純粋関数ではないことになります。
ちなみに、返り値のない関数や、引数のない関数は副作用のある関数であるサインになりますので一つの目安にすると良いでしょう。
高階関数 (Higher Order Function)・コールバック
関数を返り値として返す関数のことです。 関数型プログラミングを実践する上で多用するコーディングパターンなので覚えておけると良いです。
また、コールバック関数とは、関数を引数に受け取る関数のことです。
JS では、
- forEach
- filter
- map
- reduce
など普段からよく使うメソッドがコールバック関数になるのでこれらをスムーズに使いこなせることも重要です。
まとめ
ここまでで、関数型プログラミングの基本的な概念を紹介してきましたが、関数型プログラミングを学ぶことで新たな視点が身につき自身のコードの質をあげることにも役立ちますのでこれをきっかけに関数型プログラミングを実践できると良いでしょう。
最後に、関数型プログラミングを実践する上で注意するべきことを紹介して終わります。
再代入・破壊的変更を避ける
これは繰り返し言及していることですが、関数型プログラミングでは、代入等の破壊的変更を避け各値をイミュータブルに保つことに努めましょう。
これを実践するためには、破壊的変更メソッドやイミュータブルなデータ操作への基本的な理解が必須なのでそれらを使いこなした上で実践していきましょう。
副作用をまとめる
上で、副作用をまとめるということを説明しましたが、意識をしないと各関数から副作用を排除することは難しいです。
- 「自分が書いている関数が副作用を持っているか?」
- 「自分が書いている関数から副作用を排除できるか?」
と言ったことを意識してコードを書きましょう。
副作用を排除できる部分から副作用を排除していくことで、自然と副作用のある関数が減りまとまっていきます。 まずは副作用と純粋関数を分離していくことから始めましょう。
副作用と純粋関数をうまく分離できるようになると、うまくテストが用意で依存性の低い純粋関数を抽出することができコード全体の質を高めることができます。 このあたりが関数型プログラミングを実践する旨味でもあると思うのでぜひ実践して頂きたいです。
注釈
*1. 下記それぞれ pipeline, put 関数のコードです。
function pipeline(payload) {
return {
payload,
process: function (cb) {
const _payload = cb(payload)
return {
payload: _payload,
process: pipeline(_payload).process
}
}
}
}
const put = something => payload => {
return [...payload, something];
}