Sunday, March 09, 2008

Tips for JRuby engine: how to invoke Ruby's methods

This is the third post of "Tips for JRuby engine" series I've written in this blog. This post is focused on how Java can invoke Ruby defined methods. Users of JRuby engine might want to have Ruby as their first language to process complicated issues and use Ruby's methods in Java. Or, they already have a bunch of methods written in Ruby and feel happy if those methods are also available to reuse in Java code without any modification. JSR 223 scripting APIs have javax.script.Invocable interface and invokeFunction/invokeMethod methods defined in it. This interface is designed to enable users to invoke procedures and functions defined by dynamic languages from Java. The method, invokeFunction(), is used when a method is defined outside of classes or modules, which is known as a top-level method in Ruby. On the other hand, invokeMethod() method is applied when the method is defined in a class or module. In this post, I'll write about how to use invokeFunction() method, and the next post will be the one about invokeMethod().

To invoke Ruby defined top-level methods, a programmer need to eval scripts prior to use the invokeFunction() method. JRuby engine doesn't know the method specified in the first argument of invokeFunction() really exists and is ready to use unless it evals the script that has the definition of the method. Therefore, two steps are required to run Ruby's methods over JSR 223 APIs - first, eval(), and second, invokeFunction(). The most simple method invocation would be like this:
String script = 
"def say_something()" +
"puts ¥"I¥'m sleepy since it¥'s spring!¥"" +
"end";
engine.eval(script);
Invocable invocable = (Invocable) engine;
invocable.invokeFunction("say_something");

As for JRuby engine, "new Object[]{}" or "null" can be used as a second argument if Ruby's method doesn't need any argument. Compiler might complain about it, but the code works.

The next snippet shows the way of invoking the method with arguments. If people look at JDK 1.6 API document, they will know that invokeFunction() method can have multiple arguments to give over to Ruby's method. Argumnets are either a simple object array or varargs. A method invocation with arguments would look like:
String script =
"def come_back(type, *list)" +
"print ¥"#{type}: #{list.join(¥',¥')}¥";" +
"print ¥"...¥";" +
"list.reverse_each {|l| print l, ¥",¥"};" +
"print ¥"¥n¥";" +
"end";
engine.eval(script);
Invocable invocable = (Invocable) engine;
invocable.invokeFunction("come_back",
"sol-fa",
"do", "re", "mi", "fa", "so", "ra", "ti", "do");

The first argument of invokeFunction() method is the name of Ruby's method to invoke: rest of all arguments are the ones that Ruby's method needs to execute.

Then, a simple question might be come up with - how can I get return values over a method invocation? The JSR 223 method, invokeFunction() returns a single value whose type is java.lang.Object. This means that a programmer can get any type of Java object after executing Ruby's method if Ruby returns value(s). It is simple to get a single object as the return value since a Ruby’s object is mapped to the same type of a Java object. The perplexities might come from that Ruby’s method can return multiple values, not only one like Java. In this case, return values are elements of an array object; therefore, java.util.List typed object would be returned, and the code would look like the one below:
String script =
"def get_by_value(hash, value)" +
"hash.select { |k,v| v == value }" +
"end";
engine.eval(script);
Invocable invocable = (Invocable) engine;
Map map = new HashMap();
map.put("ruby", "red");
map.put("pearl", "white");
map.put("rhino", "gray");
map.put("rose", "red");
map.put("nimbus", "gray");
map.put("gardenia", "white");
map.put("camellia", "red");
Object object = invocable.invokeFunction("get_by_value", map, "red");
System.out.print( "red: ");
if (object instanceof List) {
for (Object element: (List)object) {
if (element instanceof List) {
for (Object param : (List)element) {
System.out.print(param + " ");
}
}
}
}
System.out.println();

