On Fri, Apr 6, 2012 at 12:17 AM, Hal Fulton <rubyhacker / gmail.com> wrote:
> On Thu, Apr 5, 2012 at 5:00 PM, Robert Klemme <shortcutter / googlemail.com>
> wrote:
>>
>> On Thu, Apr 5, 2012 at 5:18 PM, Hal Fulton <rubyhacker / gmail.com> wrote:
>> > 2012/4/5 Robert Klemme <shortcutter / googlemail.com>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
>>    
>>   >>  
>>
>>    >> end
>>
>> I find that neither difficult nor impossible.    >
>
> 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.

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 `<main>'

:-)

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

Probably not. :-)

> I have been exposed to a lot of JSON in the last week...;)

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 [](name)
    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. :-)

Happy Easter!

robert

-- 
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/