Call for comments - Structure

Greetings, fellow Rubyists…

I am revisiting a small project that has lain dormant for
some time…

As I make changes and improvements, I’d be interested in
hearing your comments.

You’re welcome to critique the idea itself, the implementation,
missing features, the code, the style, or whatever.

I’ve pasted the README here for your convenience.

Thanks,
Hal F.


Structure
Hal F.
Version 1.0.3
License: The Ruby License

This is a newer version of the older “SuperStruct” (sstruct) library.

This is an easy way to create Struct-like classes; it converts easily
between hashes and arrays, and it allows OpenStruct-like dynamic naming
of members.

Unlike Struct, it creates a “real” class, and it has real instance
variables
with predictable names.

A basic limitation is that the hash keys must be legal method names
(unless
used with send()).

Basically, ss[“alpha”], ss[:alpha], and ss.alpha all mean the same.

NOTES:

It’s like a Struct…

  • you can pass in a list of symbols for accessors
  • it will create a class for you
    but…
  • you don’t have to pass in the class name
  • it returns a “real” class
    . instance variables have the expected names
    . you can reopen and add methods
  • it doesn’t go into the Struct:: namespace
  • it preserves the order of the fields
  • you can use Strings instead of Symbols for the names

It’s like an Array…

  • you can access the items by [number] and [number]=
    but…
  • you can also access the items by [“name”] and [“name”]=
  • you can access the items by accessors

It’s like an OpenStruct…

  • (if you use .open instead of .new) you can add fields
    automatically with x.field or x.field=val
    but…
  • you can initialize it like a Struct
  • it preserves the order of the fields

It’s like a Hash…

  • data can be accessed by [“name”]
    but…
  • order (of entry or creation) is preserved
  • arbitrary objects as keys are not allowed (it does obj.to_str or
    obj.to_s)
  • keys must be valid method names

It’s like Ara Howard’s Named Array…

  • we can access elements by [“name”] or [“name”]=
    but…
  • you can access the items by accessors
  • strings must be valid method names

It’s like Florian G.'s Keyed List…
(to be done)
but…

  • it preserves the order of the fields

Some examples: (see test cases)

Need not assign to existing fields (default to nil)

myStruct = Structure.new(:alpha)
x = myStruct.new
x.alpha # nil

A value assigned at construction may be retrieved

myStruct = Structure.new(:alpha)
x = myStruct.new(234)
x.alpha # 234

Unassigned fields are nil

myStruct = Structure.new(:alpha,:beta)
x = myStruct.new(234)
x.beta # nil

An open structure may not construct with nonexistent fields

myStruct = Structure.open
x = myStruct.new(234) # error

An open structure may assign fields not previously existing

myStruct = Structure.open
x = myStruct.new
x.foo = 123
x.bar = 456

The act of retrieving a nonexistent field from an open struct will

create that field

myStruct = Structure.open
x = myStruct.new
x.foo # nil

A field (in an open struct) that is unassigned will be nil

myStruct = Structure.open
x = myStruct.new
y = x.foobar

A struct created with new rather than open cannot reference

nonexistent

fields

myStruct = Structure.new
x = myStruct.new
x.foo # error

Adding a field to a struct will create a writer and reader for that

field

An open struct will also create a writer and a reader together

A field has a real writer and reader corresponding to it

A string will work as well as a symbol

myStruct = Structure.new(“alpha”)

to_a will return an array of values

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
assert(x.to_a == [7,8,9])

Instance method ‘members’ will return a list of members (as strings)

myStruct = Structure.new(:alpha,“beta”)
x = myStruct.new
assert_equal([“alpha”,“beta”],x.members)

Class method ‘members’ will return a list of members (as strings)

myStruct = Structure.new(:alpha,“beta”)
assert_equal([“alpha”,“beta”],myStruct.members)

to_ary will allow a struct to be treated like an array in

multiple assignment

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
a,b,c = x
assert(b == 8)

to_ary will allow a struct to be treated like an array in

passed parameters

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
b = meth(*x)

to_hash will return a hash with fields as keys

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
h = x.to_hash
assert_equal({“alpha”=>7,“beta”=>8,“gamma”=>9},h)

A field name (String) may be used in a hash-like notation

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
y = x[“beta”]

A field name (Symbol) may be used in a hash-like notation

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
y = x[:beta]

[offset,length] may be used as for arrays

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
y = x[0,2]

Ranges may be used as for arrays

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
y = x[1…2]

Adding a field to an open struct adds it to the instance

myStruct = Structure.open(:alpha)
x = myStruct.new
x.beta = 5

Adding a field to an open struct adds it to the class also

