Declarative relations between object attributes

K

Knut Franke

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
:eek:n
else
:eek:ff
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
 
A

Adam Prescott

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

Joel VanderWerf

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
:eek:n
else
:eek:ff
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

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 = :eek:n

when_temperature 0..100 do
self.status = :eek:n
end

when_temperature 100..100000 do
self.status = :eek:ff
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
 
K

Knut Franke

Adam said:
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.
 
K

Knut Franke

Joel said:
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. :) I've taken the liberty to include some of the
ideas in my code.
when_temperature 0..100 do
self.status = :eek:n
end

when_temperature 100..100000 do
self.status = :eek:ff
end

It appears that for this particular example, your API is nicer than
mine. ;-) 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 Name"
self.email = "(e-mail address removed)"
self.role = "To"
calculate:)caption) { "#{self.role}: #{self.name} <#{self.email}>"
}
end
end
 
A

Adam Prescott

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.
 
K

Knut Franke

Adam said:
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. :)
 
A

Adam Prescott

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

Ralph Shnelvar

[Note: parts of this message were removed to make it a legal post.]

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 Thomas' *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
 
A

Adam Prescott

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

C.extend Observable

because of the implied "self".

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

JV> # Let's just include Observable globally. Conservatively, one would d= o:
JV> #
JV> # =C2=A0 class C
JV> # =C2=A0 =C2=A0 extend Observable
JV> # =C2=A0 =C2=A0 include Observable::Match =C2=A0 # if desired
JV> # =C2=A0 =C2=A0 ...
JV> # =C2=A0 end

I give. =C2=A0What does
=C2=A0extend Observable
do?

I see in Dave Thomas' *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? =C2=A0The class C=
object? (That is, the class that is C rather than the instance that is C.)=
=C2=A0Is this related to some singleton stuff?
 
J

Joel VanderWerf

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

class Model
cell :name, :email, :role, :caption
def initialize
self.name = "Your Name"
self.email = "(e-mail address removed)"
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 ;)
 
K

Knut Franke

Adam said:
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. :) 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. :)
 
K

Knut Franke

Joel said:
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 ;)

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

Joel VanderWerf

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).


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
 
K

Knut Franke

Joel said:
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
 
K

Knut Franke

Knut said:
Joel said:
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.
 

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

No members online now.

Forum statistics

Threads
473,756
Messages
2,569,535
Members
45,008
Latest member
obedient dusk

Latest Threads

Top