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).freezeThis behavior surprised me since freeze is defined as:
=> #
irb(main):008:0> frozen.foo
=> 1
irb(main):009:0> frozen.foo = 2
=> 2
irb(main):010:0> frozen.foo
=> 2
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?.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.
a = [ "a", "b", "c" ]
a.freeze
a << "z"
produces:
prog.rb:3:in `<<': can't modify frozen array (TypeError)
from prog.rb:3
def initialize(hash=nil)A quick check shows that if you freeze an OpenStruct instance the value hash is not frozen.
@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
irb(main):002:0> frozen = OpenStruct.new(:foo=>1).freezeTo 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):003:0> frozen.frozen?
=> true
irb(main):004:0> table = frozen.send :table
=> {:foo=>1}
irb(main):005:0> table.frozen?
=> false
irb(main):006:0> table.freezeAnother solution is to change the definition of new_ostruct_member.
=> {: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
class OpenStructThe 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.
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
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


