Переопределение переменной let'd в цикле Clojure

В ПОРЯДКЕ. Я возился с Clojure и постоянно сталкивался с одной и той же проблемой. Возьмем этот небольшой фрагмент кода:

(let [x 128]
  (while (> x 1)
    (do
      (println x)
      (def x (/ x 2)))))

Теперь я ожидаю, что это распечатает последовательность, начинающуюся со 128, так:

128
64
32
16
8
4
2

Вместо этого это бесконечный цикл, печатающий 128 снова и снова. Очевидно, мой предполагаемый побочный эффект не работает.

Итак, как мне переопределить значение x в таком цикле? Я понимаю, что это может быть не похоже на Лисп (возможно, я мог бы использовать анонимную функцию, которая рекурсивно обращается к самой себе), но если я не пойму, как установить такую ​​​​переменную, я сойду с ума.

Мое другое предположение состояло бы в том, чтобы использовать set!, но это дает «Неверная цель назначения», так как я не в форме привязки.

Пожалуйста, просветите меня, как это должно работать.


person MBCook    schedule 02.06.2009    source источник


Ответы (4)


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

Локальные объекты, созданные с помощью let, не являются переменными. После создания их ценности никогда не меняются!

(Выделение не мое.) Здесь вам не нужно изменяемое состояние для вашего примера; вы можете использовать loop и recur.

(loop [x 128]
  (when (> x 1)
    (println x)
    (recur (/ x 2))))

Если вы хотите быть необычным, вы можете полностью избежать явного loop.

(let [xs (take-while #(> % 1) (iterate #(/ % 2) 128))]
  (doseq [x xs] (println x)))

Если вы действительно хотели использовать изменяемое состояние, вам может подойти атом.

(let [x (atom 128)]
  (while (> @x 1)
    (println @x)
    (swap! x #(/ %1 2))))

(Вам не нужен do; while оборачивается для вас явным образом.) Если вы действительно, действительно хотели сделать это с помощью vars вам придется сделать что-то ужасное.

(with-local-vars [x 128]
  (while (> (var-get x) 1)
    (println (var-get x))
    (var-set x (/ (var-get x) 2))))

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

person Brian Carper    schedule 02.06.2009
comment
Спасибо. Я понимаю, что мой путь не был Lispy, поскольку побочные эффекты не одобряются. Я что-то взламывал (проблема Project Euler) и не мог заставить этот простой тестовый пример работать, доказывая, что я чего-то не понял. Спасибо за помощь. Я забыл, что цикл может содержать рекурсию, которая работает очень чисто (без дополнительной функции, выполняющей рекурсию). - person MBCook; 02.06.2009
comment
Побочные эффекты Lispy зависят от того, на какой Lisp вы смотрите. В Common Lisp вам сойдет с рук (цикл для x = 128, затем (/ x 2), в то время как (› x 1) do (print x)). Но побочные эффекты не Clojurish. - person Brian Carper; 03.06.2009
comment
Это очень старый ответ. Но это очень хороший ответ, я новичок в Clojure, это избавило меня от часов борьбы с той же проблемой. Большое спасибо @BrianCarper - person Dan Jay; 19.01.2016

Vars (это то, что вы получаете, когда что-то "определяете") не предназначены для переназначения (но могут быть):

user=> (def k 1)
#'user/k
user=> k
1

Вам ничего не мешает:

user=> (def k 2)
#'user/k
user=> k
2

Если вам нужно локальное устанавливаемое "место" потока, вы можете использовать "binding" и "set!":

user=> (def j) ; this var is still unbound (no value)
#'user/j
user=> j
java.lang.IllegalStateException: Var user/j is unbound. (NO_SOURCE_FILE:0)
user=> (binding [j 0] j)
0

Итак, вы можете написать цикл следующим образом:

user=> (binding [j 0]
         (while (< j 10)
           (println j)
           (set! j (inc j))))
0
1
2
3
4
5
6
7
8
9
nil

Но я думаю, что это довольно однообразно.

person Chris    schedule 18.06.2009

Если вы считаете, что наличие изменяемых локальных переменных в чистых функциях было бы хорошей удобной функцией, которая не приносит вреда, поскольку функция по-прежнему остается чистой, вам может быть интересно это обсуждение в списке рассылки, где Рич Хикки объясняет причины, по которым он удалил их из языка. . Почему нельзя изменить локальные переменные?

Соответствующая часть:

Если бы локальные переменные были переменными, т. е. изменяемыми, то замыкания могли бы закрываться по изменяемому состоянию, и, учитывая, что замыкания могут выходить (без какого-либо дополнительного запрета на то же самое), результат был бы небезопасным для потоков. И люди, безусловно, сделали бы это, т.е. псевдообъекты на основе замыкания. Результатом станет огромная дыра в подходе Clojure.

Без изменяемых локальных переменных люди вынуждены использовать recur, функциональную конструкцию цикла. Хотя поначалу это может показаться странным, это так же лаконично, как циклы с мутациями, и полученные шаблоны можно повторно использовать в другом месте Clojure, т. е. повторять, уменьшать, изменять, коммутировать и т. д. (логически) очень похожи. Несмотря на то, что я мог обнаруживать и предотвращать побег мутирующих замыканий, я решил сохранить этот способ для согласованности. Даже в самом маленьком контексте немутирующие циклы легче понять и отладить, чем изменяющиеся. В любом случае, Vars доступны для использования, когда это необходимо.

Большинство последующих постов касается реализации макроса with-local-vars ;)

person user7610    schedule 20.10.2014

Вместо этого вы могли бы более идиоматически использовать iterate и take-while,

user> (->> 128
           (iterate #(/ % 2))
           (take-while (partial < 1)))

(128 64 32 16 8 4 2)
user>
person ealfonso    schedule 04.07.2016
comment
Очень элегантный..! - person zooes; 06.08.2018