myStruct = Structure.open(:alpha)
x = myStruct.new
x.beta = 5

An array passed to Structure.new need not be starred

myStruct = Structure.new(%w[alpha beta gamma])
x = myStruct.new

A hash passed to #assign will set multiple values at once

myStruct = Structure.new(%w[alpha beta gamma])
x = myStruct.new
hash = {“alpha”=>234,“beta”=>345,“gamma”=>456}
x.assign(hash)

||= works properly

x = Structure.open.new
x.foo ||= 333
x.bar = x.bar || 444

attr_tester will create a ?-method

myStruct = Structure.new(:alive)
myStruct.attr_tester :alive
x = myStruct.new(true)
x.alive? # true

Hal F. wrote in post #1055070:

Greetings, fellow Rubyists…

I am revisiting a small project that has lain dormant for
some time…

GitHub - Hal9000/Structure: Newer version of sstruct

As I make changes and improvements, I’d be interested in
hearing your comments.

I’ll see whether I can look into this over the weekend.

Unlike Struct, it creates a “real” class, and it has real instance
variables
with predictable names.

Why are Struct generated classes not real? Or does this only refer to
the fact that #instance_variables returns an empty list? If so why does
it matter?

Kind regards

robert

On Thu, Apr 5, 2012 at 4:33 PM, Bartosz D. [email protected]
wrote:

No.
What do you mean by that?

Struct just does not have instance variables, which is surprising,

I know - that’s the whole point why “#instance_variables returns an
empty list”. But the question was, whether this is the reason Hal
calls them “unreal” - and whether it matters. Both can probably only
be answered by him since he coined the phrase…

if you assume that “Point = Struct :x, :y” is a shorthand for “class
Point; attr_accessor :x, :y; def initialize x, y; @x, @y = x, y; end;
end”.

Well, obviously it is not a shorthand for the code you show. :slight_smile:
Still it doesn’t explain why Hal calls them “unreal”. We are still
only guessing as to what intentions his might have been.

Cheers

robert

W dniu 5 kwietnia 2012 14:21 użytkownik Robert K.
[email protected] napisał:

Unlike Struct, it creates a “real” class, and it has real instance
variables
with predictable names.

Why are Struct generated classes not real? Or does this only refer to
the fact that #instance_variables returns an empty list? If so why does
it matter?

No. Struct just does not have instance variables, which is surprising,
if you assume that “Point = Struct :x, :y” is a shorthand for “class
Point; attr_accessor :x, :y; def initialize x, y; @x, @y = x, y; end;
end”.

For example this is not valid code:

Point = Struct.new :x, :y
class Point
def distance other
Math.sqrt((@x-other.x)**2 + (@y-other.y)**2)
end
end

