1

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?

Will Ness
  • 62,652
  • 8
  • 86
  • 167
AbstProcDo
  • 14,203
  • 14
  • 49
  • 94

3 Answers3

1

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.

Will Ness
  • 62,652
  • 8
  • 86
  • 167
  • Yes, not support by python, so could take while as a try to compensate the mind gap. I don't get your idea "not need to redefine good-enoughp" – AbstProcDo Dec 26 '19 at 14:48
  • I prefer "while True ... ", cos the following condition branches are balanced with "if and else" rather than a singular branch. When come with singular branch, I am uncertain about its exhaustiveness during second reading. – AbstProcDo Dec 27 '19 at 01:25
1

The natural way to write something like this in Python is something like this, I think:

tolerance = 0.00001

def fixed_point(f, first_guess):
    guess = first_guess
    next_guess = f(guess)
    def close_enough(a, b):
        return (abs(a - b) < tolerance)
    while not close_enough(guess, next_guess):
        guess = next_guess
        next_guess = f(guess)
    return next_guess

This:

  • uses a while loop rather than recursion in the way that is natural in Python;
  • doesn't use some horrible while True ... with an escape which is just confusing.

(In fact, since function-call in Python is generally very slow, it is probably more natural to open-code the call to close_enough and remove the local function altogether.)

But this is imperative code: it's full of assignment (the first two 'assignments' are really bindings of variables as Python doesn't distinguish the two syntactically, but the later assignments really are assignments). We want to express this in a way which doesn't have assignment. We also want to replace it by something which does not use any looping constructs or expresses those looping constructs in terms of function calls.

We can do this in two ways:

  • we can treat the top-level function as the thing we call recursively;
  • we can define some local function through which we recurse.

Which of these we do is really a choice, and in this case it probably makes little difference. However there are often significant advantages to the second approach: in general the top-level function (the function that is in some interface we might be exposing to people) may have all sorts of extra arguments, some of which may have default values and so on, which we really don't want to have to keep passing through the later calls to it; the top-level function may also just not have an appropriate argument signature at all because the iterative steps may be iterating over some set of values which are derived from the arguments to the top-level function.

So, it's generally better to express the iteration in terms of a local function although it may not always be so.

Here is a recursive version in Python which takes the chance to also make the signature of the top-level function sightly richer. Note that this approach would be terrible style in Python since Python does not do anything special with tail calls. The code is also littered with returns because Python is not an expression language (don't believe people who say 'Python is like Lisp': it's not):

default_tolerance = 0.00001

def fixed_point(f, first_guess, tolerance=default_tolerance):
    guess = first_guess
    next_guess = f(guess)
    def close_enough(a, b):
        return (abs(a - b) < tolerance)
    def step(guess, next_guess):
        if close_enough(guess, next_guess):
            return next_guess
        else:
            return step(next_guess, f(next_guess))
    return step(first_guess, f(first_guess))

Well, in Scheme this is much more natural: here is the same function written in Scheme (in fact, in Racket):

(define default-tolerance 0.00001)

(define (fixed-point f initial-guess #:tolerance (tolerance default-tolerance))
  (define (close-enough? v1 v2)
    (< (abs (- v1 v2)) tolerance))
  (define (try guess next)
    (if (close-enough? guess next)
        next
        (try next (f next))))
  (try initial-guess (f initial-guess)))

The only thing that is annoying about this is that we have to kick-off the iteration after defining try. Well, we could avoid even that with a macro:

(define-syntax-rule (iterate name ((var val) ...) form ...)
  (begin
    (define (name var ...)
      form ...)
    (name val ...)))

And now we can write the function as:

(define (fixed-point f initial-guess #:tolerance (tolerance default-tolerance))
  (define (close-enough? v1 v2)
    (< (abs (- v1 v2)) tolerance))
  (iterate try ((guess initial-guess) (next (f initial-guess)))
    (if (close-enough? guess next)
        next
        (try next (f next)))))

Well, in fact we don't need to write this iterate macro: it's so useful in Scheme that it already exists as a special version of let called 'named let':

(define (fixed-point f initial-guess #:tolerance (tolerance default-tolerance))
  (define (close-enough? v1 v2)
    (< (abs (- v1 v2)) tolerance))
  (let try ((guess initial-guess) (next (f initial-guess)))
    (if (close-enough? guess next)
        next
        (try next (f next)))))

And with any of these versions:

> (fixed-point cos 0)
0.7390822985224023
> (fixed-point cos 0 #:tolerance 0.1)
0.7013687736227565

Finally a meta-comment: I don't understand why you seem to be trying to learn Scheme using Emacs Lisp. The two languages are not alike at all: if you want to learn Scheme, use Scheme: there are probably hundreds of Scheme systems out there, almost all of which are free.

tfb
  • 13,623
  • 1
  • 10
  • 24
  • Because usually try to write some extensions in elisp for my Emacs. Scheme seems not have practical applications excluding expert at tutorials. – AbstProcDo Dec 26 '19 at 15:36
  • @Algebra: well, in that case I'd recommend to learn elisp, not Scheme. What you are doing is like learning Pascal when you want to learn C. – tfb Dec 26 '19 at 15:38
  • 1
    No, I am not learning a language with SICP, but sharpen programming skills and build solid foundations. – AbstProcDo Dec 26 '19 at 15:42
  • 1
    @Algebra: the problem is that if you write elisp (or for that matter Common Lisp) the way you would write Scheme, you end up writing catastrophically unidiomatic and often just downright *wrong* code. In the elisp you have in the question the 'local' `defun`s aren't, in fact, local at all, for instance. Scheme and elisp don't have a common ancestor this side of 1970. – tfb Dec 26 '19 at 15:54
  • Got your idea, will focus on Scheme from chapter two, thank you. – AbstProcDo Dec 26 '19 at 16:11
  • I [like](https://stackoverflow.com/a/59489577/849891) the "horrible `while True ...` with escape". :) – Will Ness Dec 26 '19 at 17:32
  • yep. :) :) I even like `prog`, too. (doesn't mean I'd use it in a production code unless absolutely necessary. I just like it) --- btw I explained my reasons in my answer. and I really don't care whether a language has some construct ready for me, or I have to use a little snippet to achieve the exact same effect. – Will Ness Dec 26 '19 at 17:39
  • s/don't care/don't mind. (your comment has disappeared; if someone flagged it, it wasn't me. :) I actually found it very funny, in good spirits.) – Will Ness Dec 27 '19 at 08:12
1

Scheme permits redefinition of top-level symbols, such as fixed-point; even the function f could redefine it! Compilers (and interpreters) need to take this into consideration, and check for a redefinition every call of fixed-point. On the other hand, try is not visible outside the definition of fixed-point, so f cannot redefine it. So, the compiler (or interpreter) can turn this tail recursive function into a loop.

Doug Currie
  • 38,699
  • 1
  • 88
  • 113