10

Is there anyway to call Clojure macros from Java?

Here is what I am trying to do:

RT.var("clojure.core", "require").invoke(Symbol.create("clojure.contrib.prxml"));
Var prxml = RT.var("clojure.contrib.prxml", "prxml");
Var withOutStr = RT.var("clojure.core", "with-out-str");
String stringXML = (String) withOutStr.invoke((prxml.invoke("[:Name \"Bob\"]")));

prxml writes to *out* by default which is why I need to wrap it with the macro with-out-str which returns the string.

I am getting this error:

 [java] java.lang.IllegalArgumentException: Wrong number of args (1) passed to: core$with-out-str
 [java]     at clojure.lang.AFn.throwArity(AFn.java:437)
 [java]     at clojure.lang.RestFn.invoke(RestFn.java:412)
 [java]     at clojure.lang.Var.invoke(Var.java:365)
 [java]     at JavaClojure.xml.main(Unknown Source)
mudgen
  • 7,213
  • 11
  • 46
  • 46
  • Can you even call a macro from Java? I don't know much about Clojure, but in Lisp, the macros are evaluated by the reader, before the code is compiled. If Clojure is the same way, then calling from Java may be too late. When do macros get expanded in Clojure? – Ryan Stewart Jul 13 '11 at 01:17
  • 1
    Yes, you can call Clojure macros from Java but you need to invoke the Clojure reader (so that the compiler gets called and expands the macro), not the specific function (since a function is already compiled!). Might be worth checking out the answers on this question : http://stackoverflow.com/questions/2181774/calling-clojure-from-java – mikera Jul 13 '11 at 11:33
  • mikera, I think you are on to something. I wonder if there is a good way to call the clojure reader on a macro in Java. – mudgen Jul 13 '11 at 12:28

4 Answers4

7

You'll have to roll your own withOutStr.

class YourClass {
    static final Var withBindings = RT.var("clojure.core", "with-bindings*");
    static final Var list = RT.var("clojure.core", "list*");
    static final Var out = RT.var("clojure.core", "*out*");
    static final Var prxml = RT.var("clojure.contrib.prxml", "prxml");

    static String withOutStr(IFn f, Object args...) {
        StringWriter wtr = new StringWriter();
        withBindings.applyTo(list.invoke(RT.map(out, wtr), f, args));
        return wtr.toString();
    }

    ...

    String stringXML = withOutStr(prxml, "[:Name \"Bob\"]");
}
kotarak
  • 17,099
  • 2
  • 49
  • 39
5

The problem is basic.

invoke (and its sister, apply) are used for functions.

Macros are not functions, so they can not be invoked. Macros need to be compiled. In normal Lisps, they could just be eval'd or macroexpanded or whatever. And in 10m of looking around, apparently Clojure doesn't have a simple RT.eval(String script) function to make this easy.

But, that's what needs to be done. You need to compile and execute this.

I saw a package that integrates Clojure with Java JSR 223, and IT has an eval (because 223 has an eval). But I don't know a) if the package is any good or b) if this is the direction you want to go.

Will Hartung
  • 115,893
  • 19
  • 128
  • 203
  • 1
    I found I could use the following to get the Clojure reader and evaluator in Java: RT.var("clojure.core", "eval").invoke(RT.var("clojure.core", "read-string").invoke("Clojure string here.") – mudgen Jul 13 '11 at 15:40
  • ah excellent. Yea, I was surprised I couldn't find a similar call simply off of RT, but that's exactly what you want to do. – Will Hartung Jul 13 '11 at 16:01
1

Disclaimer: I know very little about clojure (my experience has been with other functional languages and Java)

My gut instinct however says the problem is around prxml.invoke(). The thought here being that that statement evaluates too soon, and sends the result to withOutStr (instead of letting withOutStr evaluate it).

Looking at the sources online alone... notably RT, Var & AFn as well as the clojure doc for with-out-str I would try something along the lines of:

String stringXML = (String) withOutStr.invoke(RT.list(prxml,"[:Name \"Bob\"]"));

Edit: Also I would suspect that it is able to call clojure macros from java otherwise the isMacro() function on Var seems rather silly...

Edit 2: Downloaded clojure, and tried it... doesn't work so ignore this for now.

Edit 3: with-out-str apparently requires 2 parameters so:

final Cons consXML = (Cons) withOutStr.invoke(prxml, RT.list("[:Name \"Bob\"]"));
final Object[] objs = RT.seqToArray(consXML);
System.out.println(Arrays.toString(objs));

has an output of: [clojure.core/let, [s__4095__auto__ (new java.io.StringWriter)], (clojure.core/binding [clojure.core/*out* s__4095__auto__] (clojure.core/str s__4095__auto__))]

I wonder if that will evaluate to something useful, or not (not sure if I'm correct on the bindings, have to figure out how to evaluate the cons through Java.

Edit 4: Poking through the Compiler and more code, it seems macros actually have 2 hidden parameters. See the commit 17a8c90

Copying the method in the compiler I have:

final ISeq form = RT.cons(withOutStr, RT.cons(prxml, RT.cons("[:Name \"Bob\"]", null)));
final Cons consXML = (Cons) withOutStr.applyTo(RT.cons(form, RT.cons(null, form.next())));
System.out.println(consXML.toString());
// Output: (clojure.core/let [s__4095__auto__ (new java.io.StringWriter)] (clojure.core/binding [clojure.core/*out* s__4095__auto__] #'clojure.contrib.prxml/prxml "[:Name \"Bob\"]" (clojure.core/str s__4095__auto__)))

Which seems a bit more promising, but it still requires the evaluation of the let expression which seems to have a special case in the compiler.

Charlie
  • 7,181
  • 1
  • 35
  • 49
0

If you want to execute Clojure code from C# or Java, use Clojure's load-string function. This will take an ordinary string and execute it exactly as if you'd typed the string in the REPL. That includes dealing with macros.

Here's a short version of the C# code. The Java version won't be far from that.

    private static readonly IFn LOAD_STRING = Clojure.var("clojure.core", "load-string");

    private void executeButton_Click(object sender, EventArgs e)
    {
        try
        {
            object result = LOAD_STRING.invoke(sourceTextBox.Text);
            if (null == result)
                resultTextBox.Text = "null";
            else
                resultTextBox.Text = result.ToString() + " (" + result.GetType().Name + ")";
        }
        catch (Exception ex)
        {
            resultTextBox.Text = ex.ToString();
        }
    }

You can see the full version of the sample program here. It includes a lot of sample code to demonstrate Clojure interop.

Trade-Ideas Philip
  • 1,067
  • 12
  • 21