Skip to content

Flexible macros recipe

Andre R. edited this page Apr 26, 2015 · 1 revision

If the macros that klang come with are too inflexible you can easily replace this by your own macros. This is a template that is using transducers:

;; file-name: your/app/logging.clj -- note: /Not/ cljs
;; 
;; This requires Clojure 1.7 due to the use of transducers. But it can
;; be modified easily to use simple (predicate) functions.
;; This macro file (.clj) is used in both, your production and dev environment.
;; You'll call them differently by using different :source-paths in your
;; leiningen configuration.

;; The global atom holds the filters/transducers that determine if the log! call
;; should be elided or not:
;; These transducers live only during the compilation phase and will not result
;; in any javascript code.
;; Q: Why transducers and not just an array of predicate functions?
;; A: We may be interested in changing (ie. (map..)) the passed in keyword. For
;; instance by removing the namespace from the keyword.
;; The function transduces on (namespaced) keywords which is just the very first
;; argument of the `log!' function.

;; The transducers that are applied before emitting code:
(defonce xforms (atom [(filter (constantly true))]))

;; The clojurescript function that is called when we emit code with the macro:
(def logger 'klang.core/log!)

(defn logger! [log-sym]
  (alter-var-root (var logger) (fn[_] log-sym)))

;; True if the macro should add line information to each log! call
(def add-line-nr false)

(defn line-nr! [tf]
  (alter-var-root (var add-line-nr) (fn[_] tf)))

(defn single-transduce
  "Takes a transducer (xform) and an item and applies the transducer to the
  singe element and returnes the transduced item. Note: No reducing is
  involved. Returns nil if there was no result."
  [xform x]
  ((xform (fn[_ r] r)) nil x))

;; You may also make this a macro if you want to call it from cljs
(defn strip-ns!
  "Adds a transducer so that namespace information is stripped from the log!
  call. So: ::FOO -> :FOO"
  []
  (swap! xforms conj
         (map (fn[type] (keyword (name type)))))
  nil)

;; You'll often see return nil here because we don't want to return anything in
;; the macro calls
(defmacro init-dev! []
  (line-nr! true)
  nil)

(defmacro init-debug-prod!
  "Sets up logging for production "
  []
  ;; For production we call this log function which can do whatever:
  (logger! 'my.app.log/log->server!)
  (line-nr! false)
  (strip-ns!)
  (swap! xforms conj
         ;; Only emit :ERRO and :WARN messages log calls:
         (comp 
          (filter #(some (partial = (name %))
                         ["ERRO" "WARN"]))))
  nil)

(defmacro init-prod!
  "Production: Strip all logging calls."
  []
  (logger! nil) ;; Not needed but just in case
  (swap! xforms conj
         (filter (constantly false)))
  nil)

;; The main macro to call all thoughout your cljs code:
;; ns_type is your usual ::INFO, ::WARN etc.
(defmacro log!
  "Don't use this. Write your own."
  [ns_type & msg]
  ;; when-let returns nil which emits no code so we're good
  (when-let [nslv-td (single-transduce (apply comp @xforms) ns_type)]
    (if add-line-nr
      `(~logger ~nslv-td ~(str "#" (:line (meta &form))) ~@msg)
      `(~logger ~nslv-td ~@msg))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Some other convenience function to make logging easier:

(defmacro info! [& msg]
  `(log! ~(keyword (name (ns-name *ns*)) "INFO") ~@msg))

(defmacro warn! [& msg]
  `(log! ~(keyword (name (ns-name *ns*)) "WARN") ~@msg))

;; Note that while writing macros you may need some figwheel restarts in case of
;; crashes and/or errors.

I could not offer the same flexible functionality from the library itself since passing in transducers from clojurescript code to clojure macros is very awkward and I'd rather not make people deal with this.

This is a long template. But I think it's better to not include this in Klang since it's more flexible if users set it up themself.

You can also add file name in the meta information of &form but I see no need for it due to namespaced keywords.

Then setup and call your logging like so:

;; -- filename: your/app/setup.cljs
(ns your.app.setup
  (:require-macros
   [your.app.logging :refer [log! info! warn!] :as lgmacros]))

(lgmacros/init-dev!) ;; Or whatever you're in (use leiningen profiles)

(log! ::INFO "hello" :there)
(info! :I "like" :chatting)
(warn! :a-lot)

You can then switch over to production and get rid of all log calls or forward them to your own function.

Clone this wiki locally