Внутреннее взаимодействие `try` в `fixed-point`

Я читаю fix-point 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

Из приведенного выше случая я узнал, что одной из особенностей while является абстрактное понятие «попытаться».

#+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

Так что я мог написать итерацию на питоне только с эффективным мышлением функциональной абстракции.

Когда я размышляю о try, больше, чем «попробуй некоторое время в итерации», чему это меня учит?

Его можно переформулировать без try, но напрямую вернуть return fixed_point(f, nex).

#+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

Итак, почему SICP представил здесь try, я полагаю, что эффективность не может быть ключевым соображением автора.

Протестируйте с помощью 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

Он работает так, как ожидалось.

Кажется, что return fixed-point f next немного чище, чем внутренняя итерация с try.

При чем здесь SICP, чему он должен был научить?


person AbstProcDo    schedule 26.12.2019    source источник


Ответы (3)


Наоборот: try чище и эффективнее, потому что не нужно переопределять good-enough-p.

(также вы не должны использовать рекурсию в Python).


Версия с try лучше, чем версия, которая вызывает верхнюю функцию, fixed-point, потому что fixed-point содержит внутренние определения функций good-enough-p и try. Бесхитростный компилятор скомпилировал бы его так, чтобы при каждом вызове он фактически делал эти определения заново, снова и снова, при каждом вызове. С try такой проблемы нет, так как он уже находится во внутренней среде fixed-point, где good-enough-p уже определен, и поэтому try можно просто запустить.

(исправление/пояснение: приведенный выше код рассматривает ваш код так, как если бы он был Scheme, с внутренними defines вместо Common Lisp с defuns, как вы показываете. В конце концов, SICP — это Scheme. В Common Lisp/ELisp нет даже вопроса -- внутренние defuns всегда будут выполняться при каждом вызове объемлющей функции, просто (пере)определяя одни и те же функции на верхнем уровне снова и снова.)

Между прочим, мне нравится ваш перевод цикла Python, он является дословным переводом цикла хвостовой рекурсии Scheme, один к одному.

Ваш while перевод - это именно то, что должен делать компилятор Scheme, учитывая первый хвостовой рекурсивный код Scheme в вашем вопросе. Они совершенно одинаковы, вплоть до "ужасного while True ... с побегом", что лично мне очень нравится за его непосредственность и ясность. Это означает, что мне не нужно отслеживать, какое значение присваивается какой переменной и какая переменная возвращается в конце — вместо этого просто возвращается значение, как в схеме.

person Will Ness    schedule 26.12.2019
comment
Да, Python не поддерживает, поэтому может потребоваться некоторое время, чтобы попытаться компенсировать разрыв в разуме. Я не понимаю вашей идеи, не нужно переопределять "достаточно хорошо" - person AbstProcDo; 26.12.2019
comment
Я предпочитаю while True... , потому что следующие ветви условий сбалансированы с if и else, а не с единственной ветвью. Когда речь идет об единственном ответвлении, я не уверен в его полноте во время второго чтения. - person AbstProcDo; 27.12.2019

Я думаю, что естественный способ написать что-то подобное на Python выглядит примерно так:

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

Этот:

  • использует цикл while, а не рекурсию, как в Python;
  • не использует какое-то ужасное while True ... с побегом, что просто сбивает с толку.

(На самом деле, поскольку вызов функции в Python, как правило, очень медленный, вероятно, более естественно открыть код вызова close_enough и полностью удалить локальную функцию.)

Но это императивный код: он полон присваиваний (первые два «присваивания» на самом деле являются привязками переменных, поскольку Python не различает их синтаксически, но более поздние присваивания действительно являются присваиваниями). Мы хотим выразить это так, чтобы не было присваивания. Мы также хотим заменить его чем-то, что не использует никаких циклических конструкций или выражает эти циклические конструкции в терминах вызовов функций.

Мы можем сделать это двумя способами:

  • мы можем рассматривать функцию верхнего уровня как то, что мы вызываем рекурсивно;
  • мы можем определить некоторую локальную функцию, через которую мы рекурсивно.

