JavaScript関数型プログラミングを読む
第一章
関数型プログラミングとは
純粋関数を宣言的に評価することである。純粋関数は、外部から観測可能な副作用を回避することで、不変性を持つプログラミングを生成する。
純粋関数とは
副作用と状態変化を伴わない関数のこと。
参照透過性
純粋関数の特性であり、特定の入力値が渡されたときの戻り値が一定の値となり関数の一貫性が保たれる性質のこと。純粋関数は副作用や状態変化のない関数のため、入力値を与えた時の戻り値は一貫性を持っており参照透過性を持つ。
単一責任の原則
関数は単一の機能を持つべきである
という原則。関数型プログラミングにおけるモジュール化はこの単一責任の原則と密接な関係がある。
関数合成
f ・g = f(g(x))
関数合成とは関数を組み合わせてコードの保守性や再利用性を高めることです。関数型プログラミングにおいて純粋関数をなるべく保つためには小さな関数に分解することが大切であり、小さく分解した関数を合成して使用することが必要となるため関数型プログラミングにおいてこの関数合成は重要な概念である。
高階関数
関数を引数にとる関数のこと。上記にあるように関数型プログラミングにおいて小さな関数を合成して使用することが重要となってくるため、必然的に高階関数が頻出する。
メソッドチェーン
メソッドチェーンしないといけないわけではないが、関数合成をより直感的に表現する手段であり、メソッドチェーンを利用することで中間結果を変数に格納する必要がなくなり、コードの記述量が削減されることもある。
リアクティブプログラミング
イベント駆動、非同期処理のためのアプローチとして用いられるプログラミング手法。リアクティブプログラミングをやることは実質関数型プログラミングをやるようなもの。
第二章 関数型言語としてのJavaScript
- JavaScriptにおけるthisは副作用そのもの
- JavaScriptのObjectを完全に不変にするのは難しい
-
Object.freeze
を使えばObjectのプロパティを不変にできる - ただし、ネストしたプロパティは不変にできないので完全にObjectを不変にするなら再起的にfreezeする。
レンズ(Lenses)
オブジェクトの変更を不変的に一括管理するための関数型のアプローチ。ネストの激しいオブジェクトに不変性を保ったままアクセスできるのがいいところ、らしい。
メリット
- 全てのプロパティに対してセッターを用意するような冗長なことをしなくてよくなる。
- Objectの不変性を保てる。
- ネストが深いプロパティへのアクセスも簡潔に書ける。
JavaScriptではRamda.jsというライブラリがある。
JavaScriptのスコープの話
グローバルスコープと関数スコープとクロージャの話。不変性を保つために関数スコープの中に変数を閉じ込め、クロージャとして扱うことが関数型プログラミングに必要。JavaScriptにはブロックスコープがないけどES6以降、const, letが使えるしTypeScriptもあるのでだいぶましになってる?
第三章 データ構造の数を減らし、操作の数を増やす
- 高階関数map, reduce, filterなどを利用しすることで関数型プログラミングに近づく
- 関数型プログラミングはデータ構造ではなく処理に重きを置いている
- JavaScriptでLodashというライブラリがある。配列などをメソッドチェーンで簡潔に操作するためのライブラリ。
- Lodashの大部分はJavaScriptの標準機能で賄えるようになってるっぽい。
https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore - 関数型プログラミングでデータ構造を処理するのSQLと似ている
- 再起処理も関数型プログラミングにおいて大事。
以下のようにメソッドチェーンのみで書き切るにはLodashの力が必要そう。
const names = ['John', 'Jane', 'Jack', 'Jill', 'alice_aaa']
const newNames = _.chain(names)
.filter(name => name !== undefined && name !== null)
.map(name => name.replace('/_/', ' '))
.uniq()
.map(_.startCase)
.sort()
.value()
.forEach(name => console.log(name))
上記のようにchain
を使用することで余計な変数を宣言することなくメソッドチェーンのみで処理が書ける。(JavaのStream APIはまさにこれで、関数型プログラミングだったんだなと)
関数の遅延評価
上のメソッドチェーンの例で言うとvalue()
が呼ばれるまで関数は実行されないとあるが、ちょっとよくわからなかった。順番に関数が実行されていき実行されている関数の後ろの処理は全て遅延評価されるのはわかった。例えば、mapしてからfilterではなくfilterしてからmapみたいにするとパフォーマンス的によくできるよという話??
再起関数の末尾呼び出し最適化
再起処理は再起処理が深すぎるとメモリを食い潰しスタックオーバーフローする危険性があるよ。それを防ぐために末尾呼び出しをするといいよ。再起処理の末尾呼び出しとは以下のようにreturnで関数呼び出しをそのまま返していること。
末尾呼び出しでない例
function factorial(n: number): number {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
末尾呼び出しの例
function factorial(n: number, acc: number = 1): number {
if (n <= 1) {
return acc;
} else {
return factorial(n - 1, acc * n);
}
}
要はreturnで関数呼び出しの後に処理が続いているとその結果を保持してメモリを食うからみたいな話らしい。
4章 モジュール化によるコードの再利用
アリティとタプル
アリティ
関数の引数の数のこと。関数型言語においてできるだけアリティの少ない単項関数のほうが引数の多い関数よりも扱いやすい。また、関数型において戻り値としてより多くの情報を返したいことがある。そのような時にタプルが使える。JavaScriptにおいてはオブジェクトでも同じようなことができるが、値の不変性などを考えるとタプルの方が好ましい。タプルは関数のアリティを減らす手段だがそれで不十分な場合、関数のカリー化が使える。
カリー化
引数が複数ある関数があるとき、不足した引数で関数を呼び出したとき残りの引数を持つ新たな関数を返すことで処理を先延ばしする。このような関数をカリー化された関数と呼ぶ。
f(a, b, c)が定義されている時
f(a) -> g(b, c)
みたいなイメージ。
関数合成
composeやpipeみたいなのがそう。以下みたいなイメージ。あくまでイメージ
const run = pipe(
map(toLower),
unique(),
sort()
)
pipeは上から順で処理する。composeは下から順に処理が走る。
ポイントフリースタイル
ポイントとは関数の引数のことを指し、関数合成を駆使し新たな関数を作成することで引数の指定が必要なくなってくる。このような開発スタイルのことをポイントフリースタイルと呼ぶ。
const add = (a: number, b: number) => a + b;
これをポイントフリースタイルで書くと
const add = (a: number) => (b: number) => a + b
こうなるらしい。
// 普通の関数
const result = add(2, 3) // 5
// ポイントフリースタイルで書いた関数
const add2 = add(2)
cnnst result = add2(3) // 5
これだけだと旨味が伝わりづらいけど、関数合成を駆使することでこのようなポイントフリーのスタイルになる。
関数コンビネータ
関数型プログラミングは手続型のプログラミングにあるようなif - else
やfor
を使用しない。代わりに、その穴を埋めるようなものとして関数コンビネータ
が存在する。
composeやpipeも関数コンビネータである。
identity
const identity = x => x;
引数の値をそのまま返す
tap
tapはvoid関数を関数合成に組み込む際などに使われ、主にデバッグなどで使われる。tap関数は自らを関数に渡して自らを返す。tapの関数事態が処理の結果に影響を与えることはない。
altanation
関数を複数引数に取り、関数の結果によって返す値を変えるような制御をすることができる。
手続型のif-else
の代わりの役割を果たす。
const input = '{"name":"John","age":30}';
// JSONのパースがもし、失敗してエラーになればInvalid JSONを返す。
const parseJson = R.tryCatch(JSON.parse, R.always('Invalid JSON'));
// parseJsonが成功すれば、パースしたJSONが出力されるし、失敗であればInvalid JSONが出力される。
const result = R.either(parseJson, R.identity)(input);
console.log(result);
sequence
一連の複数の関数をループするために使用される関数です。sequenceコンビネータは2個以上の関数を引数に取り新しい関数を返します。新しい関数は、引数で与えられたすべての関数を同じ値に対して順次実行します。
互いに関連性はあるが独立した一連の処理を実行することが実現できます。seqコンビネータは値を返しません。もし、関数合成に追加したい場合はtapを使用し、他の関数との橋渡しをするようにする必要がある。
第五章 複雑性を抑えるデザインパターン
- 関数型プログラミングでは
try-catch
を使わない。 - エラーを投げることは副作用であり関数合成ができなくなる。
- なのでエラーを投げたいところではnullを返すようにするとかが考えられる。
- ただ、そうなってくると煩雑なnullチェックが扱いづらい。
- では、関数型ではどうするのか?
- エラーを投げるような処理を
try-catch
で囲うのではなく、不純処理ちしてラップする。 - 関数型プログラミングにおいてこの純粋処理と不純処理をうまく分離させることが大事。
ファンクター
値をラップし、値を操作するための方法を提供するオブジェクトのこと。例えば、JavaScriptの配列をmap
で値を取り出すことなく変更することができるが、これはJavaScriptにおける配列がmap関数を持ち関数型プログラミングにおけるArrayファンクター
の一面を持つからである。
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
ファンクターの特徴
- 副作用が存在してはならない。
- 合成可能でなければならない。
モナド
モナドとは抽象的な概念であるため具体的な概念を学ぶと理解が進む。
参考
- Maybe
- Either
- list
- ST
- identity
- Free
- IO
エラー処理やデータベースとのやりとりなどを副作用なく扱うためのものがモナド。値をラップしたり、処理を分岐させたり、エラーなどの不純な処理をラップして遅延させることで純粋関数としてそれらを扱うことができ関数の合成に追加させることができる。という理解