Detecting when an instance variable is created/set

Imagine we have a class like …

class Fred
def initialize(a, b)
@a = a
@b = b
end
end

Can I write a method (of class Object or Kernel, perhaps) that will be called
when the “@a = a” occurs? What I’m looking for would be something like …

def instance_var_created(c, name, value)
$stderr.puts "@#{name} of class #{c.class} set to #{value}"
end

Is this possible?

Imagine we have a class like …

class Fred
def initialize(a, b)
@a = a
@b = b
end
end

Can I write a method (of class Object or Kernel, perhaps) that will be called
when the “@a = a” occurs? What I’m looking for would be something like …

def instance_var_created(c, name, value)
$stderr.puts "@#{name} of class #{c.class} set to #{value}"
end

Is this possible?

this has been touched on in some of the other threads. i brought it up
myself in relation to “automagical” gui bindings. anyway, the short
answer appears to be NO.

since = is not a method you can’t redefine it in any way, nor callback
on it. i don’t know what your attempting to do, but if you create
accessor methods:

def a=(x)
@a = x
end

then of course you can get at those. but that may not be what you want.

at this point i wish to SPEAK UP and say once again, i would prefer that
EVERYTHING were OO. i choose ruby over python for this very reason!
(although i like ruby over python despite that now anyway). alas not
even ruby is pure.

~transami

···

On Sun, 2002-08-04 at 06:03, Harry Ohlsen wrote:


~transami

at this point i wish to SPEAK UP and say once again, i would prefer that
EVERYTHING were OO. i choose ruby over python for this very reason!
(although i like ruby over python despite that now anyway).

Yes.

alas not even ruby is pure.

Don’t you just feel duped? :slight_smile:

Regards,
Doug

···

On Sun, Aug 04, 2002 at 09:49:32PM +0900, Tom Sawyer wrote:

Can I write a method (of class Object or Kernel, perhaps) that will be
called when the “@a = a” occurs? What I’m looking for would be something
like …

def instance_var_created(c, name, value)
$stderr.puts "@#{name} of class #{c.class} set to #{value}"
end

Is this possible?

this has been touched on in some of the other threads. i brought it up
myself in relation to “automagical” gui bindings. anyway, the short
answer appears to be NO.

I guess what I’m trying to do is similar. Just as you’re trying to catch
updates to fields in order to automagically update GUI elements, I’m
(effectively) trying to catch instance variable creation, so that I can
coerce the value being stored so that it’s an object of a different type,
that makes a reference to the original.

What I’m trying to create is a simple database for applications that don’t
require a full-blown one. The most important thing is that it be completely
transparent to the user.

since = is not a method you can’t redefine it in any way, nor callback
on it.

True, but just as Module has method_added(), which is called whenever a new
method is added to the module, I don’t see why there could not be an
instance_added() for Class or Object or Kernel that was called whenever an
instance was added to any class.

i don’t know what your attempting to do, but if you create
accessor methods:

def a=(x)
@a = x
end

then of course you can get at those. but that may not be what you want.

Yep. The issue is that in order to get the level of transparency I’d like to
achieve, I don’t want the user to have to write any code.

I thought I might be able to maybe write my own versions of attr_writer and
attr_accessor that created a special version of field=() that included the
processing I want, but the following example shows that won’t work.

class Fred
   def a=(a)
       $stderr.puts "Setting @a to #{a}"
       @a = a
   end

   def initialize(a)
       @a = a
   end

end

f = Fred.new(123)

f.a = 456

When you run this, it prints “Setting @a to 456”. The initial setting of @a
in initialize doesn’t get caught. Ie, a=() doesn’t seem to come into effect
until AFTER the instance variable has been created.

at this point i wish to SPEAK UP and say once again, i would prefer that
EVERYTHING were OO. i choose ruby over python for this very reason!
(although i like ruby over python despite that now anyway). alas not
even ruby is pure.

My sentiments, exactly. Of course, I realise there are limits to what can be
achieved … but I wish the limits were just past everything I’d like to do,
rather than just this side :-).

To give you a feel for what I want to do …

