Search code examples
python-3.xschemeelispsicpfixed-point-iteration

The inner `try` interation in `fixed-point`


I am reading the fix-point of SICP:

#+begin_src emacs-lisp :session sicp :lexical t
(defvar tolerance 0.00001)
(defun fixed-point(f first-guess)
  (defun close-enoughp(v1 v2) 
    (< (abs (- v1 v2)) tolerance))
  (defun try(guess) ;;
    (let ((next (funcall f guess)))
      (if (close-enoughp guess next)
          next
          (try next))))
  (try first-guess))
(fixed-point #'cos 1.0)
#+end_src

#+RESULTS:
: 0.7390822985224024

From the above case, I learned that one nature of while is the abstract concept "try"

#+begin_src ipython :session sicp :results output pySrc/sicp_fixedpoint2.py
import math

def fixed_point(f, guess):
    while True:
        nex = f(guess)
        if abs(guess-nex) < 0.0001:
            return nex
        else:
            guess = nex #local assignment is nature of lambda
print(fixed_point(math.cos, 1))
#+end_src

#+RESULTS:
: 0.7390547907469174

So I could write iteration in python just with the effective functional abstraction thinking.

When reflect on try, more than "try is a while in iteration", what it teach me?

It could be reframed without try, but return return fixed_point(f, nex) directly.

#+begin_src ipython :session sicp :results output :tangle pySrc/sicp_fixedpoint.py
import math
tolerance = 0.00001

def fixed_point(f, guess):
    def good_enoughp(a, b):
        return abs(a-b) < tolerance

    nex = f(guess)
    if good_enoughp(guess, nex):
        return nex
    else:
        return fixed_point(f, nex)    

print(fixed_point(math.cos, 1))
#+end_src

#+RESULTS:
: 0.7390822985224024

So why SICP introduced try here, I guess efficiency might not be the author's key consideration.

Test with elisp

#+begin_src emacs-lisp :session sicp :lexical t
(defvar tolerance 0.00001)
(defun fixed-point(f guess)
  (defun close-enoughp(v1 v2) ;
    (< (abs (- v1 v2)) tolerance))

  (let ((next (funcall f guess)))
    (if (close-enoughp guess next)
        next
      (fixed-point f next)))
  )
;;(trace-function #'fixed-point)
(fixed-point #'cos 1.0)
#+end_src

#+RESULTS:
: 0.7390822985224024

It works as expected.

It seems that return fixed-point f next is a bit cleaner than a inner iteration with try.

What's the consideration of SICP here, what was intended to teach?


Solution

  • It's the opposite: it's cleaner and more efficient with try because it doesn't need to redefine the good-enough-p.

    (also, you're not supposed to use recursion in Python).


    The version with try is better than the version which calls the top function, fixed-point, because fixed-point contains inner definitions, of the functions good-enough-p and try. A simple-minded compiler would compile it so that on each call it actually makes those definitions anew, again and again, on each call. With try there's no such concern as it is already inside the fixed-point's inner environment where good-enough-p is already defined, and so try can just run.

    (correction/clarification: the above treats your code as if it were Scheme, with internal defines instead of the Common Lisp with defuns as you show. SICP is Scheme, after all. In Common Lisp / ELisp there's not even a question -- the internal defuns will always be performed, on each call to the enclosing function, just (re)defining the same functions at the top level over and over again.)

    Incidentally, I like your Python loop translation, it is a verbatim translation of the Scheme's tail-recursive loop, one to one.

    Your while translation is exactly what a Scheme compiler is supposed to be doing given the first tail-recursive Scheme code in your question. The two are exactly the same, down to the "horrible while True ... with an escape" which, personally, I quite like for its immediacy and clarity. Meaning, I don't need to keep track of which value gets assigned to what variable and which variable gets returned in the end -- instead, a value is just returned, just like it is in Scheme.