動機
コードを書いていて、以下のような処理が出てきました。
(defn somethig-success [])
(defn something-random
[]
(when (> (rand-int 10) 5)
(somethig-success)))
処理とタイミングが完全に結合しているのがいやらしいので、もう少し切り離して形で書きたいと思いました。
;; こう書きたい
(defn something-random
[]
(when (> (rand-int 10) 5)
(run-hook :success)))
(add-hook :success somethig-success)
思いつくままに実装
とりあえず、内部実装が見えるのはいやなので、極力外から見えないようにしました。
下の実装はletでイベントを保持する変数を隠すようにしました。
remove-hookを書いていなかったり、ところどころ手は抜いています。
(let [events (atom {})]
(defn add-hook
[name fun]
(swap! events
update name conj fun)
nil)
(defn run-hook
[name arg]
(doseq [subscriber (get @events name '())]
(subscriber arg)))
)
普通に(def ^{:private true} events (atom {}))でよかったんじゃないかと後から思いましたが、こちらのほうがまとめてコメントアウトとかが楽という利点もあります。
他のライブラリを見ながら考える
Event Emitter
;; Defining Event Emitter class.
(defclass person (event-emitter)
((name :initarg :name
:reader name)))
(defvar *user*
(make-instance 'person :name "Eitaro Fukamachi"))
;; Attach a event listener for an event ':say-hi'.
(on :say-hi *user*
(lambda () (format t "Hi!")))
;; *user* says 'Hi!' when an event ':say-hi' is invoked.
(emit :say-hi *user*)
;-> Hi!
Common Lispですが参考にしました。
クラス定義をしなければいけない点と、イベント定義したクラスを持ちまわる点が嫌だなぁと思ってこの実装は避けました。
しかし、イベントが増えてきた場合や、関数的に書く利点を考えるとこちらのほうが適切なんだろうなぁと思います。
Robert Hooke
(use 'robert.hooke)
(defn examine [x]
(println x))
(defn microscope
"The keen powers of observation enabled by Robert Hooke allow
for a closer look at any object!"
[f x]
(f (.toUpperCase x)))
(defn doubler [f & args]
(apply f args)
(apply f args))
(defn telescope [f x]
(f (apply str (interpose " " x))))
(add-hook #'examine #'microscope)
(add-hook #'examine #'doubler)
(add-hook #'examine #'telescope)
Clojureで書かれていて、alter-var-rootが使われているのでClojureScriptではだめという話でした。
もし使えるのなら、間違いなくこれがいいなぁと感じました。
メタデータを使ってhookしているので、ロジック同士の結合度も低いので素敵です。こういう発想はいつかしたいです。
Closure Library(goog.events)
ちゃんと仕組みが用意されていて、近いことはできそうでした。
しかし、あまりにもJavaScriptの世界に踏み込みすぎていて、ちょっと敬遠しました。
まじめに書くならここらへんをラップするのが現実的かなという気はしました。
Google Closure LibraryでEvent周りの処理
感想
もうちょっとマクロを使ってみたかったです。思いついたら試してみるかもしれません。
実装は意外に苦労しました。もうちょっと慣れたいです。
追記(core.async)
ayato_pさんにcore.asyncを使えばいいのではないかとアドバイスをいただいたので、それをもとに書いてみました。
;; 依存性に[org.clojure/core.async "0.4.474"]の追加が必要(バージョンは適宜変更)
(ns event.core
(:require [clojure.core.async :refer [go-loop >! <!] :as async]))
(def event-bus (async/chan))
(def event-bus-pub (async/pub event-bus first))
(defn something-random
[]
(when (> (rand-int 10) 5)
(async/put! event-bus [:params "Success!!"])))
(defn somethig-success [msg]
(println msg))
(let [ch (async/chan)]
(async/sub event-bus-pub :params ch)
(go-loop []
(let [[_ params] (async/<! ch)]
(somethig-success params))
(recur)))
;; 運が良ければ、"Success!!"と表示される
(something-random)
参考URL
http://tonsky.me/blog/datascript-chat/
若干複雑になっている感がありますが、紹介していただいたマクロを使えば、もう少し簡単に書けるようです。
基本ライブラリである点と関数的に処理できることを考えるとこちらがいいのかなと思いますが、できることは増えるようなのでこれでトライしてみます。