私はこれをメモしたかった:
_def fib(n: Int) = if(n <= 1) 1 else fib(n-1) + fib(n-2)
println(fib(100)) // times out
_
だから私はこれを書いて、これは驚くほどコンパイルして動作します(fib
は宣言で自分自身を参照しているので驚いています):
_case class Memo[A,B](f: A => B) extends (A => B) {
private val cache = mutable.Map.empty[A, B]
def apply(x: A) = cache getOrElseUpdate (x, f(x))
}
val fib: Memo[Int, BigInt] = Memo {
case 0 => 0
case 1 => 1
case n => fib(n-1) + fib(n-2)
}
println(fib(100)) // prints 100th fibonacci number instantly
_
しかし、def
内でfibを宣言しようとすると、コンパイラエラーが発生します。
_def foo(n: Int) = {
val fib: Memo[Int, BigInt] = Memo {
case 0 => 0
case 1 => 1
case n => fib(n-1) + fib(n-2)
}
fib(n)
}
_
上記はerror: forward reference extends over definition of value fib case n => fib(n-1) + fib(n-2)
のコンパイルに失敗します
Def内で_val fib
_を宣言するのに失敗するのに、クラス/オブジェクトスコープの外側で動作するのはなぜですか?
明確にするために、なぜdefスコープで再帰的メモ関数を宣言したいのか-サブセット和問題の解決策は次のとおりです。
_/**
* Subset sum algorithm - can we achieve sum t using elements from s?
*
* @param s set of integers
* @param t target
* @return true iff there exists a subset of s that sums to t
*/
def subsetSum(s: Seq[Int], t: Int): Boolean = {
val max = s.scanLeft(0)((sum, i) => (sum + i) max sum) //max(i) = largest sum achievable from first i elements
val min = s.scanLeft(0)((sum, i) => (sum + i) min sum) //min(i) = smallest sum achievable from first i elements
val dp: Memo[(Int, Int), Boolean] = Memo { // dp(i,x) = can we achieve x using the first i elements?
case (_, 0) => true // 0 can always be achieved using empty set
case (0, _) => false // if empty set, non-zero cannot be achieved
case (i, x) if min(i) <= x && x <= max(i) => dp(i-1, x - s(i-1)) || dp(i-1, x) // try with/without s(i-1)
case _ => false // outside range otherwise
}
dp(s.length, t)
}
_
Scalaを使用してメモするより良い方法を見つけました。
def memoize[I, O](f: I => O): I => O = new mutable.HashMap[I, O]() {
override def apply(key: I) = getOrElseUpdate(key, f(key))
}
次のようにフィボナッチを書くことができます:
lazy val fib: Int => BigInt = memoize {
case 0 => 0
case 1 => 1
case n => fib(n-1) + fib(n-2)
}
以下は、複数の引数を持つものです(choose関数):
lazy val c: ((Int, Int)) => BigInt = memoize {
case (_, 0) => 1
case (n, r) if r > n/2 => c(n, n - r)
case (n, r) => c(n - 1, r - 1) + c(n - 1, r)
}
そして、これがサブセット合計の問題です。
// is there a subset of s which has sum = t
def isSubsetSumAchievable(s: Vector[Int], t: Int) = {
// f is (i, j) => Boolean i.e. can the first i elements of s add up to j
lazy val f: ((Int, Int)) => Boolean = memoize {
case (_, 0) => true // 0 can always be achieved using empty list
case (0, _) => false // we can never achieve non-zero if we have empty list
case (i, j) =>
val k = i - 1 // try the kth element
f(k, j - s(k)) || f(k, j)
}
f(s.length, t)
}
編集:以下で説明するように、ここはスレッドセーフバージョンです
def memoize[I, O](f: I => O): I => O = new mutable.HashMap[I, O]() {self =>
override def apply(key: I) = self.synchronized(getOrElseUpdate(key, f(key)))
}
クラス/特性レベルval
は、メソッドとプライベート変数の組み合わせにコンパイルされます。したがって、再帰的な定義が許可されます。
一方、ローカルval
sは単なる通常の変数であるため、再帰的な定義は許可されていません。
ところで、定義したdef
が機能していても、期待したことはしません。 foo
を呼び出すたびに、新しい関数オブジェクトfib
が作成され、独自のバッキングマップが作成されます。代わりにすべきことはこれです(本当にdef
をパブリックインターフェイスにしたい場合):
private val fib: Memo[Int, BigInt] = Memo {
case 0 => 0
case 1 => 1
case n => fib(n-1) + fib(n-2)
}
def foo(n: Int) = {
fib(n)
}
Scalazにはそのためのソリューションがあります。再利用してみませんか?
import scalaz.Memo
lazy val fib: Int => BigInt = Memo.mutableHashMapMemo {
case 0 => 0
case 1 => 1
case n => fib(n-2) + fib(n-1)
}
Scalazでのメモ化 について詳しく読むことができます。
可変HashMapはスレッドセーフではありません。また、基本条件に対して個別にcaseステートメントを定義することは、不必要な特別な処理のように思われます。むしろ、Mapに初期値をロードしてMemoizerに渡すことができます。以下は、メモ(不変マップ)と式を受け入れ、再帰関数を返すMemoizerの署名です。
Memoizerは次のようになります
def memoize[I,O](memo: Map[I, O], formula: (I => O, I) => O): I => O
次のフィボナッチ式が与えられると、
def fib(f: Int => Int, n: Int) = f(n-1) + f(n-2)
memoizerのフィボナッチは、次のように定義できます。
val fibonacci = memoize( Map(0 -> 0, 1 -> 1), fib)
コンテキスト非依存の汎用Memoizerは次のように定義されます
def memoize[I, O](map: Map[I, O], formula: (I => O, I) => O): I => O = {
var memo = map
def recur(n: I): O = {
if( memo contains n) {
memo(n)
} else {
val result = formula(recur, n)
memo += (n -> result)
result
}
}
recur
}
同様に、階乗の場合、式は
def fac(f: Int => Int, n: Int): Int = n * f(n-1)
memoizerの階乗は
val factorial = memoize( Map(0 -> 1, 1 -> 1), fac)
インスピレーション:メモ化、ダグラス・クロックフォードによるJavascriptの良い部分の第4章