KVO in Ruby

  • Thread starter Joshua Ballanco
  • Start date
J

Joshua Ballanco

Hey all!

I'm still relatively new to Ruby, so forgive me if this seems like a
naive (or even stupid) question, but...

Is there a good method for implementing Key-Value Observing in Ruby?

Basically, what I'd like to be able to do is this:

class Actor
attr_accessor :var_to_change

def change_var
@var_to_change = 1
100.times {@var_to_change += 1}
end
end

class Observer
def initialize(act)
@actor = act
act.add_observer(self, var_to_change)
end

def observeValue(var, sender)
if sender == @actor
puts "The value is now #{var}"
end
end
end

act = Actor.new
obs = Observer.new(act)
thr = Thread.new {act.change_var}
thr.join

The resulting output would be:
The value is now 1
The value is now 2
...and so on.

I realize that there are all sorts of ways that similar functionality
could be achieved, but something about KVO is really attractive
(especially since Ruby is already really good at KVC). It seems to me
that this would be a fairly easy to implement as a module, but I don't
want to have to do the work if someone else already has. Does anyone
know how RubyCocoa handles KVO? or if it does?

Thanks for any help/comments/scornful rebukes. ;-)

-Josh
 
J

Joshua Ballanco

Zachary said:

Hmm...how did I miss that? This should do very well for the project I'm
currently working on.

However, looking through the Observable module, I see one major
downside: it's not automatic. That is, the Observable module should work
well enough for custom classes, but what about Built-in and Standard
Library classes? If, say, you wanted to be notified when the length of a
string was changed, you would have to extend the String class, but then
also implement a method to constantly observe the length...or am I
missing something here? The way I understand how this works in Cocoa is
that isa-swizzling is used to replace the class of the property being
observed with a thin pseudo-class that notifies any registered observers
when a change is made, then passes the change to the original class.

Is there a good reason not to do things this way? to do things this way?
The only KVO implementation that I've ever used is Cocoa's, so I'm just
curious about how/why it's done in Ruby. If I'm making a big fuss over
nothing please feel free to say so.
 
J

Joel VanderWerf

Joshua said:
Hmm...how did I miss that? This should do very well for the project I'm
currently working on.

However, looking through the Observable module, I see one major
downside: it's not automatic. That is, the Observable module should work
well enough for custom classes, but what about Built-in and Standard
Library classes? If, say, you wanted to be notified when the length of a
string was changed, you would have to extend the String class, but then
also implement a method to constantly observe the length...or am I
missing something here? The way I understand how this works in Cocoa is
that isa-swizzling is used to replace the class of the property being
observed with a thin pseudo-class that notifies any registered observers
when a change is made, then passes the change to the original class.

Is there a good reason not to do things this way? to do things this way?
The only KVO implementation that I've ever used is Cocoa's, so I'm just
curious about how/why it's done in Ruby. If I'm making a big fuss over
nothing please feel free to say so.

It will be difficult to get automatic notification of a change in the
length of a string, since there are so many ways that can happen, and it
happens in core code. (For example, the C function rb_str_append() gets
called from other C functions, so there is no way to hook in a pure ruby
observer.)

However, there are some cases where a library class has an attr_writer
(or other method) and all you want is notification of changes that go
through that method. And, of course if you are developing the library
yourself you can force all changes to go through a method.

In these cases, you might want to take a look at my observable library
(really, it should be called observable-attr or something):

http://raa.ruby-lang.org/project/observable/
http://redshift.sourceforge.net/observable/

Unlike the standard Observer library, which operates at the level of
entire objects, this library operates on individual methods. Also,
notification is automatic in the sense that the observed object doesn't
have to call notify_observers. Further, each observer can register more
than one "when" clause that is called just when the new value matches
(in the sense of #===) some pattern, class, or other matching object.

I've found this useful primarily in fxruby (the foxtails library uses it
to make data targets more friendly and responsive).

Here's an example:

