📕

(読書メモ)Functional Programming In Kotlin - Chapter1

2022/03/05に公開約3,300字

読んだ本

状態を極力イミュータブルに扱って副作用を減らした実装をすることがとても大事だなと感じ始めているので今更関数型プログラミング勉強メモ

https://www.manning.com/books/functional-programming-in-kotlin

関数型プログラミングの特徴

命令型プログラミングはコードがシンプルなうちは問題ないが
量が増え規模が大きくなってくると複雑でメンテナンスがし辛いものになる
どこかのコードがなにかしらの値の参照を暗黙的に書き換えていたり、コードの実行順番によって結果が変わったり
これらの副作用の根絶は関数型プログラミングの背後にあるコアコンセプトの1つ

副作用を持つ関数とは

単純に値を返却すること以外の処理をしている関数

  • 変更が発生するブロックの範囲を超えて変数を変更する
  • データ構造の変更
  • オブジェクトにフィールドをセットする
  • 例外を投げる、もしくはエラーで止める
  • コンソールへの印刷またはユーザー入力の読み取り
  • ファイルの入出力
  • 画面への描画

副作用のあるコード

class Cafe {
    fun buyCoffee(cc: CreditCard): Coffee {
        val cup = Coffee()
        cc.charge(cup.price)
        return cup
     }
}

問題点

  • この関数でやりたいことはcupを返すだけだが、一方でcc.chageでクレジットカード会社との通信やトランザクション検証などが走っている<-副作用
  • テスタビリティが低い

解決案(不完全)

  • クレジットカードが決済処理を持つべきではないので、Paymentsに切り出しモジュラリティを向上する
class Cafe {
    fun buyCoffee(cc: CreditCard, p: Payments): Coffee {
        val cup = Coffee()
        p.charge(cc, cup.price)
        return cup
    }
}
  • テスタビリティは多少上がったが依然として副作用はある
  • Paymentsをmockにすればテストができそうだがそれ以外の具象クラスに問題がない場合この1つのメソッドをテストするためだけに
    Paymentsのmockを作成するのは面倒かつやりすぎ

副作用以前のコードの問題

  • Coffeeを1回の注文で10杯頼みたいとなった場合に決済処理が10回走ってしまう

副作用のないコード

  • 請求の作成と実際の請求処理を分ける
class Cafe {
    fun buyCoffee(cc: CreditCard): Pair<Coffee, Charge> {
        val cup = Coffee()
        return Pair(cup, Charge(cc, cup.price))
    }
}

複数の請求を1つにまとめる

data class Charge(val cc: CreditCard, val amount: Float) {
    fun combine(other: Charge): Charge =
        if (cc == other.cc)
            Charge(cc, amount + other.amount)
        else throw Exception(
            "Cannot combine charges to different cards"
        )
}

全体のコード

  • Cafeがどのように請求の決済が行われるのか気しなくて良くなった
  • mockを使用せずとも関数のテストが可能になった
class Cafe {
    fun buyCoffee(cc: CreditCard): Pair<Coffee, Charge> = TODO()
 
    fun buyCoffees(
        cc: CreditCard,
        n: Int
    ): Pair<List<Coffee>, Charge> {
        // 注文分のCoffeeの請求を作成する
        val purchases: List<Pair<Coffee, Charge>> =
            List(n) { buyCoffee(cc) }
        // 分解宣言で取り出す
        val (coffees, charges) = purchases.unzip()
        // 1つの請求先とそれに紐づくCoffeeを返却する
        return Pair(
            coffees,
            charges.reduce { c1, c2 -> c1.combine(c2) }
        )
    }
}

自分で実装してみたメモ

  • 上記にPaymentMethodを追加してみた
sealed interface PaymentMethod {
    object CreditCard: PaymentMethod
    object CreditCard2: PaymentMethod
}

val charges = listOf(
        Charge(PaymentMethod.CreditCard, 100f),
        Charge(PaymentMethod.CreditCard2, 100f),
        Charge(PaymentMethod.CreditCard, 100f),
        Charge(PaymentMethod.CreditCard2, 100f),
    ).coalesce()
    print(charges)

// [Charge(cc=PaymentMethod$CreditCard, amount=200.0), Charge(cc=PaymentMethod$CreditCard2, amount=200.0)]

純粋関数と参照透過性

純粋関数

  • 入力が同じであればいつ何回実行しても返り値が変わらない関数
// aとbの値が同じであれば結果も同じなので純粋関数
fun add(a: Int, b: Int) = a + b
  • 入力が同じなのに結果が違うのでStringBufferのappendは純粋関数ではない
val x = StringBuffer("Hello ")
val y = x.append("World!").toString()
val z = x.append("World!").toString()
// y = Hello World!
// z = Hello World!World!

参照透過性

  • 式の構成要素がすべて同じなら式の値は常に同じになること
    • 2 + 3は常に5なので参照透過性がある
  • eをプログラムp内のeの実行箇所で式eの実行結果に置き換えてもpの結果に影響を及ぼさないのであれば式eは参照透過性がある

詳しくは以下のサイトが参考になった

http://web.sfc.keio.ac.jp/~hattori/prog-theory/ja/11.html

Discussion

ログインするとコメントできます