関数型プログラミングの基礎
- 関数型プログラミング
- 副作用の発生の阻止
- 非正格性
副作用とは
- 変数の変更
- データ構造を直接変更する
- オブジェクトのフィールドを設定する
- 例外をスローする、またはエラーで停止する
- コンソールに出力する またはユーザー入力を読み取る
- ファイルを読み取る ファイルに書き込む
- 画面上への描画
純粋関数にはモジュール性があり、モジュール性が向上することでテストや再利用が簡単になります。
副作用のあるやつ
戻り値以外に処理が発生している
class Cafe { def buyCoffee(cc: CreditCard): Coffee = { val cup = new Coffee() cc.charge(cup.price) // ココが副作用 cup } }
モジュール性とテスタビリティ向上の例(まだ副作用があるけど、テスタビリティは上がった)
class Cafe { def buyCoffee(cc: CreditCard, p: Payments): Coffee = { val cup = new Coffee() cc.charge(cc, cup.price) cup } }
Chargeオブジェクトを返すようにして、実際のチャージ処理は別のクラスでやらせることで、副作用をなくし、トランザクションを1つにまとめている
class Cafe { def buyCoffee(cc: CreditCard): (Coffee, Charge) = { val cup = new Coffee() (cup, Charge(cc, cup.price)) } } // プライマリコンストラクタを1つもち、引数リストのパラメータはclassのイミュータブルなパブリックフィールドになる 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杯のコーヒーの購入を実装する
class Cafe { def buyCoffee(cc: CreditCard): (Coffee, Charge) = ... def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = { val purchases: List[(Coffee, Charge)] = List.fill(n)(buyCoffee(cc)) .... } }
関数型プログラミングを使えば、テスタビリティや再利用の・においても良い方法である単純な例を示した。
println(List(1,2,3,4,5).groupBy(n => n%2)) // Map(1 -> List(1, 3, 5), 0 -> List(2, 4))
関数型プログラミングとは何か
- 純粋関数とは副作用を持たない関数のこと
- 純粋関数とは推論しやすいこと
関数がプログラムの実行に対して観察可能な効果を与えることはない
関数 プロシージャ
参照透過性(どのようなプログラムにおいても、プログラムの意味を変えることなく、式をその結果に置き換えることが出来ること)
参照透過ではない例
class Cafe { def buyCoffee(cc: CreditCard, p: Payments): Coffee = { val cup = new Coffee() cc.charge(cc, cup.price) cup } }
文字列はイミュータブル
scala> val x = "hello, world" x: String = hello, world scala> val r1 = x.reverse r1: String = dlrow ,olleh scala> val r2 = x.reverse r2: String = dlrow ,olleh
副作用と参照透過
scala> val x = new StringBuilder("Hello") x: StringBuilder = Hello scala> val y = x.append(", World") y: StringBuilder = Hello, World scala> val r1 = y.toString r1: String = Hello, World scala> val r2 = y.toString r2: String = Hello, World scala> val r3 = x.append(", World").toString r3: String = Hello, World, World scala> val r4 = x.append(", World").toString r4: String = Hello, World, World, World
StringBuilder.appendは純粋関数ではない…
副作用はプログラムの振る舞いについての論証を難しくする
必要なのは局所推論だけ
- 計算の部分はブラックボックス化され、出力は計算され、返されるだけとなる
- 合成も簡単。
Scala関数型プログラミングの準備
まずはやってみよう。
- object新しいシングルトン型を作成する
- defキーワードを使ってオブジェクトまたはクラス内で定義された関数またはフィールドをメソッドと呼ぶ
- Scalaにreturnはない。最後の式が戻り値となる。
- コンソール出力は副作用、ゆえにmain関数はプロシージャ、非純粋関数と呼ばれる
object MyModule { def abs(n: Int): Int = if (n < 0) -n else n private def formatAbs(x: Int) = { val msg = "The absolute value of %d is %d" msg.format(x, abs(x)) } def main(args: Array[String]): Unit = println(formatAbs(-42))
mainの入力はArray[String] 戻り値はUnit で、戻り値がUnitなのはそのメソッドに副作用があることのヒントになりうる。
プログラムの実行
REPLの使い方は覚えておけばよいでしょう
➜ ~ scalac MyModule.scala ➜ ~ ll MyModule* -rw-rw-r--. 1 shigemk2 shigemk2 1.4K 3月 24 20:41 MyModule$.class -rw-rw-r--. 1 shigemk2 shigemk2 712 3月 24 20:41 MyModule.class -rw-rw-r--. 1 shigemk2 shigemk2 257 3月 24 20:41 MyModule.scala ➜ ~ scala MyModule The absolute value of -42 is 42 ➜ ~
モジュール、オブジェクト、名前空間
object MyModule { def abs(n: Int): Int = if (n < 0) -n else n private def formatAbs(x: Int) = { val msg = "The absolute value of %d is %d" msg.format(x, abs(x)) } def main(args: Array[String]): Unit = println(formatAbs(-42))
scala> :load MyModule.scala Loading MyModule.scala... defined object MyModule scala> MyModule.abs(-42) res0: Int = 42
MyModule.abs(-42)みたいな書き方をしなければならない。これはMyModuleが名前空間であることを意味する。 (文脈が明らかな場合は、関数とメソッドを同じ意味で使う)
糖衣構文
scala> 2 + 1 res1: Int = 3 scala> 2.+(1) res2: Int = 3
こういう書き方も可能。
scala> MyModule.abs(-42) res0: Int = 42 scala> MyModule abs -42 res3: Int = 42
スコープに入れたい
scala> import MyModule.abs import MyModule.abs scala> abs(-42) res4: Int = 42
高階関数: 関数に関数を返す
関数は値である
という考え方を踏まえて、関数を変数として代入し、関数を別の関数の引数として使いたい。
のっけから末尾再帰を使っているのでそこは注意されたし。
再帰ヘルパー関数はgoまたはloopが命名としては常道。 なお、Scalaでwhileを使うのは邪道とされている。
こういうテクニックを末尾呼び出しの除去と呼ぶ。
fibonacciの末尾再帰
def fib(n: Int): Int = { @annotation.tailrec def loop(n: Int, prev: Int, cur: Int): Int = if (n == 0) prev else loop(n - 1, cur, prev + cur) loop(n, 0, 1) }
なお、高階関数のパラメータは、f g hみたいなのが慣例となっている。
多相関数 型の抽象化
単相関数 vs 多相関数
def findFirst(ss: Array[String], key: String): Int = { @annotation.tailrec def loop(n: Int): Int = if (n >= ss.length) -1 else if (ss(n) == key) n else loop(n + 1) loop(0) }
def findFirst[A](as: Array[A], p: A => Boolean): Int = { @annotation.tailrec def loop(n: Int): Int = if (n >= as.length) -1 else if (p(as(n))) n else loop(n + 1) loop(0) }
次回 P30 無名関数を使った高階関数の呼び出し から