Рекурсия по списку s-выражений в Clojure
чтобы установить некоторый контекст, я нахожусь в процессе обучения Clojure и развития Lisp в целом. На моем пути к Lisp я в настоящее время работаю над "маленькой" серией, пытаясь укрепить фундамент в функциональном программировании и решении рекурсивных решений. В "маленьком Интригане" я проработал многие упражнения, однако, я немного борюсь, чтобы преобразовать некоторые из них в Clojure. Более конкретно, я изо всех сил пытаюсь преобразовать их в использование "recur", чтобы включить TCO. Например, вот реализация на основе Clojure для функции" occurs* " (от Little Schemer), которая подсчитывает количество вхождений атома, появляющегося в списке S-выражений:
(defn atom? [l]
(not (list? l)))
(defn occurs [a lst]
(cond
(empty? lst) 0
(atom? (first lst))
(cond
(= a (first lst)) (inc (occurs a (rest lst)))
true (occurs a (rest lst)))
true (+ (occurs a (first lst))
(occurs a (rest lst)))))
по сути, (occurs 'abc '(abc (def abc) (abc (abc def) (def (((((abc)))))))))
будет оценивать до 5. Очевидная проблема заключается в том, что это определение потребляет кадры стека и взорвет стек, если список S-выражений слишком глубок.
Теперь я понимаю возможность рефакторинга рекурсивных функций для использования параметр accumulator для включения рекурсивного вызова в хвостовое положение (чтобы разрешить TCO), но я борюсь, если этот параметр применим даже к таким ситуациям, как этот.
вот насколько я понимаю, если я пытаюсь рефакторинг это с помощью "повторения" вместе с помощью параметра аккумулятора:
(defn recur-occurs [a lst]
(letfn [(myoccurs [a lst count]
(cond
(empty? lst) 0
(atom? (first lst))
(cond
(= a (first lst)) (recur a (rest lst) (inc count))
true (recur a (rest lst) count))
true (+ (recur a (first lst) count)
(recur a (rest lst) count))))]
(myoccurs a lst 0)))
Так, я чувствую, что я почти там, но не совсем. Очевидной проблемой является мое предложение "else", в котором глава списка не является атомом. Концептуально я хочу суммируйте результат повторения над первым элементом списка с результатом повторения над остальной частью списка. Я борюсь в моей голове, о том, как реструктурировать это такое, что повторяется, может быть перемещен в положение хвост.
есть ли дополнительные методы для шаблона "аккумулятор" для достижения того, чтобы ваши рекурсивные вызовы помещались в хвостовое положение, которое я должен применять здесь, или проблема просто более "фундаментальная" и что нет чистого Clojure-based решение из-за отсутствия TCO в JVM? Если последнее, вообще говоря, каким должен быть общий шаблон для использования программ Clojure, которые должны повторяться над списком s-выражений? Для чего это стоит, я видел метод multi w / lazy-seq, используемый (страница 151 "Programming Clojure" Хэллоуэя для справки), чтобы "заменить рекурсию ленью", но я не уверен, как применить этот шаблон к этому примеру, в котором я не пытаюсь построить список, а вычислить одно целое число значение.
заранее Благодарю за любые рекомендации по этому поводу.
2 ответов
во-первых, я должен посоветовать вам не беспокоиться о проблемах реализации, таких как переполнение стека, когда вы пробираетесь через маленький Схематик. Хорошо быть добросовестным в таких вопросах, как отсутствие оптимизации хвостового вызова, когда вы программируете в гневе, но главный смысл книги-научить вас думать рекурсивно. Преобразование примеров стиля передачи аккумуляторов, безусловно, хорошая практика, но это по существу отбрасывание рекурсии в пользу итерация.
однако, и я должен предварить это предупреждением спойлера, есть способ сохранить тот же рекурсивный алгоритм, не подвергаясь капризам стека JVM. Мы можем использовать стиль продолжения, чтобы сделать наш собственный стек в виде дополнительного анонимного аргумента функции k
:
(defn occurs-cps [a lst k]
(cond
(empty? lst) (k 0)
(atom? (first lst))
(cond
(= a (first lst)) (occurs-cps a (rest lst)
(fn [v] (k (inc v))))
:else (occurs-cps a (rest lst) k))
:else (occurs-cps a (first lst)
(fn [fst]
(occurs-cps a (rest lst)
(fn [rst] (k (+ fst rst))))))))
вместо стека, создаваемого неявно нашими вызовами функции non-tail, мы связываем "то, что осталось сделать" после каждого вызова occurs
, и передать его вместе как следующее продолжение k
. Когда мы вызываем его, мы начинаем с k
что представляет собой ничего не осталось делать, функция идентификации:
scratch.core=> (occurs-cps 'abc
'(abc (def abc) (abc (abc def) (def (((((abc))))))))
(fn [v] v))
5
я не буду вдаваться в подробности того, как делать CPS, так как это для более поздней главы TLS. Тем не менее, я отмечу, что это, конечно, еще не работает полностью:
scratch.core=> (def ls (repeat 20000 'foo))
#'scratch.core/ls
scratch.core=> (occurs-cps 'foo ls (fn [v] v))
java.lang.StackOverflowError (NO_SOURCE_FILE:0)
CPS позволяет нам перемещать все наши нетривиальные вызовы стека в хвостовое положение, но в Clojure нам нужно сделать дополнительный шаг заменив их на recur
:
(defn occurs-cps-recur [a lst k]
(cond
(empty? lst) (k 0)
(atom? (first lst))
(cond
(= a (first lst)) (recur a (rest lst)
(fn [v] (k (inc v))))
:else (recur a (rest lst) k))
:else (recur a (first lst)
(fn [fst]
(recur a (rest lst) ;; Problem
(fn [rst] (k (+ fst rst))))))))
увы, это не так: java.lang.IllegalArgumentException: Mismatched argument count to recur, expected: 1 args, got: 3 (core.clj:39)
. Самый последний recur
на самом деле относится к fn
прямо над ним, тот, который мы используем, чтобы представить наши продолжения! Мы можем получить хорошее поведение большую часть времени, изменив только это recur
на вызов occurs-cps-recur
, но патологически-вложенный вход все равно переполнит стек:
scratch.core=> (occurs-cps-recur 'foo ls (fn [v] v))
20000
scratch.core=> (def nested (reduce (fn [onion _] (list onion))
'foo (range 20000)))
#'scratch.core/nested
scratch.core=> (occurs-cps-recur 'foo nested (fn [v] v))
Java.lang.StackOverflowError (NO_SOURCE_FILE:0)
вместо того, чтобы позвонить в occurs-*
и ожидая, что он даст ответ, мы можем пусть он немедленно ответит ударом. Когда мы вызываем этот удар, он срабатывает и выполняет некоторую работу вплоть до рекурсивного вызова, который, в свою очередь, возвращает другой удар. Это батутный стиль, и функция, которая "отскакивает" от наших ударов, -trampoline
. Возврат thunk каждый раз, когда мы делаем рекурсивный вызов, ограничивает наш размер стека одним вызовом за раз, поэтому наш единственный предел-куча:
(defn occurs-cps-tramp [a lst k]
(fn []
(cond
(empty? lst) (k 0)
(atom? (first lst))
(cond
(= a (first lst)) (occurs-cps-tramp a (rest lst)
(fn [v] (k (inc v))))
:else (occurs-cps-tramp a (rest lst) k))
:else (occurs-cps-tramp a (first lst)
(fn [fst]
(occurs-cps-tramp a (rest lst)
(fn [rst] (k (+ fst rst)))))))))
(declare done answer)
(defn my-trampoline [th]
(if done
answer
(recur (th))))
(defn empty-k [v]
(set! answer v)
(set! done true))
(defn run []
(binding [done false answer 'whocares]
(my-trampoline (occurs-cps-tramp 'foo nested empty-k))))
;; scratch.core=> (run)
;; 1
обратите внимание, что Clojure имеет встроенный trampoline
(С ограничения на тип возврата). Используя это вместо этого, нам не нужен специализированный empty-k
:
scratch.core=> (trampoline (occurs-cps-tramp 'foo nested (fn [v] v)))
1
батут, безусловно, классная техника, но предпосылкой для батута программы является то, что она должна содержать только хвостовые вызовы; CPS-настоящая звезда здесь. Он позволяет определить ваш алгоритм с ясностью естественной рекурсии и с помощью преобразований, сохраняющих корректность, эффективно выразить его на любом хосте, который имеет один цикл и кучу.
вы не можете сделать это с фиксированным объемом памяти. Вы можете потреблять стек или кучу; это решение, которое вы должны принять. Если бы я писал это в Clojure, я бы сделал это с map
и reduce
вместо ручной рекурсии:
(defn occurs [x coll]
(if (coll? coll)
(reduce + (map #(occurs x %) coll))
(if (= x coll)
1, 0)))
обратите внимание, что более короткие решения существуют, если вы используете tree-seq
или flatten
, но в этот момент большая часть проблемы ушла, так что не так много, чтобы узнать.
редактировать
вот версия, которая не использует стек, вместо этого позволяя своей очереди становиться все больше и больше (используя кучу).
(defn heap-occurs [item coll]
(loop [count 0, queue coll]
(if-let [[x & xs] (seq queue)]
(if (coll? x)
(recur count (concat x xs))
(recur (+ (if (= item x) 1, 0)
count)
xs))
count)))