[Q] best practice for redefining methods at runtime?

I have a class that I would like to "freeze" when it reaches a particular state so that it becomes read-only. I do *not* want to use the existing #freeze method because it raises an exception when there is an attempted mutation on a frozen object and I want the attempt to be silent. It appears that the best way to enforce this (state machine pattern) is by redefining the accessors and other methods that cause mutations to be no ops.

What is the preferred practice for doing that?

Here's an example of my approach:

class Foo
  attr_accessor :bar, :baz, :quxxo
  
  def freeze!
    do_nothing = Proc.new {|val| nil}
    recreate_method(:bar=, do_nothing)
    recreate_method(:baz=, do_nothing)
    recreate_method(:quxxo=, do_nothing)
  end
  
  private
  
  def recreate_method name, meth
    self.class.send(:define_method, name, meth)
    nil
  end
end

ruby-1.9.2-p0 > f=Foo.new
=> #<Foo:0x0000010185f7d0>
ruby-1.9.2-p0 > f.bar=2
=> 2
ruby-1.9.2-p0 > f.baz=9
=> 9
ruby-1.9.2-p0 > f.freeze!
=> nil
ruby-1.9.2-p0 > f.inspect
=> "#<Foo:0x0000010185f7d0 @bar=2, @baz=9>"
ruby-1.9.2-p0 > f.bar=3
=> 3
ruby-1.9.2-p0 > f.inspect
=> "#<Foo:0x0000010185f7d0 @bar=2, @baz=9>"

Is that reasonable or is there a more idiomatic way of accomplishing this task?

cr

the only best practice I would advise is to *not* redefine methods at
run-time.

Morning Chuck.

I have a class that I would like to "freeze" when it reaches a particular
state so that it becomes read-only. I do *not* want to use the existing
#freeze method because it raises an exception when there is an attempted
mutation on a frozen object and I want the attempt to be silent. It appears
that the best way to enforce this (state machine pattern) is by redefining
the accessors and other methods that cause mutations to be no ops.

What is the preferred practice for doing that?

You could in the alternative have the ops that mutate do an "unless" at the
top of the method to bake the freeze into the method as opposed to
redefining

Using your example

class Foo
attr_accessor :frozen
attr_reader :bar, :baz, :quxxo

def bar= new_bar
   unless @frozen @bar = new_bar
end

def baz= new_baz
   unless @frozen @baz = new_baz
end

def quzzo= new_quzzo
   unless @frozen @quzzo = new_quzzo
end
end

It's a little more work and a slight amount of overhead for the unless
check. But it does eliminate the need to redefine your methods. It also very
clearly spells out what you are freezing.

John

···

On Wed, Oct 20, 2010 at 8:31 AM, Chuck Remes <cremes.devlist@mac.com> wrote:

Reasonable maybe but it is far from achieving what freeze does,
because you still have instance_variable_set and as long as this
available I can redefine the accesors, thus you would need to block
#instance_variable_set, #define_method for the class but I can still
beat that for any given instance by introducing singleton methods....
In other words it is a metaprogramming mess.sub("m","h").gsub("s","l")!

Maybe you should reconsider your approach, what about freezing the
object but accessing it in a wrapper that rescues the exceptions? This
could done via closures, and BasicObject makes the task much easier
than it was in 1.8

class Freezer < BasicObject
  freezee = nil
  define_method :initialize do | f |
    freezee = f
  end
  define_method :method_missing do | name, *args, &blk |
    begin
      freezee.send( name, *args, &blk )
    rescue ::RuntimeError => re
     # Pitty that there is no designated Exception for this BTW
      raise unless /can't modify frozen/ === re.message
    end
  end
end

a = Freezer.new( "" )
a << "1"
puts a
a.freeze
a << "2"
puts a
# But
a.xxx

HTH
R.

···

On Wed, Oct 20, 2010 at 5:31 PM, Chuck Remes <cremes.devlist@mac.com> wrote:

Is that reasonable or is there a more idiomatic way of accomplishing this task?

--
There are worse things than having people misunderstand your work. A
worse danger is that you will yourself misunderstand your work.
-- Paul Graham

Thanks for everyone for their suggestions. Instead of using metaprogramming, which in this case is apparently quite problematic, I'll go with the easiest solution of doing an "unless" check in certain key methods. Obviously that won't prevent a future developer from adding singleton methods to the object(s) or messing with them in some other way, but that will clearly break the contract. The docs will say "don't do this" and leave it at that.

Thanks again.

cr

···

On Oct 20, 2010, at 1:29 PM, John W Higgins wrote:

Morning Chuck.

On Wed, Oct 20, 2010 at 8:31 AM, Chuck Remes <cremes.devlist@mac.com> wrote:

I have a class that I would like to "freeze" when it reaches a particular
state so that it becomes read-only. I do *not* want to use the existing
#freeze method because it raises an exception when there is an attempted
mutation on a frozen object and I want the attempt to be silent. It appears
that the best way to enforce this (state machine pattern) is by redefining
the accessors and other methods that cause mutations to be no ops.

What is the preferred practice for doing that?

You could in the alternative have the ops that mutate do an "unless" at the
top of the method to bake the freeze into the method as opposed to
redefining

But why do this if you can simply use #freeze and #frozen? as they are
defined? The advantage of the regular #freeze method is that you will
immediately notice if you try to modify a frozen instance. All
approaches present here will silently eat the method call and do
nothing thus making the caller believe the method has worked regularly
while it hasn't. I think this approach to freezing is error prone and
likely to create hard to detect bugs.

Kind regards

robert

···

On Wed, Oct 20, 2010 at 8:29 PM, John W Higgins <wishdev@gmail.com> wrote:

Morning Chuck.

On Wed, Oct 20, 2010 at 8:31 AM, Chuck Remes <cremes.devlist@mac.com> wrote:

I have a class that I would like to "freeze" when it reaches a particular
state so that it becomes read-only. I do *not* want to use the existing
#freeze method because it raises an exception when there is an attempted
mutation on a frozen object and I want the attempt to be silent. It appears
that the best way to enforce this (state machine pattern) is by redefining
the accessors and other methods that cause mutations to be no ops.

What is the preferred practice for doing that?

You could in the alternative have the ops that mutate do an "unless" at the
top of the method to bake the freeze into the method as opposed to
redefining

Using your example

class Foo
attr_accessor :frozen
attr_reader :bar, :baz, :quxxo

def bar= new_bar
unless @frozen @bar = new_bar
end

def baz= new_baz
unless @frozen @baz = new_baz
end

def quzzo= new_quzzo
unless @frozen @quzzo = new_quzzo
end
end

It's a little more work and a slight amount of overhead for the unless
check. But it does eliminate the need to redefine your methods. It also very
clearly spells out what you are freezing.

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/