Использование определенной процедуры в качестве аргумента дает другой результат, чем использование лямбда-выражения в качестве аргумента.

Я пытаюсь запомнить процедуру в схеме. Код взят из SICP.

У меня есть моя процедура fib, определенная как

(define (fib n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1))
                (fib (- n 2))))))

Моя процедура запоминания выглядит следующим образом

(define (memoize f)
    (let ((table (make-table)))
        (lambda (x)
            (let ((previously-computed-result (lookup x table)))
                (or previously-computed-result
                    (let ((result (f x)))
                       (insert! x result table)
                       result))))))

Определим две процедуры

(define mem-fib (memoize fib))
(define mem-fib-lambda (memoize (lambda (n)
             (display "computing fib of ")
             (display n)
             (newline)
             (cond ((= n 0) 0)
               ((= n 1) 1)
                   (else (+ (memo-fib (- n 1))
                            (memo-fib (- n 2))))))))

Как видите, в mem-fib я использую fib в качестве аргумента, а в mem-fib-lambda я использую в качестве аргумента лямбда-выражение, что почти идентично.

Вызов этой процедуры с 5 в качестве аргумента дает разные результаты, где первый, mem-fib сохраняет последний результат в своей таблице, тогда как mem-fib-lambda сохраняет все рекурсивные вычисления по пути.

(mem-fib 5)
->computing fib of 5
->computing fib of 4
->computing fib of 3
->computing fib of 2
->computing fib of 1
->computing fib of 0
->computing fib of 1
->computing fib of 2
->computing fib of 1
->computing fib of 0
->computing fib of 3
->computing fib of 2
->computing fib of 1
->computing fib of 0
->computing fib of 1
->5
(mem-fib 5)
->5

а также

(mem-fib-lambda 5)
->computing fib of 5
->computing fib of 4
->computing fib of 3
->computing fib of 2
->computing fib of 1
->computing fib of 0
->5
(mem-fib-lambda 5)
->5 

Моя теория состоит в том, что когда я вызываю mem-fib, fib вычисляется в другой среде, тогда как mem-fib-lambda вычисляет ее в той среде, в которой она была вызвана.

В качестве попытки исправить это, я попытался сделать копию в процедуре мемоизации

(define (memoize proc)
  (define f proc) ;; Here
    (let ((table (make-table)))
        (lambda (x)
            (let ((previously-computed-result (lookup x table)))
                (or previously-computed-result
                    (let ((result (f x)))
                       (insert! x result table)
                       result))))))

Это не сработало, поэтому я попытался поместить это в выражение let. Насколько мне известно, fib должна быть частью того же фрейма, что и таблица.

(define (memoize proc)
    (let ((table (make-table))
         (f proc)) ;; Here
        (lambda (x)
            (let ((previously-computed-result (lookup x table)))
                (or previously-computed-result
                    (let ((result (f x)))
                       (insert! x result table)
                       result))))))

Это тоже ничего не дало.

Что мне не хватает? Почему есть разница в поведении? Как я могу получить желаемый результат?

Спасибо


person toregh    schedule 18.04.2018    source источник


Ответы (2)


Проблема в том, что в вашей первой функции вы рекурсивно вызываете не запомненную версию фибоначчи, а не запомненную версию fib. Чтобы обойти это, можно было бы определить fib следующим образом:

(define (fib n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (mem-fib (- n 1)) ;; Notice we're calling the memoized version here
                (mem-fib (- n 2))))))
(define mem-fib (memoize fib))

Возможно, лучшим способом было бы сделать следующее:

(define (fib n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1)) ;; Notice we're calling the NON-memoized version here
                (fib (- n 2))))))
(set! fib (memoize fib)) ;; but we redefine fib to be memoized

Это означает, что мы используем только одно имя, и оно запоминается. Нет хорошего способа иметь обе версии, но если вы хотите, вот один из способов сделать это (если вы хотите сравнить производительность или что-то в этом роде):

(define (fib n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (mem-fib (- n 1)) ;; Notice we're calling the memoized version here
                (mem-fib (- n 2))))))
