195 lines
6.9 KiB
Racket
195 lines
6.9 KiB
Racket
#lang scribble/manual
|
|
@(require scribble/example)
|
|
|
|
@title{LRU Cache (Least Recently Used) for Racket}
|
|
@author{You}
|
|
|
|
@defmodule[lru-cache]{
|
|
An in-memory LRU cache with optional item expiration. The cache keeps items in
|
|
recency order and evicts the least recently used item when the maximum capacity
|
|
is reached. Optionally, items can expire after a given number of seconds.
|
|
}
|
|
|
|
@section{Overview}
|
|
|
|
This module provides:
|
|
|
|
@itemlist[
|
|
@item{@racket[make-lru] to construct a cache with a @italic{max-count}, an
|
|
optional equality/compare function for items, and optional expiration (seconds).}
|
|
@item{Mutation operations: @racket[lru-add!], @racket[lru-clear],
|
|
@racket[set-lru-max-count!], @racket[set-lru-expire!].}
|
|
@item{Query operations: @racket[lru-has?], @racket[lru-count], @racket[lru->list],
|
|
@racket[lru-expires?], @racket[lru-expire], @racket[lru-max-count], @racket[lru?].}
|
|
]
|
|
|
|
The cache stores @emph{items} (values) and uses a comparison function to decide
|
|
whether an item is already present. Each item is associated with a last-access
|
|
timestamp that drives recency; on insert or hit, the item is bubbled to the front.
|
|
|
|
@bold{Thread-safety:} The implementation is thread-safe, but not reentrant.
|
|
|
|
@section{Data Types}
|
|
|
|
@defstruct*[lru* ([cache box?]
|
|
[compare procedure?]
|
|
[max-count (and/c integer? (>/c 0))]
|
|
[expire-in-seconds (or/c #f (and/c integer? (>/c 0)))])]{
|
|
Internal representation of the LRU cache. Use @racket[lru?] to test for this type;
|
|
do not rely on fields directly. Create instances with @racket[make-lru].
|
|
}
|
|
|
|
@defproc[(lru? [v any/c]) boolean?]{
|
|
Predicate that returns @racket[#t] when @racket[v] is an LRU instance.
|
|
}
|
|
|
|
@section{Construction}
|
|
|
|
@defproc[(make-lru
|
|
[max-count (and/c integer? (>/c 0))]
|
|
[#:cmp compare (-> any/c any/c boolean?) equal?]
|
|
[#:expire expire-in-seconds (or/c #f (and/c integer? (>/c 0))) #f])
|
|
lru?]{
|
|
Create a new LRU cache.
|
|
|
|
@itemlist[
|
|
@item{@racket[max-count] — Maximum number of items kept in the cache. When
|
|
the limit is exceeded, the least recently used item is evicted.}
|
|
@item{@racket[#:cmp compare] — Binary comparison function that returns
|
|
@racket[#t] when two items are considered equal (default: @racket[equal?]).}
|
|
@item{@racket[#:expire expire-in-seconds] — If a positive integer: items expire
|
|
automatically after that many seconds of inactivity. @racket[#f] disables
|
|
expiration (default).}
|
|
]
|
|
|
|
Returns A new empty LRU cache with the given parameters.
|
|
}
|
|
|
|
@section{Core Operations}
|
|
|
|
@defproc[(lru-add! [l lru?] [item any/c]) lru?]{
|
|
Insert @racket[item]. If it already exists (per @racket[compare]), it is
|
|
considered a hit: moved to the front and its access time is refreshed.
|
|
If capacity is reached, the least recently used item is removed.
|
|
Returns @racket[l].
|
|
}
|
|
|
|
@defproc[(lru-has? [l lru?] [item any/c]) boolean?]{
|
|
Returns @racket[#t] if @racket[item] is currently present (and not expired),
|
|
otherwise @racket[#f]. This call also performs lazy cleanup of expired items.
|
|
}
|
|
|
|
@defproc[(lru-clear [l lru?]) lru?]{
|
|
Clear all items from the cache. Returns @racket[l].
|
|
}
|
|
|
|
@defproc[(lru-count [l lru?]) (and/c integer? (>=/c 0))]{
|
|
Return the number of @emph{non-expired} items currently in the cache.
|
|
}
|
|
|
|
@defproc[(lru->list [l lru?] [#:with-expire with-expire? boolean? #f]) list?]{
|
|
Return the cache contents in recency order (most recent first).
|
|
When @racket[with-expire?] is @racket[#f] (default), returns just the list of items.
|
|
When @racket[with-expire?] is @racket[#t], returns @racket[(list item age-in-seconds)]
|
|
pairs, where @racket[age-in-seconds] is the elapsed time since last access/insert.
|
|
}
|
|
|
|
@section{Parameters and Configuration}
|
|
|
|
@defproc[(lru-max-count [l lru?]) (and/c integer? (>/c 0))]{
|
|
Read the maximum capacity.
|
|
}
|
|
|
|
@defproc[(set-lru-max-count! [l lru?] [n (and/c integer? (>/c 0))]) lru?]{
|
|
Set the maximum capacity. If the current size exceeds @racket[n], trimming
|
|
will occur on subsequent mutations or cleanups, depending on usage. Returns @racket[l].
|
|
}
|
|
|
|
@defproc[(lru-expire [l lru?]) (or/c #f (and/c integer? (>/c 0)))]{
|
|
Read the expiration period in seconds, or @racket[#f] if expiration is disabled.
|
|
}
|
|
|
|
@defproc[(set-lru-expire! [l lru?] [expire (or/c #f (and/c integer? (>/c 0)))]) lru?]{
|
|
Set the expiration period (seconds) or disable it with @racket[#f]. Returns @racket[l].
|
|
}
|
|
|
|
@defproc[(lru-expires? [l lru?]) boolean?]{
|
|
Return @racket[#t] when expiration is enabled (i.e., @racket[(lru-expire l)] is
|
|
a positive integer), otherwise @racket[#f].
|
|
}
|
|
|
|
@section{Examples}
|
|
|
|
@examples[
|
|
#:eval (make-base-eval)
|
|
(require racket/base)
|
|
|
|
;; Assuming your module is "lru.rkt" next to this file:
|
|
(require "../private/lru-cache.rkt")
|
|
|
|
(define L (make-lru 3)) ;; max 3 items, no expiration
|
|
(lru-add! L 'a)
|
|
(lru-add! L 'b)
|
|
(lru-add! L 'c)
|
|
(lru->list L) ;; '(c b a)
|
|
|
|
(lru-add! L 'b) ;; hit: bubble to front
|
|
(lru->list L) ;; '(b c a)
|
|
|
|
(lru-add! L 'd) ;; capacity 3 -> evicts LRU ('a)
|
|
(lru->list L) ;; '(d b c)
|
|
(lru-count L) ;; 3
|
|
|
|
(lru-has? L 'c) ;; #t
|
|
(lru-has? L 'a) ;; #f
|
|
|
|
(set-lru-expire! L 1) ;; items expire after 1 second
|
|
(sleep 2)
|
|
(lru-has? L 'b) ;; #f (expired)
|
|
(lru->list L) ;; '()
|
|
|
|
(lru-add! L 'x)
|
|
(lru->list L #:with-expire #t) ;; '((x 0)) -- age ~ 0 sec
|
|
|
|
(set-lru-max-count! L 1)
|
|
(lru-add! L 'y)
|
|
(lru->list L) ;; '(y)
|
|
(lru-clear L)
|
|
(lru->list L) ;; '()
|
|
]
|
|
|
|
@section{Behavior Details}
|
|
|
|
@subsection{Recency and Hits}
|
|
Any insertion or hit refreshes the access time and moves the item to the front.
|
|
When capacity is reached, the least recently used item is evicted.
|
|
|
|
@subsection{Expiration}
|
|
If @racket[lru-expire] is a positive integer, items become expired when their
|
|
last access is older than that threshold. Cleanup is performed lazily on calls
|
|
such as @racket[lru-has?], @racket[lru-count], and @racket[lru->list].
|
|
|
|
@subsection{Comparison Function}
|
|
The @racket[#:cmp] function decides item equality (e.g., @racket[equal?],
|
|
@racket[=] for numbers, or a custom predicate). For predictable behavior, it
|
|
should be symmetric and transitive.
|
|
|
|
@section{Errors and Contracts}
|
|
Contracts enforce:
|
|
@itemlist[
|
|
@item{@racket[max-count], @racket[lru-max-count], and @racket[set-lru-max-count!]
|
|
use positive integers.}
|
|
@item{@racket[#:expire] and @racket[set-lru-expire!] accept @racket[#f] or
|
|
positive integers.}
|
|
@item{@racket[#:cmp] is a two-argument function returning a @racket[boolean?].}
|
|
]
|
|
Invalid inputs raise contract violations with descriptive error messages.
|
|
|
|
@section{Notes}
|
|
@itemlist[
|
|
@item{The implementation is thread-safe but not reentrant.}
|
|
@item{Expiration cleanup is lazy. If you need stricter guarantees, trigger
|
|
periodic queries (e.g., @racket[lru-count]) or add an explicit cleanup function.}
|
|
]
|
|
|