読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

少しだけClojure 7

プログラミングClojure第2版を読みながら続き。

方針

  • 基本的に本の説明をそのままor自分の言葉に直しただけで書き直すようなことはしない
  • サンプルコードが長くて理解しづらい、練習したいなと思うものは細かく試して書いてみる
  • 読んだ時点で書かれてなかったり省略されているか、疑問に思ったことを調べて書く
  • 後から書かれていたら参照のために簡単にメモする

もうとっくに少しだけじゃないけど、後から見返しやすいようにこのタイトルでいく。

refにバリデーションを追加する

で、提示されてるチャットメッセージ的なサンプルコードなんだけど

(defrecord Message [sender text])

(defn validate-message [mes]
  (and
   (:sender mes)
   (:text mes)))

(def validate-message-list 
  (partial every? validate-message))

(def Messages (ref () :validator validate-message-list))

(defn add-message [sender text]
  (dosync (alter Messages conj (Message. sender text))))

これだとメッセージ追加の度に全体をvalidationするので相当効率悪いように見える。変更があった箇所だけvalidateするようにできないんだろうか?

あとでメッセージの内容を書き換えたりする場合も考えると、add-messageにvalidationをつけるよりdefrecordにvalidatorを指定できるといいな、と思って探したけど見つからなかった。後に学習するclassとかならできるのかな?とりあえず後回し。

非同期な更新にエージェントを使う

非同期更新は一応わかってるつもりだけど、確認しておく。

(defn next-counter []
  (printf "before: %d\n" @counter)
  (send counter inc)
  (printf "after: %d\n" @counter))

試してみる。

user=> (next-counter)
before: 0
after: 0
nil
user=> @counter
1

予想通り、afterの後に更新されている。

が、次のコードで予想外の結果が出てちょっと慌てた

(defn do-next-counter []
  (do
    (next-counter)
    (printf "do-after: %d\n" @counter)))

これでdo-afterでも更新前の値が表示されると思ってたけど、更新された後になるときもある。

user=> (do-next-counter)
before: 0
after: 0
do-after: 1
nil

user=> (do-next-counter)
before: 1
after: 1
do-after: 2
nil

user=> (do-next-counter)
before: 2
after: 2
do-after: 2
nil

なんでそう思ってたかって、JavaScriptシングルスレッド非同期ばっかり触ってたからだ。agentは普通にマルチスレッドか。というわけでごく短いコードだとこれぐらいの命令数でも更新終了することがあるので気をつけておこう。

それとawaitの例が無かったので書いてみた

(defn next-counter []
  (printf "before: %d\n" @counter)
  (send counter inc)
  (printf "after: %d\n" @counter)
  (await counter)
  (printf "after-await: %d\n" @counter))

実行例

user=> (next-counter)
before: 0
after: 0
after-await: 1
nil

user=> (next-counter)
before: 1
after: 1
after-await: 2
nil

エージェントのバリデーションとエラー処理

ref-set、reset!みたいな値の更新だけの関数が無いので作る。なんでだろう?agentの使い方でそういう事するのってありえないっけ?長い時間がかかる関数の結果を手軽に取得したいときとか使えそうと思ったけど、まずいかな?別スレッドを生成してその中でトランザクションしないと危険、ってことなのかな。

(defn set-counter [v] (send counter (fn [_] identity v)))

試してみる。

user=> (set-counter "a")
#<Agent@7850bd4a FAILED: 0>

