blindguy
blindguy

Reputation: 1009

Clojure macros call function from macro

I have this ns with a macro in it. The annoying thing im dealing with is that the taoensso.timbre macro only works as a variadic expression (timbre/info "a" "b" "c"). A list of items wont log right (timbre/info ["a" "b" "c"]). Im trying to create a wrapper macro that lets the code call (logger/info) in the same variadic form, then process all elements, and then pass to timbre/info

(ns logger
  (:require [taoensso.timbre :as timbre :include-macros true])) ; a third party logger

;; A bit of pseudo code here. If you pass in a vector of args, you should get a vector of args with some changes
(defn scrub [args]
  (if (listy)
     (mapv (fn [a] (scrub args) args)
     (if (is-entry a) {:a "xxx"} a)

(defmacro info
  [& args]
  `(timbre/info ~@(scrub args)))

This doesnt work because scrub is called immediately and wont resolve symbols passed in. I need something like either of these that dont work.

(defmacro info
  [& args]
  `(timbre/info @(scrub-log-pii ~args)))

(defmacro info
  [& args]
  `(timbre/info ~@('scrub-log-pii args)))

My last thought was to try to wrap the timbre macro in a function so the macro and evaluation happen in the right order. There is however, no way to "apply" to a macro.

(defn info3
  [& args]
  (timbre/info (scrub-log-pii (vec args))))

Any ideas?

Upvotes: 1

Views: 431

Answers (2)

Alan Thompson
Alan Thompson

Reputation: 29984

You have encountered a problem of using macros known as "turtles all the way down". That is, instead of using function composition, you may need to write a wrapper macro, then another wrapper macro for that, etc.

The detailed steps to writing a macro are described in this answer:

For your specific problem, we could to this:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test)
  (:require
    [clojure.pprint :as pp]))

(defn infix-impl
  [a op b]
  (list op a b))

(defmacro infix
  "Allows user to have Java-style infix operators:

        (infix 2 + 3)
  "
  [a op b] (infix-impl a op b))

(defn infix-seq-impl
  [args]
  `(let [form#   (cons 'infix ~args)
         result# (eval form#)]
     result#))

(defmacro infix-seq
  [args] (infix-seq-impl args))

(dotest
  (is= 5 (infix 2 + 3))

  (let [params '[2 + 3]]
    (pp/pprint (infix-seq-impl 'params))

    (is= 5 (infix-seq params))))

Here we use the infix macro to show how to create a wrapper macro infix-seq that accepts a sequence of params instead of N scalar params. The printed output shows the generated code:

(clojure.core/let
  [form__24889__auto__    (clojure.core/cons 'tst.demo.core/infix params)
   result__24890__auto__  (clojure.core/eval form__24889__auto__)]
 result__24890__auto__)

A more general version

The applied macro below allows you to pass in the name of the macro to be "applied" to the param sequence:


(defn applied-impl
  [f args]
  `(let [form#   (cons ~f ~args)
         result# (eval form#)]
     result#))

(defmacro applied 
  [f args] (applied-impl f args))

(dotest
  (nl)
  (let [params '[2 + 3]]
    ;      (applied 'infix params)   ; using a single quote fails
    (is= 5 (applied `infix params)) ; using a backquote works
    (is= 5 (applied 'tst.demo.core/infix params)) ; can also use fully-qualified symbol with single-quote
    (is= 5 (applied (quote tst.demo.core/infix) params)) ; single-quote is a "reader macro" for (quote ...)
    ))

Upvotes: 2

leetwinski
leetwinski

Reputation: 17849

not exactly an answer to the question as phrased (macro application stuff), but rather the practical timbre solution, that may be applicable in your specific case:

here you can see that all timbre macros use log! macro, which in turn accepts the collection of args.

so, just implementing your procedure as

(defmacro info* [args] `(log! :info :p ~args ~{:?line (fline &form)}))

should do the trick.

Upvotes: 2

Related Questions