Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repeat-until-stable #165

Closed
bo-tato opened this issue Jan 18, 2024 · 3 comments
Closed

repeat-until-stable #165

bo-tato opened this issue Jan 18, 2024 · 3 comments

Comments

@bo-tato
Copy link
Contributor

bo-tato commented Jan 18, 2024

This is a utility function to consider adding to serapeum. It repeatedly applies a function to an argument until the output is the same as the input: (repeat-until-stable #'f x) will call (f x) then (f (f x)) then (f (f (f x))) etc until the result is unchanging.
Every year I use it in a couple advent of code problems, and occasionally outside of advent of code. Here is prior art of people making the same utility function in clojure and haskell. I chose the name repeat-until-stable as it seems people either call it repeat-until-stable or iterate-until-stable and iterate names are used by the iterate package and the only related function I find in any lisp library is repeat-function from agutil, so it seemed a better fit for existing CL names.

Here is a simple tail-recursive implementation:

(defun repeat-until-stable (f x &key (test #'eql))
  "Repeatedly call (f x), then (f (f x)), etc until the result doesn't change
according to TEST."
  (let ((next (funcall f x)))
    (if (funcall test next x)
        x
        (repeat-until-stable f next :test test))))

sbcl at least on default settings optimizes the tail recursion, but maybe to be safe on all implementations here is a non-recursive version:

(defun repeat-until-stable (f x &key (test #'eql))
  "Repeatedly call (f x), then (f (f x)), etc until the result doesn't change
according to TEST."
  (loop for previous = current
        for current = x then (funcall f current)
        when (funcall test previous current)
          return current))

If you'd like to add this utility function to reduce your workload a little I can submit a PR adding this and some unit tests and documentation to reference.md and whatever else is needed.

@ruricolist
Copy link
Owner

This looks like a good idea. It might also be a good use case for defloop.

One possible enhancement I would like to see is the ability to specify a maximum recursion depth as a keyword argument.

@bo-tato
Copy link
Contributor Author

bo-tato commented Jan 20, 2024

Interesting I didn't know about defloop/nlet for guaranteeing tail recursion. Here's an implementation using defloop and accepting a keyword argument with maximum recursion depth:

(defloop repeat-until-stable (f x &key (test #'eql) max-depth)
  "Repeatedly call (f x), then (f (f x)), etc until the result doesn't change
according to TEST. If MAX-DEPTH is specified, stop after calling F MAX-DEPTH times."
  (if (eql 0 max-depth)
      x
      (let ((next (funcall f x)))
        (if (funcall test next x)
            x
            (repeat-until-stable f next :test test
                                        :max-depth (when max-depth
                                                     (1- max-depth)))))))

@ruricolist
Copy link
Owner

This looks good if you'd like to make a pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants