Friday, November 07, 2008

ConstantizeAttribute to support Renum on Rails

The most obvious shortcoming of Renum is that there hasn't been any clean way to use enumerated values as ActiveRecord attribute values. I've finally fixed that.

There were a couple of Rails features that seemed like they might be helpful but turned out not to be.

Rails' built-in serialize macro class method uses YAML to store items, which is fine for arrays and hashes but hideous for anything else (if you ever look at your database directly). Though YAML serialization worked on Renum-created enumerated values, deserializing a value created a new instance. That instance might turn out to work fine for your needs, but it wouldn't actually be the instance it ought to be (i.e., the one the constant points to), so it might surprise and disappoint you in subtle ways. The other huge downside to serializing the instance (and its instance variables) is that a change in the encapsulated contents of the enumerated value could break things.

Rails' built-in composed_of macro class method could have been made to work, but it wouldn't have been pretty. The default handling would have required Renum to redefine new in generated enumeration classes to do a lookup instead of allocating and initializing an instance. The latest releases of Rails provide :constructor and :converter options that would have allowed me to avoid messing with new, but it still would've been ugly, not to mention requiring a very recent version of Rails.

What I finally realized is that the beauty of constants is that I didn't need to write a lookup mechanism: Ruby already does that. All I needed to do was store the name of the constant when writing the attribute and constantize it when reading to get the proper value. I also realized that approach is in no way tied to Renum: It would also allow classes and modules to be attribute values, which could be helpful if, for example, you have a module to mix in or a service class to call based on some reference data.

So I wrote a tiny little Rails plugin called ConstantizeAttribute that does this for you. This example pretty much says it all:

# ... your migration ...
create_table :robots do |t|
  t.column :behavior_module, :string
end

# ... your model ...
class Robot < ActiveRecord::Base
  constantize_attribute :behavior_module
end

# ... some classes or modules you want to store as attribute values ...
module RobotBehaviors
  module Handy
    def self.encounter
      "Is there anything I can help you with?"
    end
  end

  module Evil
    def self.encounter
      "I will destroy all humans."
    end
  end
end

# ... so now,
robby = Robot.create :behavior_module => RobotBehaviors::Evil

# Now "RobotBehaviors::Evil" is in the behavior_module column.

robby.behavior_module.encounter # => "I will destroy all humans."

robby.update_attribute :behavior_module, RobotBehaviors::Handy

# Now "RobotBehaviors::Handy" is in the behavior_module column.

robby = Robot.find :first

robby.behavior_module.encounter # => "Is there anything I can help you with?"

Install ConstantizeAttribute with

script/plugin install git://github.com/duelinmarkers/constantize_attribute.git

if your Rails is recent enough to install from git or grab a copy of the repo manually and drop the plugin in place. (There's no install script to worry about.)

The cool thing about this is that it works with old version of Renum without any changes, and it all happens in only about 15 lines of code.

Saturday, October 18, 2008

Module#to_proc h0tness

I don't need this right now, so I'm not putting it into any of the codebases I touch. But if I ever do need it, I haven't thought of a reason yet that I shouldn't pull it in.

describe "Module#to_proc" do
  it "provides a proc that mixes the module into the yielded object" do
    
    # Say you've got some stuff,
    
    stuff = ["jimmy", "jane", Object.new]
    
    # and you want to have all that stuff extend a certain Module.
    
    module Fooable
      def foo() "This #{self.class} is fooing"; end
    end
    
    # stuff.each {|thing| thing.extend Fooable } is just so many
    # characters to type!
    
    # With a little hackery in Module, you can do this.
    
    stuff.each &Fooable
    
    # Now all that stuff will have your module mixed in,
    
    stuff.each {|thing| thing.should be_a_kind_of(Fooable) }
    
    # and have interesting new behavior.
    
    stuff.collect {|o| o.foo }.should == [
      "This String is fooing",
      "This String is fooing",
      "This Object is fooing"]
  end
end

Here's all it takes to make it pass, plus some simple memoization so it's cheaper to call.

class Module
  def to_proc
    @to_proc ||= Proc.new {|o| o.extend self }
  end
end

Friday, September 12, 2008

Keep Backtraces Honest: Set Forwardable::debug

If you use Forwardable at all, you may have noticed that when you misspell or forget to implement something, the backtraces can be a little baffling. Take this example.

require 'forwardable'

class Foo
  extend Forwardable
  def_delegator :a, :bar # a is not defined
  def_delegator :b, :baz # b is defined but returns nil
  def b; end
end

f = Foo.new

When you call f.bar, you'll get an unsurprising NameError: undefined local variable or method 'a' for #<Foo:0x1044fec>. The backtrace will point at the line where you called bar, which is a little weird, but since that line doesn't have any mention of 'a' on it, you'll probably know to go looking for bar and discover the missing (or misspelled) delegate accessor.

When you call f.baz, you'll get an unsurprising NoMethodError: undefined method 'baz' for nil:NilClass. But, again, the backtrace will point at the line where you called baz, and here you're much more likely to go chasing down the wrong problem. If you really created your own instance of Foo just before that line, you're probably not going to worry that Foo.new returned nil. But what if you'd gotten that instance of Foo from some other call? The backtrace suggests that other call returned nil.

It's a dirty lie!

You have a perfectly good instance of Foo. The real problem is that its delegate accessor returned nil.

So why does the backtrace lie?

The first time I ran into this, I assumed Forwardable was implemented using some weird native magic that kept it from appearing in the backtrace. That seemed odd, since it doesn't do anything you couldn't do from normal Ruby code, but I didn't have time to dig into it.

When I finally did, I was surprised to find that it's normal Ruby code, but the method it writes (via module_eval) is (as interpolated for this example):

def baz(*args, &block)
  begin
    b.__send__(:baz, *args, &block)
  rescue Exception
    $@.delete_if{|s| /^\\(__FORWARDABLE__\\):/ =~ s} unless Forwardable::debug
    Kernel::raise
  end
end

As you'll have guessed, "(__FORWARDABLE__)" is passed as the file name to module_eval, so Forwardable's default behavior is to delete itself from backtraces, potentially making them misleading and wasting a lot of debugging time.

I don't know why it does that, but thankfully the authors realized you may not want that and made the hiding conditional on Forwardable::debug being false.

I highly recommend that any application using Forwardable has some bootstrapping code set that flag.

Forwardable.debug = true