;;; -*- Mode: LISP; Base: 10; Syntax: ANSI-COMMON-LISP; Package: (MEMOIZE) -*- ;; File - memoize.lisp ;; Description - memoization ;; Author - Tim Bradshaw (tfb at lostwithiel) ;; Created On - 1995? ;; Last Modified On - Thu Apr 19 23:11:58 2007 ;; Last Modified By - Tim Bradshaw (tfb at fowey.cley.com) ;; Update Count - 13 ;; Status - Unknown ;; ;; $Id: //depot/www-tfeb-org/main/www-tfeb-org/html/programs/lisp/memoize.lisp#2 $ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; * Memoization ;;; Norvig p269-275 ;;; memoize.lisp is copyright 1995-2000 by me, Tim Bradshaw, and may ;;; be used for any purpose whatsoever by anyone. It has no warranty ;;; whatsoever. I would appreciate acknowledgement if you use it in ;;; anger, and I would also very much appreciate any feedback or bug ;;; fixes. ;;; Note that memoized functions are not currently thread-safe, since ;;; calling them can modify the structure holding the memos. ;;; (defpackage :org.tfeb.hax.memoize (:use :cl) (:export #:memoize-function #:unmemoize-function #:unmemoize-functions #:clear-memoized-function #:clear-memoized-functions #:function-memoized-p #:def-memoized-function #:memoized-labels)) (in-package :org.tfeb.hax.memoize) (provide :org.tfeb.hax.memoize) (defvar *memoized-functions* '() ;; stores an alist of (name table old-def) ) (defun make-memo (fn key test) ;; Return wrapper & table (declare (type function fn key test)) (let ((table (make-hash-table :test test))) (values #'(lambda (&rest args) (declare (dynamic-extent args)) (let ((k (funcall key args))) (multiple-value-bind (val found-p) (gethash k table) (if found-p val (setf (gethash k table) (apply fn args)))))) table))) ;;; semi user-interface fns (defun memoize-function (fn-name &key (key #'first) (test #'eql)) "Memoize FN-NAME, a symbol, causing its results to be stashed. KEY is a function which is given the arglist of FN-NAME, and should return a key to hash on for memoizing. TEST is a function which the test for the ashtable. See Norvig P269-275. Note this function may not work on self-recursive functions because the compiler can optimize away self-calls in various ways. DEF-MEMOIZED-FUNCTION should work for those cases as it is careful to ensure the function can not be inlined like this." (declare (type symbol fn-name) (type function key test)) (when (not (fboundp fn-name)) (error "~A is not FBOUNDP" fn-name)) (when (assoc fn-name *memoized-functions*) (error "~A is already memoized" fn-name)) (multiple-value-bind (wrapper table) (make-memo (symbol-function fn-name) key test) (push (list fn-name table (symbol-function fn-name)) *memoized-functions*) (setf (symbol-function fn-name) wrapper) fn-name)) (defun unmemoize-function (fn-name) "Remove memoization for FN-NAME" (declare (type symbol fn-name)) (let ((hit (assoc fn-name *memoized-functions*))) (when (not hit) (error "~A is not memoized" fn-name)) (setf (symbol-function fn-name) (third hit)) (setf *memoized-functions* (delete hit *memoized-functions*)) fn-name)) (defun unmemoize-functions () ;; complain about all the double-lookup & consing & I'll laugh at ;; you. "Unmemoize all functions" (mapcar #'unmemoize-function (mapcar #'car *memoized-functions*))) (defun clear-memoized-function (fn-name) "Clear memoized results for FN-NAME" (declare (type symbol fn-name)) (let ((hit (assoc fn-name *memoized-functions*))) (when (not hit) (error "~A is not memoized" fn-name)) (clrhash (second hit)) fn-name)) (defun clear-memoized-functions () "Clear memoized results for all functions" (mapcar #'clear-memoized-function (mapcar #'car *memoized-functions*))) (defun function-memoized-p (fn-name) "Is FN-NAME memoized?" (declare (type symbol fn-name)) (if (assoc fn-name *memoized-functions*) t nil)) (defmacro def-memoized-function (fnspec args &body bod) "Define a memoized function. FNSPEC is either the name of the function, or a list suitable as an arglist for MEMOIZE-FUNCTION. ARGS & BOD are passed off to DEFUN. This will declare FNSPEC NOTINLINE, which may be necessary to prevent good compilers optimizing away self calls & stuff like that." ;; the sorts of fns that are usefully inlineable and those that are ;; usefully memoizable are probably disjoint... (let* ((normalized-fnspec (etypecase fnspec (symbol (list fnspec)) (list fnspec))) (name (car normalized-fnspec))) (when (function-memoized-p name) (unmemoize-function name)) `(progn ;; ??? is this right? I want to ensure that the function is ;; really called, and avoid bright compilers doing TRO or not ;; calling through the SYMBOL-FUNCTION (kind of a strange thing ;; to want in general). I think that a NOTINLINE declaration ;; does this. (declaim (notinline ,name)) (defun ,name ,args ;; ??? can we need NOTINLINE here as well? ,@bod) (apply #'memoize-function (list ',(car normalized-fnspec) ,@(cdr normalized-fnspec))) ',name))) #|| (def-memoized-function fib (n) (if (<= n 1) 1 (+ (fib (- n 1)) (fib (- n 2))))) ||# (defmacro memoized-labels ((&rest labdefs) &body bod) "A version of LABELS that memoizes the local functions. See MEMOIZE-FUNCTION and DEF-MEMOIZED-FUNCTION. If code that uses this is compiled (either by COMPILE or COMPILE-FILE, then the table of memoized results will be unique, if interpreted then a new table may be generated for each use. The function `names' are generalised in the same way as for DEF-MEMOIZED-FUNCTION." ;; this is a pretty hairy macro, perhaps unnecessarily so. It uses ;; an interestingly-large amount of the features of CL. The use of ;; LOAD-TIME-VALUE is an attempt to get literal hashtables into the ;; compiled code, which seems to be non-portable the obvious way ;; (binding them in the macro & then splicing the literal in to the ;; expansion). Can MAKE-LOAD-FORM do this better? `(labels ,(loop for (fspec fargs . fbod) in labdefs collect (destructuring-bind (fname &key (key '(function first)) (test '(function eql))) (if (listp fspec) ;; FSPEC is of the form (NAME :key ;; .. :test ..), where we use the keywords ;; to get the key from the arglist and ;; decide what test to use for the ;; hashtable. fspec (list fspec :key '(function first) :test '(function eql))) (let ((htn (make-symbol "HT")) ;hashtable name (kn (make-symbol "K")) ;key from arglist name (vn (make-symbol "V")) ;value found name (fpn (make-symbol "FP")) ;foundp name (argsn (make-symbol "ARGS"))) ;args name ;; here's the definition clause in the LABELS: ;; note we have to generalise rthe args to an ;; &REST, but hopefully the DYNAMIC-EXTENT ;; avoids too much lossage. `(,fname (&rest ,argsn) (declare (dynamic-extent ,argsn) ;stop consing (notinline ,fname)) ;stop TRO (?) ;; this use of LOAD-TIME-VALUE should ensure ;; that the hashtable is unique in compiled ;; code. This has kind of interesting ;; effects, as it's shared amongst seperate ;; closures that you might return, so use of ;; one can speed up another! (let ((,htn (load-time-value (make-hash-table :test ,test))) (,kn (funcall ,key ,argsn))) (multiple-value-bind (,vn ,fpn) (gethash ,kn ,htn) (if ,fpn ,vn ;found in table: return value ;; didn't find it: compute value (setf (gethash ,kn ,htn) (apply #'(lambda ,fargs ,@fbod) ,argsn))))))))) ,@bod)) ;;; indentation for zmacs #+Genera (pushnew 'memoized-labels zwei:*definition-list-functions*)