I have this in-memory implementation of a simple Cache in Scala using cats effects.
我在Scala中使用cats效果实现了一个简单Cache的内存实现。
Here is my trait:
这是我的特点:
trait Cache[F[_], K, V] {
def get(key: K): F[Option[V]]
def put(key: K, value: V): F[Cache[F, K, V]]
}
import cats.effect.kernel.Async
case class ImmutableMapCache[F[_]: Async, K, V](map: Map[K, V]) extends Cache[F, K, V] {
override def get(key: K): F[Option[V]] =
Async[F].blocking(map.get(key))
override def put(key: K, value: V): F[Cache[F, K, V]] =
Async[F].blocking(ImmutableMapCache(map.updated(key, value)))
}
object ImmutableMapCache {
def empty[F[_]: Async, K, V]: F[Cache[F, K, V]] =
Async[F].pure(ImmutableMapCache(Map.empty))
}
Is this a good enough implementation? I'm restricting my effect to Async. Can I make it even more generic to work with other effect types in my ImmutableMapCache?
这是一个足够好的实现吗?我将我的效果限制为异步。我可以在ImmutableMapCache中使用其他效果类型吗?
What other pitfalls are there with my approach?
我的方法还有哪些陷阱?
EDIT:
编辑:
Is this a better implementation where I wrap the Map in a Cats Ref context?
这是一个更好的实现吗?我将Map封装在Cats-Ref上下文中?
import cats.effect.{Ref, Sync}
import cats.syntax.all._
class SimpleCache[F[_]: Sync, K, V] extends Cache[F, K, V] {
private val cache: Ref[F, Map[K, V]] = Ref.unsafe[F, Map[K, V]](Map.empty)
override def put(key: K, value: V): F[Unit] = cache.update(_.updated(key, value))
override def get(key: K): F[Option[V]] = cache.get.map(_.get(key))
}
更多回答
Why are you using an Immutable Map for something like a cache which you would want to support high performance mutations ? Also... if are you using blocking
in the Async
context then why even use Async ? In creating an Immutable Cache... you are likely to be updating the variable holding the cache to the new cache on every put... but this is opening the doors for race conditions when accessed by multiple threads. You are doing a lot of things which you would absolutely not want from a Cache implementation.
为什么要将不可变映射用于缓存之类的东西,而您希望支持高性能突变?而且如果您在Async上下文中使用阻塞,那么为什么还要使用Async呢?在创建不可变缓存时。。。您可能会在每次put时将保存缓存的变量更新为新的缓存。。。但这为多个线程访问时的竞争条件打开了大门。您正在做许多您绝对不希望从Cache实现中得到的事情。
Ref
uses AtomicReference
underneath so it is safe to use in multithreaded context.
Ref在下面使用AtomicReference,因此在多线程上下文中使用它是安全的。
优秀答案推荐
First of all, the right answer is not to reinvent the wheel, and just use a library that already does all this, like mules.
首先,正确的答案不是重新发明轮子,而是使用一个已经做了这一切的库,就像骡子一样。
However, for the sake of learning, let's take a look to some of the things you could improve.
然而,为了学习,让我们来看看你可以改进的一些地方。
- Interface. Especially
put
def put(key: K, value: V): F[Cache[F, K, V]]
If putting a new value in the Cache
returns a new one, it means it is immutable, if it is immutable there is no need for effects, meaning it is just a simple Map
.
You want your put
to mutate something; but in a concurrent-safe way. So it could actually be used to share data between different Fibers
. Thus, put
should be defined like this:
如果在缓存中放入一个新值会返回一个新的值,这意味着它是不可变的,如果它是不可更改的,则不需要效果,也就是说它只是一个简单的Map。你想让你的推杆变异一些东西;但是以同时安全的方式。因此,它实际上可以用于在不同的光纤之间共享数据。因此,put应该这样定义:
def put(key: K, value: V): F[Unit]
- Creation. Since a
Cache
is a mutable state, its creation must be an effect as well; like your original example but unlike your attempt with Ref
def empty[F[_], K, V]: F[Cache[F, K, V]]
- Implementation. Your intuition was right, we want to use a mutable reference to an immutable
Map
. Now, in order to make it concurrent safe, we need to either look the access to it, or use a CAS loop. cats-effect provides both options: AtomicCell
and Ref
respectively. In this case is better to just use a CAS loop so we go with Ref
object Cache {
def empty[F[_], K, V]: F[Cache[F, K, V]] =
Ref[F].of(Map.empty[K, V]).map { ref =>
new Cache[F[_], K, V] {
override def get(key: K): F[Option[V]] =
ref.get.map(map => map.get(key))
override def put(key: K, value: V): F[Unit] =
ref.update(map => map.updated(key, value))
}
}
}
- The constraints, the above code needs a constraint on
F
to actually work. But actually, we don't need all the Async
power, but rather just Concurrent
, since all we need is to create a Ref
:)
def empty[F[_] : Concurrent, K, V]: F[Cache[F, K, V]] = ...
更多回答
Thanks for the explanation. So with that addition of the empty method, I can get rid of the local val cache that I create using Ref. Correct?
谢谢你的解释。因此,通过添加空方法,我可以摆脱使用Ref.创建的本地val缓存。正确吗?
@joesan yes, especially because that one was "technically" wrong :)
@是的,尤其是因为那个“技术上”是错误的:)
Ok, so that empty method definition goes in the trait. Correct? I guess I also do not need the type constraint on the empty method as I have it on the trait.
好的,那么空的方法定义就进入了trait。对的我想我也不需要空方法上的类型约束,因为我在trait上有它。
@joesan no, it would go on the companion object, it is a factory.
@乔:不,它会出现在配套对象上,它是一个工厂。
我是一名优秀的程序员,十分优秀!