是否可以在Scala中执行此操作(不改变类的内部状态)?

时间:2013-06-08 08:22:54

标签: scala functional-programming

比方说,有一个名为RemoteIdGetter的班级。它从服务器返回一个密钥。但是它只向服务器发出请求,如果密钥不够“新鲜”,意味着它最后一次被请求,则大于或等于5分钟(300秒)。否则,它返回密钥的本地“缓存”值。

我需要这样做没有没有 var)改变RemoteIdGetter的内部状态或使用纯粹的功能方法即可。

可能看起来像这样:

class RemoteIdGetter {
  def key = {
    if (!needToAskServer) // return the local "cached" value of the key
    else makeRequest

  }

  def makeRequest = // make a request to a remote server to get the key
  def time = // current date-time
  def lastUpdatedTime = // the last date-time a key has been updated 
                        // (requested from the server)
  def needToAskServer = time - lastUpdatedTime >= 300
}

我想知道,这可能吗?我为什么需要它?我只是好奇,如果可能的话。

2 个答案:

答案 0 :(得分:6)

每次使用相同的参数调用纯函数时都应该返回相同的结果,所以如果你想在没有可变状态的情况下执行此操作,那么每次获得这样的键时都必须生成新的RemoteIdGetter

case class RemoteIdGetter(cachedKey: Option[KeyType] = None, lastUpdatedTime: Option[DateTime] = None) {
  def getKey(time: DateTime) = {
    val (key, t) = (for {
      k <- cachedKey
      lt <- lastUpdatedTime
      if (time - lt < cachePeriod)
    } yield k -> lt).getOrElse(makeRequest -> time)
    key -> RemoteIdGetter(Some(key), Some(t))
  }
}

用法:

val (key, newGetted) = oldGetter.getKey(currentDateTime)

每次都必须使用最新生成的RemoteIdGetter

或者你可以隐藏可变状态。例如,您可以使用actor:

import akka.actor.ActorDSL._
val a = actor(new Act {
  become {
    case GetKey => replyAndBecome(sender)
  }

  def replyAndBecome(sender: ActorRef): {
    val key = makeRequest
    sender ! key
    become getState(key, time)
  }

  def getState(key: KeyType, lastUpdatedTime: DateTime): Receive = {
    case GetKey =>
      if (time - lastUpdatedTime < cachePeriod)
        sender ! key
      else 
        replyAndBecome(sender)
  }
})

没有可见可变状态(如var或可变集合),但存在隐藏的可变状态 - actor行为。

答案 1 :(得分:4)

执行此操作的最简单方法是使RemoteIdGetter返回包含1.请求密钥的对,以及2.缓存请求密钥的新实例。针对RemoteIdGetter的新实例进行进一步查找。实例的自动线程可以使用monad完成。

您在Scala中没有特别需要它,因为您可以通过改变类中的缓存映射来实现相同的目的。

有点有趣的是你可以编写一个普通的RemoteIdGetter函数来从在线获取密钥,然后编写一个包含任何代价高昂的计算的通用缓存函数。这与memoization的概念非常相似,只保留一些关于何时要丢弃缓存结果的元数据。 RemoteIdGetter本身就没有真正的理由,当它可以被卸载到专门用于此的其他通用函数时。


General cacher

所以,假设你有一个功能可以做一些昂贵的事情。在这种情况下,为了简单起见,我只是为了让它接收用户输入。

def get_id():
    try:
        return int(input("Enter an id: "))
    except ValueError:
        return get_id()

这会询问用户是否为整数。如果用户未能输入,则只需再次询问。我们不想一直打扰用户,因此我们希望缓存用户输入的值以供进一步使用。我们可以缓存get_id函数中的内容,但这不是一个理想的情况,因为我们需要separation of concerns

所以我们要做的是创建一个通用的Cacher对象,它可以缓存任何值。我们希望能够做到这样的事情:

cached_id = Cacher(get_id)
cached_id.get()

然后获取缓存ID或询问用户该号码,具体取决于该号码是否已缓存/最近更新。

Cacher对象需要保存数据和数据的到期时间。在此ID情况下,转换为cacher中的两个字段。您可能希望在真实世界的缓存对象中使用map / dict,但我试图保持简单。

class Cacher:
    value = None
    expires = 0

如果没有缓存该值,则使用函数初始化缓存对象,这样只是

    def __init__(self, function):
        self.external = function

然后有趣的是get()方法,其最简单的形式看起来像

    # Get a value and in case it is retrieved, make it good for 30 seconds
    def get(self, ttl=30):
        if not self.value or datetime.now() > self.expires:
            # Get a new value from the external source
            self.value = self.external()
            # and update the new expiration time to ttl seconds from now
            self.expires = datetime.now() + timedelta(seconds=ttl)

        return self.value

然后,当你第一次调用cached_id.get()时,你必须输入一个整数,但是在30秒内任何连续的调用都只会检索最后输入的整数。

这个可变的Python对象当然可以作为Scala中的actor实现,我只想展示原理。