irb(main):007:0> a = Point.new 1, 2
=> #
irb(main):008:0> b = Point.new -1, 3
=> #
irb(main):009:0> a.distance b
NoMethodError: undefined method -' for nil:NilClass from (irb):4:in distance’
from (irb):9
from F:/Ruby193/bin/irb:12:in `’

– Matma R.

2012/4/5 Robert K. [email protected]

end".

Well, obviously it is not a shorthand for the code you show. :slight_smile:
Still it doesn’t explain why Hal calls them “unreal”. We are still
only guessing as to what intentions his might have been.

Bartosz has understood my point. I don’t mind that instance_variables
returns an empty list, but I have been annoyed at the difficulty or
impossibility
of reopening the class to add useful methods that manipulate the
instance data.

As a side issue, I wanted more flexible access to the members and more
freedom in creating classes and objects. (In general, I would rather say
obj.this.that than to say obj[‘this’][‘that’], and I’d like to freely
convert to
and from hashes.)

But of course, it is perfectly valid for anyone to say they don’t like
the
idea.
It would not be the first time I tried to solve a problem that many or
most
others did not perceive at all.

Hal

Hal F. wrote in post #1055070:

Greetings, fellow Rubyists…

Hi!
“The Ruby Way” book is one of my Mentors :slight_smile:

I am revisiting a small project that has lain dormant for
some time…

GitHub - Hal9000/Structure: Newer version of sstruct

I’m writing a gem too, and i think that likes your project.

As I make changes and improvements, I’d be interested in
hearing your comments.

My first impression is below.

to_ary will allow a struct to be treated like an array in

multiple assignment

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
a,b,c = x
assert(b == 8)

to_ary will allow a struct to be treated like an array in

passed parameters

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
b = meth(*x)

to_hash will return a hash with fields as keys

myStruct = Structure.new(“alpha”,“beta”,“gamma”)
x = myStruct.new(7,8,9)
h = x.to_hash
assert_equal({“alpha”=>7,“beta”=>8,“gamma”=>9},h)

I guess, this cast method names are expected in some special cases.
#to_ary, #to_hash (e.g.: #to_str, #to_int)

I recommend below names in our project.
#to_a, #to_h, #to_s, #to_i

Kenichi

On Thu, Apr 5, 2012 at 5:00 PM, Robert K.
[email protected]wrote:

alias to_i value
end

I find that neither difficult nor impossible. What did you have in mind?

You are correct, of course. But it feels a little clumsy to me not to
have
direct access to the instance variables. That is purely subjective, and
many will disagree with me.

What about the automated conversion of a hash to an object with
accessors?
Note that from_hash will handle one level and parse_hash will proceed
recursively.

Am I the only one who likes obj.foo.bar better than obj[‘foo’][‘bar’] ?

I have been exposed to a lot of JSON in the last week… :wink:

Hal

On Thu, Apr 5, 2012 at 5:18 PM, Hal F. [email protected] wrote:

if you assume that “Point = Struct :x, :y” is a shorthand for "class
of reopening the class to add useful methods that manipulate the
instance data.

That I don’t understand, maybe that’s the reason for the previous
discussion: for me it is fairly easy and straightforward

Counter = Struct.new :value do
def incr(x = 1)
self.value += x
end

alias to_i value
end

I find that neither difficult nor impossible. What did you have in
mind?

It would not be the first time I tried to solve a problem that many or most
others did not perceive at all.

:slight_smile:

Cheers

robert

I think this is a good project idea on two counts 1) better Struct b/c
it makes a real class and 2) indifferent access of keys.

Might not be a bad idea to add some alternate constructors too, e.g.
wouldn’t mind seeing a DSL builder style constructor:

Book = Structure.new(:title, :isbn)

Book.build do
title “A Book”
isbn “000000000”
end

Also, what about default values? e.g.

Point = Structure.new(:x=>0,:y=>1)

Also,

#to_a, #to_h, #to_s, #to_i

+1

On Fri, Apr 06, 2012 at 07:17:07AM +0900, Hal F. wrote:

Am I the only one who likes obj.foo.bar better than obj[‘foo’][‘bar’] ?

Actually . . . obj.foo.bar looks pretty nice.

On Sat, Apr 7, 2012 at 10:04 AM, Robert K.
[email protected] wrote:

Hal, are you still following?

Cheers

robert

On Fri, Apr 6, 2012 at 12:17 AM, Hal F. [email protected]
wrote:

self.value += x
many will disagree with me.
OK, understood. I thought I might have missed something.

What about the automated conversion of a hash to an object with accessors?
Note that from_hash will handle one level and parse_hash will proceed
recursively.

Well, but

irb(main):020:0> o=Structure.parse_hash(a:{:b=>:c})
ArgumentError: wrong number of arguments (2 for 1)
from
/home/robert/projects/Hal9000-Structure-9f96182/structure.rb:30:in
parse_hash' from /home/robert/projects/Hal9000-Structure-9f96182/structure.rb:38:in block in parse_hash’
from
/home/robert/projects/Hal9000-Structure-9f96182/structure.rb:34:in
each' from /home/robert/projects/Hal9000-Structure-9f96182/structure.rb:34:in parse_hash’
from (irb):20
from /usr/local/bin/irb19:12:in `’

:slight_smile:

Am I the only one who likes obj.foo.bar better than obj[‘foo’][‘bar’] ?

Probably not. :slight_smile:

I have been exposed to a lot of JSON in the last week… :wink:

Ah, I see. First thing which stroke me odd was that Structure.new
might actually return an instance or a class. I find that bad for a
public interface because this is bound to cause confusion.

In the implementation I would replace “Structure.” in method
definitions with “self.” and throw it out completely inside methods to
reduce redundancy. Also, I am not sure whether it is good to cling to
a lot of state via all these closures created for define_method calls.

I tried to come up with a systematic list of what you want to do in
terms of input variants because I needed that for me to better
understand the matter. This is what I cam up with:

  1. modifiability
    a) fixed set of members
    b) members can be added on the fly

  2. values
    a) not provided
    b) provided in a Hash

  3. recursive conversion from Hash (for 2b only)
    a) no
    b) yes

Conclusions:

It occurred to me that with 1a the list of members is conceptually a
property of the class, while in 1b it is a property of each instance.
From that it followed to me that it does not make sense to generate
classes for 1b. Thus you can define a replacement class for
OpenStruct and be done (either replacing the original, augmenting it
or having your own - I’d do the latter). Consequently there would be
no class factory method for that case - at most an instance factory
method.

