Ответ 1
У меня нет книги, но я предполагаю, что мотивирующим примером является
(defn fact-n [n]
(if (zero? n)
1
(* n (recur (dec n)))))
;=> CompilerException: Can only recur from tail position
И эта последняя форма должна быть записана (* n (fact-n (dec n)))
вместо этого, а не хвостовой рекурсивной. Проблема заключается в том, что после рекурсии остается что-то еще, а именно умножение на n
.
То, что продолжает прохождение, делает это наизнанку. Вместо того, чтобы применить то, что осталось от текущего контекста/продолжения после возврата рекурсивного вызова, передать контекст/продолжение в рекурсивный вызов, чтобы применить его после завершения. Вместо того, чтобы неявно хранить продолжения в стеке как кадры вызова, мы явно накапливаем их с помощью композиции функций.
В этом случае мы добавляем дополнительный аргумент k
к нашей факториалу, которая выполняет то, что мы сделали бы после возврата рекурсивного вызова.
(defn fact-nk [n k]
(if (zero? n)
(k 1)
(recur (dec n) (comp k (partial * n)))))
Первый k
in является последним. В конечном итоге здесь мы просто хотим вернуть вычисленное значение, поэтому первая k
in должна быть тождественной функцией.
Здесь базовый случай:
(fact-nk 0 identity)
;== (identity 1)
;=> 1
Здесь n = 3
:
(fact-nk 3 identity)
;== (fact-nk 2 (comp identity (partial * 3)))
;== (fact-nk 1 (comp identity (partial * 3) (partial * 2)))
;== (fact-nk 0 (comp identity (partial * 3) (partial * 2) (partial * 1)))
;== ((comp identity (partial * 3) (partial * 2) (partial * 1)) 1)
;== ((comp identity (partial * 3) (partial * 2)) 1)
;== ((comp identity (partial * 3)) 2)
;== ((comp identity) 6)
;== (identity 6)
;=> 6
Сравнение с нерекурсивной версией
(fact-n 3)
;== (* 3 (fact-n 2))
;== (* 3 (* 2 (fact-n 1)))
;== (* 3 (* 2 (* 1 (fact-n 0))))
;== (* 3 (* 2 (* 1 1)))
;== (* 3 (* 2 1))
;== (* 3 2)
;=> 6
Теперь, чтобы сделать это немного более гибким, мы могли бы опередить zero?
и *
и сделать вместо них переменные аргументы.
Первый подход был бы
(defn cps-anck [accept? n c k]
(if (accept? n)
(k 1)
(recur accept?, (dec n), c, (comp k (partial c n)))))
Но так как accept?
и c
не меняются, мы можем тогда отпустить и снова вернуться к внутренней анонимной функции. Clojure имеет для этого особый вид, loop
.
(defn cps-anckl [accept? n c k]
(loop [n n, k k]
(if (accept? n)
(k 1)
(recur (dec n) (comp k (partial c n))))))
И, наконец, мы можем захотеть превратить это в генератор функций, который втягивает n
.
(defn gen-cps [accept? c k]
(fn [n]
(loop [n n, k k]
(if (accept? n)
(k 1)
(recur (dec n) (comp k (partial c n)))))))
И вот как я напишу mk-cps
(обратите внимание: последние два аргумента изменились).
(def factorial (gen-cps zero? * identity))
(factorial 5)
;=> 120
(def triangular-number (gen-cps #{1} + identity))
(triangular-number 5)
;=> 15