JavaScript ( 時々 TypeScript ) で学ぶ関数型プログラミングの基礎の基礎 #4 - 純粋関数について

前置き ( ※ 読み飛ばしていただいても OK )

JavaScript は関数型ライクなエッセンスを一部含んではいるものの、決して Haskell のような純粋関数型言語ではありません。JavaScript では変数やオブジェクトの状態を自由に書き換えるようなプログラミングスタイルを通常としているからです。したがって JavaScript で関数型プログラミングをまともに行うのは本来ナンセンスなのかもしれません。しかし関数型の持つ要素の一部だけを取り入れたプログラミングをすることは可能であり、これらを習得することは大規模かつ堅牢な web アプリケーションを設計するのに少なからず恩恵をもたらします。また、 JavaScript には Underscore.js / LodashImmutable.jsRamda.js といった便利なリスト操作ライブラリや RxJS のような非同期処理ライブラリは、 JavaScript で関数型プログラミングをするうえで強力な手助けとなります。

当初は Ramda.js の入門エントリを書くつもりだったのですが、これを理解するには関数型プログラミングの基礎知識が求められます。そこでいきなり Ramda.js と戯れる前に関数型プログラミングの基礎の基礎について学んでみるとしましょう。

シリーズ一覧

当シリーズは関数型プログラミングの全てを習得することを目的としたものではありません。あくまで JavaScript プログラミングに関数型のエッセンスをほんの少し取り入れるところまでを目的とした入門者向けの内容を目指しています。関数型プログラミングを本格的に学びたいという方は、 素直に Haskell や Lisp などを題材に学習されることを強くおすすめします。

純粋関数の定義

純粋関数型言語では、参照透過性が常に保たれるという意味において、全ての式や関数の評価時に副作用を生まない。純粋関数型言語であるHaskellやCleanは非正格な評価を基本としており、引数はデフォルトで遅延評価される。( 中略 ) たとえば Haskell ではモナド、Clean では一意型という特殊な型を通して一貫性のある表現を提供する。
関数型言語 - Wikipedia

などとありますが、純粋関数を簡単に定義するならこのような感じになります。

  • 同じ入力値を渡せば、決まって同じ出力値が得られる
  • 式や関数の呼び出しをその結果と置き換えたとしても、プログラムの振る舞いが決して変わらない

🤔 なるほど……? ではもう少し詳細に落とし込んでみましょう。だいたいこんな感じでしょうか。

  • 結果は引数として与えられた値からのみ計算される
  • 関数の外部で変更される可能性のあるデータに一切依存しない
  • 関数実行部の外側に存在する何かの状態を一切変更しない

細かく突っ込んでいくとキリがありませんが、多くの場合においてこれら三つの条件をすべて満たしたものを純粋 ( な ) 関数といいます。逆に一つでも満たせていないものは、不純 ( な ) 関数となります。

他にも純粋関数には『必ず戻り値がある』という定義がありますが、JavaScript は return を定義しないと暗黙的に undefined を返すという言語仕様のため、今回はこの件には触れません。

不純性について理解する

以下の JS 標準 API は純粋関数でないものです。これらを含めるとその関数の純粋性を損なう可能性があります ( 関数の不純化 ) 。

Date.now     //=> 実行する度に結果が変わる
Math.random  //=> 実行する度に結果が変わる
console.log  //=> 出力が関数の外部となっている

今度は自分で純粋でないコードを書いてみましょう。以下は純粋でないコードの例です。分かりやすくするために TypeScript で型を明記しています。

let val: number = 0;
function demo() {
  val += 1;
  val *= 2;
}
demo();  //=> 2
demo();  //=> 6
demo();  //=> 14

この例は先ほど挙げた純粋関数の定義の真逆を全て満たしたものです。まず引数を持っていません。そして実行結果が外部のグローバル変数 ( val ) に依存しています。それと同時に実行結果がそのグローバル変数の値 ( 状態 ) を変更しています。今日びここまで露骨なコードはそうそう無いかと思いますが、似たようなプロダクションコードを目にしたことはあるのではないでしょうか。

では次に引数を持つ関数を使った例を見てみましょう。

let PI: number = Math.PI;
function areaOfArticle(radius: number): number {
    return PI * radius * radius;
}
areaOfArticle(3);  //=> 28.274333882308138

areaOfArticle 関数は引数 radius を受取っています。外部にある要素 PI に使っていますが、これに変更を与えるようなことはしていません。一見するとどこにも問題のない純粋な関数のように思えるかもしれません。しかし PI という変数に不備があります。あえて const でなく再代入可能な let で変数定義しました。もしこれに 3 という簡略された円周率が再代入されてしまうと、27 が出力されてしまいます。このように予測できない外部条件に頼る関数は、往々にしてテストをするのが困難になります。なぜなら全てのテストケースは、テスト目的のために全く同じ条件を設定しなくてはならないからです。

これらのように出力結果が外部に依存していて一意でない結果を返す関数を、『副作用』を持つ関数といいます。

純粋な関数に修正する

この例に関して言えば、変数 PI を使わずに areaOfArticle 関数内で Math.PI を直接呼び出せば純粋な関数になります。

function areaOfArticle(radius: number): number {
	return Math.PI * radius * radius;
}

純粋と不純を分離してみる

JS はその言語の特性からして決して完全なる純粋性を持つことはありません。これは厳密な型を持つ TypeScript においても同様であり、FlowType を導入したからといって解消されるものではありません。しかし、書き手の工夫次第で変更の影響を最小限に留めることは可能です。純粋な部分を不純な部分から切り離してコードを再構築すれば良いのです、先ほどの demo 関数を純粋と不純に分解してみましょう。

let val: number = 0;
// 純粋関数
function inc(x: number): number {
  return x + 1;
}
// 純粋関数
function double(x: number): number {
  return x * 2;
}
// 不純な関数
// 結果が外部で変更される可能性のあるデータに依存
function getGlobalVal(): number {
  return val;
}
// 不純な関数
// 外側に存在する変数の状態を変更してる
function updateVal(x: number) {
  val = x;
}

実務でここまでシンプルなものを書くことはそうそう無いと思いますが、先の定義を全て満たす関数がどういうものなのかが想像出来たのではないでしょうか。ひとまずはじめのうちは、それが分かれば充分です。

締め

純粋関数とは、関数自身がコントロールできる範囲の外にある全ての変数に対して一切の変更を加えず、返さず、そして依存することのない関数のことです。また、引数 ( オリジナル ) に変更を加えることも原則としてありませんが、内部変数の変更に必要な場合においては容認しても良いと僕は考えます。

純粋な関数はテストが容易であるだけでなく、コード全体の見通しも良い

当エントリでは関数と純粋と不純とで分離する例を簡単ではありますがご紹介しました。純粋関数は外部の要素に依存せず、決まった引数を渡せばいつでも必ず同じ結果を返します。つまり単体テストが容易ということです。テストコードは基本的に固定値を使って行うのですから当然ですよね。必然と機能もシンプルなものに落ち着きやすくなるため、たとえ数が多くなってもそれぞれの関数を理解するのが簡単になります。それは同時に合成された関数を理解するのも簡単であるということです。

参考

脚注

脚注
1 一部のサンプルコードを参考にさせていただきました 🙇