web-dev-qa-db-fra.com

Existe-t-il un moyen générique de mémoriser à Scala?

Je voulais mémoriser ceci:

def fib(n: Int) = if(n <= 1) 1 else fib(n-1) + fib(n-2)
println(fib(100)) // times out

J'ai donc écrit ceci et cela compile et fonctionne de manière surprenante (je suis surpris parce que fib se référence dans sa déclaration):

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

Mais lorsque j'essaie de déclarer fib à l'intérieur d'un def, j'obtiens une erreur de compilation:

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)
} 

Ci-dessus ne parvient pas à compiler error: forward reference extends over definition of value fib case n => fib(n-1) + fib(n-2)

Pourquoi la déclaration de val fib À l'intérieur d'un def échoue mais à l'extérieur dans la portée de classe/objet fonctionne?

Pour clarifier, pourquoi je pourrais vouloir déclarer la fonction récursive mémorisée dans la portée def - voici ma solution au problème de somme de sous-ensemble:

/**
   * 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)
  }
48
pathikrit

J'ai trouvé une meilleure façon de mémoriser en utilisant Scala:

def memoize[I, O](f: I => O): I => O = new mutable.HashMap[I, O]() {
  override def apply(key: I) = getOrElseUpdate(key, f(key))
}

Vous pouvez maintenant écrire fibonacci comme suit:

lazy val fib: Int => BigInt = memoize {
  case 0 => 0
  case 1 => 1
  case n => fib(n-1) + fib(n-2)
}

En voici un avec plusieurs arguments (la fonction 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)
}

Et voici le problème de la somme des sous-ensembles:

// 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)
}

EDIT: Comme discuté ci-dessous, voici une version thread-safe

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)))
}
34
pathikrit

Le niveau de classe/trait val se compile en une combinaison d'une méthode et d'une variable privée. Une définition récursive est donc autorisée.

Les vals locaux, en revanche, ne sont que des variables régulières et la définition récursive n'est donc pas autorisée.

Soit dit en passant, même si le def que vous avez défini fonctionnait, il ne ferait pas ce que vous attendiez. À chaque appel de foo, un nouvel objet fonction fib sera créé et il aura sa propre carte de support. Ce que vous devriez faire à la place est le suivant (si vous voulez vraiment qu'un def soit votre interface publique):

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)
} 
19
missingfaktor

Scalaz a une solution pour ça, pourquoi ne pas la réutiliser?

import scalaz.Memo
lazy val fib: Int => BigInt = Memo.mutableHashMapMemo {
  case 0 => 0
  case 1 => 1
  case n => fib(n-2) + fib(n-1)
}

Vous pouvez en savoir plus sur mémorisation dans Scalaz .

8
michau

Mutable HashMap n'est pas sûr pour les threads. Définir séparément les instructions de cas pour les conditions de base semble une manipulation spéciale inutile, plutôt que Map peut être chargé avec des valeurs initiales et passé à Memoizer. Voici la signature de Memoizer où il accepte un mémo (carte immuable) et une formule et renvoie une fonction récursive.

Memoizer ressemblerait à

def memoize[I,O](memo: Map[I, O], formula: (I => O, I) => O): I => O

Maintenant donné une formule de Fibonacci suivante,

def fib(f: Int => Int, n: Int) = f(n-1) + f(n-2)

fibonacci avec Memoizer peut être défini comme

val fibonacci = memoize( Map(0 -> 0, 1 -> 1), fib)

où Memoizer à usage général indépendant du contexte est défini comme

    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
      }

De même, pour factorielle, une formule est

def fac(f: Int => Int, n: Int): Int = n * f(n-1)

et factorielle avec Memoizer est

val factorial = memoize( Map(0 -> 1, 1 -> 1), fac)

Inspiration: Memoization, Chapter 4 of Javascript good parts by Douglas Crockford

0
Boolean