Declarative relations between object attributes

Some time ago I stumbled over Cells[1], a Common Lisp extension allowing
one
to declare relations between instance variables (called slots in CL);
i.e. a
change to one variable will automatically recompute other variables
depending
on it. I think it’s pretty neat, but using it in any interesting context
is
somewhat hampered by the fact that few people go through the
considerable
trouble involved in learning Common Lisp.

Just today it occurred to me that it should be pretty easy to do
something
similar in Ruby. Indeed, a couple of codelines later, I had a Ruby
version of
the standard Cells example (well, a simplified version) running:

class Motor
cell :temperature, :status
def initialize
self.temperature = 0
calculate :status do
if self.temperature < 100
:on
else
:off
end
end
end
end
m = Motor.new
m.observe(:temperature) { |old, new| puts “temperature: #{old} ->
#{new}” }
m.observe(:status) { |old, new| puts “status: #{old} -> #{new}” }
m.temperature = 80
m.temperature = 110

=>

temperature: 0 -> 80
temperature: 80 -> 110
status: on -> off

That’s certainly not dramatically new; you can do similar things with
the
observer pattern or Qt signals, for example. However, I like the idea of
just
declaring that one variable (status) is a certain function of one or
more
other variables (temperature) and have the library take care of all the
rest.
Makes for cleaner code, particularly if you do model/view programming
(see
model-view.rb in [2]).

I’m not yet sure what to do with this. Possibly similar/better solutions
already exist. Certainly the code needs some work. So I figured that
before
going further I’d try to get some feedback; particularly

  • Do you think this is useful?
  • Do you know related projects?
  • What could be improved?

I’ve put the code on github[1]. Any input appreciated. Thanks.

Knut

[1] http://common-lisp.net/project/cells/
[2] http://github.com/nome/ruby-cells

This is a neat bit of code, but is there any chance you could
relicense it under something more permissive, such as MIT?

On 09/12/2010 02:10 PM, Knut Franke wrote:

trouble involved in learning Common Lisp.
self.temperature = 0
m.observe(:temperature) {|old, new| puts “temperature: #{old} ->
#{new}” }
m.observe(:status) {|old, new| puts “status: #{old} -> #{new}” }
m.temperature = 80
m.temperature = 110

I wrote a library called observable for use in GUI MVC programming (with
FXRuby), something along those lines. One nice feature was pattern
matching on the changed value, so you could have separate handlers for
different ranges etc. Ruby’s #=== methods are very cool and somewhat
underused. That might be a nice feature to add to your #observe method.

Here’s the code:

http://redshift.sourceforge.net/observable/

Here’s my version of your example:

require ‘observable’

Let’s just include Observable globally. Conservatively, one would do:

class C

extend Observable

include Observable::Match # if desired

end

include Observable
include Observable::Match

class Motor
observable :temperature, :status

def initialize
self.temperature = 0
self.status = :on

 when_temperature 0..100 do
   self.status = :on
 end

 when_temperature 100..100000 do
   self.status = :off
 end

end
end

m = Motor.new

m.when_temperature CHANGES do |new, old|
puts “temperature: #{old} -> #{new}”
end

m.when_status CHANGES do |new, old|
puts “status: #{old} -> #{new}”
end

m.temperature = 80
m.temperature = 110

END

temperature: -> 0
status: -> on
temperature: 0 -> 80
temperature: 80 -> 110
status: on -> off

Joel VanderWerf wrote:

I wrote a library called observable for use in GUI MVC programming (with
FXRuby), something along those lines. One nice feature was pattern
matching on the changed value, so you could have separate handlers for
different ranges etc. Ruby’s #=== methods are very cool and somewhat
underused. That might be a nice feature to add to your #observe method.

Here’s the code:

http://redshift.sourceforge.net/observable/

Thanks for the link; I managed to miss that one when trying to google
existing solutions. :slight_smile: I’ve taken the liberty to include some of the
ideas in my code.

 when_temperature 0..100 do
   self.status = :on
 end

 when_temperature 100..100000 do
   self.status = :off
 end

It appears that for this particular example, your API is nicer than
mine. :wink: Here’s another one (distilled from model-view.rb) which
probably better illustrates the idea behind the cells approach:

class Model
cell :name, :email, :role, :caption
def initialize
self.name = “Your N.”
self.email = “[email protected]
self.role = “To”
calculate(:caption) { “#{self.role}: #{self.name} <#{self.email}>”
}
end
end

Indeed, being able to mix it in with other code is good!

I noticed you removed your license file from the github repository.
I’m not sure how this works, exactly, but I don’t think that undoes
the GPL licensing. Maybe make it explicit with a license notice?

Nice model-view.rb distilled example.

Adam P. wrote:

I noticed you removed your license file from the github repository.
I’m not sure how this works, exactly, but I don’t think that undoes
the GPL licensing. Maybe make it explicit with a license notice?

I’m not sure I’m following you there. Make what exactly explicit?
Prohibit people from using the code under GPL? Why would I want to do
that?

Nice model-view.rb distilled example.

Thanks. :slight_smile:

Adam P. wrote:

This is a neat bit of code, but is there any chance you could
relicense it under something more permissive, such as MIT?

Yeah, I guess it’s a bit pointless to publish this one when you can’t
combine it with lots of other code around. I read the MIT license to be
compatible with most if not all free software licenses.

Sunday, September 12, 2010, 6:19:53 PM, you wrote:

JV> # Let’s just include Observable globally. Conservatively, one would
do:
JV> #
JV> # class C
JV> # extend Observable
JV> # include Observable::Match # if desired
JV> # …
JV> # end

I give. What does
extend Observable
do?

I see in Dave T.’ Programming Ruby 1.9 that extend “Adds to obj
the instance methods from each module given as a parameter”.

So … what object is “extend Observable” referring to? The class C
object? (That is, the class that is C rather than the instance that is
C.) Is this related to some singleton stuff?

And how is the extend different than the include?

Ralph

I mean make the licensing specific. Since there’s no longer a license
notice, it’s unclear what the copyright status is.

Extend is include on the eigenclass/metaclass/singleton class. In this
case, it’s like saying

C.extend Observable

because of the implied “self”.

Adam P. wrote:

I mean make the licensing specific. Since there’s no longer a license
notice, it’s unclear what the copyright status is.

Ah, now I got you. :slight_smile: I had included the complete license terms at the
top of cells.rb, replacing the standard reference to the GPL.

I’ve just added a copy of the license in a separate file; hopfully this
will avoid any confusion about the copyright status. :slight_smile:

On 09/13/2010 11:46 PM, Knut Franke wrote:

Thanks for the link; I managed to miss that one when trying to google
existing solutions. :slight_smile: I’ve taken the liberty to include some of the
ideas in my code.

:slight_smile:

class Model
cell :name, :email, :role, :caption
def initialize
self.name = “Your N.”
self.email = “[email protected]
self.role = “To”
calculate(:caption) { “#{self.role}: #{self.name}<#{self.email}>”
}
end
end

I was going to say: why not just do

 def self.caption
   "#{self.role}: #{self.name} <#{self.email}>"
 end

but the calculate method makes it possible to observe(:caption), right?

How does #observe know that caption depends on #role, #name, and #email?
It seems difficult to determine (efficiently) whether to call the
caption observers when one of the cells changes. The fact that caption
depends on role, name, and email (but no others) is embedded opaquely in
a proc. Maybe I should just read your code :wink:

Joel VanderWerf wrote:

I was going to say: why not just do

 def self.caption
   "#{self.role}: #{self.name} <#{self.email}>"
 end

but the calculate method makes it possible to observe(:caption), right?

Yes, for this example that’s the main advantage. More generally,
caption=() could do other interesting stuff; like updating a GUI (see
model-view.rb).

How does #observe know that caption depends on #role, #name, and #email?

By calling the block and keeping track of which cell getters are called
(using a global variable). That’s certainly not perfect, but compared to
trying to parse the block content somehow it has the advantage of
working also if the source cells are accessed indirectly via other
methods (not to mention it’s much easier to implement).

Maybe I should just read your code :wink:

If anything’s still unclear: It’s not much code, and I’ve tried to make
it readable. :slight_smile:

Joel VanderWerf wrote:

Oh, better watch out for evaluations with branches then:

Yes; as I said, it’s not perfect (I guess a “perfect” solution just
doesn’t exist).

calculate(:caption) do
case role
when PublicEmailRole
“#{self.role}: #{self.name} <#{self.email}>”
else
“#{self.role}: #{self.name}”
end
end

At least it’s relatively easy to fix: Just make sure you always
reference all relevant cells. For instance,

calculate(:caption) do
case role
when PublicEmailRole
“#{self.role}: #{self.name} <#{self.email}>”
else
self.email # dummy
“#{self.role}: #{self.name}”
end
end

Knut Franke wrote:

Joel VanderWerf wrote:

Oh, better watch out for evaluations with branches then:
[…]
At least it’s relatively easy to fix: Just make sure you always
reference all relevant cells. For instance,

I think I’ve found a better solution. Quite simply, given that changes
to a cell can change which other cells a dependent cells sources, its
dependencies need to be re-evaluated along with its value. Your example
will now just work, without relying on formula blocks to always
reference all cells they’ll ever need.

On 09/14/2010 02:14 PM, Knut Franke wrote:

caption=() could do other interesting stuff; like updating a GUI (see
model-view.rb).

How does #observe know that caption depends on #role, #name, and #email?

By calling the block and keeping track of which cell getters are called
(using a global variable).

Oh, better watch out for evaluations with branches then:

class PublicEmailRole

def to_s; …; end
end

calculate(:caption) do
case role
when PublicEmailRole
“#{self.role}: #{self.name} <#{self.email}>”
else
“#{self.role}: #{self.name}”
end
end

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs