tags:

views:

157

answers:

1

I've got a Clojure file with a lot of string constants. I want to collect these strings in a collection by wrapping them in a macro. After several tries I succeeded, but my solution looks fairly hideous.

(ns Memorable)
(def MEMORY (atom []))
(defmacro memorize [s] (swap! MEMORY conj s) s)

(prn (str (memorize "hello") " brave new " (memorize "world")))  ;  test

(defmacro make-memories-constant [] `(def MEMORIES ~(deref MEMORY)))
(make-memories-constant)

Are there more elegant solutions for this problem?

+1  A: 

As far as code cleanup is concerned, I'd remove the make-memories-constant macro -- you can just do

(def MEMORIES @MEMORY)

or even

(def MEMORY @MEMORY)

so as not to clutter your namespace with another Var. As long as you put this after all calls to memorize, this will save a snapshot of MEMORY with all memorized strings inside.

I'd say this is actually pretty clean, with the actual code executing at runtime no different from what it would have been without any memorize-ing...

Another approach would be to prepare a sort of "memorisation framework" as a separate namespace exporting a macros called setup-memorization, say, which could look something like this (just a rough sketch which won't work without some polishing... updated with a (not so thoroughly) tested version -- this actually works!... still, please tweak it to your needs):

(ns memorization-framework)

(defmacro setup-memorization []
  (let [MEMORY (gensym "MEMORY")
        MEMORIES (gensym "MEMORIES")]
    `(do (def ~MEMORY (atom []))
         (defmacro ~'memorize [s#] (swap! ~MEMORY conj s#) s#)
         (defmacro ~'get-memory [] @~MEMORY)
         (defmacro ~'defmemories [~MEMORIES]
           `(do (def ~~MEMORIES @~~MEMORY)
                (ns-unmap ~~*ns* '~'~'get-memory)
                (ns-unmap ~~*ns* '~'~'memorize)
                (ns-unmap ~~*ns* '~'~MEMORY)
                ~~MEMORIES)))))

Then you'd use the memorization-framework namespace, do (setup-memorization) towards the top of your namespace to set things up, call memorize like you do in your example code to memorise things and finally use end-memorization defmemories to store the string collection somewhere in a Var and remove the temporary Var used to store the atom used for construction-time storage along with the ability to make further calls to memorize from this namespace. (The end-memorization macro is meant to just return the collection to wherever you call it from when called with no arguments or define a new Var to store it if given a single argument, which must be a symbol to be used to name the Var. Update: I've only tested the defmemories version, so I'm leaving it here and removing the "in-place return" variant.)

An example interaction at the REPL while in namespace foo (note I defined setup-memorization in the user namespace):

foo> (user/setup-memorization)
#'foo/defmemories
foo> (get-memory)
[]
foo> (memorize "foo")
"foo"
foo> (memorize "bar")
"bar"
foo> (get-memory)
["foo" "bar"]
foo> (defmemories quux)
["foo" "bar"]
foo> quux
["foo" "bar"]
foo> (get-memory)
; Evaluation aborted.
foo> (memorize)
; Evaluation aborted.

Those "Evaluation aborted." messages indicate that calls to get-memory and memorize throw exceptions after defmemories has been called. This is by design, as mentioned above. Also, get-memory is here mostly to ease testing / debugging.

If you're only going to use all this once, the setup-memorization approach might be overkill, but I guess it'll remove some boilerplate for you if you use it more.

Michał Marczyk