在Scala中执行函数并缓存值

3
我有一个如下所示的函数:
def runOnce(request: Request): Future[Result] = {
}

当我调用这个runOnce函数时,如果它还没有运行过,我希望它运行某个方法并返回结果。如果已经运行过,我只希望它返回原始结果(请求将保持相同)。如果没有参数,我可以这样做。
lazy val hydratedModel = hydrateImpl(request)

future for efficient filtering
def fetchHydratedModel(): Future[HydratedModelRequest] = {
   hydratedModel
}

在第一种情况下该怎么做?


这个函数会不会被一个 request 调用多次?你想要的技术叫做记忆化,但是将其应用于请求似乎非常奇怪。关于记忆化,请参见 Is there a generic way to memoize in Scala? - Suma
你真的需要一个函数吗?这更多与按名调用和按值调用的评估策略有关。 - Pavel
2个回答

6
问题的一个通用解决方案是函数记忆化;对于一个纯函数(没有副作用的函数-它不适用于非纯函数),函数调用的结果应该始终对于相同的参数值是相同的。因此,一种优化方式是在第一次调用时缓存该值,并在后续调用中返回该值。

您可以使用以下类似代码来实现这一点(用于单个参数的纯函数的记忆化类,已更新-请参见下面的注释以使其线程安全):

/** Memoize a pure function `f(A): R`
 *
 *  @constructor Create a new memoized function.
 *  @tparam A Type of argument passed to function.
 *  @tparam R Type of result received from function.
 *  @param f Pure function to be memoized.
 */
final class Memoize1[A, R](f: A => R) extends (A => R) {

  // Cached function call results.
  private val result = scala.collection.mutable.Map.empty[A, R]

  /** Call memoized function.
   *
   *  If the function has not been called with the specified argument value, then the
   *  function is called and the result cached; otherwise the previously cached
   *  result is returned.
   *
   *  @param a Argument value to be passed to `f`.
   *  @return Result of `f(a)`.
   */
  def apply(a: A) = synchronized(result.getOrElseUpdate(a, f(a)))
}

/** Memoization companion */
object Memoize1 {

  /** Memoize a specific function.
   *
   *  @tparam A Type of argument passed to function.
   *  @tparam R Type of result received from function.
   *  @param f Pure function to be memoized.
   */
  def apply[A, R](f: A => R) = new Memoize1(f)
}

假设你要进行备忘录的函数为hydrateImpl,那么你可以按以下方式定义和使用runOnce(请注意,它变成了val而不是def):
val runOnce = Memoize1(hydrateImpl)
runOnce(someRequest) // Executed on first call with new someRequest value, cached result subsequently.

更新:关于线程安全。

针对user1913596的评论,答案是“不是”;scala.collection.mutable.Map.getOrElseUpdate不是线程安全的。然而,同步访问相当简单,我已经相应地更新了原始代码(在synchronized(...)中嵌入调用)。

锁定访问所带来的性能损失应该可以通过提高执行时间来抵消(假设f是非平凡的)。


这也适用于多线程环境吗?在 result 映射设置值之前,多个线程是否可以调用 runOnce(someRequest) ? 这似乎并非如此。在并发访问情况下,您会提出什么建议?不要多次评估 f() - user1913596
1
@user1913596 很好的观点!不,Scala 的可变 Map 不是线程安全的。然而,解决这个问题相当容易。我已经相应地更新了我的答案。感谢您指出这一点! - Mike Allen
1
感谢更新。为了给未来的读者补充一点,从 Scala 版本 2.11.12开始,仅使用 scala.collection.concurrent.Mapscala.collection.concurrent.TrieMap 并不能确保线程安全。在两种情况下,getOrElseUpdate 都不是原子操作,即使文档确保在 TrieMapop 仅被评估一次。解决方案是使用同步块,如 @Mike Allen 所述。 - user1913596

0

根据您的设置,可能有更好的方法来完成这个任务,但一个简单的解决方案是按照以下步骤进行操作:

private var model: Option[Future[HydratedModelRequest]] = None

def runOnce(request: Request): Future[Request] = {
  if (model.isEmpty) {
    model = hydrateImpl(request)
  }

  model.get
}

如果每次调用的request确实相同,另一个选择是隐式地要求请求并进行惰性填充。
implicit val request: Request
lazy val hydratedRequest: Future[HydratedModelRequest] = hydrateImpl(request)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接