Beginners YAML question

I needed a class that acted like a container, as well as doing other things.
I tried making it a subclass of Array, instead of adding an Array member. It
was working fine until I tried to reload it from YAML. Here is some code
that reproduces the problem. It seems like the Size parameter is not being
output in the YAML string.

- Why doesn't it work?
- How can I make it work?

···

----

require 'Yaml'

class Box< Array
def initialize size
@size = size
end
def size
@size
end
end

box = Box.new (2)
box << "orange"
box_yaml = box.to_yaml
p box_yaml

# prints "--- !ruby/array:Box \n- orange"

box2 = YAML::load(box_yaml)

# produces:
=============== ArgumentError =====================
c:\ruby\lib\ruby\1.8/yaml/rubytypes.rb:239:in `initialize'
o = obj_class.new
c:\ruby\lib\ruby\1.8/yaml/rubytypes.rb:239:in `new'
o = obj_class.new
c:\ruby\lib\ruby\1.8/yaml/rubytypes.rb:239
o = obj_class.new
c:\ruby\lib\ruby\1.8/yaml/rubytypes.rb:235:in `call'
array_proc = Proc.new { |type, val|
c:\ruby\lib\ruby\1.8/Yaml.rb:119:in `transfer'
yp = @@parser.new.load( io )
c:\ruby\lib\ruby\1.8/Yaml.rb:119:in `load'
yp = @@parser.new.load( io )
c:\ruby\lib\ruby\1.8/Yaml.rb:119:in `load'
yp = @@parser.new.load( io )
c:\ruby\lib\ruby\1.8\test.rb:21
box2 = YAML::load(box_yaml)
c:\ruby\lib\ruby\site_ruby\1.8/rubygems/custom_require.rb:18:in `require__'
require__ path
c:\ruby\lib\ruby\site_ruby\1.8/rubygems/custom_require.rb:18:in `require'
require__ path

=============================================
Exception: wrong number of arguments (0 for 1)

Thanks,
-Adam

Adam Shelly wrote:

[...] It was working fine until I tried to reload it
from YAML. Here is some code that reproduces the problem.

Thanks - makes it much easier :slight_smile:

- Why doesn't it work?

A bug which seems to be fixed in a later version.

- How can I make it work?

Edit this file:
  C:\ruby\lib\ruby\1.8\yaml\rubytypes.rb

Find two occurrences (lines 80 & 239) of:

     o = obj_class.new

.... and replace both with:

     o = obj_class.allocate

Thanks,
-Adam

No probs,

daz

Thanks,
That fixed the load error, but there is still a problem with the to_yaml
routine. The additional @size parameter never gets written to the YAML
string.

require 'Yaml'

class Box< Array
def initialize size
@size = size
end
def size
@size
end
end

box = Box.new(2)
print "ERROR: Box, #{box.size}!=2" unless box.size == 2
box2 = YAML::load(box.to_yaml)
print "ERROR: Box2, #{box2.size}!=2" unless box2.size == 2
p box.to_yaml

ERROR: Box2, !=2
--- !ruby/array:Box

Any ideas?
-Adam

···

On 8/30/05, daz <dooby@d10.karoo.co.uk> wrote:

Adam Shelly wrote:

> [...] It was working fine until I tried to reload it
> from YAML. Here is some code that reproduces the problem.

Thanks - makes it much easier :slight_smile:

> - Why doesn't it work?

A bug which seems to be fixed in a later version.

> - How can I make it work?

Edit this file:
C:\ruby\lib\ruby\1.8\yaml\rubytypes.rb

Find two occurrences (lines 80 & 239) of:

o = obj_class.new

.... and replace both with:

o = obj_class.allocate

> Thanks,
> -Adam

No probs,

daz

Adam Shelly wrote:

Thanks,
That fixed the load error, but there is still a problem
with the to_yaml routine. The additional @size parameter
never gets written to the YAML string.

require 'yaml'

class Box < Array
  def initialize size
    @size = size
  end
  def size
    @size
  end
end

box = Box.new(2)
print "ERROR: Box, #{box.size}!=2" unless box.size == 2
box2 = YAML::load(box.to_yaml)
print "ERROR: Box2, #{box2.size}!=2" unless box2.size == 2
p box.to_yaml

ERROR: Box2, !=2
--- !ruby/array:Box

Any ideas?
-Adam

In short, @size is an attribute of Box ... but you're
using the inherited Array#to_yaml to serialise your object.

Some pointers ...

1) By defining an initialize method in Box, you /override/
   the Array initialize - so, an empty Array is allocated.
   To fix this, you need to call "super" (see 2)

2) Array already has a size attribute, so this works:

     require 'yaml'

     class Box < Array
       def initialize(*args, &block)
         super
       end
     end

     box = Box.new(3) {|x| 2*x}
     box2 = YAML::load(box.to_yaml)
     print "ERROR: box2 != box" unless box2 == box
     puts box.to_yaml

