onsdag, juli 11, 2007

Some JRuby tricks

I have spent a few hours adding some useful features these last days. Nothing extraordinary, but things that might come in handy at one point or another. The problem with these features is that they are totally JRuby specific. That means you could probably implement them for MRI, but noone has done it. That means that if you want to use it, beware. Further, they exploit a few tricks in the JRuby implementation, meaning it can't be implemented in pure Ruby.

So, that was the disclaimer; now onto the fun stuff!

Breaking encapsulation (even more)
As you know, in Ruby everything is accessible in some form or another, and you can do almost everything with the metaprogramming facilities. Well, except for one small detail which I found out while working on the AR-JDBC database drivers.

We have some code there which needs to be separate for each database, and it just so happens that core ActiveRecord have already implemented them in a very good way. So, what do we do? Mix in them and remove the methods we don't want? No, because ActiveRecord adapters are classes, not modules, and you can't mix in classes. There is no way to get hold of a method and add that to an unrelated other class or module. Except if you're on JRuby, of course:
require 'jruby/ext'

class A
def foo
puts "A#foo"
end
def bar
puts "A#bar"
end
end

class B;end

class C;end

b = B.new
b.steal_method A, :foo
b.foo
B.new.foo rescue nil #will raise NoMethodError

C.steal_methods A, :foo, :bar
C.new.foo
C.new.bar
Of course, using this should be avoided at all costs. But it's interesting that such a powerful thing can be implemented using about 15 lines of Java code.

Introspection
JRuby parses Ruby code into an Abstract Syntax Tree. For a while now, the JRuby module have allowed you to parse a string and get the AST representation by executing:
require 'jruby'

JRuby.parse "puts 'hello'", 'filename.rb', false
This returns the Java AST representation directly, using the Java Integration features. That is old. What is new is that I have added pretty inspecting, a nice YAML format and some navigation features which makes it very easy to see exactly how the AST looks. Just do an inspect or to_yaml on an AST node and you will get the relevant information.

That is interesting. But what is even more nice is the ability to run and use arbitrary pieces of the AST (as long as they make sense together) and also run them:
require 'jruby'

ast_one = JRuby::ast_for("n = 1; n*(n+3)*(n+2)")
ast_two = JRuby::ast_for("n = 42; n*(n+1)*(n+2)")

p (ast_one.first.first + ast_two.first[1]).run
p (ast_two.first.first + ast_one.first[1]).run
As you can see, I take two fragments from different code, add them together and run them. You can also see that I'm using an alias for parse here, called ast_for. That makes much more sense when using the second parse feature, which we already know from ParseTree:
require 'jruby'

JRuby::ast_for do
puts "Hello"
end
Well, I guess that's all I wanted to show right now. These last small things I've added because I believe they will be highly useful for debugging JRuby code.

I also have some more ideas that I want to implement. I'll keep you posted about it.

6 kommentarer:

Daniel Berger sa...

require 'evil'

class Bar
include Array.as_module
end

b = Bar.new
b.push(1,2,3)
p b # => [1,2,3]

flgr sa...

Yup, I think I would have allowed including classes as well. evil.rb also offers Module#inherit so you can do:

class Bar
inherit Array
end

I think that is a bit nicer than steal_methods.

Still, very cool stuff. Being able to get at the AST is especially cool. Bonus points if you can add it to Method objects and Proc objects. :)

Anonym sa...

hey ola

can you try to add code indention to your code?

right now i.e. ruby code lacks indention, and it makes it a bit hard to read the ruby code without indention :)

Anonym sa...

whops, sorry, i forgot to add:
i meant code indention in your BLOG _here_ :)

Anonym sa...

Daniel, do you know if evil.rb works outside of MRI?

Ola, nice going - very cool. What approaches have you tried in simulating this behavior in Ruby?

Is it a function of simply knowing the AST and what would and would not work, or have you tried doing this?

I tried by instantiating A, then getting foo as a method, converting it to a proc, and using defined method with that info on B.

That works for your simple example, but as you probably know, if you have foo accessing some member variable, you will not get what you want - it uses A's members, at the time you stole the method.

Daniel Berger sa...

@sammy,

No, because evil.rb uses DL, which is a C extension. However, there's nothing preventing a DL wrapper in JRuby via JNI, at least in theory.

And no, I'm not going to write one. I'll let YOU write one. :)