Friday, February 05, 2010

Hacking JRuby - add all Hash methods to Map

I recently filed JRUBY-4528, whose patch adds all Ruby's Hash methods to a java.util.Map type object. Applying the patch, I confirmed that I could use "add_ruby_methods" method on Map type object, then, Hash methods for Map object. This would be useful especially for embedding API users since they often want to share Map object between Java and Ruby, back and forth.

What's the problem of current JRuby? When an instance of java.util.HashMap is sent into Ruby code, the object is converted into a "usable" Java object in Ruby world. This is what org.jruby.javasupport.JavaEmbedUtils.javaToRuby() method does, and we can't get Java Map converted into Ruby Hash automatically. Why? People might want to use that object as it is, HashMap type object itself, for other Java APIs used in Ruby. However, no built-in method converts Map to Hash so far although some of methods are added to.

My patch is attempt to add all Hash methods to Map type object by "add_ruby_methods" method.
For example:

irb(main):004:0> require 'java'
=> true
irb(main):005:0> jhash =
=> {}
irb(main):006:0> jhash.put("1", 100)
=> nil
irb(main):007:0> jhash.put("2", 200)
=> nil
irb(main):008:0> jhash.inspect
=> "{2=200, 1=100}"
irb(main):009:0> rhash = jhash.add_ruby_methods
=> {"2"=>200, "1"=>100}
irb(main):010:0> rhash.inspect
=> "{\"2\"=>200, \"1\"=>100}"
irb(main):011:0> p rhash.values
[200, 100]
=> nil
irb(main):012:0> rhash.merge!({"2"=>222, "3"=>333})
=> {"3"=>333, "2"=>222, "1"=>100}
irb(main):013:0> jhash.inspect
=> "{3=333, 2=222, 1=100}"

On jirb, I created java.util.HashMap object and put two key-value pairs using Java API, which was inspected by automatically added "inspect" method while converting. Then, I used add_ruby_methods method. After that, I could use Hash's inspect, values and merge! methods. Operations for "rhash" object above is also operations to "jhash," so when I inspected jhash, key-value pairs were also updated.

What if I create a Map object in Java code and give it to Ruby? Key-value pairs in a Java Map object was completely manipulated by Ruby. For example, see code below:

ScriptingContainer container = new ScriptingContainer(LocalContextScope.SINGLETHREAD);
ConcurrentHashMap map1 = new ConcurrentHashMap();
map1.put("a", 100);
map1.put("b", 200);
Map map2 = new HashMap();
map2.put("b", 254);
map2.put("c", 300);
container.put("h1", map1);
container.put("h2", map2);
container.put("num", 0);
String script =
"rh = h1.add_ruby_methods\n" +
"puts \"num: #{num}\"\n" +
"rh.merge!(h2.add_ruby_methods) {|k,o,n| num += 1; o+n }";
Set entries = map1.entrySet();
for (Map.Entry entry : entries) {
System.out.print(entry.getKey() + ": " + entry.getValue() + ", ");


b: 454, a: 100, c: 300,

As you see, merge! method worked. All java.util.Map type such as ConcurrentHashMap or TreeMap are available to apply the method.

Possible problem of this attempt is that contents of an object after add_ruby_methods applied are Java objects. For this reason, a direct comparison to a Ruby Hash object fails. In this case, to_hash method would work since to_hash method returns real Ruby Hash of converted Java Map.

This is just an attempt, and I'm not sure this patch will be applied or not. If you think this is useful, leave a comment on JIRA.

No comments: