user2545464
user2545464

Reputation: 191

How to simplify some code using defmacro

I wrote two functions like this, but as you see most part of them are identical, so I want to write a macro to simplify them.

I understand the simple macro examples in my textbook, but I don't know how to write my own.

Here's my code:

 (defn load-dict
       ;  "Load database from a txt file previous saved"
       [fname]
       (with-open [rdr (io/reader fname)]
                  (doseq [line (line-seq rdr)]
                         (println line)
                         (def vvv (clojure.string/split line #"\s"))
                         ;(println (str "count of vvv is " (count vvv)))
                         (if (< 1 (count vvv))
                           (add- dict (gen-word (nth vvv 0) (nth vvv 2) (nth vvv 1))))
                         )))
 (defn load-article
       ;  "Load article from a txt file"
       [fname]
       (with-open [rdr (io/reader fname)]
                  (doseq [line (line-seq rdr)]
                         (println line)
                         (def vvv (clojure.string/split line #"\s"))
                         ;(println (str "count of vvv is " (count vvv)))
                         (if (< 1 (count vvv))
                           (add- article vvv ))
                         )))

Should I write a macro like:

(defmacro load- [target fname &expr)
  `(... 
    (add- ~target expr)))

I actually don't know how to write such a macro. I just hate duplicate code.

PS, the tow function work fine. I don't care about the variable this is part of the code.

Upvotes: 1

Views: 170

Answers (2)

Arthur Ulfeldt
Arthur Ulfeldt

Reputation: 91554

user1944838 is correct here in that you don't need a macro, and since macros make code ,which does not need them, slightly harder to work with in some contexts (you can't pass it to map or apply for instance), using functions is preferable in practice. Understanding how to write macros correctly is however very important.

I would write this as a template macro that binds a name you pass it to each word and then call the body you pass to the macro which would in turn use that word through the symbol name.

(defmacro with-loaded-article                                                                                                                                             
  [[name-for-line fname] & body]                                                                                                                                          
  `(with-open [rdr# (io/reader ~fname)]                                                                                                                                   
     (doseq [line# (line-seq rdr#)]                                                                                                                                       
       (println line#)                                                                                                                                                    
       (let [~name-for-line (clojure.string/split line# #"\s")]                                                                                                           
         ~@body)))) 
  • The [name-for-line fname] expression destructures the first argument into a single "binding form" that will be used to generate a symbol and the values that it will resolve to. This format is very common in "with-*" macros such as with-open except here I only take one binding form to keep the code smaller.
  • the rdr# and line# symbols inside the syntax-quote are a feature of syntax quote called "auto gensyms" that causes any symbol in side the syntax quote ending with # to be replaced by a unique, though consistent symbol in the resulting expression.
  • the ~@ is the splicing-unquote feature of syntax-quotes that causes body in this case to be inserted without adding ( ) around it.

We can see how this expands with macroexpand-1 and pprint hint: (use 'clojure.pprint)

hello.core> (pprint (macroexpand-1 
                      `(with-loaded-article [line "tmp.txt"] (println line))))                                                                                 
(clojure.core/with-open                                                                                                                                                   
 [rdr__6337__auto__ (clojure.java.io/reader "tmp.txt")]                                                                                                                   
 (clojure.core/doseq                                                                                                                                                      
  [line__6338__auto__ (clojure.core/line-seq rdr__6337__auto__)]                                                                                                          
  (clojure.core/println line__6338__auto__)                                                                                                                               
  (clojure.core/let                                                                                                                                                       
   [hello.core/line (clojure.string/split line__6338__auto__ #"\s")]                                                                                                      
   (clojure.core/println hello.core/line))))                                                                                                                              

And when we run the resulting code we get the lines of the file as sequences:

hello.core> (with-loaded-article [line "tmp.txt"] (println line))                                                                                                         
hello world                                                                                                                                                               
[hello world]                                                                                                                                                             
world hello                                                                                                                                                               
[world hello]                                                                                                                                                             
and internet hello as well                                                                                                                                                
[and internet hello as well]

Upvotes: 3

Leon Grapenthin
Leon Grapenthin

Reputation: 9266

I would use a let block instead of def. Using def will bind a var and define vvv in your namespace. A macro is not really needed. You could simplify your code like this:

(defn load-from
   "Load database from a txt file previous saved"
   [fname load-fn]
   (with-open [rdr (io/reader fname)]
              (doseq [line (line-seq rdr)]
                     (println line)
                     (let [vvv (clojure.string/split line #"\s")]
                       (when (< 1 (count vvv))
                         (load-fn vvv))))))

And invoke it like this

(load-from "myfile.txt" #(add- dict (apply gen-word (take 3 %))))
(load-from "myfile.txt" #(add- article %))

Upvotes: 5

Related Questions