require 'observable'

# A prexisting class with a method that doesn't expect to be observed.
class Base
attr_writer :foo
end

class Speaker < Base
extend Observable

# make the inherited method :foo be observable and define an
# additional observable attribute, :bar.
observable :foo, :bar

def run
self.foo = "1"
self.bar = [4,5,6]
self.foo = "2"
self.bar << 7 # Caution: no notification here!
self.bar += [8] # notification
self.foo = "3"
@foo = "4" # No notification here, since writer wasn't called
end
end

class Listener
def initialize(speaker)
speaker.when_foo /2/ do |v|
puts "#{self} saw foo change to have a 2 in #{v.inspect}"
end

speaker.when_foo /\d/ do |v|
puts "#{self} saw foo change to have a digit in #{v.inspect}"
end

# This would override the first when_foo clause
#speaker.when_foo /2/ do |v|
# puts "#{self} saw foo change to have a 2 in #{v.inspect}
[overridden]"
#end

# listen for _any_ changes (note that #=== is used to match value,
# so Object matches everything, including the initial nil)
speaker.when_bar Object do |v, old_v|
puts "#{self} saw bar change from #{old_v.inspect} to #{v.inspect}"
end
end
end

sp = Speaker.new
Listener.new(sp)
sp.run

__END__

Output:

#<Listener:0xb7a56628> saw bar change from nil to nil
#<Listener:0xb7a56628> saw foo change to have a digit in "1"
#<Listener:0xb7a56628> saw bar change from nil to [4, 5, 6]
#<Listener:0xb7a56628> saw foo change to have a 2 in "2"
#<Listener:0xb7a56628> saw foo change to have a digit in "2"
#<Listener:0xb7a56628> saw bar change from [4, 5, 6, 7] to [4, 5, 6, 7, 8]
#<Listener:0xb7a56628> saw foo change to have a digit in "3"
 
Z

Zachary Holt

Hmm...how did I miss that? This should do very well for the project
I'm
currently working on.

However, looking through the Observable module, I see one major
downside: it's not automatic. That is, the Observable module should
work
well enough for custom classes, but what about Built-in and Standard
Library classes? If, say, you wanted to be notified when the length
of a
string was changed, you would have to extend the String class, but
then
also implement a method to constantly observe the length...or am I
missing something here?
I suppose you wouldn't have to modify the String class, if you just
want to observe one string.

require 'observer'

s = "Something fishy"
class << s
include( Observable )
#etc..
end
The way I understand how this works in Cocoa is
that isa-swizzling is used to replace the class of the property being
observed with a thin pseudo-class that notifies any registered
observers
when a change is made, then passes the change to the original class.
Could be. It's been a while since I played with NSAnything.

Is there a good reason not to do things this way? to do things this
way?
The only KVO implementation that I've ever used is Cocoa's, so I'm
just
curious about how/why it's done in Ruby. If I'm making a big fuss over
nothing please feel free to say so.
You could do something like this. I'm not sure whether masking the
class like this is kosher, however. It would take some tinkering to
make it work with more complex classes, but you get the idea.

require 'observer'

class Observee
include Observable
def initialize( thing )
@thing = thing
end
def method_missing( meth, *args )
changed true
notify_observers( meth, @thing.dup, @thing.send( meth, *args ) )
end
def class ; @thing.class ; end
end

class Observer
def update( meth, ol, ne )
puts "#{meth} #{
ol == ne ? 'did not change' : 'changed'
} the #{ol.class.name}"
end
end

s = Observee.new( "Something fishy" )
puts s.class.name
so = Observer.new
s.add_observer( so )
s.gsub!( /([aeiouy])/, 'Y' )
s << ' Mmm. Tasty'
s.gsub!( /([aeiouy])/, '' )

h = Observee.new( :a => 'b' )
puts h.class.name
ho = Observer.new
h.add_observer( ho )
h[:c] = 'd'
h.delete( :a )
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top