Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make a namespace qualified function name in a macro

Tags:

clojure

I have a bunch of functions that map to and from some codes defined by an external system:

(defn translate-from-ib-size-tick-field-code [val]
  (condp = val
    0 :bid-size
    3 :ask-size
    5 :last-size
    8 :volume))

(defn translate-to-ib-size-tick-field-code [val]
  (condp = val
    :bid-size 0
    :ask-size 3
    :last-size 5
    :volume 8))

I'd like to make a macro to remove the duplication:

#_ (translation-table size-tick-field-code
                      {:bid-size 0
                       :ask-size 3
                       :last-size 5
                       :volume 8})    

I started the macro like this:

(defmacro translation-table [name & vals]
  `(defn ~(symbol (str "translate-to-ib-" name)) [val#]
     (get ~@vals val#)))

The resulting function body seems right, but the function name is wrong:

re-actor.conversions> (macroexpand `(translation-table monkey {:a 1 :b 2}))
(def translate-to-ib-re-actor.conversions/monkey 
     (.withMeta (clojure.core/fn translate-to-ib-re-actor.conversions/monkey      
     ([val__10589__auto__] 
        (clojure.core/get {:a 1, :b 2} val__10589__auto__))) (.meta ...

I'd like the "translate-to-ib-" to appear as part of the function name, instead of a prefix to the namespace, as it turned out.

How can I do this with clojure macros? If I am just doing it wrong and shouldn't use macros for this for some reason, please do let me know, but I would also like to know how to create function names like this to just improve my understanding of clojure and macros in general. Thanks!

like image 422
Chris Bilson Avatar asked Oct 04 '11 15:10

Chris Bilson


2 Answers

The macro issue is twofold:

1) You're using a backtick when quoting the form passed to macroexpand, which namespace-qualifies the symbols within:

`(translation-table monkey {:a 1 :b 2})
=> (foo.bar/translation-table foo.bar/monkey {:a 1, :b 2})

where foo.bar is whatever namespace you're in.

2) You're constructing the name of the defn item using the symbol name, which, when it is namespace-qualified, will stringify to "foo.bar/monkey". Here's a version that will work:

(defmacro translation-table [tname & vals]
  `(defn ~(symbol (str "translate-to-ib-" (name tname))) [val#]
     (get ~@vals val#)))

Notice that we're getting the name of tname without the namespace part, using the name function.

As for whether a macro is the right solution here, probably not :-) For a simple case like this, I might just use maps:

(def translate-from-ib-size-tick-field-code 
  {0 :bid-size
   3 :ask-size
   5 :last-size
   8 :volume})

;; swap keys & vals
(def translate-to-ib-size-tick-field-code
  (zipmap (vals translate-from-ib-size-tick-field-code)
          (keys translate-from-ib-size-tick-field-code)))

(translate-from-ib-size-tick-field-code 0)
=> :bid-size

(translate-to-ib-size-tick-field-code :bid-size)
=> 0

If speed is of the essence, check out case.

like image 120
Justin Kramer Avatar answered Oct 17 '22 19:10

Justin Kramer


Some unsolicited advice on a different point: (get ~@vals val#) is extremely suspicious. Your macro alleges to take any number of arguments, but if it gets more than two it will just do something that doesn't make any sense. Eg,

(translation-table metric {:feet :meters} 
                          {:miles :kilometers}
                          {:pounds :kilograms})

aside from being a terrible translation table, expands to code that always throws an exception:

(defn translate-to-ib-metric [val]
  (get {:feet :meters} 
       {:miles :kilometers}
       {:pounds :kilograms}
       val)))

get doesn't take that many arguments, of course, and it's not really what you meant anyway. The simplest fix would be to only permit two arguments:

(defmacro translation-table [name vals] 
  (... (get ~vals val#)))

But note that this means the value map gets reconstructed every time the function is called - problematic if it's expensive to compute, or has side effects. So if I were writing this as a macro (though see Justin's answer - why would you?), I would do it as:

(defmacro translation-table [name vals]
  `(let [vals# ~vals]
     (defn ~(symbol (str "translate-to-ib-" name)) [val#]
       (get vals# val#))))
like image 34
amalloy Avatar answered Oct 17 '22 18:10

amalloy