きよしのズンドコ節をTransducerとして実装する
同僚氏の古い記事をうっかり見つけてしまって、そういえば昔そういうのが流行ったときがあったなーと思ったのと、最近core.asyncをもう少し使いこなせるようになりたいと思っていたところだったので、練習がてら書いてみる(core.asyncそのものというよりはその周辺技術ではあるので、アレですが)。 ↑kohyamaさんの解はシンプルにきれいで美しいですよね。私は結構好きです。
まずはステートマシンを用意する
Clojureの場合、適当にget-inあたりでうまく動くようなマップを用意すればよいです。nilを初期値として扱っているので、シュッとしています。 code: clojure
(def fsm
{nil {"zun" :zun1}
:zun1 {"zun" :zun2}
:zun2 {"zun" :zun3}
:zun3 {"zun" :zun4}
:zun4 {"doko" :kiyoshi}})
;; => :zun1
;; => :zun4
こんな感じですね。これを適当にreduceで動かしてみると以下のようにうまく動作します。
code: clojure
nil
;; => :kiyoshi
nil
;; => :zun1
ズンドコ川を作る
これはClojureの得意分野なので、簡単です。
code: clojure
(defn zundoko-stream []
その名もzundoko-stream。なんかシュールですね。まじめにやってるつもりなんですが…。これは期待通りに動きます。
code: clojure
(take 10 (zundoko-stream))
;; => ("doko" "doko" "doko" "zun" "zun" "zun" "zun" "zun" "doko" "zun")
(take 10 (zundoko-stream))
;; => ("doko" "zun" "zun" "doko" "zun" "doko" "zun" "zun" "zun" "doko")
これをさっきのステートマシンと組み合わせると、きれいに動きます。さっきのreduceと異なり、:kiyoshiを見つけた時点でreducedを使って停止させるのがミソです(zundoko-streamは無限に続くので怠ると停止しなくなります)。
code: clojure
(if (= :kiyoshi s)
(reduced s)
s)))
nil
(zundoko-stream))
;; => :kiyoshi
Transducerを実装してみる
実際にTransducerを実装してみます。ClojureのドキュメントにTransducersの実装方法が書いてあるので参考にします。
先程のreduceに渡した関数の本質的な計算部分はステートマシンを動かして、:kiyoshiになったら計算を停止させる、ということでした。なので、これをTransducerとして実装する場合、ステートフルなTransducerとして実装すると良さそうです。
code: clojure
(defn zundoko-xf []
(fn
([] (xf))
(vswap! state (fn s (get-in fsm s input))) (if (= :kiyoshi @state)
(reduced (xf result input))
(xf result input)))))))
そんなわけで、これでズンドコきよしが完成しました。zundoko-streamからの取得が終了したら、ズンドコが完成した合図なので最後に合いの手を入れると良い感じになりました。
code: clojure
(-> (into [] (zundoko-xf) (zundoko-stream))
(conj "kiyoshi")
println)
;; => nil
そして、これはTransducerなので良い感じに合成して、新たなTransducerを作ることができます。
code: clojure
(let [xf (comp (zundoko-xf)
(map {"zun" "ズン" "doko" "ドコ"}))]
(-> (into [] xf (zundoko-stream))
(conj "キ・ヨ・シ!")
(->> (reduce str))
println))
;; ズンドコズンズンドコズンズンドコドコドコズンズンズンドコドコドコドコドコズンズンズンズンドコキ・ヨ・シ!
;; => nil
code: clojure
(def c (async/chan))
(let [zundoko-ch (async/chan 1 (comp (zundoko-xf)
(map {"zun" "ズン" "doko" "ドコ"})))
piped (async/pipe c zundoko-ch)]
(async/go-loop []
(do
(print w)
(recur))
(println "キ・ヨ・シ!"))))
(async/go
(async/>! c "zun")
(async/>! c "zun")
(async/>! c "zun")
(async/>! c "zun")
(async/>! c "doko"))
;; ズンズンズンズンドコキ・ヨ・シ!
めでたしめでたし…。と、したかったところですが、よくよく考えると駄目なケースがあります。以下のように"zun"が多いとステートマシンが余分に動いてしまうので、うまく動かないというのをここまで書いたところで思い立ったので宿題にしておきます。
code: clojure
nil
;; => nil
宿題
athosさんからスッと答えが返ってきたので、これでよさそう。 ステートマシンに {:zun4 {"zun" :zun4}} の遷移を足せばzunが4つ以上連続した場合でも対応できそうですかね