(define mem-fib (memoize fib))
(set! fib (lambda (n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1)) ;; Notice we're calling the NON-memoized version here
                (fib (- n 2)))))))
person Triclops200    schedule 18.04.2018
comment
Да! Спасибо! Я думаю, что переопределение это путь! Это тоже имеет смысл. Целый день ломаю голову! не могу проголосовать :( - person toregh; 18.04.2018
comment
define в уже существующей привязке вызовет ошибку. Следует использовать (set! fib (memoize fib)) - person Sylwester; 18.04.2018
comment
@Sylwester Какую реализацию схемы вы используете? Я использую хитрость, и это сработало для меня. Тем не менее, редактирование для кросс-совместимости. - person Triclops200; 18.04.2018
comment
Я использую #!r5rs и #!r6rs в DrRacket, но это ни при чем. В отчете указано, что обновление привязки должно выполняться с помощью set!. Поведение в REPL может отличаться от поведения при запуске программы другим способом, поэтому одна и та же реализация может дать сбой в чем-то, что нормально в REPL. - person Sylwester; 19.04.2018

Вот альтернативный подход к этой проблеме. Это в Racket, а не в Scheme, за что я извиняюсь (это может быть Scheme, за исключением того, что я не знаю, как хеш-таблицы работают в Scheme).

Прежде всего, вот функция Racket для запоминания функции с произвольным количеством аргументов в ее первом аргументе. Через мгновение станет понятно, почему нам нужно допускать дополнительные аргументы.

(define (memoize f (table (make-hasheqv)))
  ;; Memoize a function on its first argument.
  ;; table, if given, should be a mutable hashtable
  (λ (k . more)
    ;; hash-ref! looks up k in table, and if it is not there
    ;; sets it to be the result of calling the third argument (or
    ;; to the third argument if it's not callable).  thunk just makes
    ;; a function of no arguments.
    (hash-ref! table k
               (thunk (apply f k more)))))

Теперь вот в чем хитрость: вместо того, чтобы определять fib как рекурсивную функцию, мы определяем fib/c как не-рекурсивную функцию, которая знает, как выполнить один шаг вычисления ряда Фибоначчи, и обращается к другой функции, чтобы выполнить отдых. Он также сообщает вам, что он делает, как и ваша функция.

(define (fib/c n c)
  ;; fib/c does one step of the fibonacci calculation,
  ;; calling c to do the remaining steps.
  (printf "fib of ~A~%" n)
  (if (<= n 2)
      1
      (+ (c (- n 1) c)
         (c (- n 2) c))))

Основываясь на этом, мы можем очень легко определить fib/u, не запомненную функцию Фибоначчи, которая имеет ужасную производительность, которую вы ожидаете, просто передав саму fib/c в качестве второго аргумента:

(define (fib/u n)
  ;; unmemoized fib
  (fib/c n fib/c))

Но теперь мы можем ее запомнить и определить версию с запоминаниями, fib/m (здесь вы видите, почему мне нужно было memoize, чтобы разрешить более одного аргумента: нам нужно продолжать передавать запомненную функцию вниз:

(define (fib/m n)
  ;; and here's a memoized fib
  (let ((fib/m (memoize fib/c)))
    (fib/m n fib/m)))

А теперь (4 — первый случай, когда они отличаются):

> (fib/u 4)
fib of 4
fib of 3
fib of 2
fib of 1
fib of 2
3
> (fib/m 4)
fib of 4
fib of 3
fib of 2
fib of 1
3

И со снятой печатью:

> (time (fib/u 40))
cpu time: 8025 real time: 7962 gc time: 26
102334155
> (time (fib/m 40))
cpu time: 1 real time: 1 gc time: 0
102334155

Обратите внимание, что такой подход, когда вы пишете нерекурсивную функцию и превращаете ее в рекурсивную, тесно связан с тем, как выполняется вывод Y (хотя обычно они очень строги и настаивают на функциях только с одним аргументом, так что вы получите (λ (f) (λ (n) ... (f ...) ...)), а не (λ (n c) ... (c ...) ...) Оказывается, это довольно полезный трюк.

person Community    schedule 19.04.2018