(ns robert.hooke "Hooke your functions! (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 (str 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) ;; Now when we examine something: (examine \"something\") > S O M E T H I N G > S O M E T H I N G Use the add-hook function to wrap a function in your a hook.") (defn- hooks [v] (-> @v meta ::hooks)) (defn- original [v] (-> @v meta ::original)) (defn- compose-hooks [f1 f2] (fn [& args] ;; TODO: tracing (apply f2 f1 args))) (defn- join-hooks [original hooks] (reduce compose-hooks original hooks)) (defn- run-hooks [hooks original args] (apply (join-hooks original hooks) args)) (defn- prepare-for-hooks [v] (when-not (hooks v) (let [hooks (atom {})] (alter-var-root v (fn [original] (with-meta (fn [& args] (run-hooks (vals @hooks) original args)) (assoc (meta original) ::hooks hooks ::original original))))))) (defonce hook-scopes []) (defn start-scope [] (locking hook-scopes (alter-var-root #'hook-scopes conj {}))) (defn- scope-update-fn [scopes target-var] (conj (pop scopes) (update-in (peek scopes) [target-var] #(if % % @(hooks target-var))))) (defn- possibly-record-in-scope [target-var] (locking hook-scopes (when (seq hook-scopes) (alter-var-root #'hook-scopes scope-update-fn target-var)))) (defn end-scope [] (locking hook-scopes (let [head (peek hook-scopes)] (alter-var-root #'hook-scopes pop) (doseq [[var old-hooks] head] (reset! (hooks var) old-hooks))))) (defmacro with-scope "Defines a scope which records any change to hooks during the dynamic extent of its body, and restores hooks to their original state on exit of the scope." [& body] `(try (start-scope) ~@body (finally (end-scope)))) (defn add-hook "Add a hook function f to target-var. Hook functions are passed the target function and all their arguments and must apply the target to the args if they wish to continue execution." ([target-var f] (add-hook target-var f f)) ([target-var key f] (prepare-for-hooks target-var) (possibly-record-in-scope target-var) (swap! (hooks target-var) assoc key f))) (defn- clear-hook-mechanism [target-var] (alter-var-root target-var (constantly (original target-var)))) (defn remove-hook "Remove hook identified by key from target-var." [target-var key] (when-let [hooks (hooks target-var)] (swap! hooks dissoc key) (when (empty? @hooks) (clear-hook-mechanism target-var)))) (defn clear-hooks "Remove all hooks from target-var." [target-var] (when-let [hooks (hooks target-var)] (swap! hooks empty) (clear-hook-mechanism target-var))) (defmacro prepend [target-var & body] `(add-hook (var ~target-var) (fn [f# & args#] ~@body (apply f# args#)))) (defmacro append [target-var & body] `(add-hook (var ~target-var) (fn [f# & args#] (let [val# (apply f# args#)] ~@body val#)))) (defmacro with-hooks-disabled [f & body] `(do (when-not (#'hooks (var ~f)) (throw (Exception. (str "No hooks on " ~f)))) (with-redefs [~f (#'original (var ~f))] ~@body)))