Что из этого мы делаем, это действительно выбор, и в данном случае это, вероятно, не имеет большого значения. Однако у второго подхода часто есть значительные преимущества: в общем случае функция верхнего уровня (функция, которая находится в каком-то интерфейсе, который мы можем предоставить людям) может иметь всевозможные дополнительные аргументы, некоторые из которых могут иметь значения по умолчанию, и поэтому on, который мы действительно не хотим передавать через более поздние вызовы; функция верхнего уровня также может просто не иметь соответствующей сигнатуры аргумента, потому что итерационные шаги могут выполнять итерацию по некоторому набору значений, полученных из аргументов функции верхнего уровня.

Таким образом, обычно лучше выражать итерацию в терминах локальной функции, хотя это может быть не всегда так.

Вот рекурсивная версия на Python, которая также делает сигнатуру функции верхнего уровня заметно богаче. Обратите внимание, что этот подход был бы ужасным стилем в Python, поскольку Python не делает ничего особенного с хвостовыми вызовами. Код также замусорен returns, потому что Python не является языком выражений (не верьте людям, которые говорят, что «Python подобен Lisp»: это не так):

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))

Что ж, в Scheme это гораздо естественнее: вот та же самая функция, написанная на Scheme (на самом деле, в 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)))

Единственное, что в этом раздражает, так это то, что мы должны запускать итерацию после определения try. Ну, мы могли бы избежать даже этого с помощью макроса:

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

И теперь мы можем написать функцию как:

(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)))))

Ну, на самом деле нам не нужно писать этот макрос iterate: он настолько полезен в Scheme, что уже существует в виде специальной версии let под названием '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)))))

И с любой из этих версий:

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

Наконец, мета-комментарий: я не понимаю, почему вы пытаетесь изучить Scheme с помощью Emacs Lisp. Эти два языка совсем не похожи: если вы хотите изучить Scheme, используйте Scheme: вероятно, существуют сотни систем Scheme, почти все из которых бесплатны.

person Community    schedule 26.12.2019
comment
Потому что обычно пытаются написать какие-нибудь расширения в elisp для моего Emacs. Схема, похоже, не имеет практического применения, за исключением экспертных руководств. - person AbstProcDo; 26.12.2019
comment
@Algebra: ну, в таком случае я бы рекомендовал изучать elisp, а не Scheme. То, что вы делаете, похоже на изучение Pascal, когда вы хотите изучить C. - person ; 26.12.2019
comment
Нет, я не изучаю язык с помощью SICP, а оттачиваю навыки программирования и закладываю прочный фундамент. - person AbstProcDo; 26.12.2019
comment
@Algebra: проблема в том, что если вы пишете elisp (или, если на то пошло, Common Lisp) так, как вы пишете Scheme, вы в конечном итоге пишете катастрофически однообразный и часто просто совершенно неправильный код. Например, в elisp, который у вас есть в вопросе, «локальные» defun на самом деле вообще не являются локальными. Scheme и elisp не имеют общего предка по эту сторону 1970 года. - person ; 26.12.2019
comment
Понял вашу идею, сосредоточусь на Схеме из второй главы, спасибо. - person AbstProcDo; 26.12.2019
comment
Мне нравится ужасный while True ... с побегом. :) - person Will Ness; 26.12.2019
comment
Ага. :) :) Мне даже prog тоже нравится. (это не значит, что я буду использовать его в производственном коде, если в этом нет крайней необходимости. Мне это просто нравится) --- кстати, я объяснил свои причины в своем ответе. и мне действительно все равно, есть ли в языке какая-то готовая конструкция для меня, или мне нужно использовать небольшой фрагмент для достижения точно такого же эффекта. - person Will Ness; 26.12.2019
comment
с/не волнует/не возражаю. (ваш комментарий исчез; если кто-то и отметил его, то это был не я. :) На самом деле я нашел его очень забавным, в хорошем настроении.) - person Will Ness; 27.12.2019

Схема позволяет переопределять символы верхнего уровня, такие как fixed-point; даже функция f может его переопределить! Компиляторы (и интерпретаторы) должны принимать это во внимание и проверять наличие переопределения при каждом вызове fixed-point. С другой стороны, try не виден вне определения fixed-point, поэтому f не может его переопределить. Итак, компилятор (или интерпретатор) может превратить эту хвостовую рекурсивную функцию в цикл.

person Doug Currie    schedule 26.12.2019