user=> (agent-errors counter)
(#<IllegalStateException java.lang.IllegalStateException: Invalid reference state>)

user=> (set-counter "a")

IllegalStateException Invalid reference state  clojure.lang.ARef.validate (ARef.java:33)

エージェントをトランザクション中で使う

ブロックする可能性のある関数をsendに送ってはいけない。他のエージェントの動作を止めてしまうかもしれないからだ。

ということは、agentのためのスレッドがあってsendならそこに送られる、send-offなら説明通り別のスレッドプールということかな。

で、send-offはだいたいわかるのでいいとして、spitがシレっと出てきてる。後で説明あるみたいだけど、便利そうなので見てみる。

user=> (doc spit)
-------------------------
clojure.core/spit
([f content & options])
  Opposite of slurp.  Opens f with writer, writes content, then
  closes f. Options passed to clojure.java.io/writer.
nil

user=> (doc slurp)
-------------------------
clojure.core/slurp
([f & opts])
  Opens a reader on f and reads all its contents, returning a string.
  See clojure.java.io/reader for a complete list of supported arguments.
nil

フーム、spitでclojureのデータをそのまま書き出せて、slurpで文字列として読み込んでread-stringすればデータが再構築できるってことかな?やってみよう。

user=> (add-message "utachan" "gomen na sai ne")
(#mycode.mycode.Message{:sender "utachan", :text "gomen na sai ne"})

user=> (spit "./testdata.clj" @Messages)
nil

user=> (read-string (slurp "./testdata.clj"))
(#mycode.mycode.Message{:sender "utachan", :text "gomen na sai ne"})

user=> (first (read-string (slurp "./testdata.clj")) )
#mycode.mycode.Message{:sender "utachan", :text "gomen na sai ne"}

user=> (:sender (first (read-string (slurp "./testdata.clj")) ))
"utachan"

おお、できてる。これで簡単なデータの保存・読み込みはできるな〜

varでスレッドごとの状態を管理する

その前にスレッドのスタートがシレッと出てきてるのでちょっと試してみる。

user=> (.start (Thread. (fn [] (println "hoge"))))
hoge
nil

user=> (.start (Thread. (println "hoge") (fn [] (println "hoge"))))
hoge
hoge
nil

user=> (.start (Thread. (println "hoge") (println "hoge") (println "hoge") ))
hoge
hoge
hoge

NullPointerException name cannot be null  java.lang.Thread.init (Thread.java:366)

user=> (.start (Thread. (println "hoge") (println "hoge") (println "hoge") (println "hoge")))
hoge
hoge
hoge
hoge
NullPointerException   clojure.lang.RT.longCast (RT.java:1194)

Thread.が普通に&bodyなカンジかな?と思ったけど違うみたいだ。どう調べていいのかわからないのでとりあえず放置してfnに入れていく。

まず普通にルート束縛した変数と、動的ルート束縛にしたものが他のスレッドから参照できることを確かめる。

user=> (def foo 3)
#'user/foo

user=> (.start (Thread. (fn [] (println foo))))
3
nil

user=> (def ^:dynamic bar 3)
#'user/bar

user=> (.start (Thread. (fn [] (println bar))))
3
nil

bindingが関数呼び出しも束縛しているか試す。

user=> (defn hogex [] 10)
#'user/hogex
user=> (defn do-hogex [] (hogex))
#'user/do-hogex
user=> (let [hogex (fn [] 5)] (do-hogex))
10

ここまではいい。では、bindingはどうだろう

user=> (binding [hogex (fn [] 5)] (do-hogex))

IllegalStateException Can't dynamically bind non-dynamic var: user/hogex  clojure.lang.Var.pushThreadBindings (Var.java:320)

アレッ?!動的束縛だけを書き換えるのか!^:dynamicと何が関係あるの?とスルーしてた。これ説明わからない人もいるんじゃないかな。。

気を取り直して再度hogexを動的束縛として定義しなおす。ついでに、別スレッドではルート束縛が参照されているか確かめる。

(defn ^:dynamic hogex [] 10)
(defn do-hogex [] (println (hogex)))

(defn test-hogex-binding []
  (binding [hogex (fn [] 5)] (do-hogex)))

(defn test-hogex-binding-with-threads []
  (binding [hogex (fn [] 5)]
    (do-hogex) ;; ここはbindingを定義したスレッドなので影響を受けて5を返すはず
    (.start (Thread. do-hogex)) ;; ここは別スレッドなのでルート束縛のhogexが10を返すはず
    ))

テスト実行

user=> (test-hogex-binding)
5
nil

user=> (test-hogex-binding-with-threads)
5
10
nil

なるほど!

感想など

  • ナナメ読みした時は簡単そうじゃん?と思ったところだから、結構いろいろ引っかかっておろろいた
  • プログラミングClojure、プログラミング詳しい人向けだなこれ……読んでサンプルコード試すだけだったら理解できてなかった可能性高い。疑問書いていくスタイルで読んでて良かった。
  • PAIPもちょっと読み始めて、同じように疑問書いていこうかと思ったけど、こっちは解説がかなり詳しいのと、演習問題があるのでそのまま答えたり遊んだりすれば良さそうだ。でも疑問に思ったところは取り組んでメモしていきたいところ。
広告を非表示にする