Initial import
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -15,3 +15,9 @@ compiled/
|
|||||||
# Dependency tracking files
|
# Dependency tracking files
|
||||||
*.dep
|
*.dep
|
||||||
|
|
||||||
|
/*.bak
|
||||||
|
/private/*.bak
|
||||||
|
/scribblings/*.bak
|
||||||
|
/scribblings/*.html
|
||||||
|
/scribblings/*.css
|
||||||
|
/scribblings/*.js
|
||||||
|
|||||||
20
info.rkt
Normal file
20
info.rkt
Normal 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
6
main.rkt
Normal 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
171
private/lru-cache.rkt
Normal 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
195
scribblings/lru-cache.scrbl
Normal 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.}
|
||||||
|
]
|
||||||
|
|
||||||
Reference in New Issue
Block a user