Initial import
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
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