Есть ли общий способ запоминания в Scala?

Я хотел запомнить это:

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

Но когда я пытаюсь объявить fib внутри def, я получаю ошибку компилятора:

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)

Почему объявление val fib внутри определения терпит неудачу, но работает снаружи в области класса/объекта?

Чтобы пояснить, почему я могу захотеть объявить рекурсивную мемоизированную функцию в области определения - вот мое решение проблемы суммы подмножества:

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

person pathikrit    schedule 27.04.2013    source источник
comment
См. мой сообщение в блоге для другого варианта запоминания рекурсивных функций.   -  person michid    schedule 02.05.2013
comment
Прежде чем что-либо опубликовать в SO, я погуглил, и ваш пост в блоге был первым результатом :) Согласен, это правильный способ сделать это - использовать Y-комбинатор. Но я думаю, что использование моего стиля и использование lazy val выглядит чище, чем иметь 2 определения (рекурсивное и Y-комбинированное) для каждой функции. Выглядит как чисто [выглядит](1) [1]: github.com/pathikrit/scalgos/blob/master/src/main/scala/com/   -  person pathikrit    schedule 02.05.2013
comment
Меня смутила некоторая краткость синтаксиса в вашей проблеме выше (в частности, использование класса case для расширения (A => B). Я разместил вопрос об этом: stackoverflow.com/questions/19548103/   -  person chaotic3quilibrium    schedule 24.10.2013
comment
Осторожно используйте этот шаблон с проблемой параллелизма, вызванной Map: 6807324#6807324" title="использует ли val с хэш-таблицей в scala решение проблем параллелизма"> stackoverflow.com/questions/6806123/   -  person lcn    schedule 08.12.2013
comment
Вопрос, заданный в теле, и принятый ответ не имеют ничего общего с названием этого вопроса. Не могли бы вы изменить название?   -  person user239558    schedule 16.03.2015


Ответы (5)


Я нашел лучший способ запоминать с помощью 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)
}

Вот один с несколькими аргументами (функция выбора):

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)))
}
person pathikrit    schedule 30.04.2016
comment
Я не думаю, что это (или большинство реализаций, которые я видел на основе mutable.Map) потокобезопасны? Но выглядит как хороший синтаксис, если используется в однопоточном контексте. - person Gary Coady; 01.05.2016
comment
Я не уверен, может ли изменяемая реализация HashMap каким-либо образом привести к сбою и/или повреждению данных, или основная проблема заключается только в отсутствии обновлений; отсутствующие обновления, вероятно, будут приемлемы для большинства случаев использования. - person Gary Coady; 01.05.2016
comment
@Gary Coady: тривиально заменить HashMap на TrieMap, если вы хотите параллелизма - person pathikrit; 05.05.2016
comment
Конечно, это просто то, о чем должен знать пользователь, и иногда решения копируются/вставляются из SO без учета таких проблем ;-) - person Gary Coady; 05.05.2016
comment
Интересно, можно ли зайти в тупик даже на TrieMap. В конце концов, доступ к карте осуществляется рекурсивно внутри метода getOrElseUpdate. - person VasiliNovikov; 08.05.2016
comment
@VasyaNovikov: Затем мы можем просто сделать замок более грубым, окружив getOrElseUpdate self.synchronized {getOrElseUpdate} - person pathikrit; 26.08.2016
comment
TrieMap — это final, поэтому его нельзя разделить на подклассы, как указано выше. Вот что я собрал, чтобы использовать TrieMap: def memoize[A, B](f: A => B): (A => B) = { val cache = collection.concurrent.TrieMap[A, B](); (a: A) => cache.getOrElseUpdate(a, f(a)) }. - person Jeff Klukas; 23.01.2017
comment
@JeffKlukas: Что не так с версией self.synchronized? - person pathikrit; 24.01.2017
comment
@pathikrit: я не вижу ничего плохого в версии self.synchronized с использованием mutable.HashMap. Мой комментарий здесь в основном является пояснением к обсуждению TrieMap в комментариях выше, поскольку оказывается, что невозможно просто добавить TrieMap к данному коду. - person Jeff Klukas; 25.01.2017
comment
это не работает для меня, и это сообщает not found: value getOrElseUpdate - person luochen1990; 06.08.2018
comment
memoize является универсальным, что хорошо, но поскольку функции, определенные с помощью vals, должны быть мономорфными, это решение не будет работать для запоминания универсальных функций. Есть ли обходной путь для этого? - person Paul Carey; 06.11.2020

Уровень класса/признака val компилируется в комбинацию метода и закрытой переменной. Следовательно, допускается рекурсивное определение.

С другой стороны, локальные val являются обычными переменными, поэтому рекурсивное определение не допускается.

Кстати, даже если 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)
} 
person missingfaktor    schedule 27.04.2013
comment
«foo» и «fib» - это просто упрощение - в моем случае foo - это проблема с суммой подмножества, а fib - это рекурсивная мемоизация входного набора, и поэтому я не могу просто извлечь мою мемоизированную функцию вне метода. Можете ли вы объяснить, что вы подразумеваете под компиляцией val на уровне класса для комбинации метода и части частной переменной? Каковы другие различия, о которых я должен знать между классом и методом vals? - person pathikrit; 28.04.2013
comment
i) Что мешает вам извлечь его вне метода? ii) Когда вы пишете val x = N на уровне класса/черты, вы получаете def x = _x и private val _x = N. Вы должны найти это объяснение в любой книге по Scala. Я не могу вспомнить какие-либо другие различия между полем vals и локальным vals. - person missingfaktor; 28.04.2013
comment
Обходной путь, который вы можете использовать даже в локальной области: сделайте fib lazy val. Тогда вы сможете повторить это и в локальной области. - person missingfaktor; 28.04.2013
comment
Если он использовал изменяемое состояние и val. Означает ли это, что он не является потокобезопасным? - person ses; 22.04.2014
comment
@ses, если только эта изменяемая часть состояния не имеет гарантий потокобезопасности. (Вы можете быть изменчивым и потокобезопасным. Это просто... сложнее.) - person missingfaktor; 03.05.2014
comment
Доступно больше голосов, если вы можете показать, как сделать общие функции с n-арностью. - person user48956; 29.04.2016

У 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.

person michau    schedule 10.09.2016

Mutable HashMap не является потокобезопасным. Кроме того, определение операторов case отдельно для базовых условий кажется ненужной специальной обработкой, скорее Map может быть загружен с начальными значениями и передан в 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)

Вдохновение: Memoization, Глава 4 хороших частей Javascript Дугласа Крокфорда

person Boolean    schedule 26.08.2017
comment
› отдельное определение операторов case для базовых условий кажется ненужным. На самом деле выдумка — один из редких примеров с простыми базовыми случаями. Как бы вы решили проблему с рюкзаком? ="nofollow noreferrer">github.com/pathikrit/scalgos/blob/master/src/main/scala/com/), используя это? - person pathikrit; 27.08.2017
comment
В случае с Фибоначчи или где-либо, где значения известны заранее, они должны быть предварительно загружены в карту. Это делает функцию формулы более близкой к ее математическому определению, ИМО. Если формула требует сравнений (операторов case или блоков if...else), например, при решении задачи о рюкзаке, вполне нормально использовать операторы case. - person Boolean; 28.08.2017

ZIO#cached — это подход к запоминанию в ZIO.

person Hartmut P.    schedule 19.06.2020