Implementing a cache

A MemoizationKit.AbstractCache is the container behind GlobalCache and TaskLocalCache. Besides LRU and ClockCache, any subtype C{K, V} <: MemoizationKit.AbstractCache{K, V} implementing these methods can be used:

MethodContract
C{K, V}(; maxsize, by)An empty cache. maxsize bounds the number of entries, or the sum of by(value) if by !== nothing.
get!(default, c, key)The value stored for key, or else default(), stored if it fits. Keys of different types are different entries.
empty!(c)Remove all entries, keeping the statistics.
resize!(c; maxsize)Set the size limit, evicting entries until they fit.
MemoizationKit.cache_stats(c)(; hits, misses, length, currentsize, maxsize, by)

They may be called from any task, so they must be thread-safe, and get! must not hold a lock while it calls default, which may recurse into the same cache or throw. show is derived from cache_stats; the other AbstractDict methods are optional.

Example: first in, first out

using MemoizationKit

mutable struct FIFO{K, V} <: MemoizationKit.AbstractCache{K, V}
    const entries::Dict{Any, Tuple{V, Int}} # (typeof(key), key) => (value, size)
    const order::Vector{Any}                # keys of `entries`, oldest first
    const lock::ReentrantLock
    const by::Any
    maxsize::Int
    currentsize::Int
    hits::Int
    misses::Int
end

FIFO{K, V}(; maxsize = 10_000, by = nothing) where {K, V} =
    FIFO{K, V}(Dict{Any, Tuple{V, Int}}(), [], ReentrantLock(), by, maxsize, 0, 0, 0)

function Base.get!(default::Base.Callable, c::FIFO{K, V}, key) where {K, V}
    k = (typeof(key), key)
    @lock c.lock begin
        haskey(c.entries, k) && (c.hits += 1; return c.entries[k][1])
        c.misses += 1
    end
    v = convert(V, default())::V # without the lock
    @lock c.lock begin
        haskey(c.entries, k) && return c.entries[k][1] # stored by another task meanwhile
        sz = c.by === nothing ? 1 : Int(c.by(v))
        c.entries[k] = (v, sz)
        push!(c.order, k)
        c.currentsize += sz
        evict!(c)
    end
    return v
end

function evict!(c::FIFO)
    while c.currentsize > c.maxsize
        c.currentsize -= pop!(c.entries, popfirst!(c.order))[2]
    end
    return c
end

Base.empty!(c::FIFO) = @lock c.lock (empty!(c.entries); empty!(c.order); c.currentsize = 0; c)
Base.resize!(c::FIFO; maxsize::Integer) = @lock c.lock (c.maxsize = maxsize; evict!(c))
MemoizationKit.cache_stats(c::FIFO) =
    @lock c.lock (; c.hits, c.misses, length = length(c.entries), c.currentsize, c.maxsize, c.by)

@cached square(x) = x^2
MemoizationKit.CacheStyle(::typeof(square), x) = GlobalCache{FIFO}()

square.(1:3); square(3); square(3.0)
set_cache_size!(square, 2) # evicts square(1) and square(2)
square(1)
only(cache_info(square)).second

# output

FIFO{Any, Any}(2/2 entries, 1 hits, 5 misses)

TaskLocalCache{FIFO}() works the same way. This FIFO boxes its keys, so unlike LRU and ClockCache its hits allocate.