Is it possible to do that in Scala (without mutating the internal state of a class)?

时间秒杀一切 提交于 2020-01-06 08:15:15

问题


Let's say, there is a class called RemoteIdGetter. It returns a key from a server. But it only makes a request to the server if the key is not "fresh" enough, meaning the last time it's been requested, is more or equal to 5 minutes (300 seconds). Otherwise, it returns the local "cached" value of the key.

I need to do that without (no var) mutating the internal state of RemoteIdGetter or with a pure functional approach.

It might look like this:

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
}

I wonder, is it possible? Why do I need it? I'm just curious if it's possible.


回答1:


Pure function should return same result every time you call it with the same parameter, so if you want to do this with no mutable state at all you have to generate new RemoteIdGetter every time you get key like this:

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

Usage:

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

You have to use latest generated RemoteIdGetter every time.

Alternatively you could hide mutable state. For instance you could use 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)
  }
})

There is no visible mutable state (like var, or mutable collection), but there is a hidden mutable state - actors behavior.




回答2:


The simplest way to do this is to make the RemoteIdGetter return a pair consisting of 1. the requested key, and 2. a new instance of itself with the requested key cached. Further lookups are done against the new instance of the RemoteIdGetter. Automatic threading of the instances can be done with monads.

There's no particular reason you need it in Scala, since you could achieve the same thing by mutating a cache map in your class.

Something that's a little interesting is that you could just write a normal RemoteIdGetter function that fetches the key from online, and then write a generic caching function that wraps around any costly computation. This is very similar to the concept of memoization, only you keep some metadata about when you want to discard the cached result. There is no real reason the RemoteIdGetter should do the caching itself, when it can be offloaded to some other generic function that specialises in that.


General cacher

So, say you have a function which does something expensive. In this case, I'm just going to make it receive user input for simplicity's sake.

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

This asks the user for an integer. If the user fails to enter one, it simply asks again. We don't want to bother the user with an integer all the time, so we want to cache the value the user entered for further uses. We could cache things in the get_id function, but that isn't an ideal situation, since we want separation of concerns.

So what we do is we create a generic Cacher object, which can cache any value. We want to be able to do something like this:

cached_id = Cacher(get_id)
cached_id.get()

and then get either a cached ID or ask the user for the number, depending on whether or not the number is cached/recently updated.

The Cacher object needs to hold data and an expiration time for the data. In this ID case, that translates to two fields in the cacher. You would probably want a map/dict here in a real-world caching object, but I'm trying to keep it simple.

class Cacher:
    value = None
    expires = 0

You initialise the caching object with a function to call if the value isn't cached, so that's just

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

and then the interesting bit is the get() method, which, in its simplest form, looks like

    # 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

Then when you call cached_id.get() for the first time you will have to enter an integer, but any consecutive calls within 30 seconds will just retrieve the last entered integer.

This mutable Python object can of course be implemented as an actor in Scala, I just wanted to show the principle.



来源:https://stackoverflow.com/questions/16997573/is-it-possible-to-do-that-in-scala-without-mutating-the-internal-state-of-a-cla

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!