web-dev-qa-db-ja.com

Clojureループでlet'd変数を再定義する

OK。私はClojureをいじくり回してきましたが、同じ問題が発生し続けています。この小さなコードの断片を見てみましょう。

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

これで、128で始まるシーケンスが次のように出力されることを期待しています。

128
64
32
16
8
4
2

代わりに、それは無限ループであり、128を何度も印刷します。明らかに私の意図した副作用は機能していません。

では、このようなループでxの値を再定義するにはどうすればよいでしょうか。これはLISPのようなものではないかもしれませんが(おそらく、それ自体で再帰する無名関数を使用できます)、このような変数の設定方法がわからない場合は、気が狂います。

私の他の推測はset!を使用することですが、私は拘束力のある形式ではないので、それは「無効な代入ターゲット」を与えます。

これがどのように機能するかについて教えてください。

37
MBCook

defは、関数または一部のコードの内部ループで使用する場合でも、最上位の変数を定義します。 letで取得するものはvarではありません。あたり letのドキュメント

letで作成されたローカルは変数ではありません。一度作成すると、値は変更されません!

(強調は私のものではありません。)ここでの例では、可変状態は必要ありません。 looprecurを使用できます。

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

本当に可変状態を使用したい場合は、 atom が機能する可能性があります。

(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コードを書き込もうとすると、間違いなく夢中になります。しばらくすると、実際に可変変数が必要になることはめったにないので、嬉しい驚きに気付くかもしれません。

50
Brian Carper

Vars(何かを「定義」したときに得られるもの)は、再割り当てすることを意図したものではありません(ただし、再割り当てすることはできます)。

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

あなたがするのを妨げるものは何もありません:

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

スレッドローカルで設定可能な「place」が必要な場合は、「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

しかし、これはかなり単調だと思います。

13
Chris

純粋関数に可変ローカル変数を含めることは、害を及ぼさない便利な機能であると考える場合、関数は純粋のままであるため、リッチヒッキーが言語からそれらを削除する理由を説明するこのメーリングリストのディスカッションに興味があるかもしれません。 。 なぜ可変ローカルではないのですか?

関連部分:

ローカルが変数である場合、つまり可変である場合、クロージャは可変状態で閉じることができ、クロージャがエスケープできることを考えると(同じことを特別に禁止することなく)、結果はスレッドセーフになります。そして人々は確かにそうするでしょう、例えばクロージャベースの疑似オブジェクト。その結果、Clojureのアプローチに大きな穴ができます。

変更可能なローカルがないと、人々は機能的なループ構造であるrecurを使用せざるを得なくなります。これは最初は奇妙に思えるかもしれませんが、突然変異を伴うループと同じくらい簡潔であり、結果のパターンはClojureの他の場所で再利用できます。つまり、繰り返し、削減、変更、通勤などはすべて(論理的に)非常に似ています。変化するクロージャがエスケープするのを検出して防ぐことはできましたが、一貫性を保つためにこの方法を維持することにしました。最小のコンテキストでも、非変更ループは、変更ループよりも理解とデバッグが容易です。いずれの場合も、必要に応じてVarを使用できます。

その後の投稿の大部分は、with-local-varsマクロの実装に関するものです;)

6
user7610

代わりに、より慣用的にiteratetake-whileを使用できます。

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

(128 64 32 16 8 4 2)
user>
3
ealfonso