In the constructor of, say, Album, I’d like something like this, without
forcing the user to write the “Wrapper.new()”

class Album
def initialize(title, songs)
@title = Wrapper.new(title)
@songs = songs.map { |s| Wrapper.new(s) }
end
end

Ie, what I want the user to be able to write is

class Album
def initialize(title, songs)
@title = title
@songs = songs
end
end

Now, if I could catch the “@x = …” lines I could automate this.

Actually, in my real system, I would only modify what gets stored in @songs,
not what gets stored in @title, but I have a way to determine whether I need
to make the conversion.

Tom Sawyer wrote:

at this point i wish to SPEAK UP and say once again, i would prefer that
EVERYTHING were OO. i choose ruby over python for this very reason!
(although i like ruby over python despite that now anyway). alas not
even ruby is pure.

How would you have assignment be a method? Should everyone redefine and
add to the complexity of one method for their own neferious purposes?
Or should variables be objects, instances of various subclasses of type
Variable? Such as InstanceVariable, so that you could do:
class InstanceVariable
def =(argument)
# funky stuff here
end
end

…which would drag us back towards a kind of typed variables, but not
the statically typed language kind. Hey, this might even be feasible.
Already part of this typing is done with symbols such as @ for instance,
@@ for class and, $ for global variable. But I can’t see much use for
it, unless you’d start making instances of your own subclass of
InstanceVariable, with overriden assignment. (Since there would be only
one InstanceVariable class) But this bites you in the tail and requires
you to explicitly state somewhere in your using code what class your
variables is, and that bit of code that you thusly need in your
initialize or whatever, could just as well have been the explicit
assigment you wanted to begin with.

Ouch, now my head hurts. I am reminded of the saying
"Everything should be as simple as possible, but no simpler."
gsub “simple”, “object-oriented” #:stuck_out_tongue_winking_eye:

···


