戦略 Scala 日記

素人プログラマの思考のセンス

【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はインタフェースでも構わない
  • 問題点
    • Paymentsをインタフェースにせざるを得ない
      • モック実装は必ずしも使いやすいとは言えない
        • buyCoffeeの呼出し後に調べなければならない内部状態が実装に含まれる
          • chargeの呼び出しによって適切に変更されていることをテストで確認する必要がある
          • モックフレームなどを利用する方法はある
            • buyCofeeがコーヒー1杯分をきっちり請求することをテストするだけならそこまでする必要はない
    • buyCoffeeメソッドの再利用が難しくなる
      • ある1人がコーヒーを複数注文した場合
        • クレジットカードに複数回チャージすることになる
    • つまり副作用がある
      • buyCoffeeテストにクレジットカードサーバーが必要
      • 2つのトランザクションを1つにまとめることが出来ない
  • 解決策
    • コードを再利用できるように
    • 合成ができるようにする
    • チャージの作成をチャージの処理または解釈から切り離す
    • buyCoffeeが副作用を発生させずにChargeオブジェクトを返す
    • テストをしやすい
      • 支払いを処理する仕掛けを切り離している
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杯のコーヒーの購入を実装
    • buyCoffeesbuyCoffeeをベースとした実装
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つのチャージにまとめる
      • クレジットカードの手数料を節約
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)とは無関係
  • 参照透過性(式が参照透過である)
    • プログラムの意味を変えることなく、式をその結果に置き換えることができる

      式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())と同じ振る舞いである
  • 置換モデル
    • 関数が実行するすべてのことがその戻り値によって表される
    • 等価による置換
    • 参照透過性によりプログラムの等式推論が可能になる
  • 合成可能関数

Kindle版も出版されています。
電子書籍で買えばよかった…。