Ruby Beans
Sep. 13th, 2009 09:02 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
I was thinking about beans a little bit this weekend.
There are two things, I think, that make Ruby special: it has a really easy parser for a structured file format built right in (YAML), and it has a meta-object protocol.
A meta-object protocol just means that Ruby is just one language, not two. See, Java is two, a class-definition language, and a function-body language.
When you're defining classes, all you can do is define things: you can add methods and declare variables, but you can't call functions or make loops. Likewise, inside functions, you can put code in loops or use variables, but you can't define methods or new classes:
So, you end up writing a lot of things twice, or making a lot of repetitive code (like getX and getY there). In Ruby, it's always Ruby, so you can use the loops and things that you use to structure your repetitive functions to also structure repetitive class definitions.
Like beans.
Java has this thing called "JavaBeans". It's not really an API, it's just sort of a standard naming scheme, something we would now call a design pattern. It works like this:
Suppose you have a class that contains a pair of numbers, x and y. As a bean, this class would look like this:
So, I started thinking, what would the Ruby equivalent of a bean be? My first idea was this:
Now, what we've done here is something that's simply impossible in Java. We haven't written a bean, we've written the concept of a bean. Something that can't be done in Java. You can't say "I want a class with methods shaped like this, following these naming conventions, for all these names." That's why Java beans aren't an API, they're a standard written out in a document in English.
We're not quite there yet though. A hypothetical Java programmer might complain that part of what a bean does is validate its input, at the very least that it fits certain static types. We can't make a PointBean with an x coordinate of "banana".
So, let's add that in:
After Java 1.4, though, something big changed: imagine if we had a bean to represent a bank account. We'd have a list of transactions in the account, like this:
So, now we can do pretty much everything Java beans can do, and in a much cooler way: instead of a document that we wrote that we have to write repetitive boilerplate code against, we have a class that generates the boilerplate part for us. There's no need for a bean standard, because "bean" is a concept that's actually part of our program.
We've done something pretty remarkable here, too: in Java, the fact that a bean contains certain fields, and what their names and types are, is part of the code. It's immutable by other code, it's decided when the programmer first writes it. With our Ruby bean system, though, the fields and their whole type structure is just data, just a hash that we pass into the bean when we make its class. We've turned code into data.
And data can come from anywhere. Including a config file.
The second thing, other than the meta-object protocol, that makes Ruby special is that it comes out of the box able to understand structured text files, in YAML. So, we can take a file like this:
And now we can store the structure of our data classes in a text file! And retain all the type-checking we would get from a language like Java.
In fact, without too much trouble, we could query a database for this. As in, not store the fields in a database table, but actually look up the columns that the table has, and automatically make a bean class for every table in a database. Maybe with some hints for things that are arrays or references to other objects.
That kind of thing is a huge deal in Java, whole frameworks like Spring and EJB just to deal with it, as much as can even be dealt with (eventually Java has to resort to code generators to make it work), and in Ruby it's under a page of code. Because in Ruby, it's all code, because the code can modify itself.
Anyway, I was just thinking a little bit about beans this weekend.
There are two things, I think, that make Ruby special: it has a really easy parser for a structured file format built right in (YAML), and it has a meta-object protocol.
A meta-object protocol just means that Ruby is just one language, not two. See, Java is two, a class-definition language, and a function-body language.
When you're defining classes, all you can do is define things: you can add methods and declare variables, but you can't call functions or make loops. Likewise, inside functions, you can put code in loops or use variables, but you can't define methods or new classes:
class PointBean { private int x, y; public PointBean(int x, int y){ this.x = x; // Can use variables here, but can't define more methods! this.y = y; } public int getX(){return x;} // Can't use a loop here! public int getY(){return y;} }
So, you end up writing a lot of things twice, or making a lot of repetitive code (like getX and getY there). In Ruby, it's always Ruby, so you can use the loops and things that you use to structure your repetitive functions to also structure repetitive class definitions.
Like beans.
Java has this thing called "JavaBeans". It's not really an API, it's just sort of a standard naming scheme, something we would now call a design pattern. It works like this:
Suppose you have a class that contains a pair of numbers, x and y. As a bean, this class would look like this:
public class PointBean implements Serializable { private int x, y; public PointBean(){ x=0; y=0; } public int getX(){return x;} public void setX(int new_x){x=new_x;} public int getY(){return y;} public void setY(int new_y){y=new_y;} }There's a getThing and a setThing for each member of the class. The naming of these methods is what makes it a bean. Java has some really basic reflection tools that things like NetBeans can use to show all these methods in a GUI; there are some other tools that are designed to work with any class that follows the bean standard.
So, I started thinking, what would the Ruby equivalent of a bean be? My first idea was this:
class Bean def self.create_bean *fields Class.new Bean do |c| fields.each do |field| c.send :attr_accessor, field end end end endThis satisfies most of the bean requirements: you can create a class like this:
PointBean = Bean.create_bean :x, :yand then use it like the Java one:
// Java: PointBean pb = new PointBean(); pb.setX(3); pb.getX(); # Ruby: pb = PointBean.new pb.x = 3 pb.xThe code is a little different to account for Ruby style, using x= instead of setX, but the concept is the same. Make a class with a getter and a setter for a specified set of fields.
Now, what we've done here is something that's simply impossible in Java. We haven't written a bean, we've written the concept of a bean. Something that can't be done in Java. You can't say "I want a class with methods shaped like this, following these naming conventions, for all these names." That's why Java beans aren't an API, they're a standard written out in a document in English.
We're not quite there yet though. A hypothetical Java programmer might complain that part of what a bean does is validate its input, at the very least that it fits certain static types. We can't make a PointBean with an x coordinate of "banana".
So, let's add that in:
class Bean def self.create_bean fields={} Class.new Bean do |c| fields.each do |name, type| c.send :attr_reader, field c.add_field name, type end end end def self.add_field name, type define_method "#{name}=" do |new_value| unless new_value.is_a? type raise "Invalid type #{new_value.class}, expected #{type}" else instance_variable_set "@#{name}", new_value end end end endNow instead of just calling
attr_accessor
, we call attr_reader
to make just the getX() portion, then define the setX equivalent ourselves in add_field. We make the new beans like this:PointBean = Bean.create_bean :x=>Integer, :y=>Integer PointBean.new.x=3 # works PointBean.new.x="foo" # failsVery simple! And this mirrors pretty much exactly what we could do with beans in Java up until Java 1.4.
After Java 1.4, though, something big changed: imagine if we had a bean to represent a bank account. We'd have a list of transactions in the account, like this:
AccountBean = Bean.create_bean :transactions => ArrayThis is bad though, because we could easily put the wrong thing in that array and never know it. It might be an array of integers and we put in a string, say. Java 1.4 and earlier would have no way of catching that, and neither does our current bean system, but it's easy to add:
class Bean def self.type_matches? value, type if type.nil? true elsif type.is_a? Class value.is_a? type elsif type.is_a? Hash value.is_a? Hash and value.keys.all?{|key| Bean.type_matches?(key,type.keys.first)} and value.values.all?{|val| Bean.type_matches?(val,type.values.first)} elsif type.is_a? Enumerable value.is_a?(type.class) and value.all?{|v| Bean.type_matches?(v,type.first)} else false end end endWe've added a
type_matches?
function in, that will get called in add_field
, where we're calling new_value.is_a? type
now. This will act the same unless the type is a hash or array, in which case it will look inside to see what the type of all the values are. So:AccountBean = Bean.create_bean :transactions=>[Fixnum] AccountBean.new.transactions=[3] # works AccountBean.new.transactions="foo" # fails AccountBean.new.transactions=["foo"] # failsBecause
type_matches?
is recursive, we can even go to an arbitrary depth:AccountBean = Bean.create_bean :transactions=>[{Time => Fixnum}] AccountBean.new.transactions = [{Time.now => 5}]Or, even define beans containing beans:
TransactionBean = Bean.create_bean :amount=>Fixnum, :name=>String, :timestamp=>Time AccountBean = Bean.create_bean :transactions=>[TransactionBean]Before testing this one, let's also add in a nicer constructor. Since all Ruby beans are subclasses of the basic Bean class, we can just add this there:
class Bean def initialize fields={} fields.each do |name, value| send "#{name}=", value end end endSo now, we use our two beans like this:
t = TransactionBean.new :amount=>5, :name=>"Bill", :timestamp=>Time.now a = AccountBean.new :transactions=>[t]Also, of course, since Ruby classes are open, we can add methods to these if we need anything other than field accessors:
class AccountBean def balance return 0 unless transactions transactions.map(&:amount).inject(&:'+') end end
So, now we can do pretty much everything Java beans can do, and in a much cooler way: instead of a document that we wrote that we have to write repetitive boilerplate code against, we have a class that generates the boilerplate part for us. There's no need for a bean standard, because "bean" is a concept that's actually part of our program.
We've done something pretty remarkable here, too: in Java, the fact that a bean contains certain fields, and what their names and types are, is part of the code. It's immutable by other code, it's decided when the programmer first writes it. With our Ruby bean system, though, the fields and their whole type structure is just data, just a hash that we pass into the bean when we make its class. We've turned code into data.
And data can come from anywhere. Including a config file.
The second thing, other than the meta-object protocol, that makes Ruby special is that it comes out of the box able to understand structured text files, in YAML. So, we can take a file like this:
TransactionBean: amount: Fixnum name: String timestamp: Time AccountBean: transactions: ['TransactionBean']and we can make a function that makes beans out of it:
class Bean def self.create_from_file filename Object.module_eval do YAML.load(File.read(filename)).each do |name, fields| const_set name, Bean.create_bean(fields) end end end endAnd, we'll need to modify
type_matches?
a little bit to handle strings, because the type names will now come through as strings instead of constants:class Bean def self.type_matches? value, type if type.nil? true elsif type.is_a? Class value.is_a? type elsif type.is_a? String type_matches? value, Object.module_eval("::#{type}") elsif type.is_a? Hash value.is_a? Hash and value.keys.all?{|key| Bean.type_matches?(key,type.keys.first)} and value.values.all?{|val| Bean.type_matches?(val,type.values.first)} elsif type.is_a? Enumerable value.is_a?(type.class) and value.all?{|v| Bean.type_matches?(v,type.first)} else false end end end
And now we can store the structure of our data classes in a text file! And retain all the type-checking we would get from a language like Java.
In fact, without too much trouble, we could query a database for this. As in, not store the fields in a database table, but actually look up the columns that the table has, and automatically make a bean class for every table in a database. Maybe with some hints for things that are arrays or references to other objects.
That kind of thing is a huge deal in Java, whole frameworks like Spring and EJB just to deal with it, as much as can even be dealt with (eventually Java has to resort to code generators to make it work), and in Ruby it's under a page of code. Because in Ruby, it's all code, because the code can modify itself.
Anyway, I was just thinking a little bit about beans this weekend.