moonbitstack/moonstl/cache does not have a README file

    ArcCache

    pub struct ArcCache[K, V] {
    index :
    IndexMap
    [K, Int]
    slots : Array[ArcNode[K, V]?]
    free : Array[Int]
    heads : Array[Int]
    tails : Array[Int]
    sizes : Array[Int]
    capacity : Int
    recent_target : Int
    hits : Int64
    misses : Int64
    evictions : Int64
    on_evict : (K, V) -> Unit?
    }

    A cache that keeps the recency and the frequency of what it sees in balance, in the manner of ARC: a hit on a re-requested key raises the share of the cache given to frequent entries (recent_target moves up), a key that falls back into the ghost lists takes that share down. lists reports the four list sizes, which is how the adaptation is observed.

    test {
    let cache : @cache.ArcCache[String, Int32] = @cache.ArcCache::new(capacity=2)
    let _ = cache.set("a", 1)
    let _ = cache.set("b", 2)
    let _ = cache.get("a")
    let _ = cache.set("c", 3)
    let (t1, t2, b1, b2) = cache.lists()
    @test.assert_eq(t1 + t2, 2)
    @test.assert_eq(b1 + b2 <= 2, true)
    }
    impl Eq for ArcCache[K, V]
    impl Show for ArcCache[K, V]

    ArcCache::capacity

    fn[K, V] ArcCache::capacity(self : ArcCache[K, V]) -> Int

    ArcCache::clear

    fn[K, V] ArcCache::clear(self : ArcCache[K, V]) -> Unit

    ArcCache::contains

    fn[K : Hash + Eq, V] ArcCache::contains(self : ArcCache[K, V], key : K) -> Bool

    true when the key is resident. A key that is only remembered in a ghost list is not contained.

    ArcCache::copy

    fn[K, V] ArcCache::copy(self : ArcCache[K, V]) -> ArcCache[K, V]

    ArcCache::equal

    fn[K : Hash + Eq, V : Eq] ArcCache::equal(self : ArcCache[K, V], other : ArcCache[K, V]) -> Bool

    ArcCache::get

    fn[K : Hash + Eq, V] ArcCache::get(self : ArcCache[K, V], key : K) -> V?

    ArcCache::is_empty

    fn[K, V] ArcCache::is_empty(self : ArcCache[K, V]) -> Bool

    ArcCache::iter

    fn[K, V] ArcCache::iter(self : ArcCache[K, V]) -> Iter[(K, V)]

    ArcCache::keys

    fn[K, V] ArcCache::keys(self : ArcCache[K, V]) -> Iter[K]

    ArcCache::length

    fn[K, V] ArcCache::length(self : ArcCache[K, V]) -> Int

    ArcCache::lists

    fn[K, V] ArcCache::lists(self : ArcCache[K, V]) -> (Int, Int, Int, Int)

    The sizes of the four lists: the two resident ones and the two ghost ones.

    ArcCache::new

    fn[K, V] ArcCache::new(capacity~ : Int, on_evict? : (K, V) -> Unit, hash_seed? : Int) -> ArcCache[K, V] raise
    Refused

    ArcCache::not_equal

    fn[K : Hash + Eq, V : Eq] ArcCache::not_equal(x : ArcCache[K, V], y : ArcCache[K, V]) -> Bool

    ArcCache::output

    fn[K : Show, V : Show] ArcCache::output(self : ArcCache[K, V], logger : &Logger) -> Unit

    ArcCache::remove

    fn[K : Hash + Eq, V] ArcCache::remove(self : ArcCache[K, V], key : K) -> V?

    ArcCache::set

    fn[K : Hash + Eq, V] ArcCache::set(self : ArcCache[K, V], key : K, value : V) -> (K, V)?

    Stores value under key and returns the resident the insert displaced, if any. A key that returns from a ghost list is admitted straight into the frequent list and the share given to frequent entries moves in its favour.

    ArcCache::stats

    fn[K, V] ArcCache::stats(self : ArcCache[K, V]) -> CacheStats

    ArcCache::to_string

    fn[K : Show, V : Show] ArcCache::to_string(self : ArcCache[K, V]) -> String

    ArcCache::values

    fn[K, V] ArcCache::values(self : ArcCache[K, V]) -> Iter[V]

    ArcNode

    pub struct ArcNode[K, V] {
    key : K
    value : V?
    list : Int
    prev : Int
    next : Int
    }

    A node of any of the four lists. A ghost carries None as its value: the key is remembered so that a returning key can be recognised, the value is gone.

    CacheStats

    pub(all) struct CacheStats {
    hits : Int64
    misses : Int64
    evictions : Int64
    } derive(Eq,
    Debug
    )

    Counters every cache keeps. hits and misses count lookups, evictions counts the entries a policy dropped to make room — a remove is not an eviction, and neither is a replacement.
    impl Show for CacheStats

    CacheStats::equal

    fn CacheStats::equal(CacheStats, CacheStats) -> Bool

    CacheStats::hit_rate

    fn CacheStats::hit_rate(self : CacheStats) -> Double

    The hit rate, or 0 when nothing has been looked up.

    test {
    let stats : @cache.CacheStats = { hits: 3, misses: 1, evictions: 0, }
    @test.assert_eq(stats.hit_rate(), 0.75)
    }

    CacheStats::not_equal

    fn CacheStats::not_equal(x : CacheStats, y : CacheStats) -> Bool

    CacheStats::output

    fn CacheStats::output(self : CacheStats, logger : &Logger) -> Unit

    CacheStats::to_string

    fn CacheStats::to_string(self : CacheStats) -> String

    ExpNode

    pub struct ExpNode[K, V] {
    key : K
    value : V
    expire_at : Int64
    stamp : Int
    prev : Int
    next : Int
    }

    A node of the recency list, with the moment it stops being valid.

    ExpiringCache

    pub struct ExpiringCache[K, V] {
    index :
    IndexMap
    [K, Int]
    slots : Array[ExpNode[K, V]?]
    free : Array[Int]
    capacity : Int
    ttl : Int64
    now : () -> Int64
    head : Int
    tail : Int
    heap : Array[(Int64, Int, Int)]
    stamp_counter : Int
    evictions : Int64
    }

    A cache whose entries expire: an entry is dropped when it is read after its deadline, when a purge finds it, and by the least-recently-used rule when the capacity is full. Expiry is lazy, so a purge is the only thing that walks the expired entries in bulk.

    The cache never reads a clock of its own — now is the caller's, which is what lets a test move time without waiting. ttl and every deadline are in the units of that clock.

    test {
    let mut clock : Int64 = 0L
    let cache : @cache.ExpiringCache[String, Int32] = @cache.ExpiringCache::new(
    ttl=10L,
    capacity=2,
    now=() => clock,
    )
    let _ = cache.set("a", 1)
    @test.assert_eq(cache.get("a"), Some(1))
    clock = 11L
    @test.assert_eq(cache.get("a"), None)
    }
    impl Show for ExpiringCache[K, V]

    ExpiringCache::capacity

    fn[K, V] ExpiringCache::capacity(self : ExpiringCache[K, V]) -> Int

    ExpiringCache::clear

    fn[K, V] ExpiringCache::clear(self : ExpiringCache[K, V]) -> Unit

    ExpiringCache::contains

    fn[K : Hash + Eq, V] ExpiringCache::contains(self : ExpiringCache[K, V], key : K) -> Bool

    ExpiringCache::expire_at

    fn[K : Hash + Eq, V] ExpiringCache::expire_at(self : ExpiringCache[K, V], key : K) -> Int64?

    The deadline of an entry, or None when it is not in the cache.

    ExpiringCache::get

    fn[K : Hash + Eq, V] ExpiringCache::get(self : ExpiringCache[K, V], key : K) -> V?

    The value stored under key. An entry read after its deadline is dropped and reads as absent.

    ExpiringCache::is_empty

    fn[K : Hash + Eq, V] ExpiringCache::is_empty(self : ExpiringCache[K, V]) -> Bool

    ExpiringCache::iter

    fn[K : Hash + Eq, V] ExpiringCache::iter(self : ExpiringCache[K, V]) -> Iter[(K, V)]

    The entries from least to most recently used, expired ones already dropped.

    ExpiringCache::keys

    fn[K : Hash + Eq, V] ExpiringCache::keys(self : ExpiringCache[K, V]) -> Iter[K]

    ExpiringCache::length

    fn[K : Hash + Eq, V] ExpiringCache::length(self : ExpiringCache[K, V]) -> Int

    The number of live entries. Expired entries are dropped first, so the count is the count of what a get would find.

    ExpiringCache::new

    fn[K, V] ExpiringCache::new(ttl~ : Int64, capacity~ : Int, now~ : () -> Int64, hash_seed? : Int) -> ExpiringCache[K, V] raise
    Refused

    ExpiringCache::output

    fn[K : Hash + Eq + Show, V : Show] ExpiringCache::output(self : ExpiringCache[K, V], logger : &Logger) -> Unit

    ExpiringCache::purge

    fn[K : Hash + Eq, V] ExpiringCache::purge(self : ExpiringCache[K, V]) -> Int

    Drops every entry whose deadline has passed and reports how many went.

    ExpiringCache::remove

    fn[K : Hash + Eq, V] ExpiringCache::remove(self : ExpiringCache[K, V], key : K) -> V?

    ExpiringCache::set

    fn[K : Hash + Eq, V] ExpiringCache::set(self : ExpiringCache[K, V], key : K, value : V, ttl? : Int64) -> (K, V)?

    Stores value under key and gives it a deadline: ttl after the clock now, or the deadline given. Returns the entry the insert displaced.

    ExpiringCache::set_expire

    fn[K : Hash + Eq, V] ExpiringCache::set_expire(self : ExpiringCache[K, V], key : K, at : Int64) -> Bool

    Moves the deadline of an entry. false when the key is not in the cache.

    ExpiringCache::stats

    fn[K, V] ExpiringCache::stats(self : ExpiringCache[K, V]) -> CacheStats

    ExpiringCache::to_string

    fn[K : Hash + Eq + Show, V : Show] ExpiringCache::to_string(self : ExpiringCache[K, V]) -> String

    ExpiringCache::values

    fn[K : Hash + Eq, V] ExpiringCache::values(self : ExpiringCache[K, V]) -> Iter[V]

    LfuBucket

    pub struct LfuBucket {
    head : Int
    tail : Int
    }

    One frequency list. Buckets are indexed by frequency and grown on demand.

    LfuCache

    pub struct LfuCache[K, V] {
    index :
    IndexMap
    [K, Int]
    slots : Array[LfuNode[K, V]?]
    free : Array[Int]
    buckets : Array[LfuBucket]
    capacity : Int
    min_freq : Int
    hits : Int64
    misses : Int64
    evictions : Int64
    on_evict : (K, V) -> Unit?
    }

    A least-frequently-used cache: an insert past the capacity drops the entry with the smallest access count, and among equals the one that has gone untouched the longest. A hit moves the entry into the list of the next frequency, which is what keeps both operations O(1).

    test {
    let cache : @cache.LfuCache[String, Int32] = @cache.LfuCache::new(capacity=2)
    let _ = cache.set("a", 1)
    let _ = cache.set("b", 2)
    let _ = cache.get("a")
    let _ = cache.set("c", 3)
    @test.assert_eq(cache.get("b"), None)
    @test.assert_eq(cache.frequency("a"), Some(2))
    }
    impl Eq for LfuCache[K, V]
    impl Show for LfuCache[K, V]

    LfuCache::capacity

    fn[K, V] LfuCache::capacity(self : LfuCache[K, V]) -> Int

    LfuCache::clear

    fn[K, V] LfuCache::clear(self : LfuCache[K, V]) -> Unit

    LfuCache::contains

    fn[K : Hash + Eq, V] LfuCache::contains(self : LfuCache[K, V], key : K) -> Bool

    LfuCache::copy

    fn[K, V] LfuCache::copy(self : LfuCache[K, V]) -> LfuCache[K, V]

    LfuCache::equal

    fn[K : Hash + Eq, V : Eq] LfuCache::equal(self : LfuCache[K, V], other : LfuCache[K, V]) -> Bool

    LfuCache::frequency

    fn[K : Hash + Eq, V] LfuCache::frequency(self : LfuCache[K, V], key : K) -> Int?

    The access count of key, which is one more than the number of get calls that have hit it since it was inserted.

    LfuCache::get

    fn[K : Hash + Eq, V] LfuCache::get(self : LfuCache[K, V], key : K) -> V?

    LfuCache::is_empty

    fn[K, V] LfuCache::is_empty(self : LfuCache[K, V]) -> Bool

    LfuCache::iter

    fn[K, V] LfuCache::iter(self : LfuCache[K, V]) -> Iter[(K, V)]

    LfuCache::keys

    fn[K, V] LfuCache::keys(self : LfuCache[K, V]) -> Iter[K]

    LfuCache::length

    fn[K, V] LfuCache::length(self : LfuCache[K, V]) -> Int

    LfuCache::new

    fn[K, V] LfuCache::new(capacity~ : Int, on_evict? : (K, V) -> Unit, hash_seed? : Int) -> LfuCache[K, V] raise
    Refused

    LfuCache::not_equal

    fn[K : Hash + Eq, V : Eq] LfuCache::not_equal(x : LfuCache[K, V], y : LfuCache[K, V]) -> Bool

    LfuCache::output

    fn[K : Show, V : Show] LfuCache::output(self : LfuCache[K, V], logger : &Logger) -> Unit

    LfuCache::peek

    fn[K : Hash + Eq, V] LfuCache::peek(self : LfuCache[K, V], key : K) -> V?

    The value stored under key without counting the access.

    LfuCache::remove

    fn[K : Hash + Eq, V] LfuCache::remove(self : LfuCache[K, V], key : K) -> V?

    LfuCache::resize_capacity

    fn[K : Hash + Eq, V] LfuCache::resize_capacity(self : LfuCache[K, V], capacity~ : Int) -> Unit raise
    Refused

    LfuCache::set

    fn[K : Hash + Eq, V] LfuCache::set(self : LfuCache[K, V], key : K, value : V) -> (K, V)?

    LfuCache::stats

    fn[K, V] LfuCache::stats(self : LfuCache[K, V]) -> CacheStats

    LfuCache::take

    fn[K : Hash + Eq, V] LfuCache::take(self : LfuCache[K, V], key : K) -> V?

    Removes the entry and returns it, which is how a cache is drained one entry at a time.

    LfuCache::to_string

    fn[K : Show, V : Show] LfuCache::to_string(self : LfuCache[K, V]) -> String

    LfuCache::values

    fn[K, V] LfuCache::values(self : LfuCache[K, V]) -> Iter[V]

    LfuNode

    pub struct LfuNode[K, V] {
    key : K
    value : V
    freq : Int
    prev : Int
    next : Int
    }

    A node of the frequency lists. Every live entry sits in the list of its own frequency, most recently used first, so the entry to drop is the head of the lowest non-empty list.

    LruCache

    pub struct LruCache[K, V] {
    index :
    IndexMap
    [K, Int]
    slots : Array[LruNode[K, V]?]
    free : Array[Int]
    capacity : Int
    head : Int
    tail : Int
    hits : Int64
    misses : Int64
    evictions : Int64
    on_evict : (K, V) -> Unit?
    }

    A least-recently-used cache with a fixed capacity: get marks an entry as the most recent, and an insert past the capacity drops the entry that has gone untouched the longest.

    test {
    let cache : @cache.LruCache[String, Int32] = @cache.LruCache::new(capacity=2)
    let _ = cache.set("a", 1)
    let _ = cache.set("b", 2)
    let _ = cache.get("a")
    let _ = cache.set("c", 3)
    @test.assert_eq(cache.get("b"), None)
    @test.assert_eq(cache.get("a"), Some(1))
    }
    impl Eq for LruCache[K, V]
    impl Show for LruCache[K, V]

    LruCache::capacity

    fn[K, V] LruCache::capacity(self : LruCache[K, V]) -> Int

    LruCache::clear

    fn[K, V] LruCache::clear(self : LruCache[K, V]) -> Unit

    LruCache::contains

    fn[K : Hash + Eq, V] LruCache::contains(self : LruCache[K, V], key : K) -> Bool

    LruCache::copy

    fn[K, V] LruCache::copy(self : LruCache[K, V]) -> LruCache[K, V]

    LruCache::equal

    fn[K : Hash + Eq, V : Eq] LruCache::equal(self : LruCache[K, V], other : LruCache[K, V]) -> Bool

    LruCache::get

    fn[K : Hash + Eq, V] LruCache::get(self : LruCache[K, V], key : K) -> V?

    The value stored under key, marking the entry as the most recently used.

    test {
    let cache : @cache.LruCache[Int32, String] = @cache.LruCache::new(capacity=1)
    let _ = cache.set(1, "one")
    @test.assert_eq(cache.get(1), Some("one"))
    @test.assert_eq(cache.get(2), None)
    @test.assert_eq(cache.stats().misses, 1L)
    }

    LruCache::is_empty

    fn[K, V] LruCache::is_empty(self : LruCache[K, V]) -> Bool

    LruCache::iter

    fn[K, V] LruCache::iter(self : LruCache[K, V]) -> Iter[(K, V)]

    The entries from least to most recently used.

    LruCache::keys

    fn[K, V] LruCache::keys(self : LruCache[K, V]) -> Iter[K]

    LruCache::length

    fn[K, V] LruCache::length(self : LruCache[K, V]) -> Int

    LruCache::new

    fn[K, V] LruCache::new(capacity~ : Int, on_evict? : (K, V) -> Unit, hash_seed? : Int) -> LruCache[K, V] raise
    Refused

    LruCache::not_equal

    fn[K : Hash + Eq, V : Eq] LruCache::not_equal(x : LruCache[K, V], y : LruCache[K, V]) -> Bool

    LruCache::output

    fn[K : Show, V : Show] LruCache::output(self : LruCache[K, V], logger : &Logger) -> Unit

    LruCache::peek

    fn[K : Hash + Eq, V] LruCache::peek(self : LruCache[K, V], key : K) -> V?

    The value stored under key, without changing the order. A cache that peeks keeps whatever it looked at alive, which is how a scan warms entries without keeping them.

    LruCache::remove

    fn[K : Hash + Eq, V] LruCache::remove(self : LruCache[K, V], key : K) -> V?

    Removes the entry under key. A removal is not an eviction: on_evict is not called and the counter does not move.

    LruCache::resize_capacity

    fn[K : Hash + Eq, V] LruCache::resize_capacity(self : LruCache[K, V], capacity~ : Int) -> Unit raise
    Refused

    Changes the capacity, dropping the least recently used entries when the new capacity is smaller than the number of entries held.

    LruCache::set

    fn[K : Hash + Eq, V] LruCache::set(self : LruCache[K, V], key : K, value : V) -> (K, V)?

    Stores value under key and returns the entry the insert displaced: the pair it replaced when the key was already present, the pair the policy dropped otherwise, None when there was room. Compare the key of the result to tell the two apart.

    test {
    let cache : @cache.LruCache[String, Int32] = @cache.LruCache::new(capacity=1)
    @test.assert_eq(cache.set("a", 1), None)
    @test.assert_eq(cache.set("b", 2), Some(("a", 1)))
    @test.assert_eq(cache.set("b", 3), Some(("b", 2)))
    }

    LruCache::stats

    fn[K, V] LruCache::stats(self : LruCache[K, V]) -> CacheStats

    LruCache::to_string

    fn[K : Show, V : Show] LruCache::to_string(self : LruCache[K, V]) -> String

    LruCache::values

    fn[K, V] LruCache::values(self : LruCache[K, V]) -> Iter[V]

    LruNode

    pub struct LruNode[K, V] {
    key : K
    value : V
    prev : Int
    next : Int
    }

    A node of one of the order lists. The slot array is indexed by a slab-style handle; a free slot holds None, so a stale handle cannot be mistaken for a live one.

    memoize

    fn[K : Hash + Eq, V] memoize(f : (K) -> V, capacity~ : Int) -> ((K) -> V) raise
    Refused

    Wraps f so that each argument is computed once and remembered, the way functools.lru_cache does it: the returned function keeps the last capacity results and recomputes anything that fell out.

    test {
    let mut calls = 0
    let square : (Int32) -> Int32 = @cache.memoize(
    key => {
    calls += 1
    key * key
    },
    capacity=8,
    )
    @test.assert_eq(square(4), 16)
    @test.assert_eq(square(4), 16)
    @test.assert_eq(calls, 1)
    }