1
votes

I'm tangled in a macro mess.

Background: The idea is that I define a 'situation object' (a hash map) that contains a logical condition, some text, and a variable number of 'options'. Each option has also a condition, some text and some effects. The conditions and effects are turned into anonymous functions, the text is just preserved.

So I want to define a macro that transforms the expression into a couple nested hashmaps, including some generated lambda functions for evaluating in runtime. I've copied some ideas from here for making the make-fn macro to use map inside other macros, but maybe that's the wrong way to go...

The code:

; Two basic (silly maybe? let me know if I'm reinventing the wheel)
; macros for turning conditions and effects into functions.
(defmacro lambdify [& body] `(fn [] (do ~@body)))
(defmacro condify [body] `(fn [] ~body))

(defmacro make-fn [m] 
  `(fn [& args#] 
    (eval `(~'~m ~@args#))))

; option and option-in-list do the same but one takes a single parameter
; as a list. I'm trying out different things.
(defmacro option
  [cnd text & effs]
  `(hash-map :cond (condify ~cnd)
             :text ~text
             :effects (lambdify ~@effs)))

(defmacro option-in-list
  [expr]
  `(hash-map :cond (condify ~(first expr))
             :text ~(second expr)
             :effects (lambdify ~@(next (next expr)))))

(defmacro options
  [& opts]
  `(map (make-fn option-in-list) ~opts))

(defmacro situation
  [conds title text & opt-list]
    `(hash-map :cond (condify ~conds)
               :title ~title
               :text ~text :opts (options ~@opt-list)))

I want to start with the situation, so if I define this:

(situation (< 1 5) "title" "desc"
  ((> 1 2) "option1" (println "option 1 chosen"))
  ((< 1 5) "option2" (println "option 2 chosen"))))

I expect to have a hash map like

 { :cond (.. the fn for (< 1 5)),
   :title "title" ,
   :text "desc",
   :opts [{:cond ( the fn for (> 1 2))
           :text "option1"
           :effects: ( the fn for println... )}
          { .. the second option also as a hash}}

I know that option and option-in-list work:

(option (= 1 2) "option2" (println "option 2 chosen"))

{:text "option2", :effects #, :cond #}

(option-in-list ((= 1 2) "option2" (println "option 2 chosen")))

{:text "option2", :effects #, :cond #}

But when I evaluate either options or situation, I get:

(options ((< 1 5) "option1" (println "option 1 chosen"))
     ((= 1 2) "option2" (println "option 2 chosen")))

java.lang.ClassCastException: java.lang.Boolean cannot be cast to clojure.lang.IFn

As I understand, that means that somewhere it's trying to evaluate a list that has a boolean as it's first member, and therefore it's attempting to cast it to a function, right?

But I cannot find where (or how) that is happening.

Any guidance will be appreciated, I don't have much experience with Clojure and I'm not even sure if I'm going in a completely wrong direction.

EDIT

I've managed to make something similar that works, but I would like to know why..

If I do this:

(def sit
 (situation (< 1 5) "title" "desc"
  (option (> 10 2) "option1" (println "option 1 chosen"))
  (option (< 1 5) "option2" (println "option 2 chosen"))))

(declaring explicitly option in the situation declaration) then it works. So There is something wrong in my (understand of) invocation of macros from within macros. Is there a way to make it work without using the option macro directly (i.e. making it called from within the situation macro)?

1
situation needs to be a macro, but the things it calls, directly or indirectly, had better be functions.Thumbnail
I'm not sure if I follow exactly what you're saying, but I don't think that's the case, as you can have macros that get expanded to other macros and so on (like in macroexpand)Sebastian
@Thumbnail No, because situation isn't actually calling any of those functions; it is expanding to a call to them. You could rewrite things so that these helper macros are functions, but there's no need to.amalloy
But I don't see how both condify and lambdify could be functions as they need exactly what macros do: to generate code without evaluating it. Is there a way to make so with functions, and will those functions be available while processing the macros?Sebastian

1 Answers

0
votes

Ok, finally managed to make it work.

(defmacro option
  [expr]
  `(hash-map :cond (condify ~(first expr))
            :text ~(second expr)
            :effects (lambdify ~@(next (next expr)))))

(defmacro options
  [opts]
  `(list ~@(map (make-fn option) opts)))

(defmacro situation
  [conds title text & opt-list]
    `(hash-map :cond (condify ~conds)
               :title ~title
               :text ~text
               :opts (options ~opt-list)))

I had a unquote and splice-unquote mess, passing things in a list instead of as a list of things, which later was evaluated and therefore the errors for casting a bool as a fn.