问题
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