So far, programmers could come to invoke Ruby's method from Java satisfactorily, but should they put all objects along with in the arguments' row of invokeFunction() method? Even if the programmers can put multiple arguments whatever they need to run Ruby's method correctly, it is confusing as the number of arguments increases. Using global variables would help to reduce putting many arguments. Here's a example code that reduced arguments from above snippet by using a global variable:
String script =
"def get_by_value(value)" +
"$hash.select { |k,v| v == value }" +
"end";
engine.put("hash", map);
engine.eval(script);
Invocable invocable = (Invocable) engine;
Object object = invocable.invokeFunction("get_by_value", "white");

The code below is an entire class to perform all of above snippets:
package canna;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class InvokingFunctionsExample {
private InvokingFunctionsExample()
throws ScriptException, NoSuchMethodException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("jruby");
invokeSimpleFunction(engine);
invokeFunctionWithArguments(engine);
Map map = new HashMap();
map.put("ruby", "red");
map.put("pearl", "white");
map.put("rhino", "gray");
map.put("rose", "red");
map.put("nimbus", "gray");
map.put("gardenia", "white");
map.put("camellia", "red");
invokeFunctionWithReturns(engine, map);
invokeFunctionWithGlobalVariables(engine, map);
}

private void invokeSimpleFunction(ScriptEngine engine)
throws ScriptException, NoSuchMethodException {
String script =
"def say_something()" +
"puts \"I\'m sleepy because I went to bed three in the morning!\"" +
"end";
engine.eval(script);
Invocable invocable = (Invocable) engine;
invocable.invokeFunction("say_something");
}

private void invokeFunctionWithArguments(ScriptEngine engine)
throws ScriptException, NoSuchMethodException {
String script =
"def come_back(type, *list)" +
"print \"#{type}: #{list.join(\',\')}\";" +
"print \"...\";" +
"list.reverse_each {|l| print l, \",\"};" +
"print \"\n\";" +
"end";
engine.eval(script);
Invocable invocable = (Invocable) engine;
invocable.invokeFunction("come_back",
"sol-fa",
"do", "re", "mi", "fa", "so", "ra", "ti", "do");
}

private void invokeFunctionWithReturns(ScriptEngine engine, Map map)
throws ScriptException, NoSuchMethodException {
String script =
"def get_by_value(hash, value)" +
"hash.select { |k,v| v == value }" +
"end";
engine.eval(script);
Invocable invocable = (Invocable) engine;
Object object = invocable.invokeFunction("get_by_value", map, "red");
printValues("red", object);
object = invocable.invokeFunction("get_by_value", map, "gray");
printValues("gray", object);

}

private void printValues(String value, Object object) {
System.out.print(value + ": ");
if (object instanceof List) {
for (Object element: (List)object) {
if (element instanceof List) {
for (Object param : (List)element) {
System.out.print(param + " ");
}
}
}
}
System.out.println();
}

private void invokeFunctionWithGlobalVariables(ScriptEngine engine, Map map)
throws ScriptException, NoSuchMethodException {
String script =
"def get_by_value(value)" +
"$hash.select { |k,v| v == value }" +
"end";
engine.put("hash", map);
engine.eval(script);
Invocable invocable = (Invocable) engine;
Object object = invocable.invokeFunction("get_by_value", "white");
printValues("white", object);
}

public static void main(String[] args)
throws ScriptException, NoSuchMethodException {
new InvokingFunctionsExample();
}
}

When this code gets run successfully, it will produce following outputs.
I'm sleepy because I went to bed three in the morning!
sol-fa: do,re,mi,fa,so,ra,ti,do...do,ti,ra,so,fa,mi,re,do,
red: rose red camellia red ruby red
gray: rhino gray nimbus gray
white: gardenia white pearl white

Tow methods defined in JSR 223 APIs, invokeFunction() and invokeMethod(), are powerful, but have a little API flaw. We can't give a block over when we invoke Ruby's methods from Java. For example, Ruby allows us to use "yield" in a method and give different blocks needed to run a bit differently:
def search(array)
for item in array
return item if yield(item)
end
end
result = search(["camellia", "gardenia", "nimbus"]) {|str| str[0] ==?c}
print result, "\n"

Unofortunately, we don't have any way of doing this.

No comments: