Initial import

This commit is contained in:
2026-03-07 21:53:31 +01:00
parent e6db2c661c
commit b9eff3bd57
5 changed files with 398 additions and 0 deletions

195
scribblings/lru-cache.scrbl Normal file
View File

@@ -0,0 +1,195 @@
#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 uses a @racket[box] internally and is
@emph{not} intrinsically thread-safe. Use external synchronization for concurrent access.
@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 not thread-safe; use locks for concurrent access.}
@item{Expiration cleanup is lazy. If you need stricter guarantees, trigger
periodic queries (e.g., @racket[lru-count]) or add an explicit cleanup function.}
]