Metaprogramming with Ruby: Mapping Java Packages Onto Ruby Modules
Metaprogramming lets you define new constructs in your programming language. In safety languages like Java, metaprogramming is not a standard feature, although it can be done. Not surprisingly, the reaction most Java developers have to metaprogramming typically ranges from "useless" to "catastrophic". I was once in that category. But in the words of Paul Graham, sometimes doing the right thing involves "changing the language to suit the problem". This is a powerful concept that, when used with care, can greatly reduce development time. The Ruby language is especially well-equipped for metaprogramming. This article will show a simple metaprogramming technique that extends Ruby so that Java packages are mapped onto to Ruby modules.
Prerequisites
This tutorial uses Ruby Java Bridge (RJB). RJB can be installed either on Windows or Linux using the RubyGems packaging mechanism.
Defining the Problem
The large amount of existing Java code makes RJB one of Ruby's most useful integration tools. RJB provides a lightweight mechanism for working with Java classes from Ruby, while simultaneously allowing for the use of C-extensions and everything else the C implementation of Ruby offers.
Let's say you'd like to do image processing with Java's ImageIO
class. This could be accomplished with RJB via:
require 'rubygems'
require_gem 'rjb'
require 'rjb'
@ImageIO = Rjb::import 'javax.imageio.ImageIO'
@ImageIO.getReaderFormatNames # => an array of format names
Here, the instance variable ImageIO
holds a reference to the Java class of the same name. This isn't too bad, but can we do better?
Imagine a situation in which many Java classes need to be imported. This would lead to many variable assignments like the one above. The resulting duplications of Java class names and variable names doesn't smell especially good, nor does it scale well. In addition, the large number of these variable assignments would add a lot of mental overhead when reading and writing the code.
What we'd really like is if Ruby had a way for us to map a Java package/class hierarchy onto Ruby module/class hierarchy. We could then forget about the differences between Ruby and Java, and just get to work. For example:
#...
jrequire 'javax.imageio.ImageIO'
#...
Javax::Imageio::ImageIO.getReaderFormatNames
A Solution
Our solution will involve translating nested Java package/class constructs into nested Ruby module/class constructs at runtime. We'll need the ability to create new module hierarchies in running code, one of the problems metaprogramming solves. We'll also have to deal with capitalization: Java package names are all lowercase, but Ruby module names start with a capital letter. So java.lang.System
will become Java::Lang::System
. By mapping the Java package namespace onto the Ruby module namespace, we'll reduce the odds of creating a Ruby/Java class name collision, such as with java.lang.String
.
Let's create a small library to illustrate these points. The code will take advantage of Ruby's const_set
method, which allows new constants (and therefore new modules and classes) to be defined at runtime. Save the following code into a file called java.rb:
require 'rubygems'
require_gem 'rjb'
require 'rjb'
module Kernel
def jrequire(qualified_class_name)
java_class = Rjb::import(qualified_class_name)
package_names = qualified_class_name.to_s.split('.')
java_class_name = package_names.delete(package_names.last)
new_module = self.class
package_names.each do |package_name|
module_name = package_name.capitalize
if !new_module.const_defined?(module_name)
new_module = new_module.const_set(module_name, Module.new)
else
new_module = new_module.const_get(module_name)
end
end
return false if new_module.const_defined?(java_class_name)
new_module.const_set(java_class_name, java_class)
return true
end
end
Usage
Using this library consists of requiring
it, applying the new jrequire
command, and manipulating the resulting class:
require 'java'
jrequire 'javax.imageio.ImageIO'
Javax::Imageio::ImageIO.getReaderFormatNames # => ["BMP", "jpeg", "bmp", "wbmp", "gif", "JPG", "png", "jpg", "WBMP", "JPEG"]
You can eliminate the need to use the fully-qualified module name by adding an include
statement, just as you would with any other Ruby module:
require 'java'
jrequire 'javax.imageio.ImageIO'
include Javax::Imageio
ImageIO.getReaderFormatNames # => ["BMP", "jpeg", "bmp", "wbmp", "gif", "JPG", "png", "jpg", "WBMP", "JPEG"]
One Possible Variation
An interesting variation on the approach given here would be to override Ruby's require
method itself to accept fully-qualified Java class names. Then something even more Rubyesque could be used:
require 'java'
require 'javax/imageio/ImageIO'
Javax::Imageio::ImageIO.getReaderFormatNames
Other Examples of Ruby-Java Metaprogramming
Ola Bini has written an article on JRuby metaprogramming that takes a slightly different approach than the one detailed here.
Conclusions
This tutorial has shown a simple and practical application of Ruby's built-in metaprogramming capabilities. The careful use of metaprogamming is a powerful way to reduce code complexity and build a more consistent programming environment. Look for more metaprogramming techniques to appear in future releases of the Ruby CDK library.