;; Least recently used cache library for Chicken Scheme ;; Copyright (C) 2026 Tweag SARL ;; ;; This library is free software: you can redistribute it and/or modify ;; it under the terms of the GNU Lesser General Public License as ;; published by the Free Software Foundation, either version 3 of the ;; License, or (at your option) any later version. ;; ;; This library is distributed in the hope that it will be useful, but ;; WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; Lesser General Public License for more details. ;; ;; You should have received a copy of the GNU Lesser General Public ;; License along with this library. If not, see ;; . (module lru-cache ;; Exports (make-lru-cache lru-cache-size lru-cache-capacity lru-cache-ref lru-cache-set! lru-cache-delete! lru-cache-clear! lru-cache-has-key? lru-cache-for-each lru-cache-fold lru-cache->alist lru-cache-keys lru-cache-values define-memoised/lru memoise/lru) (import scheme (chicken base) (chicken format) (chicken type) (srfi 69) matchable) ;; Doubly linked list helpers ; Doubly linked list node type (define-type dll-node (pair 'v (pair 'k 'k))) (: dll-set-previous! (dll-node 'k -> void)) (define (dll-set-previous! node previous-key) (set-car! (cdr node) previous-key)) (: dll-set-next! (dll-node 'k -> void)) (define (dll-set-next! node next-key) (set-cdr! (cdr node) next-key)) ;; LRU cache implementation ; Cache type (somewhat impenetrable!) (define-type lru-cache-closure (symbol #!rest * -> *)) (: make-lru-cache (#!optional integer -> lru-cache-closure)) (define (make-lru-cache #!optional (max-size 64)) ; The cache is a hash table, that represents a doubly linked list; its ; keys are arbitrary (*), with values of the form: ; ; ( . ( . ) ; ; A sentinel symbol is used to denote the termini of the list, but we ; also cache both the head and tail nodes for efficiency. (let* ((terminus (cons 'sentinel '())) (terminus? (lambda (x) (eq? x terminus)))) (letrec ((head terminus) (tail terminus) (cache (the hash-table (make-hash-table #:size max-size))) ; Does the node exist in the cache? (has-node? (lambda (key) (hash-table-exists? cache key))) ; Get the node, which we assume exists, from the cache and ; reorder the list (get-node! (lambda (key) (define node (hash-table-ref cache key)) ; Reorder the list (match node ; Node is already the head ((_ . ((? terminus?) . _)) (void)) ; Node is the tail ((_ . (previous . (? terminus?))) ; Previous node becomes the tail (dll-set-next! (hash-table-ref cache previous) terminus) (set! tail previous) ; Move node to head (dll-set-previous! (hash-table-ref cache head) key) (dll-set-previous! node terminus) (dll-set-next! node head) (set! head key)) ; Node is somewhere in the middle ((_ . (previous . next)) ; Point previous node to next and vice versa (dll-set-next! (hash-table-ref cache previous) next) (dll-set-previous! (hash-table-ref cache next) previous) ; Move node to head (dll-set-previous! (hash-table-ref cache head) key) (dll-set-previous! node terminus) (dll-set-next! node head) (set! head key))) node)) ; Add a node, which we assume doesn't exist in the cache, ; to its head (add-node! (lambda (key value) ; Evict the tail node when at capacity (when (= (hash-table-size cache) max-size) (remove-node! tail)) (hash-table-set! cache key `(,value . (,terminus . ,head))) ; Make the old head point back to the new one (unless (eq? head terminus) (dll-set-previous! (hash-table-ref cache head) key)) ; Update the head and tail pointers (set! head key) (when (eq? tail terminus) (set! tail key)))) ; Remove a node from the cache, by key (remove-node! (lambda (key) (unless (eq? key terminus) ; Reorder the list (match (hash-table-ref cache key) ; When there's only one cached item ((_ . ((? terminus?) . (? terminus?))) (hash-table-delete! cache key) (set! head terminus) (set! tail terminus)) ; Node is the head ((_ . ((? terminus?) . next)) (hash-table-delete! cache key) (dll-set-previous! (hash-table-ref cache next) terminus) (set! head next)) ; Node is the tail ((_ . (previous . (? terminus?))) (hash-table-delete! cache key) (dll-set-next! (hash-table-ref cache previous) terminus) (set! tail previous)) ; Node is somewhere in the middle ((_ . (previous . next)) (hash-table-delete! cache key) (dll-set-next! (hash-table-ref cache previous) next) (dll-set-previous! (hash-table-ref cache next) previous))))))) (lambda msg (match msg ; Size of the cache (`(size) (hash-table-size cache)) ; Capacity of the cache (`(capacity) max-size) ; Get cache entry (`(entry ,key) (if (has-node? key) (car (get-node! key)) (error "no such key" key))) ; Get cache entry, with fallback computation (`(entry ,key ,thunk) (if (has-node? key) (car (get-node! key)) (let ((value (thunk))) (add-node! key value) value))) ; Set a cache entry (`(set! ,key ,value) (if (has-node? key) (set-car! (get-node! key) value) (add-node! key value))) ; Delete a cache entry by key (`(delete! ,key) (if (has-node? key) (remove-node! key) (error "no such key" key))) ; Clear the cache (`(clear!) (hash-table-clear! cache) (set! head terminus) (set! tail terminus)) ; Does the cache have a given key (`(has-key? ,key) (has-node? key)) ; Apply a function to each (key, value) pair, in ; MRU-to-LRU order, without updating the key order (`(for-each ,proc) (let loop ((key head)) (unless (terminus? key) (let ((node (hash-table-ref cache key))) (proc key (car node)) (match node ((_ . (_ . (? terminus?))) (void)) ((_ . (_ . next)) (loop next))))))) ; Otherwise fail (_ (error "Unknown or invalid message"))))))) ;; Public API (: lru-cache-size (lru-cache-closure -> integer)) (define (lru-cache-size lru-cache) (lru-cache 'size)) (: lru-cache-capacity (lru-cache-closure -> integer)) (define (lru-cache-capacity lru-cache) (lru-cache 'capacity)) (: lru-cache-ref (lru-cache-closure 'k #!rest procedure -> 'v)) (define lru-cache-ref (case-lambda ((lru-cache key) (lru-cache 'entry key)) ((lru-cache key thunk) (lru-cache 'entry key thunk)))) (: lru-cache-set! (lru-cache-closure 'k 'v -> void)) (define (lru-cache-set! lru-cache key value) (lru-cache 'set! key value)) (: lru-cache-delete! (lru-cache-closure 'k -> void)) (define (lru-cache-delete! lru-cache key) (lru-cache 'delete! key)) (: lru-cache-clear! (lru-cache-closure -> void)) (define (lru-cache-clear! lru-cache) (lru-cache 'clear!)) (: lru-cache-has-key? (lru-cache-closure 'k -> boolean)) (define (lru-cache-has-key? lru-cache key) (lru-cache 'has-key? key)) (: lru-cache-for-each (lru-cache-closure ('k 'v -> *) -> void)) (define (lru-cache-for-each lru-cache proc) (lru-cache 'for-each proc)) (: lru-cache-fold (lru-cache-closure ('k 'v 'a -> 'a) 'a -> 'a)) (define (lru-cache-fold lru-cache proc init) (let ((acc init)) (lru-cache-for-each lru-cache (lambda (key value) (set! acc (proc key value acc)))) acc)) (: lru-cache->alist (lru-cache-closure -> (list-of (pair 'k 'v)))) (define (lru-cache->alist lru-cache) (reverse (lru-cache-fold lru-cache (lambda (key value acc) (cons `(,key . ,value) acc)) '()))) (: lru-cache-keys (lru-cache-closure -> (list-of 'k))) (define (lru-cache-keys lru-cache) (map car (lru-cache->alist lru-cache))) (: lru-cache-values (lru-cache-closure -> (list-of 'v))) (define (lru-cache-values lru-cache) (map cdr (lru-cache->alist lru-cache))) (define-syntax define-memoised/lru (syntax-rules () ; Default capacity ((_ (name arg ...) body ...) (define name (let ((cache (make-lru-cache))) (lambda (arg ...) (lru-cache-ref cache (list arg ...) (lambda () body ...)))))) ; Explicit capacity ((_ capacity (name arg ...) body ...) (define name (let ((cache (make-lru-cache capacity))) (lambda (arg ...) (lru-cache-ref cache (list arg ...) (lambda () body ...)))))))) (: memoise/lru (procedure #!optional integer -> procedure)) (define (memoise/lru proc #!optional (max-size 64)) (let ((cache (make-lru-cache max-size))) (lambda args (lru-cache-ref cache args (lambda () (apply proc args)))))))