([ Kent Dahl ]/)_ ~ [ http://www.stud.ntnu.no/~kentda/ ]/~
))_student
/(( _d L b_/ NTNU - graduate engineering - 4. year )
( __õ|õ// ) )Industrial economics and technological management(
_
/ö____/ (_engineering.discipline=Computer::Technology)

Sun, 4 Aug 2002 22:32:44 +0900, Harry Ohlsen harryo@zip.com.au pisze:

When you run this, it prints “Setting @a to 456”. The initial setting of @a
in initialize doesn’t get caught. Ie, a=() doesn’t seem to come into effect
until AFTER the instance variable has been created.

It doesn’t matter if it existed. @a=… just doesn’t call a=().
Only obj.a=… does.

Otherwise the body of a=() which contains @a=… would loop forever.

···


__("< Marcin Kowalczyk
__/ qrczak@knm.org.pl
^^ http://qrnik.knm.org.pl/~qrczak/

Tom Sawyer wrote:

at this point i wish to SPEAK UP and say once again, i would prefer that
EVERYTHING were OO. i choose ruby over python for this very reason!
(although i like ruby over python despite that now anyway). alas not
even ruby is pure.

It is pure by my standards: everything you can talk about in the ruby
language is an object. To “talk about” a thing means: to pass it to a
function, whether as an argument or as the receiver of a method, or to
assign it to a variable. If you want to be able to pass variables
themselves to a method (i.e., call by reference, as opposed to passing
their values), or assign variables themselves to other variables (as
opposed to assigning their values), then you are headed back to p**l and
its references…

Whatever “pure” means, ruby is better off without those backslash nasties.

tru’nuff. that won’t do it.

just a wild guess, but my problem was fianlly solved with this (thank
rich kilmer for this particular variation)

class Object

utility method for dynamically overriding a method

to addtionally call a proc/block --Rich Kilmer

def bind(meth, proc=nil, &block)
proc = block unless proc
meth = meth.to_s
alias_meth = meth[-1]==61 ? “#{meth[0…-2]}=” : "#{meth}"
self.instance_eval {
@procs = {} unless @procs
@procs[meth]=proc
}
return if self.respond_to? alias_meth
self.instance_eval <<-"EOS"
class << self
alias_method :#{alias_meth}, :#{meth}
def #{meth}(*args, &block)
return if $trigger == self
$trigger = self if $trigger == nil
send(:#{alias_meth}, *args, &block)
x = @procs[’#{meth}’].call(*args)
$trigger = nil
return x
end
end
EOS
end
end

it may tak a bit to “decode” this, but perhaps a reworking of or
something like this might help?

···

On Sun, 2002-08-04 at 07:32, Harry Ohlsen wrote:

Yep. The issue is that in order to get the level of transparency I’d like to
achieve, I don’t want the user to have to write any code.

I thought I might be able to maybe write my own versions of attr_writer and
attr_accessor that created a special version of field=() that included the
processing I want, but the following example shows that won’t work.

class Fred
   def a=(a)
       $stderr.puts "Setting @a to #{a}"
       @a = a
   end

   def initialize(a)
       @a = a
   end

end

f = Fred.new(123)

f.a = 456

When you run this, it prints “Setting @a to 456”. The initial setting of @a
in initialize doesn’t get caught. Ie, a=() doesn’t seem to come into effect
until AFTER the instance variable has been created.


~transami

Hi Harry,

if you cange the initialize method to

def initialize(title, songs)
self.title = title
self.songs = songs
end

the accessor methods are called.

Regards,
Pit

···

On 4 Aug 2002, at 22:42, Harry Ohlsen wrote:

To give you a feel for what I want to do …

In the constructor of, say, Album, I’d like something like this,
without forcing the user to write the “Wrapper.new()”

class Album
def initialize(title, songs)
@title = Wrapper.new(title)
@songs = songs.map { |s| Wrapper.new(s) }
end
end

Ie, what I want the user to be able to write is

class Album
def initialize(title, songs)
@title = title
@songs = songs
end
end

Now, if I could catch the “@x = …” lines I could automate this.

Actually, in my real system, I would only modify what gets stored in
@songs, not what gets stored in @title, but I have a way to determine
whether I need to make the conversion.

Would it be enough for you to catch creation of instance variables in
each method before it returns, to do what you want with them
(e.g. swapping their value with a wrapper of the value)?

If so, try the following. Beware, hacking into that class_eval is
quoting hell, as there are two levels of weak quotes.

module MonitorVariables
def self.append_features(klass)
klass.instance_methods.each do |method|

  klass.class_eval <<-EOF
  alias ___#{method} #{method}
  def #{method}(*params,&block)
    old_instance_variables = instance_variables

    ___#{method}(*params,&block)

    diff = instance_variables - old_instance_variables
    diff.empty? or puts "variable(s) " + diff.inspect + " added in method #{method}"
  end
  EOF

end

end
end

class Test
def foo
@hello = 1
end

def bar
# no var added
end

include MonitorVariables
end

t = Test.new

t.foo
t.bar

I guess the thing could be refactored into append_code_to_method() and
on_variable_added_event()…

Massimiliano

···

On Sun, Aug 04, 2002 at 10:32:44PM +0900, Harry Ohlsen wrote:

def instance_var_created(c, name, value)
$stderr.puts "@#{name} of class #{c.class} set to #{value}"
end

Is this possible?

Hi –

···

On Sun, 4 Aug 2002, Harry Ohlsen wrote:

I thought I might be able to maybe write my own versions of attr_writer and
attr_accessor that created a special version of field=() that included the
processing I want, but the following example shows that won’t work.

class Fred
   def a=(a)
       $stderr.puts "Setting @a to #{a}"
       @a = a
   end

   def initialize(a)
       @a = a
   end

end

f = Fred.new(123)

f.a = 456

When you run this, it prints “Setting @a to 456”. The initial setting of @a
in initialize doesn’t get caught. Ie, a=() doesn’t seem to come into effect
until AFTER the instance variable has been created.

It’s not that – it’s that there’s no call to #a= in your code. @a=a
is just an assignment to a variable, and a=(a) (in the typical
attr_writer case) is a wrapper for @a=a, not the other way around.

(See Pit’s suggestion about self.a = a, which calls the method a=().)

David


David Alan Black
home: dblack@candle.superlink.net
work: blackdav@shu.edu
Web: http://pirate.shu.edu/~blackdav

Good point! I figured there was some good reason that a=() wasn’t being
called, but that hadn’t dawned on me.

In theory, though, having a method that’s called when an instance variable is
created, as opposed to every time it’s assigned, would work, since the “@a
= something” inside that method would not be another instance variable
creation (because, at that point the variable exists), just an assignment to
one.

Obviously, the interpreter knows when an instance variable is created. It has
to, in order to add it to the class’s list of variables.

I guess what I’m looking for is effectively something like trace_var(), but
that works for more than just globals. Maybe trace_instance_var() ?

I realise, though, that there are probably a million places one could have
hooks like this and we don’t necessarily want all of them. I do think this
one is potentially useful … well, actually useful, since I have a use for
it right now :-).

···

On Mon, 5 Aug 2002 03:41, you wrote:

Sun, 4 Aug 2002 22:32:44 +0900, Harry Ohlsen harryo@zip.com.au pisze:

When you run this, it prints “Setting @a to 456”. The initial setting of
@a in initialize doesn’t get caught. Ie, a=() doesn’t seem to come into
effect until AFTER the instance variable has been created.

It doesn’t matter if it existed. @a=… just doesn’t call a=().
Only obj.a=… does.

Otherwise the body of a=() which contains @a=… would loop forever.

Would it be enough for you to catch creation of instance variables in
each method before it returns, to do what you want with them
(e.g. swapping their value with a wrapper of the value)?

Unfortunately, for it to be useful to me, I’d specifically need it to work in
initialize() as well.

If so, try the following. Beware, hacking into that class_eval is
quoting hell, as there are two levels of weak quotes.

Wow! By the time I feel comfortable that I completely understand that block
of code, I’ll be able to put “Ruby guru” on my resume! You obviously already
have it on yours :-).

Since it doesn’t catch initialize(), it won’t solve my problem, but it’s
certainly given me something to think about !!

Harry O.

Interesting! I tried another approach, a generic wrapping procedure.

Script below reported:
#<A:0x810bc78>: initialize: @init = “foo” (created)
#<A:0x810bc78>: a=: @a = 1 (created)
#<A:0x810bc78>: set_to_x: @a = “x” (was 1)

IVTRACED_REVISION =
’$kNotwork: ivtraced.rb,v 1.6 2002/08/05 00:10:10 gotoken Exp $’

module MethodWrapped
GLOBAL_LOCK = []
WRAPPED_METHODS = {}

def MethodWrapped.wrap(mod, id)
mod.module_eval do
arity = instance_method(id).arity
# to preserve arity…
if arity > 0
dummy_argument = (1…arity).map{|i| “arg#{i}”}.join(",")+","
elsif arity == 0
dummy_argument = "“
elsif arity == -1
dummy_argument = “*opt, “
else
dummy_argument = (1…-arity-1).map{|i| “arg#{i}”}.join(”,”)+”,*opt,"
end
da_bare= “#{dummy_argument}&block” # for ruby-mode.el
da = “(#{da_bare})” # for ruby-mode.el

  module_eval src = (<<-EOS)
  oldID = :#{id.to_s}
  newID = :__method_wrapper_#{id.to_i}__
  alias_method newID, oldID
    
  def #{id.to_s}#{da}
    cookie = [Time.now, rand]
    type.method_started cookie,__id__,:#{id.to_s},#{da_bare}
    begin
      val = __method_wrapper_#{id.to_i}__#{da}
    rescue Exception
      bt = $!.backtrace
      bt.delete_at(1)
      $!.set_backtrace(bt)
      raise
    end
    type.method_returned cookie,__id__,:#{id.to_s},val,#{da_bare}
    val
  end
  MethodWrapped::WRAPPED_METHODS[mod] ||= {}
  MethodWrapped::WRAPPED_METHODS[mod][oldID] = newID
  EOS
end

end

def self.append_features(mod)
super
unless mod.public_methods.include?(“method_started”)
def mod.method_started(cookie, oid, mid, *args, &block)
# cookie: [time, float] formed unique array
# oid: object id (Fixnum)
# mid: method id (Symbol)
# args: array which was passed to mid
# block: proc which was passed to mid
end
end

unless mod.public_methods.include?("method_returned")
  def mod.method_returned(cookie, oid, mid, val, *args, &block)
    # cookie: identical to corresponding method_started
    # oid: ditto
    # mid: ditto
    # args: ditto
    # block: ditto
    # val: retured value
  end
end

if public_methods.include?("method_added")
  mod.module_eval do
    alias_method(:__original_method_added__, :method_added)
  end
else
  def mod.__original_method_added__(id) end
end

def mod.method_added(id)
  val = __original_method_added__(id)
  return val if MethodWrapped::GLOBAL_LOCK.first
  MethodWrapped::GLOBAL_LOCK.push true
  _old_critical = Thread.critical
  begin
    Thread.critical = true
    MethodWrapped::wrap(self, id)
  ensure
    Thread.critical = _old_critical
    MethodWrapped::GLOBAL_LOCK.pop
  end
end

def mod.wrapped_methods
  MethodWrapped::WRAPPED_METHODS[self].dup
end

end
end

module InstanceVariableTraced
private

def __ivtab
val = {}
instance_variables.each{|i| val[i] = instance_eval(i)}
val
end

IVT = InstanceVariableTraced
IVTRACE__ = {}

def self.append_features(mod)
super
mod.module_eval{include MethodWrapped}

unless mod.public_methods.include?("instance_variable_created")
  def mod.instance_variable_created(obj, iv, mid, val)
    # obj: receiver
    # iv: instance variable name (String)
    # mid: method id (Symbol)
    # val: new value
    $stderr.printf("#<%s:0x%x>: %s: %s = %s (created)\n",
                   self, obj.id<<1, mid, iv, val.inspect)
  end
end

unless mod.public_methods.include?("instance_variable_set")
  def mod.instance_variable_set(obj, iv, mid, old_val, val)
    # obj: receiver
    # iv: instance variable name (String)
    # mid: method id (Symbol)
    # old_val: old value
    # val: new value
    $stderr.printf("#<%s:0x%x>: %s: %s = %s (was %s)\n", 
                   self, obj.id<<1, mid, iv, val.inspect, old_val.inspect)
  end
end

def mod.method_started(cookie, oid, mid, *args)
  return if mid == :__ivtab
  IVT::IVTRACE__[cookie] = ObjectSpace._id2ref(oid).__send__(:__ivtab)
end

def mod.method_returned(cookie, oid, mid, val, *args)
  return if mid == :__ivtab
  begin
    obj = ObjectSpace._id2ref(oid)
    ivtrace_now = obj.__send__(:__ivtab)
    ivtrace_old = IVT::IVTRACE__[cookie]
    ivtrace_old.each{|iv,val_old|
      val = ivtrace_now[iv]
      if val_old != val
        instance_variable_set(obj, iv, mid, val_old, val)
      end
      ivtrace_now.delete(iv)
    }
    ivtrace_now.each{|iv,val|
      instance_variable_created(obj, iv, mid, val)
    }
  ensure
    IVT::IVTRACE__.delete(cookie)
  end
end

end
end

if FILE == $0
class A
include InstanceVariableTraced
attr_writer :a

def initialize
  @init = "foo"
end

def set_to_x
  @a = "x"
end

end

a = A.new
a.a = 1
a.set_to_x
end

···

At Mon, 5 Aug 2002 01:36:15 +0900, Massimiliano Mirra wrote:

module MonitorVariables
def self.append_features(klass)
klass.instance_methods.each do |method|

  klass.class_eval <<-EOF
  alias ___#{method} #{method}
  def #{method}(*params,&block)
    old_instance_variables = instance_variables

    ___#{method}(*params,&block)

    diff = instance_variables - old_instance_variables
    diff.empty? or puts "variable(s) " + diff.inspect + " added in method #{method}"
  end
  EOF

end

end
end

Would it be enough for you to catch creation of instance variables in
each method before it returns, to do what you want with them
(e.g. swapping their value with a wrapper of the value)?

Unfortunately, for it to be useful to me, I’d specifically need it to work in
initialize() as well.

I don’t know why it does not work in initialize (I assumed it would).
A dirty fix:

module Initializer
def initialize(*args)
puts "Here I’ll handle these variables: #{instance_variables.inspect}"
end
end

module MonitorVariables

end

class Test
def initialize(*args)
@a = 1
@b = 'world’
super(*args)
end

include MonitorVariables, Initializer
end

t = Test.new

-> Here I’ll handle these variables: ["@a", “@b”]

I also don’t know why, if put in MonitorVariables, initialize won’t be
called, and needs to be put in a different module.

If so, try the following. Beware, hacking into that class_eval is
quoting hell, as there are two levels of weak quotes.

Wow! By the time I feel comfortable that I completely understand that block
of code, I’ll be able to put “Ruby guru” on my resume! You obviously already
have it on yours :-).

Nah, just a tad more than a rookie, really.

Massimiliano

···

On Mon, Aug 05, 2002 at 04:59:40AM +0900, Harry Ohlsen wrote:

Now Harry, that is what a guru looks like. :slight_smile:

Massimiliano

···

On Mon, Aug 05, 2002 at 09:12:12AM +0900, GOTO Kentaro wrote:

Interesting! I tried another approach, a generic wrapping procedure.

Script below reported:

Unfortunately, for it to be useful to me, I’d specifically need it to
work in initialize() as well.

I don’t know why it does not work in initialize (I assumed it would).
A dirty fix:

module Initializer
def initialize(*args)
puts "Here I’ll handle these variables: #{instance_variables.inspect}"
end
end

module MonitorVariables

end

class Test
def initialize(*args)
@a = 1
@b = 'world’
super(*args)
end

What is that super() calling? It seems to be the initialize() in Initializer,
but I don’t understand why. I would have thought that it would be calling
the one from perhaps Object (since Test doesn’t specifically subclass
anything else).

I also don’t know why, if put in MonitorVariables, initialize won’t be
called, and needs to be put in a different module.

I added the code in your Initializer module into MonitorVariables and it
worked OK, so while it seems initialize() seems to need to be handled
separately, it doesn’t appear to need to be in a different module.

I still have the issue, though, that with this module included, all of the
instance variables end up being set to “true”, rather than to the values they
were actually assigned.

Any idea why that is and how to fix it?

Nah, just a tad more than a rookie, really.

In that case, I look forward to becoming a rookie !!

Cheers,

Harry O.

Hi –

···

On Mon, 5 Aug 2002, Massimiliano Mirra wrote:

On Mon, Aug 05, 2002 at 04:59:40AM +0900, Harry Ohlsen wrote:

Would it be enough for you to catch creation of instance variables in
each method before it returns, to do what you want with them
(e.g. swapping their value with a wrapper of the value)?

Unfortunately, for it to be useful to me, I’d specifically need it to work in
initialize() as well.

I don’t know why it does not work in initialize (I assumed it would).

It’s because initialize is a private method, so it doesn’t appear in
your klass.instance_methods call.

David


David Alan Black
home: dblack@candle.superlink.net
work: blackdav@shu.edu
Web: http://pirate.shu.edu/~blackdav

Hi –

···

On Mon, 5 Aug 2002, Harry Ohlsen wrote:

Unfortunately, for it to be useful to me, I’d specifically need it to
work in initialize() as well.

I don’t know why it does not work in initialize (I assumed it would).
A dirty fix:

module Initializer
def initialize(*args)
puts "Here I’ll handle these variables: #{instance_variables.inspect}"
end
end

module MonitorVariables

end

class Test
def initialize(*args)
@a = 1
@b = 'world’
super(*args)
end

What is that super() calling? It seems to be the initialize() in Initializer,
but I don’t understand why. I would have thought that it would be calling
the one from perhaps Object (since Test doesn’t specifically subclass
anything else).

It is indeed the initialize in Initializer. super basically looks for
a method of the same name as the one being executed, but starting the
search one link up in the method search chain. Including a module (as
well as subclassing a class) adds to that chain.

David


David Alan Black
home: dblack@candle.superlink.net
work: blackdav@shu.edu
Web: http://pirate.shu.edu/~blackdav