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

6
.gitignore vendored
View File

@@ -15,3 +15,9 @@ compiled/
# Dependency tracking files
*.dep
/*.bak
/private/*.bak
/scribblings/*.bak
/scribblings/*.html
/scribblings/*.css
/scribblings/*.js

20
info.rkt Normal file
View File

@@ -0,0 +1,20 @@
#lang info
(define pkg-authors '(hnmdijkema))
(define version "0.1.1")
(define license 'Apache-2.0)
(define pkg-desc "An expiring Least Recently Used (LRU) Cache implementation for racket. O(n) behaviour, not optimized for O(1).")
(define scribblings
'(
("scribblings/lru-cache.scrbl" () (library) "lru-cache")
)
)
(define deps
'("base"))
(define build-deps
'("racket-doc"
"rackunit-lib"
"scribble-lib"))

6
main.rkt Normal file
View File

@@ -0,0 +1,6 @@
#lang racket/base
(require "private/lru-cache.rkt")
(provide (all-from-out "private/lru-cache.rkt"))

171
private/lru-cache.rkt Normal file
View File

@@ -0,0 +1,171 @@
#lang racket/base
(require racket/contract)
(provide make-lru
lru?
lru-max-count
set-lru-max-count!
lru-expires?
lru-expire
set-lru-expire!
lru-has?
lru-add!
lru-count
lru->list
lru-clear
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Struct
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-struct lru*
(cache
compare
[max-count #:mutable]
[expire-in-seconds #:mutable]
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Supporting functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (max-count? c)
(and (integer? c) (> c 0)))
(define (larger-equal0? c)
(and (integer? c) (>= c 0)))
(define (cmp-procedure? cmp-f)
(procedure? cmp-f))
(define (expire? e)
(or (eq? e #f) (max-count? e)))
(define (find-and-bubble count max-count e cache cmp expire-s current-s el)
(if (or (null? cache)
(>= count max-count))
'()
(let ((front (car cache)))
(if (and (not (eq? expire-s #f))
(< (cadr front) expire-s))
(find-and-bubble count max-count e (cdr cache) cmp expire-s current-s el)
(if (cmp e (car front))
(find-and-bubble count max-count e (cdr cache) cmp expire-s current-s (car front))
(cons front
(find-and-bubble (+ count 1) max-count
e (cdr cache) cmp expire-s current-s el))
)
)
)
)
)
(define (find cache cmp-f el)
(if (null? cache)
#f
(if (cmp-f el (caar cache))
#t
(find (cdr cache) cmp-f el)
)
)
)
(define (cleanup-expired lru)
(if (eq? (lru*-expire-in-seconds lru) #f)
'done
(let ((expire-s (- (current-seconds) (lru*-expire-in-seconds lru)))
(lcache (unbox (lru*-cache lru))))
(letrec ((f (λ (cache)
(if (null? cache)
'()
(if (< (cadar cache) expire-s)
(f (cdr cache))
(cons (car cache) (f (cdr cache))))))))
(set-box! (lru*-cache lru) (f lcache)))
'cleaned)))
(define (find-and/or-add lru el)
(let ((ncache (find-and-bubble
1 (lru*-max-count lru)
el (unbox (lru*-cache lru))
(lru*-compare lru)
(if (eq? (lru*-expire-in-seconds lru) #f)
#f
(- (current-seconds) (lru*-expire-in-seconds lru)))
(current-seconds)
#f)))
(set-box! (lru*-cache lru) (cons (list el (current-seconds)) ncache)))
lru)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Supporting functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define/contract (make-lru max-count #:cmp [compare equal?] #:expire [expire-in-seconds #f])
(->* (max-count?) (#:cmp cmp-procedure? #:expire expire?) lru*?)
(make-lru* (box '()) compare max-count expire-in-seconds))
(define/contract (lru-has? lru el)
(-> lru*? any/c boolean?)
(cleanup-expired lru)
(find (unbox (lru*-cache lru)) (lru*-compare lru) el)
)
(define/contract (lru-add! lru el)
(-> lru*? any/c lru*?)
(find-and/or-add lru el)
lru)
(define/contract (lru-clear lru)
(-> lru*? lru*?)
(set-box! (lru*-cache lru) '())
lru)
(define/contract (lru->list lru #:with-expire [we #f])
(->* (lru*?) (#:with-expire boolean?) list?)
(cleanup-expired lru)
(let ((c (current-seconds)))
(map (λ (e)
(if we
(list (car e) (- c (cadr e)))
(car e))) (unbox (lru*-cache lru))))
)
(define/contract (lru-count lru)
(-> lru*? larger-equal0?)
(cleanup-expired lru)
(length (unbox (lru*-cache lru))))
(define (lru? l)
(lru*? l))
(define/contract (lru-max-count l)
(-> lru*? max-count?)
(lru*-max-count l))
(define/contract (lru-expire l)
(-> lru*? expire?)
(lru*-expire-in-seconds l))
(define/contract (set-lru-max-count! l c)
(-> lru*? max-count? lru*?)
(set-lru*-max-count! l c)
l)
(define/contract (set-lru-expire! l e)
(-> lru*? expire? lru*?)
(set-lru*-expire-in-seconds! l e)
l)
(define/contract (lru-expires? l)
(-> lru*? boolean?)
(let ((e (lru*-expire-in-seconds l)))
(if e
(and (integer? e) (> e 0))
#f)))

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.}
]