1

I am trying to do a few things at once with a Clojure macro but it's not working. I want to either learn how to fix it or understand why it is impossible.

I want to:

  1. Create a Java interface in Clojure
  2. ... that is immediately usable from the REPL (i.e., no AOT/compilation step needed, and I am strenuously avoiding writing this in Java)
  3. ... that has method- and parameter-level Java annotations
  4. ... that also correctly provides the method-level annotation with a String array instead of a single String.

I want to do this because I am playing with Langchain4j and I want a way to define my LLM prompts inline, right in my REPL (CIDER). I don't want to store my prompts in a resource or Java file because (1) Clojure is my thinking language, so I want to keep as much in it as possible, and (2) I want to keep the maximal amount of context in front of me in one buffer so I don't have to pay the cognitize tax of frequently switching buffers.

I want to invoke my macro like this:

(defprompt foo
  "You are a {{role}} and need to solve this: {{problem-statement}}.")

I want that macro to basically translate into the code below. The code below works but requires that I store the prompt in a resource file, because I can't pass a String array to the annotation this way:

(definterface Foo
  (^{UserMessage {:fromResource "foo-prompt.txt"}} ;; can't see my prompt right here :'(
   ^String foo [^{V "role"} role ^{V "problem-statement"} problem-statement]))

Now for my attempt. I am inexperienced with macros and so used Claude, and then tweaked it in my own experiments. (Updated 2025-04-14; see update at bottom of question.)

(defmacro defprompt
  "Define a Langchain4j prompt interface for AiServices.
  
  Arguments:
    name - the name for the interface (will be capitalized)
    prompt - a string prompt template with {{variable}} placeholders 
    method-name - the name of the single method
    
  Example:
    (defprompt brainstorming
      \"ACT as a professional {{role}} and brainstorm on this problem statement: {{problemStatement}} Give different possible solutions that make sense.\")
  
  Returns the interface symbol that can be passed to AiServices/create."
  [name prompt]
  (let [interface-name (symbol (str name))
        
        ;; Extract variables from the prompt
        pattern #"\{\{([^}]+)\}\}"
        param-names (->> (re-seq pattern prompt)
                         (map second)
                         (distinct))
                         
        ;; Create parameter declarations with annotations
        param-decls (mapv (fn [param]
                            (let [param-sym (symbol param)]
                              (with-meta param-sym
                                {V {:value param}
                                 :tag String})))
                          param-names)]
    
    `(definterface ~interface-name
       (~(with-meta 'prompt
          {:tag String
           UserMessage {:value (into-array java.lang.String (list prompt))}})
         [~@param-decls]))))

The problem is that the interface is defined but it doesn't have the method it should have. Sounds like an AOT/compilation thing; I ran into this problem previously with gen-class and realized in that situation that I needed to compile the namespace to make my gen'd class usable. As I said, I want to avoid AOT or compilation; I want the interface available immediately in the REPL (as it was with my definterface solution) so that I can quickly iterate on ideas in my REPL.

Resources I've followed:

Update, 2025-04-14

  1. When I say that the method isn't present, I mean that the method foo doesn't appear in the output of this command:
(->> Foo
     .getClass
     .getDeclaredMethods
     #_(mapcat #(.getAnnotations %))
     pprint
)
  1. I haven't written the Java interface I want; I used the definterface approach shown above. But the Java would probably look like:
interface Foo {
  @UserMessage("The {{role}} says {{sound}}.")
  String foo(@V("role") String role, @V("sound") String sound);
}
  1. I modified the macro above to my latest version. When I macroexpand-1 it with *print-meta*, I get this — which looks right. But Langchain4j is complaining that it doesn't see my parameter (@V) )annotations.
(clojure.core/definterface Foo
  (^{:tag java.lang.String
     dev.langchain4j.service.UserMessage {:value #object["[Ljava.lang.String;" 0x92b8d3e "[Ljava.lang.String;@92b8d3e"]}}
   prompt
   [^{dev.langchain4j.service.V "role",
      :tag java.lang.String} role
    ^{dev.langchain4j.service.V "sound",
      :tag java.lang.String} sound]))
  1. Thanks to @amalloy, I ran javap -v -p on the interface .class file. It has correct types (so type hints are right) but no annotations.
7
  • 1
    What do you mean, it doesn't have the method it should have? Surely that definterface has one method. Do you mean it has the expected method but with the wrong annotations? Try (set! *print-meta* true) before macroexpanding - then you can see whether the problem is that you're failing to produce the metadata you want, or whether the metadata you (correctly) produce doesn't lead to the annotations you want. Commented Apr 14 at 6:15
  • Also it would help if you explicitly laid out the Java version of the interface you want to generate. Commented Apr 14 at 9:13
  • @amalloy Responded to your two questions above. And thanks for the tip about *print-meta*. Commented Apr 14 at 15:39
  • Thanks for the engagement, @amalloy. I might close this question. When I use (.getDeclaredMethods (.getClass foo)), the method isn't on the list. But when I use clojure.reflect/reflect, the method is present. I thought those two approaches were equivalent but they are apparently not. Commented Apr 14 at 16:51
  • 1
    If you want to debug further, you could use AOT generation to emit the interface to a .class file, and then decompile it with javap to see how the annotations differ from what you expect. As far as I know it should be possible to generate annotations like the ones you want, but I've never done it so I'm not certain. Commented Apr 14 at 20:30

1 Answer 1

1

The answer came from a member of the Clojurian Slack community: To pass an array to an annotation, just pass the value as a vector. I.e., look in the example from this doc for annotation SupportedOptions:

javax.annotation.processing.SupportedOptions ["foo" "bar" "baz"]

Also, for anyone running into something like this, remember @amalloy's suggestion to look at the compiled interface using javap -v -p.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.