汎関数プログラミング(2)-汎関数プログラミングを初めて体験


汎関数プログラミングは数学方程式の解題と似ている.ある方法で問題の答えを見つける.汎関数プログラミングの一般的な方法には、パターンマッチング(pattern matching)および再帰的思考(Recursive thinking)が含まれる.まず体験してみましょう.(このシリーズのブログの文章を読む前に、読者はすでにScala言語とREPLの使い方を知っていると信じています.ここではScalaの文法の意味を説明しません.)
まず簡単なものをください.
def reportError(msgId: Int): String = msgId match {
     | case 1 => "Error number 1."
     | case 2 => "Error number 2."
     | case 3 => "Error number 3."
     | case _ => "Unknown error!"
     | }
reportError: (msgId: Int)String

明らかに,この関数は純粋な関数であり,完全な関数でもある.関数ボディにはすべての入力値が含まれているためです(注意:case_=>).任意の入力msgId値によって生じる結果を予知することができる.また、関数には中間変数は使用されません.参照を参照:
reportError(2)
res3: String = Error number 2.

scala> reportError(-1)
res4: String = Unknown error!

私たちが予測した結果のようだ.
再帰(Recursion)の例を見てみましょう.階乗(Factorial)は古典的な例です.
def factorial(n: Int): Int = {
  	if ( n == 1) n
  	else n * factorial(n-1)
  }                                               //> factorial: (n: Int)Int
  factorial(4)                                    //> res48: Int = 24

モードマッチング方式を使用することもできます.
def factorial_1(n: Int): Int = n match {
  	case 1 => 1
  	case k => k * factorial(n-1)
  }                                               //> factorial_1: (n: Int)Int
  factorial_1(4)                                  //> res49: Int = 24

パターンマッチング方式で関数の意味表現をより簡潔で明瞭にする.
「等量置換」方式で徐々に約化(reduce)してみた.
factorial(4)
  4 * factorial(3)
  4 * (3 * factorial(2))
  4 * (3 * (2 * factorial(1)))
  4 * (3 * (2 * 1)) = 24

予想通りの答えが出る.
再帰プログラムはloopで実現できる.主な目的はスタックオーバーフロー防止(stack overflow).しかし、これは私たちが再帰的な思考で問題を解決するのを妨げるものではありません.階乗はwhile loopで書きます.
def factorial_2(n: Int): Int = {
 		var k: Int = n
 		var acc: Int = 1
 		while (k > 1) { acc = acc * k; k = k -1}
 		acc
  }                                               //> factorial_2: (n: Int)Int
  factorial_2(4)                                  //> res50: Int = 24

注意factorial_2ローカル変数k,accを用いた.表現形式から汎関数プログラミングの優雅さは失われたが,スタックオーバーフローの問題を解決できるほか,実行効率も再帰方式より最適化できる.しかし、これは「不可変性」(Immutability)に完全に反するという意味ではない.変数は関数の内部にロックされているからです.
最後に、tail recursion方式で階乗を記述することもできます.コンパイラ(compiler)にプログラムをloopデザインに変更させる:
def factorial_3(n: Int): Int = {
    @annotation.tailrec
  	def go(n: Int, acc: Int): Int = n match {
  		case 1 => acc
  		case k => go(n-1,acc * k)
  	}
  	go(n,1)
  }                                               //> factorial_3: (n: Int)Int
  factorial_3(4)                                  //> res51: Int = 24

同じように正しい答えが出た.このプログラムでは@annotationが使用されています.tailrec.標準の関数がtail recusionの要件に合致しない場合、compilerはプロンプトを表示します.