【FPIS】Scala関数型デザイン&プログラミング 読書メモ
出版から半年ほど時間が経ってしまいましたが、「Scala関数型デザイン&プログラミング」を読み進めていく際の自分用のメモを公開しようと思います。
関数型プログラミングとは
副作用のない関数=純粋関数
- 純粋関数だけで構築=関数型プログラミング
- プログラムの書き方を制約する
- 純粋関数だけで構築=関数型プログラミング
副作用のある関数=結果を返す以外の何かをする関数
- 変数を変更
- データ構造を直接変更
- オブジェクトのフィールドを設定
- 例外をスロー、またはエラーで停止
- コンソールに出力、入力を読み取る
- ファイルを読み取る、ファイルに書き込む
- 画面上に描画する
関数型プログラミングの利点
- モジュール性の向上
- バグが発生しづらい
- テスト
- 再利用
- 並列化
- 一般化
- 推論
- バグが発生しづらい
副作用の問題点
- テストしにくい
副作用を持つプログラム
副作用を持つScalaプログラム
class Cafe { def buyCofee(cc: CreditCard): Coffee = { val cup = new Cofee() cc.charge(cup.price) cup } }
- クレジットカードへのチャージ
- クレジットカード会社への問い合わせ
- カード決済の承認・課金
- 決済記録を内部システムに保存
- 問題点
cc.charge(cup.price)
が副作用- クレジットカードへのチャージ
- 外部とのやり取りが必要
- クレジットカードへのチャージ
- 解決策
CreditCard
にクレジットカードへのチャージを組み込まないPayments
オブジェクトを利用
paymentsオブジェクトの追加
class Cafe { def buyCoffee(cc: CreditCard, p: Payments): Coffee = { val cup = new Coffee() p.charge(cc, cup.price) cup } }
p.charge(cc, cup.price)
部分は副作用を持つ- テスタビリティは改善
Payments
はインタフェースでも構わない
- 問題点
- 解決策
class Cafe { def buyCoffee(cc: CreditCard): (Coffee, Charge) = { val cup = new Coffee() (cup, Charge(cc, cup.price)) } }
疑問 cupと、Chargeを返しているけれど、 クレジットカードへのチャージが失敗した場合でも、Coffeeを返してしまう? トランザクションをどのようにはるかが課題
case class Charge(cc: CreditCard, amount: Double) { def combine(other: Charge): Charge = if (cc == other.cc) Charge(cc, amount + other.amount) else throw new Exception("Can't combine charges to different cards") }
- n杯のコーヒーの購入を実装
buyCoffees
はbuyCoffee
をベースとした実装
class Cafe { def buyCoffee(cc: CreditCard): (Coffee, Charge) = ... def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = { val purchaces: List[(Coffee, Charge)] = List.fill(n) (buyCoffee(cc)) val (coffees, charges) = purchases.unzip (coffees, charges.reduce((c1, c2) => c1.combine(c2))) } }
Charge
がどのように処理されるか、Cafe
は全く関知しないCharge
をファーストクラスの値として扱う- 複数の注文をコーヒーショップが1つのチャージにまとめる
- クレジットカードの手数料を節約
- 複数の注文をコーヒーショップが1つのチャージにまとめる
def coalesce(charges: List[Charge]): List[Charge] = charges.groupBy(_.cc).values.map(_.reduce(_ combine _)).toList
疑問 最後にtoListするのはなんで?
関数とはいったい何か
- 純粋関数は推論しやすい
- 入力型A、出力型Bの関数fは、A型のすべての値aをB型の1つの値bに関連付ける
- bがaの値によってのみ決定されるようにする計算
A => B
は単一の型- 内部プロセスまたは外部プロセスの状態は計算結果f(a)とは無関係
- 入力型A、出力型Bの関数fは、A型のすべての値aをB型の1つの値bに関連付ける
- 参照透過性(式が参照透過である)
- プログラムの意味を変えることなく、式をその結果に置き換えることができる
式eがあり、すべてのプログラムpにおいて、pの意味に影響をあたえることなく、p内すべてのeをeの評価結果と置き換えることができるとしたら、eは参照透過です。関数fがあり、式f(x)が参照透過なすべてのxに対して参照透過であるとしたら、fは純粋関数です。
- プログラムの意味を変えることなく、式をその結果に置き換えることができる
参照透過性、純粋性、置換モデル
def buyCoffee(cc: CreditCard): Coffee = { val cup = new Coffee() cc.charge(cup.price) cup }
buyCoffee
が純粋であるためにはp(buyCoffee(aliceCreditCard))
が任意のpに対して、p(new Coffee())
と同じ振る舞いである- しかし、クレジットカード会社に問い合わせが発生している
- 置換モデル
- 関数が実行するすべてのことがその戻り値によって表される
- 等価による置換
- 参照透過性によりプログラムの等式推論が可能になる
- 合成可能関数