Friday, June 16, 2006

OpenStruct freeze behavior

While looking at OpenStruct, I noticed what I considered to be unexpected behavior.
irb(main):007:0> frozen = OpenStruct.new(:foo=>1).freeze
=> #
irb(main):008:0> frozen.foo
=> 1
irb(main):009:0> frozen.foo = 2
=> 2
irb(main):010:0> frozen.foo
=> 2
This behavior surprised me since freeze is defined as:
Prevents further modifications to obj. A TypeError will be raised if modification is attempted. There is no way to unfreeze a frozen object. See also Object#frozen?.

a = [ "a", "b", "c" ]
a.freeze
a << "z"

produces:

prog.rb:3:in `<<': can't modify frozen array (TypeError)
from prog.rb:3
To find out what was happening I opened ostruct.rb. The OpenStruct class defines methods based on the keys of the constructor hash parameter and stores the values in a hash.
def initialize(hash=nil)
@table = {}
if hash
for k,v in hash
@table[k.to_sym] = v
new_ostruct_member(k)
end
end
end

def new_ostruct_member(name)
name = name.to_sym
unless self.respond_to?(name)
meta = class << self; self; end
meta.send(:define_method, name) { @table[name] }
meta.send(:define_method, :"#{name}=") { |x| @table[name] = x }
end
end
A quick check shows that if you freeze an OpenStruct instance the value hash is not frozen.
irb(main):002:0> frozen = OpenStruct.new(:foo=>1).freeze
=> #
irb(main):003:0> frozen.frozen?
=> true
irb(main):004:0> table = frozen.send :table
=> {:foo=>1}
irb(main):005:0> table.frozen?
=> false
To fix this issue you could redefine freeze and delegate the call to the freeze to both the value hash as well as the object. The problem with this solution is that when the TypeError exception is raised it will return hash as the frozen object, not the OpenStruct.
irb(main):006:0> table.freeze
=> {:foo=>1}
irb(main):007:0> frozen.foo = 2
TypeError: can't modify frozen hash
from /opt/local/lib/ruby/1.8/ostruct.rb:75:in `[]='
from /opt/local/lib/ruby/1.8/ostruct.rb:75:in `foo='
from (irb):7
from :0
Another solution is to change the definition of new_ostruct_member.
class OpenStruct
def new_ostruct_member(name)
name = name.to_sym
unless self.respond_to?(name)
meta = class << self; self; end
meta.send(:define_method, name) { @table[name] }
meta.send(:define_method, :"#{name}=") do |x|
raise TypeError, "can't modify frozen #{self.class}", caller(1) if self.frozen?
@table[name] = x
end
end
end
end
The above change will raise OpenStruct as the frozen class when a modification attempt is made. To verify the change works correctly I wrote the following test.
class OpenStructTest < Test::Unit::TestCase
def test_struct_does_not_modify_table_if_frozen
f = OpenStruct.new(:bar=>1).freeze
assert_raise(TypeError) { f.bar = 2 }
end
end

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.