So, class generation only makes sense for 1a. If we have 1a2b one
must decide what to do with the values. To keep the interface
consistent (my remark above) it seems most natural to me to use them
as default values for every created instance and not as values for a
single created instance which is immediately returned.

Now, in that case would 1a2b3b make sense? Or put differently: what
happens to recursive Hashes if we pass one? Since we must use keys of
the top level Hash to define members of the newly generated class, we
could either define more classes for nested Hashes or just use them as
is as default values. I tend to just use them as is because creating
a whole bunch of classes from a single factory method does not feel
right because the user does not have a chance to get hold of them.
(They would need to be referenced from the top level class so it could
construct instances properly). So I would leave out 1a2b3b
completely.

Implementation strategy:

My guiding rule is usually: use as few meta programming as possible.
Let’s see how far we can get with that. The open case (1b) is done
already. Now for the closed case (1a). I would implement a regular
class completely with all dynamic methods (i.e. those Hash like
accesses like foo[“bar”]) which translates these to calls of
#instance_variable_get and #instance_variable_set and only relies on
it’s class having a method #members which returns a frozen Array of
Symbol containing member names. These will be stored in an instance
variable @members of the class. I would also depend on a method
#normalize_member with 1 argument in the class which raises an
exception if the argument is an invalid member and otherwise returns
the argument normalized (instance variable name). The class will have
another optional member @values which contains the value Hash if one
has been provided at class creation time. Now the class constructor
method (Structure.new) will only have to do this

  1. create a subclass of the base class
  2. set instance_variable “@members
  3. set instance_variable “@values” if present
  4. class <<cl;self;end.class_eval { attr_accessor *members }

So we have something like

class Structure
class OpenStructure < Structure
def self.normalize_member(name) “@#{name}” end
def members; instance_variables.map {|iv| iv[/\A@(.*)\z/, 1].to_sym}
end
end

class <<self
attr_reader :members

def new(*args)
  ...
  Class.new(self).tap do |cl|
    cl.instance_variable_set("@members", members)
    cl.instance_variable_set("@values", values) if values
  end
end

end

def []=(name, value)
instance_variable_set(self.class.normalize(name), value)
end

def
instance_variable_get(self.class.normalize(name))
end

def hash
members.sort.inject(0) {|ha,o| ((ha << 3) ^ o.hash} # bad example!
end

def members
self.class.members
end


end

Done. :slight_smile:

Happy Easter!

robert

Hal F. wrote in post #1055190:

What about the automated conversion of a hash to an object with
accessors?

If the main driver is taking Hash-like data, depending on the
application it might be better to wrap the underlying Hash rather than
copy it into the instance variables of another object.

Note that from_hash will handle one level and parse_hash will proceed
recursively.

Am I the only one who likes obj.foo.bar better than obj[‘foo’][‘bar’] ?

Oh, I do too. In fact, my main problem with OpenStruct is that it only
implements the first form and not the second. If it did both I’d be very
happy with it.

For an old project (couchtiny) I implemented the following:

Extend a Hash with this module to get semantics of a Javascript

object:

me.foo is the same as me[‘foo’]

module JSObjectMixin
def method_missing(meth,*rest,&blk)
key = meth.to_s
if key[-1] == ?=
self[key[0…-2]] = rest.first
else
self[key]
end
end
end

This class is like a Hash but with the semantics of a Javascript

object:

me.foo is the same as me[‘foo’]

class JSObject < Hash
include JSObjectMixin
end

Of course, if you are using the accessor methods a lot, this may be less
efficient than having defined accessor methods in the class pointing
directly at instance variables.

Aside: newer versions of the JSON library let you do this:

JSON.parse(src, :object_class => ::JSObject)

so that you get a tree of JSObjects instead of Hashes.

On Thu, Apr 5, 2012 at 9:29 AM, Hal F. [email protected] wrote:

You’re welcome to critique the idea itself, the implementation,
missing features, the code, the style, or whatever.

I like what I read in the README, and would definitely use it in future
if
it’s a well-maintained project.

My comment: prefer symbols over strings. For instance, the “members”
method should return an array of symbols, not strings. Accept strings
by
all means, but only produce symbols.

I second Intransition’s comments on default values and ways of
initialising.

I don’t think the following needs to happen:

The act of retrieving a nonexistent field from an open struct will

create that field

myStruct = Structure.open
x = myStruct.new
x.foo # nil

If it were me, fields would only be created on assignment, not on
reference. I would still return nil when x.foo was called.

Nice work! I hope you revive the project.

Gavin