Extend OpenStruct's functionality with explicit default value

R

Robert Klemme

Hi,

I just had a use case where I wanted to have several counters and not
store them in a Hash because of the nicer syntax of OpenStruct.
Currently, the code has to do

counters = OpenStruct.new
...
counters.foo ||= 0
counters.foo += 1

For obvious reasons I'd like to get rid of the initialization.

The suggestion would be to do this: if the argument to
OpenStruct#initialize is not a Hash use it as default value which is
returned for undefined properties. As far as I can see this won't
break existing code since the default is nil. With the change one
could do

counters = OpenStruct.new 0
...
counters.foo += 1

We could go even further and copy the Hash approach by also allowing a
block which is invoked when a property is accessed for the first time.
Block argument would be the symbol of the property and the return
value would be used to initialize the property. Then you could do

data = OpenStruct.new {[]}
...
data.animals << "cat" << "dog"

or even

data = OpenStruct.new do |sym|
case sym
when :animals : []
when :dictionary : {}
end
end

In other words: declarative lazy initialization.

Again, existing code would not be affected.

What do others think?

Kind regards

robert
 
T

Trans

Hi,

I just had a use case where I wanted to have several counters and not
store them in a Hash because of the nicer syntax of OpenStruct.
Currently, the code has to do

counters =3D OpenStruct.new
...
counters.foo ||=3D 0
counters.foo +=3D 1

For obvious reasons I'd like to get rid of the initialization.

The suggestion would be to do this: if the argument to
OpenStruct#initialize is not a Hash use it as default value which is
returned for undefined properties. As far as I can see this won't
break existing code since the default is nil. =A0With the change one
could do

counters =3D OpenStruct.new 0
...
counters.foo +=3D 1

We could go even further and copy the Hash approach by also allowing a
block which is invoked when a property is accessed for the first time.
=A0Block argument would be the symbol of the property and the return
value would be used to initialize the property. =A0Then you could do

data =3D OpenStruct.new {[]}
...
data.animals << "cat" << "dog"

or even

data =3D OpenStruct.new do |sym|
=A0 case sym
=A0 when :animals : []
=A0 when :dictionary : {}
=A0 end
end

In other words: declarative lazy initialization.

Again, existing code would not be affected.

What do others think?

I recently submitted a patch that allowed OpenStruct to take a self
yielding block, e.g you could do:

data =3D OpenStruct.new do |o|
o.animals =3D []
o.dictionary =3D {}
end

I don't really see that much use for complex lazy initialization as
you suggest. Though, I can see the Hash block form being useful. Maybe
that would be a better use of the block. Actually, both could be
supported if we differentiate on the arity of the block. With two
args:

OpenStruct.new{ |o, k| o[k] =3D [] }

Note, my patch also add #[] and #[]=3D, which are more important
changes.

Robert would you like to update my patch to support the Hash block
notation and resubmit it?

T.
 
P

Peña, Botp

From: ara.t.howard [mailto:[email protected]]=20
# why not simply
#=20
# o =3D OpenStruct.new :foo =3D> 0

because that would mean knowing in advanced the vars and mentioning =
them, wc loses the dynamism of ostruct

compare to=20

o =3D OpenStruct.new(0)

unknown/new attribs will be initialized to 0 as compared to default nil


kind regards -botp
 
A

ara.t.howard

because that would mean knowing in advanced the vars and mentioning =20=
them, wc loses the dynamism of ostruct

compare to

o =3D OpenStruct.new(0)

unknown/new attribs will be initialized to 0 as compared to default =20=


in all fairness that's *probably* not true. aka, if the code reads

o.foo

and not

o.send :foo

then it's probably not actually doing dynamic accumulation as the =20
accumulator names are know in advance and reside in the source code


if it is doing dynamic accumulation then '+=3D' won't do, and the code =20=

would have to read something like

o.send( key, o.send(key) + 1 )

which is hardly worse than

o.send( key, o.send(key)||0 + 1 )

the difficulty with the default argument approach is what to do with =20
this

hash =3D { :foo =3D> 42 }

o =3D OpenObject.new hash

p o.foo

does it return

42

or

{ :foo =3D> 42 }

in otherwords it seems rather strange that you can have any default =20
value *except* a hash itself to me.



i see the usefulness though, and just added this to 'openobject'


cfp:~/src/ruby/openobject/openobject-0.0.3 > cat a.rb
# by default you cannot retrive unset values
#
o =3D oo
begin; o.foo; rescue NameError; p NameError; end
#
#=3D> NameError