#-> --- !ruby/array:Box
#-> - 0
#-> - 2
#-> - 4

3) Note that if you /don't/ inherit from Array,
   to_yaml saves as a !ruby/object:Box along with
   all current instance variables of Box.

     require 'yaml'

     class Box
       def initialize(*args)
         @high, @wide = *args
       end
     end

     box = Box.new(4,7)
     puts box.to_yaml
     box2 = YAML::load(box.to_yaml)
     puts box2.to_yaml

#-> --- !ruby/object:Box
#-> high: 4
#-> wide: 7
#-> --- !ruby/object:Box
#-> high: 4
#-> wide: 7

# Note: I left out the "box2 != box" comparison
# because Boxes are not comparable until you
# define the mechanism.

4) If you want Box to behave like an Array but with
   attributes of its own, there are ways.
   Ask again (giving details) if you need help with
   that or any other aspect.

Cheers,

daz

In short, @size is an attribute of Box ... but you're
using the inherited Array#to_yaml to serialise your object.

Some pointers ...

1) By defining an initialize method in Box, you /override/
   the Array initialize - so, an empty Array is allocated.
   To fix this, you need to call "super" (see 2)

Right.

2) Array already has a size attribute, so this works:

'size' was an extremely bad choice of attribute name. I was trying to
reduce my complex real code to a simple example, but I oversimplified.

3) Note that if you /don't/ inherit from Array...

But I do want to inherit from Array...

4) If you want Box to behave like an Array but with
   attributes of its own, there are ways.
   Ask again (giving details) if you need help with
   that or any other aspect.

That's exactly what I want. A "Box" object which acts like an array,
but has other attributes too:
For example:

class Box< Array
    def initialize label, location, initial_size = 0, initial_value =
nil, &block
        super initial_size, initial_value
        @label = label
        @loc = location
    end
    def location
        @loc
    end
    def move_to new_loc
        @loc = new_loc
    end
    def label
        @label
    end
    def look_at
        "This is a box with a label that says " + (@label || "_nothing_")
    end
end

box = Box.new("Books", "Hall Closet")
puts box.look_at
puts box.to_yaml

box2 = YAML::load(box.to_yaml)
puts box2.look_at
puts "ERROR: Box not reloaded correctly" unless box2.label == box.label

What I can't figure out is how to get the extra attributes in the
YAML. Do I have to override the to_yaml method?

Thanks for the help.
-Adam

···

On 9/2/05, daz <dooby@d10.karoo.co.uk> wrote:

Hey, Adam!

  So sorry - I missed this post.
  (Just found it whilst browsing through the archives.)

Yes, you'll need to override the to_yaml method that
is already overridden by Array. Ideally, you'd just
want to undo the override but I don't think that's
advisable, even if it's possible.

Either:

1) Add this to Box#initialize

  self.class.class_eval do
    ( Object.instance_methods - instance_methods(false) ).grep(/yaml/) do |meth|
        define_method( meth, Object.instance_method(meth) )
    end
  end

That'll copy all yaml methods from Object into Box that were
concealed as a consequence of inheriting the same-named methods
from Array. (Array is interposed between Box and Object).

- OR -

2) The canonical way, AFAICS, is to redefine them
   in the Box class, which would look similat to this:
      (see: ruby/lib/ruby/1.8/yaml/rubytypes.rb)

  def to_yaml_type
    "!ruby/object:#{self.class}"
  end

  def to_yaml_properties
    instance_variables # all ... OR ...
    ### ['@label', '@loc'] # your selection
  end

  def to_yaml( opts = {} )
    YAML.quick_emit(nil, opts) do |out|
      out.map(to_yaml_type) do |map|
        to_yaml_properties.sort.each do |iv|
          map.add( iv[1..-1], instance_eval(iv) )
        end
      end
    end
  end

···

On 2005/09/06, Adam Shelly wrote:

On 2005/09/02, daz wrote:
> In short, @size is an attribute of Box ... but you're
> using the inherited Array#to_yaml to serialise your object.
>
[...]

That's exactly what I want. A "Box" object which acts like an array,
but has other attributes too:
For example:
[...]

What I can't figure out is how to get the extra attributes in the
YAML. Do I have to override the to_yaml method?

-------------

I think the definition of 'to_yaml' above looks like it's
doing something tailored to your class, so here's another
alternative:

  def to_yaml( opts = {} )
    Object.instance_method(:to_yaml).bind(self).call(opts)
  end

-------------

You probably worked it out yourself but this is
something to compare against.

BTW, an alternative to Box < Array is to delegate.
The top of your class would look like this:

   require 'delegate'

   class Box < DelegateClass(Array)
     def initialize label, location, initial_size ...
       super Array.new(initial_size, initial_value)

Have a look here:
http://www.rubycentral.com/book/lib_patterns.html#S1

Cheers,

daz