なっとく!関数型プログラミング 学習メモ
純粋関数のルール
ルール(復習時の確認用のためトグルにする)
- 戻り値は1つのみ
- 引数にのみ基づいて戻り値を計算
- 既存の値を変更しない
命令型のプログラムを関数型プログラミングに直すぞーって書き換えたコードが純粋関数になっていなかった笑
純粋関数が非純粋関数になってしまう原因
引数が共有ミュータブル状態であること
共有ミュータブル状態とは、
- 共有:どこからでもアクセスできる
- ミュータブル:変更可能
- 状態:コードからアクセスできる値
である。
命令型プログラミングの要素。
共有ミュータブル状態であると、認知負荷や思いもよらないところでのバグの原因となったりするため、変更容易性が低下する。
ミュータブルな値が渡ってきたとしても、引数のコピーを関数内で操作し、返却することで既存の値の変更を防ぐ。
関数型プログラミングでは、イミュータブルな値を操作する純粋関数を使う。
StringのAPIをイメージするとわかりやすい。
カリー化って関数型プログラミングで出てくるテクニックだったのか。
前見た時はよくわからなかったけど、段階的に見ていくと理解できてきた。
実習があるのがめっちゃ助かる。
関数を返す関数を返す関数を返す関数・・・ってマトリョーシカみたいだな。
for内包表記(入れ子のflatMapを見やすくした書き方)見やすいな
関数型プログラミングでは、処理実行時に例外が発生する処理を、処理できた場合に返す値、処理できなかった場合に返す値にを返すように変更する。
例外を発生する処理の場合、その処理ごとにtry catchを書く必要があり、コードの可読性が悪くなる。
一旦、処理できなかったものがあった場合は問答無用で処理できなかった場合に返す値、処理できた場合は処理できた値を返すようにする。
この場合、処理できなかった場合に何が原因で処理ができなかったのかが全くわからない。
この点を解消する必要がある。
Eitherを使用することで何が原因で処理ができなかったのかを呼び出し元へ伝達できる。
シグネチャが嘘をつかないって考え方の良さを実際に嘘をついているものを見たことで再確認できた。
使用箇所でどんな値が返却されるかイメージできなくて実装を見に行かないといけなくなるっていうのはほんとにその通りだった。
newtype
プリミティブ型に別名をつけてラップした型。TSのtypeエイリアスというとイメージしやすい。
これによって、プリミティブ型のパラメータの時に発生していた、関数のパラメータの順序間違えがなくなる。
ADT(代数的データ型)
-
直積型
パラメータの組み合わせを1つの型とし、意味のある概念を表現する。
直積型で表現した型をさらに直積型で使用し、表現したい概念を定義する。
これによってパラメータの組み合わせによる意味を型から推測できるようになる。 -
直和型
有限集合であるパラメータを型の中で定義し、それ以外の値を使用できないようにできる。
これにより、型の情報から有効な値を知ることができる。
IO処理をどう扱うか
ScalaではIO型が用意されており、非純粋関数はこの型を返却型とすることで、その関数が非純粋関数であることを明示する。
IO[A]は、A型の値を計算する処理を表現しているだけなので、その計算結果の値のAを取得するには最終的に実行する必要がある。実行されるまでは何も起こらない。
IO型は、サブタイプの中にPureとDelayがある。PureはIOの中身が実行される前に評価される(先行評価)が、Delayは遅延評価される。
処理が失敗した場合は例外を発生させず、orElseを使って失敗時の処理を記述できる。
orElseの記載箇所を変えることでどこでリカバリを行うか柔軟に対応できる。
これによって特定の処理が失敗した場合に関数全体を失敗させないようにできる。
また、引数として非純粋関数を渡すようにすれば、IOで非純粋処理をラップしている場合にはできなかった純粋関数の部分と非純粋関数の部分を分離することができる。
引数として受け取った純粋関数は、値として非純粋関数を扱うことができる(例外時のハンドリングは非純粋関数側に寄せれる)。
データストリームの扱い方
Streamを使うことで無限に続く可能性のあるデータストリームを扱う。
ScalaのStreamは遅延評価されるため、使用側で実行するまで処理されない。KotlinのFlowに似ている。
これにより、関心事の分離ができる。つまり、ビジネスロジックに集中できる。