『Scala 関数型デザイン&プログラミング』第1章の読書ノートをメモっとく。
第1章 関数型プログラミングとは
第1章では関数型プログラミングの意味と利点が説明されている。関数型プログラミングが初めてな人にとっては聞き慣れない用語が登場する。また、単純な例として、副作用を持つプログラムから、関数型プログラミングによって副作用を排除することが示されている。
本章の目標は用語を整理して理解すること、そして例が示す利点を理解することだ。
用語集
- 関数型プログラミング
- 純粋関数だけを使ってプログラムを構築すること。関数型プログラミングはプログラムの書き方を制約するが、表現可能なプログラムの種類を制約しない。
- 純粋関数 (pure function)
- 副作用のない関数のこと。純粋関数はモジュール性がある。
- モジュール性
- モジュール性があるとテスト・再利用・並列化・一般化・推論が容易になる。
- 参照透過性 (referential transparency)
- どのようなプログラムにおいても、プログラムの意味を変えることなく、式をその結果に置き換えることができること。
- 式
- 1つの結果として評価できるプログラムの任意の部分のこと。
- 置換モデル
- 手順ごとに項をそれに相当するものに置き換えること。
- 等式推論 (equational reasoning)
- 等価による置換に基づいた計算のこと。
副作用の例
- 変数を変更する
- データ構造を直接変更する
- オブジェクトのフィールドを設定する
- 例外をスローする、またはエラーで停止する
- コンソールに出力する、またはユーザー入力を読み取る
- ファイルを読み取る、またはファイルに書き込む
- 画面上に描画する
関数型プログラミングの利点:単純な例
副作用を持つ Scala プログラム
例にコードを追加して実際に動作するようにしてみた。IntelliJ IDEA か Scala IDE for Eclipse の Scala Worksheet に貼り付けて実行してみてほしい。
class Coffee {
val price = 5.0
}
class CreditCard(name: String) {
// Side effect!
def charge(price: Double): Unit = println(s"$name: Charged $$$price")
}
class Cafe {
def buyCoffee(cc: CreditCard): Coffee = {
val cup = new Coffee()
cc.charge(cup.price)
cup
}
}
val cafe = new Cafe
val cc = new CreditCard("JCB")
cafe.buyCoffee(cc) // JCB: Charged $5.0
このコードで重要なのは charge
メソッドが副作用を持つことだ。この例ではコンソールへの出力という副作用で表現しているが、実用的なプログラムではカード会社への問い合わせという副作用が実行されることになる。
ひとつの解決策として Payments
オブジェクトを導入してみる例が次のコードだ。
class Coffee {
val price = 5.0
}
class CreditCard(name: String) {
// Side effect!
def charge(price: Double): Unit = println(s"$name: Charged $$$price")
}
class Payments {
// Side effect!
def charge(cc: CreditCard, price: Double): Unit = cc.charge(price)
}
class Cafe {
def buyCoffee(cc: CreditCard, p: Payments): Coffee = {
val cup = new Coffee()
p.charge(cc, cup.price)
cup
}
}
val cafe = new Cafe
val cc = new CreditCard("JCB")
val p = new Payments
cafe.buyCoffee(cc, p) // JCB: Charged $5.0
副作用はあるものの Payments
オブジェクトをモックオブジェクトにすることで、テスト時のカード会社への問い合わせを避けることができる。
しかし、buyCoffee
メソッドの再利用が難しいという問題は残る。12杯のコーヒーを購入する処理で buyCoffee
メソッドを使用すると、カード会社への問い合わせが12回実行されることになってしまう。12回分の決済手数料が発生すると考えると再利用すべきではないことは明らかだ。
関数型プログラミングのソリューション:副作用の排除
関数型プログラミングのソリューションでは buyCoffee
メソッドがコーヒーと一緒に Charge
オブジェクトを返すようにすることで副作用を排除する。
class Coffee {
val price = 5.0
}
class CreditCard(name: String) {
// Side effect!
def charge(price: Double): Unit = println(s"$name: Charged $$$price")
}
class Payments {
// Side effect!
def charge(cc: CreditCard, price: Double): Unit = cc.charge(price)
}
case class Charge(cc: CreditCard, amount: Double)
class Cafe {
def buyCoffee(cc: CreditCard): (Coffee, Charge) = {
val cup = new Coffee()
(cup, Charge(cc, cup.price))
}
}
val cafe = new Cafe
val cc = new CreditCard("JCB")
val p = new Payments
val (coffee, charge) = cafe.buyCoffee(cc)
p.charge(charge.cc, charge.amount) // JCB: Charged $5.0
これで buyCoffee
メソッドの呼び出しで副作用が発生しなくなり、buyCoffee
メソッドを再利用して buyCoffees
メソッドを実装できるようになる。
class Coffee {
val price = 5.0
override def toString: String = "Coffee"
}
class CreditCard(name: String) {
// Side effect!
def charge(price: Double): Unit = println(s"$name: Charged $$$price")
override def toString: String = name
}
class Payments {
// Side effect!
def charge(cc: CreditCard, price: Double): Unit = cc.charge(price)
}
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")
}
class Cafe {
def buyCoffee(cc: CreditCard): (Coffee, Charge) = {
val cup = new Coffee()
(cup, Charge(cc, cup.price))
}
def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = {
val purchases: List[(Coffee, Charge)] = List.fill(n)(buyCoffee(cc))
val (coffees, charges) = purchases.unzip
(coffees, charges.reduce((c1, c2) => c1.combine(c2)))
}
}
val cafe = new Cafe
val cc = new CreditCard("JCB")
val p = new Payments
val (coffee, c1) = cafe.buyCoffee(cc)
val (coffees, c2) = cafe.buyCoffees(cc, 5)
p.charge(c1.cc, c1.amount) // JCB: Charged $5.0
p.charge(c2.cc, c2.amount) // JCB: Charged $25.0
Charge
オブジェクトをまとめる coalesce
メソッドを追加することも簡単にできる。
class CreditCard(name: String) {
// Side effect!
def charge(price: Double): Unit = println(s"$name: Charged $$$price")
override def toString: String = name
}
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")
}
def coalesce(charges: List[Charge]): List[Charge] =
charges.groupBy(_.cc).values.map(_.reduce(_ combine _)).toList
val amex = new CreditCard("AMEX")
val visa = new CreditCard("Visa")
coalesce(List(Charge(amex, 5.0), Charge(visa, 5.0), Charge(amex, 5.0)))
関数とはいったい何か
参照透過性と純粋性
式 e があり、すべてのプログラム p において、p の意味に影響を与えることなく、p 内のすべての e を e の評価結果と置き換えることができるとしたら、e は参照透過です。関数 f があり、式 f(x) が参照透過なすべての x に対して参照透過であるとしたら、f は純粋関数です。
参照透過性、純粋性、置換モデル
すべての式が参照透過である例。
val x = "Hello, world"
val r1 = x.reverse
val r2 = x.reverse
置換しても結果は変わらない。
val r1 = "Hello, World".reverse
val r2 = "Hello, World".reverse
すべての式が参照透過でない例。
val x = new StringBuilder("Hello")
val y = x.append(", World")
val r1 = y.toString
val r2 = y.toString
置換すると結果が変わる。
val x = new StringBuilder("Hello")
val r1 = x.append(", World").toString
val r2 = x.append(", World").toString
チャプターノート
GitHub にあるチャプターノート Chapter 1: What is functional programming? で推薦されている Why Functional Programming Matters(日本語訳)は一読の価値がある。