• Top
    • Documentation
    • Books
    • Boolean-reasoning
    • Projects
    • Debugging
    • Std
    • Proof-automation
    • Macro-libraries
    • ACL2
      • Theories
      • Rule-classes
      • Proof-builder
      • Recursion-and-induction
      • Hons-and-memoization
        • Memoize
          • Memoize-partial
            • Protect-memoize-statistics
            • Profile-all
            • Profile-ACL2
            • Memoized-prover-fns
            • Unmemoize
            • Verify-guard-implication
            • Memsum
            • Set-bad-lisp-consp-memoize
            • Memoize-summary
            • Save-and-clear-memoization-settings
            • Never-memoize
            • Clear-memoize-tables
            • Clear-memoize-statistics
            • Clear-memoize-table
            • Restore-memoization-settings
          • Fast-alists
          • Hons
          • Set-max-mem
          • Hons-enabled
          • Unmemoize
          • Number-subtrees
        • Events
        • Parallelism
        • History
        • Programming
        • Operational-semantics
        • Real
        • Start-here
        • Debugging
        • Miscellaneous
        • Output-controls
        • Macros
        • Interfacing-tools
      • Interfacing-tools
      • Hardware-verification
      • Software-verification
      • Math
      • Testing-utilities
    • Memoize

    Memoize-partial

    Memoize a partial version of a limited (`clocked') function

    See memoize for relevant background. Here we describe a utility that supports effective memoization for functions that have been introduced by defun, as follows, under certain restrictions.

    (defun fn-limit (x1 ... xn limit)
      (declare ...) ; optional, as usual for defun ; ;
      (if (zp limit)
          <base>
        (let ((limit (1- limit)))
          <body>)))

    Here fn-limit represents a function symbol and (x1 ... xn limit) denotes a non-empty list of formal parameters, where we call the final formal (here, shown as limit) the ``limit variable''. <Base> is any syntactically legal expression, but <body> is restricted as follows: the occurrences of the limit variable are exactly as the final parameter in each recursive call of fn. More precisely, that restriction must hold after <body> is converted to a translated term (see term). In other words, the translation of <body> must have the property that the limit variable only occurs in recursive calls, and in those calls it occurs exactly as the final argument and not in any other arguments.

    Such use of a ``limit'' argument to obtain termination is an old trick that pre-dates ACL2. Such ``limited'' functions have traditionally often been called ``clocked'' functions. In this documentation we refer to such a function as a ``total'' function, because the limit argument guarantees that its evaluation terminates. We will be generating a corresponding ``partial'' function that avoids the limit argument, and hence might not terminate — but when it does, it computes the desired value, and uses memoization to do so, which can speed up computation.

    We organize the rest of this topic as follows: General Form, Basic Example, Example with Mutual Recursion, Detailed Documentation, and Optional Technical Remarks.

    General Form:

    (memoize-partial fn+ ; see below
                     :kwd1 val1 ... :kwdn valn)

    where the supplied :kwdi and vali are valid keywords and values for memoize. In the simplest case, fn+ is a symbol. Later below, after providing an example, we give more complete documentation, including discussion of mutual-recursion and the general case for fn+. None of the arguments is evaluated.

    Basic Example

    The following example from community-book file books/demos/memoize-partial-input.lsp illustrates the use of memoize-partial. Consider the Collatz Conjecture, sometimes known as the 3n+1 problem. For any positive integer n, compute a new integer as n/2 if n is even, else as 3*n+1. The Collatz conjecture states that this process eventually reaches 1. Since this is an open problem, we can't realistically hope to prove termination, so we may code the above algorithm by using a limit argument, as follows. This function returns, unless the limit is reached, the number of steps required for the computation to terminate (reach 1).

    (defun collatz-limit (n limit)
      (declare (xargs :guard (and (natp n)
                                  (natp limit))
                      :measure (acl2-count limit)))
      (if (zp limit)
          (prog2$ (er hard? 'collatz-limit
                      "Limit exceeded!")
                  0)
        (let ((limit (1- limit)))
          (if (int= n 1)
              0
            (1+ (collatz-limit (if (evenp n)
                                   (floor n 2)
                                 (1+ (* 3 n)))
                               limit))))))

    A moment's reflection suggests that if this algorithm terminates, then there are no cycles, so memoization from a single call will be useless. However, memoization can be useful if we make distinct calls. But memoization might not be very useful even then, because after we save the result of a call (collatz-limit n0 limit0), a later call on n0 might be with a different limit, say, (collatz-limit n0 limit1). So we prefer to memoize using the corresponding partial function, without a limit argument. We can do that as follows.

    (memoize-partial collatz)

    Under the hood this generates, essentially, the following Common Lisp definition, which is to be memoized.

    (defun collatz (n)
      (if (int= n 1)
          0
        (1+ (collatz (if (evenp n)
                         (floor n 2)
                       (1+ (* 3 n)))))))

    Since the limit argument has disappeared, memoization is now more effective. But how can one define collatz in the ACL2 logic? The memoize-partial call shown above actually generates the following events, which define collatz to capture the notion of the ``eventual stable value, as limit goes to infinity, of (collatz n limit)''. It probably is not important to read these events unless one plans to reason about collatz, and even then one might be able to complete proofs without reading these events.

    (DEFCHOOSE COLLATZ-LIMIT-CHANGE (LARGE)
      (N LIMIT)
      (AND (NATP LARGE)
           (<= LIMIT LARGE)
           (NOT (EQUAL (COLLATZ-LIMIT N LIMIT)
                       (COLLATZ-LIMIT N LARGE)))))
    (DEFCHOOSE COLLATZ-LIMIT-STABLE (LIMIT)
      (N)
      (AND (NATP LIMIT)
           (EQUAL (COLLATZ-LIMIT N LIMIT)
                  (COLLATZ-LIMIT N (COLLATZ-LIMIT-CHANGE N LIMIT)))))
    (DEFUN COLLATZ (N)
      (DECLARE (XARGS :GUARD (LET ((LIMIT 0))
                                  (DECLARE (IGNORABLE LIMIT))
                                  (AND (NATP N) (NATP LIMIT)))))
      (COLLATZ-LIMIT N (NFIX (COLLATZ-LIMIT-STABLE N))))

    The upshot is a logical definition of (collatz n) as the eventual value (if any), as limit increases without bound, of (collatz n limit) — together with an installed Common Lisp definition that is actually executed and, moreover, is memoized.

    We note that the call (memoize-partial collatz) also generates a table event as well as the form (MEMOIZE 'COLLATZ :TOTAL 'COLLATZ-LIMIT). However, these details are best ignored by the user; we do not expect those events to be generated by hand, and in particular it is probably inadvisable to use the :TOTAL keyword of memoize other than indirectly using memoize-partial. Failure to heed this advice could make failures more difficult to debug.

    See community-book file books/demos/memoize-partial-input.lsp for an application of this use of memoize-partial, to compute the total number of steps required (without accounting for memoization) to run the Collatz algorithm to completion on the first n positive integers.

    Example with Mutual Recursion

    The following example, also from community-book file books/demos/memoize-partial-input.lsp, illustrates the use of memoize-partial on functions that have been defined using mutual-recursion. (The suffix ``-bdd'' below is intended to denote ``bounded'', to suggest what the ``-limit'' suffix suggested in the first example.)

    (mutual-recursion
     (defun evenlp-bdd (x bound)
       (declare (xargs :guard (natp bound)))
       (if (zp bound)
           'ouch
         (let ((bound (1- bound)))
           (if (consp x) (oddlp-bdd (cdr x) bound) t))))
     (defun oddlp-bdd (x bound)
       (declare (xargs :guard (natp bound)))
       (if (zp bound)
           'ouch
         (let ((bound (1- bound)))
           (if (consp x) (evenlp-bdd (cdr x) bound) nil)))))
    
    (memoize-partial ((evenlp evenlp-bdd) (oddlp oddlp-bdd)))

    Notice that this time the argument of memoize-partial is a list of tuples. Each tuple is a list with two elements: the name of the partial function to be defined, followed by the corresponding total (limited) function that has already been defined. Indeed, the preceding example abbreviates (memoize-partial ((collatz collatz-limit))). Such abbreviations are discussed in the Detailed Documentation section below.

    Recall that in the first example, the memoize-partial call defined functions collatz-limit-change and collatz-limit-stable using defchoose, and then function collatz using defun. In the present example such a trio of functions is introduced for each of the functions defined in the corresponding mutual-recursion — equivalently, for each of the tuples supplied to memoize-partial.

    (DEFCHOOSE EVENLP-BDD-CHANGE (LARGE)
      (X BOUND)
      (AND (NATP LARGE)
           (<= BOUND LARGE)
           (NOT (EQUAL (EVENLP-BDD X BOUND)
                       (EVENLP-BDD X LARGE)))))
    (DEFCHOOSE EVENLP-BDD-STABLE (BOUND)
      (X)
      (AND (NATP BOUND)
           (EQUAL (EVENLP-BDD X BOUND)
                  (EVENLP-BDD X (EVENLP-BDD-CHANGE X BOUND)))))
    (DEFUN EVENLP (X)
      (DECLARE (XARGS :GUARD (LET ((BOUND 0))
                                  (DECLARE (IGNORABLE BOUND))
                                  (NATP BOUND))))
      (EVENLP-BDD X (NFIX (EVENLP-BDD-STABLE X))))
    (DEFCHOOSE ODDLP-BDD-CHANGE (LARGE)
      (X BOUND)
      (AND (NATP LARGE)
           (<= BOUND LARGE)
           (NOT (EQUAL (ODDLP-BDD X BOUND)
                       (ODDLP-BDD X LARGE)))))
    (DEFCHOOSE ODDLP-BDD-STABLE (BOUND)
      (X)
      (AND (NATP BOUND)
           (EQUAL (ODDLP-BDD X BOUND)
                  (ODDLP-BDD X (ODDLP-BDD-CHANGE X BOUND)))))
    (DEFUN ODDLP (X)
      (DECLARE (XARGS :GUARD (LET ((BOUND 0))
                                  (DECLARE (IGNORABLE BOUND))
                                  (NATP BOUND))))
      (ODDLP-BDD X (NFIX (ODDLP-BDD-STABLE X))))

    The corresponding Common Lisp functions, to be executed for guard-verified calls of evenlp and oddlp, are defined as one might expect, in analogy to the Common Lisp definition in the first example. Moreover, they are memoized. Here are (again, essentially) the Common Lisp definitions of evenlp and oddlp.

    (DEFUN EVENLP (X)
      (IF (CONSP X)
          (ODDLP (CDR X))
          T))
    (DEFUN ODDLP (X)
      (DECLARE (IGNORABLE X))
      (IF (CONSP X)
          (EVENLP (CDR X))
          NIL))

    Detailed Documentation

    Recall the General Form

    (memoize-partial fn+ ; see below
                     :kwd1 val1 ... :kwdn valn)

    where the supplied :kwdi and vali represent keyword options to be used for a generated call of memoize. We discuss later below how fn+ specifies either a recursively-defined function symbol or a sequence of function symbols defined by mutual-recursion — all guard-verified. We call the final formal parameter, which must be the same for each function symbol defined in the mutual-recursion case, the ``limit variable''. The translated body (see term regarding the notion of ``translated'') of each of these function symbols must be of the following form, or of the corresponding form using cond in place of if, where here limit denotes the limit variable.

    (if (zp limit)
        <base>
      (let ((limit (1- limit)))
        <body>))

    Moreover, each limit variable occurring in <body> must occur as the final argument of a call of the defined function — in the mutual recursion case, of the functions defined by the mutual-recursion — and conversely, each such call must have the limit variable as its final argument.

    Let us refer to each specified function as the ``total'' function. A successful invocation admits a sequence of three definitions for each total function, which we call the ``changed'', ``stable'', and ``partial'' function. Here are those three definitions, where ``...'' denotes the formals of the partial function (that is, the formals of the total function other than the final formal, which is the limit variable). We write LIMIT below to denote the limit variable. Note that LARGE is replaced by a fresh variable when necessary, to avoid duplicating an existing formal.

    (DEFCHOOSE <changed> (LARGE)
      (... LIMIT)
      (AND (NATP LARGE)
           (<= LIMIT LARGE)
           (NOT (EQUAL (<total> ... LIMIT)
                       (<total> ... LARGE)))))
    (DEFCHOOSE <stable> (LIMIT)
      (...)
      (AND (NATP LIMIT)
           (EQUAL (<total> ... LIMIT)
                  (<total> ... (<change> ... LIMIT)))))
    (DEFUN <partial> (...)
      (DECLARE (XARGS :GUARD (LET ((LIMIT 0))
                                  (DECLARE (IGNORABLE LIMIT))
                                  <guard_of_total>)))
      (<total> ... (NFIX (<stable> ...))))

    Under the hood, however, a Common Lisp definition is installed for <partial> that avoids the limit variable, essentially by eliminating it in <body> to produce a corresponding term, <body'>, with the following result. (Low-level details about exactly what is generated are discussed in the Optional Technical Remarks below, but should probably be ignored by most users.)

    (defun <partial> (...)
      <body'>

    We turn now to the first argument of memoize-partial, denoted fn+ above, with discussion of its legal forms and its relation to the other (keyword) arguments of memoize-partial. The most flexible form for fn+ is a list of tuples, where each tuple is of the following form and the keyword arguments are optional and in any order.

    (<partial> <total>
     :change <change>
     :stable <stable>
     :kwd1 val1 ... :kwdn valn)

    The symbols <partial>, <total>, <change>, and <stable> are as discussed above, and the arguments :kwd1 val1 ... :kwdn valn are as supplied to memoize. There should be a single tuple in the singly recursive case. In the case that the partial functions were defined by a mutual-recursion defining functions f1, ..., fn, then the <partial> components of the tuples (their cars) must also be f1, ..., fn.

    The keyword/value pairs of the tuple are optional. The default for <change> is obtained by adding the suffix "-CHANGE" to <total>, to create a symbol in the same package as that of <total>; similarly for <stable> and the suffix "-STABLE". The <total> function is defined as described above, with an under-the-hood Common Lisp definition also as described above, and memoized. The memoize call uses the keywords supplied in the tuple other than :change and :stable. For any memoize keywords not specified in the tuple, the keywords specified at the top level of the memoize-partial call — that is, the keyword arguments (if any) following the first argument (that is, following fn+) — are also used in that generated memoize call.

    There are some abbreviations allowed for fn+, as follows.

    • A symbol fn for fn+ abbreviates the list (fn), which is an abbreviation as noted below.
    • If fn+ is a list (t1 ... tk), then each ti that is a symbol, say fn, abbreviates the tuple (fn fn-limit), where fn-limit is obtained by adding the suffix "-LIMIT" to fn, to create a symbol in the same package as that of fn.

    Consider our first example, (memoize-partial collatz). In this case fn+ is collatz. By the first rule above, this abbreviates (collatz), which the second rule says is an abbreviation for (collatz collatz-limit). The default values are computed to treat this as the following tuple.

    (collatz collatz-limit
             :change collatz-limit-change
             :stable collatz-limit-stable)

    Note that the keyword value :condition nil for memoize can be used to install executable definitions without actually saving values, essentially by merely profiling (except that unlike profile the recursive calls are also affected by the memoize call). See the example involving ``worse-than'' functions in community book books/demos/memoize-partial-input.lsp.

    We conclude this section by discussing restrictions pertaining to stobjs. Recall that memoization is illegal (other than profiling) for functions that return a stobj or have the ACL2 state as an input. Those same restrictions apply to memoize-partial. However, it is legal for the given (``total'') functions to have user-defined stobjs as inputs, as long as they are not returned. In that case, the generated definition of the partial function will be made with a call of non-exec wrapped around the body.

    Optional Technical Remarks.

    Warning: These are technical remarks that are completely optional.

    Our general remark is to invite readers who want to dive below the user level, including implementation and foundational issues, to read the comment entitled ``Essay on Memoization with Partial Functions (Memoize-partial)'' in ACL2 source file other-events.lisp.

    Our more specific remark concerns generated Common Lisp definitions. Recall that in the Basic Example above, we say that the Common Lisp definition of collatz is essentially as follows.

    (defun collatz (n)
      (if (int= n 1)
          0
        (1+ (collatz (if (evenp n)
                         (floor n 2)
                       (1+ (* 3 n)))))))

    We say ``essentially'' because the actual Common Lisp definition of collatz is a bit different, as follows.

    (DEFUN COLLATZ (N)
      (DECLARE (IGNORABLE N))
      (FLET ((COLLATZ-LIMIT (N LIMIT)
                            (DECLARE (IGNORE LIMIT))
                            (COLLATZ N)))
            (DECLARE (INLINE COLLATZ-LIMIT))
            (LET ((LIMIT 0))
                 (DECLARE (IGNORABLE LIMIT))
                 (IF (INT= N 1)
                     0
                     (1+ (COLLATZ-LIMIT
                          (IF (EVENP N) (FLOOR N 2) (1+ (* 3 N)))
                          LIMIT))))))

    A little reflection will conclude that these two definitions are equivalent. The latter, however, avoids a macroexpansion issue discussed in the aforementioned Essay.