18

How to achieve Aspect-Oriented Programming in Clojure? Do we need AOP in Clojure?
Let's say we want plain vanilla Clojure solution (no AspectJ).

Chiron
  • 20,081
  • 17
  • 81
  • 133

4 Answers4

26

Aspect-Oriented Programming is typically used to add cross-cutting functionality to code that would otherwise get hopelessly intertwined with business logic. A great example is logging - you don't really want logging code scattered everywhere in your code base.

You don't really need AOP in Clojure because it's easy to achieve this with other techniques in Clojure.

For example, you can use higher-order functions to "wrap" other functions with cross cutting functionality:

; a simple function - the "business logic"
(defn my-calculation [a b]
  (+ a b))

; higher order function that adds logging to any other function
(defn wrap-with-logging [func]
  (fn [& args]
    (let [result (apply func args)]
      (println "Log result: " result)
      result)))

; create a wrapped version of the original function with logging added
(def my-logged-calculation (wrap-with-logging my-calculation))

(my-logged-calculation 7 9)
=> Log result:  16
=> 16
mikera
  • 105,238
  • 25
  • 256
  • 415
  • 7
    The problem with that example, as a comparison to AOP, is that now the developer must invoke the new wrapper method instead of the original one. Ideally, it would be possible to achieve the logging behavior by invoking the original method. That would be closer to what AOP provides, right? – jcrossley3 Apr 07 '11 at 17:59
  • 9
    @jcrossley3 - you could always redefine the original function with (def my-calculation (wrap-with-logging my-calculation)) if you wanted.... – mikera Apr 07 '11 at 19:40
  • 13
    That's not safe for re-evaluating though. For a more flexible formalization of this, see Robert Hooke: https://github.com/technomancy/robert-hooke –  Apr 11 '11 at 18:32
  • @jcrossley3 it depends how you use AOP. E.g. you could have used `around` operator, that surrounds the function invocation. This is exactly what is demonstrated here – denis631 Feb 18 '19 at 10:59
15

AOP IMHO is just an artifact of certain kinds of static programming languages. AFAIKS it's usually just a bunch of non-standard compiler extensions. I've not yet seen any application of AOP that can't be solved better & natively in more dynamic languages. Clojure is certainly dynamic enough, and that's without even considering macros.

I may be wrong, but if so, I'd need to see an actual AOP use case that can't be implemented just as well in pure clojure.

Edit: just to be clear: I refuse to see things like elisp's advice as aspect oriented. In dynamic languages, those are just techniques to be used whenever you need them, with no need for language support other than rebinding of function definitions - which all lisps support anyway.

There's no need to treat them as special - you can easily define your own defadvice-like function in clojure. See for example, compojure's wrap! macro, which is actually deprecated since you generally don't even need it.

Joost Diepenmaat
  • 17,633
  • 3
  • 44
  • 53
  • 3
    It's kind of weird to say macros are dynamic since they run at compile time (they are hooks into the compiler). When you change a macro, you need to recompile all the code that calls it. Not very dynamic in my book... – Marek Mar 11 '14 at 22:19
  • 3
    Also, AOP is intimately related to MOP, the metaobject protocol of the Common Lisp (also Pascal Constanza is famous for advocating AOP for CL). Do you think Common Lisp is also static? This answer seems to be a random collection of (un)educated guesses... – Marek Mar 11 '14 at 22:22
  • 2
    I think this answer seems to imply (to one who isn't well versed in Clojure) that Clojure somehow eliminates cross-cutting concerns without any extra effort. mikera's answer is much clearer in regards to this and also includes examples, I think this should be the accepted answer. The fact that that answer and its example also highlights some difficulties only furthers the conclusion that AOP/Cross-cutting concerns (by any other name) are not solved automatically in Clojure. – Sprague Oct 01 '14 at 10:57
10

Aspect oriented programming is a great way to achieve seperation of concernes in Java. Clojure's composable abstractions achieve this very well. See this question also. The topic is covered really well in The Joy Of Clojure.

as for an example of Aspect Oriented Clojure by another name check out the Ring web framework

Community
  • 1
  • 1
Arthur Ulfeldt
  • 90,827
  • 27
  • 201
  • 284
1

Well you could be more AOP w/ Clojure easily. Just use metadata in functions to informe when you want logs:

(defn ^:log my-calculation 
  [a b]
  (+ a b))

Then you can redefine all functions, wrapping them w/ logging automatically. Part of this code (together w/ unwrap functions bellow):

(defn logfn
  [f topic severity error-severity]
  (fn [& args]
    (try
      (if severity
        (let [r (apply f args)]
          (log* topic {:args args, :ret r} severity)
          r)
        (apply f args))
      (catch Exception e
        (if error-severity
          (let [data {:args args, :error (treat-error e), :severity error-severity}]
            (log* topic data error-severity)
            (throw e))
          (throw e))))))

(defn logfn-ns
  "Wrap function calls for logging on call or on error.

  By default, do nothing. When any :log or :log-error, enables logging. If ^:log,
  only log on error (default severity error).

  Can customize log severity w/ e.g. ^{:log info} or on error log severity likewise."
  [ns alias]
  (doseq [s (keys (ns-interns ns))
          :let [v (ns-resolve ns s)
                f @v
                log (-> v meta :log)
                log-error (-> v meta :log-error)]
          :when (and (ifn? f)
                     (-> v meta :macro not)
                     (-> v meta :logged not)  ;; make it idempotent
                     (or log log-error))]

    (let [log (if (= log true) nil log)
          log-error (or log-error "error")
          f-with-log (logfn f
                            (str alias "/" s)
                            log
                            log-error)]
      (alter-meta! (intern ns s f-with-log)
                   (fn [x]
                     (-> x
                         (assoc :logged true)
                         (assoc :unlogged @v)))))))

(defn unlogfn-ns
  "Reverts logfn-ns."
  [ns]
  (doseq [s (keys (ns-interns ns))
          :let [v (ns-resolve ns s)]
          :when (-> v meta :logged)]
    (let [f-without-log (-> v meta :unlogged)]
      (alter-meta! (intern ns s f-without-log)
                   (fn [x]
                     (-> x
                         (dissoc :logged)
                         (dissoc :unlogged)))))))

You just call (log/logfn-ns 'my.namespace "some alias") and all is wrapped w/ logging (and some).

PS: My custom logger above have a topic which is "some alias/function name" PS2: Also wrapped w/ try/catch. PS3: Didn't like this so much. Reverted to have explicit logging.

dqc
  • 416
  • 4
  • 8