# but you can set anything
#
o =3D oo
o.foo =3D 42
p o.foo
#
#=3D> 42

# blocks extend openobjects
#
o =3D oo{ def bar() 42 end }
p o.bar
#
#=3D> 42

# you can set a default value which will be returned for anything
# missing value
#
o =3D oo{ default 42 }
p o.bar
#
#=3D> 42

# and the default value itself can be someting block/proc-like
#
n =3D 40
o =3D oo{ default{ n +=3D 2 } }
p o.foo
#
#=3D> 42



cfp:~/src/ruby/openobject/openobject-0.0.3 > ruby -r lib/openobject.rb =20=

a.rb
NameError
42
42
42
42





pushed to rubyforge just now (0.0.3)



cheers.

a @ http://codeforpeople.com/
 
P

Peña, Botp

From: ara.t.howard [mailto:[email protected]]=20
#...
# o =3D oo
# ...
# pushed to rubyforge just now (0.0.3)
#=20

uver cool. cool name too, oo :))
thanks, i'll check it out.

kind regars -botp
 
T

Trans

Sorry, somehow I seem to have forgotten to send this earlier.

2008/9/5 Trans <[email protected]>:




Hi,
I just had a use case where I wanted to have several counters and not
store them in a Hash because of the nicer syntax of OpenStruct.
Currently, the code has to do
counters =3D OpenStruct.new
...
counters.foo ||=3D 0
counters.foo +=3D 1
For obvious reasons I'd like to get rid of the initialization.
The suggestion would be to do this: if the argument to
OpenStruct#initialize is not a Hash use it as default value which is
returned for undefined properties. As far as I can see this won't
break existing code since the default is nil. =A0With the change one
could do
counters =3D OpenStruct.new 0
...
counters.foo +=3D 1
We could go even further and copy the Hash approach by also allowing a
block which is invoked when a property is accessed for the first time.
=A0Block argument would be the symbol of the property and the return
value would be used to initialize the property. =A0Then you could do
data =3D OpenStruct.new {[]}
...
data.animals << "cat" << "dog"
or even
data =3D OpenStruct.new do |sym|
=A0 case sym
=A0 when :animals : []
=A0 when :dictionary : {}
=A0 end
end
In other words: declarative lazy initialization.
Again, existing code would not be affected.
What do others think?
I recently submitted a patch that allowed OpenStruct to take a self
yielding block, e.g you could do:
=A0data =3D OpenStruct.new do |o|
=A0 o.animals =3D []
=A0 o.dictionary =3D {}
=A0end
I don't really see that much use for complex lazy initialization as
you suggest.

I don't view it so much as complex initialization but rather
declarative lazy initialization because it saves you the effort of
writing all those getter methods.
Though, I can see the Hash block form being useful. Maybe
that would be a better use of the block. Actually, both could be
supported if we differentiate on the arity of the block. With two
args:
=A0OpenStruct.new{ |o, k| o[k] =3D [] }
Note, my patch also add #[] and #[]=3D, which are more important
changes.

Yes, that's a good idea!

Glad you agree. Hell of a lot faster then send(key) and
send("#{key}=3D", val).

Mention it on ruby-core!
I would rather not want to have two interpretations for the block
because this can easily lead to confusion and subtle bugs can creep in
when accidentally having the wrong arity.

That was my first though too, but then I considered it a bit more and
think it makes enough sense. If we are asking for just the OpenStruct
object, ie. OpenStruct.new{ |o| ... } then clearly we are interested
in working with the object. If we ask for the key as well, ie.
OpenStruct.new{ |o, k| ... } then it is also clear we are instead
interested in doing something with a key. It's really not any
different in principle from other method interfaces, like using one or
two arguments with #slice.

T.
 
R

Robert Dober

I followed this discussion rather loosely, but sometimes that gives a
different perspective.

In order to avoid the hash vs. hash semantics why not just add a
different constructor?

OpenStruct::new :a =3D> 42
behaves as before
OpenStruct::defaulted :a =3D> 42
sets the hash :a =3D> 42 as default value.
Naming has never been one of my many ;) talents, but I guess you got the id=
ea.

Cheers
Robert

--=20
C'est v=E9ritablement utile puisque c'est joli.

Antoine de Saint Exup=E9ry
 
R

Robert Dober

Sorry forgot:

and implement defaulted also as a self returning instance method to
allow for this:
OpenStruct::new( :a => 42).defaulted( :a => 42 )
 

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,982
Messages
2,570,190
Members
46,736
Latest member
zacharyharris

Latest Threads

Top