如何在Clojure中实现钩子函数

6
我有一个情况,我正在创建和销毁对象在一个Clojure命名空间中,并希望另一个命名空间进行协调。但是,我不想让第一个命名空间在对象销毁时显式地调用第二个命名空间。
在Java中,我可以使用监听器。不幸的是,底层的Java库没有在对象销毁时发出事件信号。如果我在Emacs-Lisp中,那么我会使用钩子来完成这个任务。
现在,在Clojure中我不太确定。我找到了Robert Hooke库https://github.com/technomancy/robert-hooke。但这更像是elisp术语中的defadvice,我正在组合函数。而且文档说:
“Hooks旨在扩展您无法控制的函数;如果您拥有目标函数,显然有更好的方法来更改其行为。”
不幸的是,我没有发现其他明显的解决方案。
另一个可能性是使用add-watch,但这被标记为alpha版本。
我是否漏掉了其他明显的解决方案?
添加示例:
所以第一个命名空间...
(ns scratch-clj.first
   (:require [scratch-clj.another]))

(def listf (ref ()))

(defn add-object []
  (dosync
    (ref-set listf (conj
               @listf (Object.))))
  (println listf))


(defn remove-object []
  (scratch-clj.another/do-something-useful (first @listf))
  (dosync
     (ref-set listf (rest @listf)))
  (println listf))


(add-object)
(remove-object)

第二个命名空间

(ns scratch-clj.another)


(defn do-something-useful [object]
   (println "object removed is:" object))

这里的问题在于scratch-clj.first必须要求另一个命名空间,并明确推送删除事件。这有点笨拙,而且如果我有“yet-another”命名空间也想监听,则无法工作。
因此,我考虑挂钩第一个函数。

嗯,要小心使用“销毁”这个术语。我认为对象的销毁只会在垃圾回收时发生。删除对象和销毁对象是不同的,尽管一个可能导致另一个。 - Adrian Mouat
3个回答

2

这个解决方案是否符合您的要求?

scratch-clj.first:

(无需翻译)
(ns scratch-clj.first)

(def listf (atom []))
(def destroy-listeners (atom []))
(def add-listeners (atom []))

(defn add-destroy-listener [f]
  (swap! destroy-listeners conj f))

(defn add-add-listener [f]
  (swap! add-listeners conj f))

(defn add-object []
  (let [o (Object.)]
   (doseq [f @add-listeners] (f o))
   (swap! listf conj o)
   (println @listf)))

(defn remove-object []
  (doseq [f @destroy-listeners] (f (first @listf)))
  (swap! listf rest)
  (println @listf))

一些听众:
(ns scratch-clj.another
  (:require [scratch-clj.first :as fst]))

(defn do-something-useful-on-remove [object]
  (println "object removed is:" object))

(defn do-something-useful-on-add [object]
  (println "object added is:" object))

初始化绑定:

(ns scratch-clj.testit
  (require [scratch-clj.another :as another]
           [scratch-clj.first :as fst]))

(defn add-listeners []
  (fst/add-destroy-listener another/do-something-useful-on-remove)
  (fst/add-add-listener another/do-something-useful-on-add))

(defn test-it []
  (add-listeners)
  (fst/add-object)
  (fst/remove-object))

测试:

(test-it)
=> object added is: #<Object java.lang.Object@c7aaef>
   [#<Object java.lang.Object@c7aaef>]
   object removed is: #<Object java.lang.Object@c7aaef>
   ()

是的,我认为可以。我觉得手表也可以做到这一点,但这种方法对我来说更自然,而且更通用。非常感谢! - Phil Lord

1

听起来很像你所描述的是回调函数。

类似于:

(defn make-object 
  [destructor-fn] 
  {:destructor destructor-fn :other-data "data"})

(defn destroy-object
  [obj]
  ((:destructor obj) obj))

; somewhere at the calling code...

user> (defn my-callback [o] (pr [:destroying o]))
#'user/my-callback
user> (destroy-object (make-object my-callback))
[:destroying {:destructor #<user$my_callback user$my_callback@73b8cdd5>, :other-data "data"}]
nil
user> 

这是一个合理的想法,但它只是将问题转移了。现在我需要能够钩入我的make-object函数,以便我可以添加一个析构函数。以你评论后面添加的例子为例 - 对象的创建和销毁发生在命名空间“first”中,而我的回调相当于其他地方。 - Phil Lord
说得那么直白,Clojure 的一个问题是它并不太适合对象具有析构函数的概念。如果你把(几乎)所有的对象都视为“哑”记录,你通常会更容易处理。从你的代码中,你不想看到对象是否被销毁,而是想在 listf 集合修改时执行某种操作。Watches 是显而易见的工具,它们不太可能很快被移除。 - Joost Diepenmaat
在这种情况下,所涉及的对象(就像我的例子一样)是Java对象--我正在使用Clojure来操作Java API。一旦从列表中删除,这些对象将被销毁。引用监视器是否保证会被调用?它不能被否决吗? - Phil Lord

0

所以,这是我根据mobytes的建议得出的最终解决方案。多做了一些工作,但我猜以后会需要它。

感谢所有的帮助。

;; hook system
(defn make-hook []
  (atom []))

(defn add-hook [hook func]
  (do
    (when-not
        (some #{func} @hook)
      (swap! hook conj func))
    @hook))

(defn remove-hook [hook func]
  (swap! hook
         (partial
          remove #{func})))

(defn clear-hook [hook]
  (reset! hook []))

(defn run-hook
  ([hook]
      (doseq [func @hook] (func)))
  ([hook & rest]
      (doseq [func @hook] (apply func rest))))

(defn phils-hook []
  (println "Phils hook"))

(defn phils-hook2 []
  (println "Phils hook2"))


(def test-hook (make-hook))
(add-hook test-hook phils-hook)
(add-hook test-hook phils-hook2)
(run-hook test-hook)
(remove-hook test-hook phils-hook)
(run-hook test-hook)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接