YAML question (hi _why!)

_why,

Just saw you post and was reminded to ask: How are we coming on a
"good" solution to my default values problem?

For those curious, what I want to do is establish defaults for
fields in an object, such that:

   1. When I dump an object, fields that still have the default
      values will be left unspecified.
   2. When I load an object, unspecified fields will be given their
      default values.

Right now I'm using an initialize_copy hack and some other stuff.

Something like this can greatly compress the YAML, I've found.
Very convenient when you read or hand-edit the file.

Cheers,
Hal

+1 for this, I've wanted something very similar.

Extra points if it can trickle down to the deepest nested node :slight_smile:

Leon

···

On Thu, 25 Nov 2004 16:53:31 +0900, Hal Fulton <hal9000@hypermetrics.com> wrote:

For those curious, what I want to do is establish defaults for
fields in an object, such that:

   1. When I dump an object, fields that still have the default
      values will be left unspecified.
   2. When I load an object, unspecified fields will be given their
      default values.

Hal,

Would this be appropriate for the YAML equivalent of DTDs?

In XML this can be handled by DTDs (and I assume schemas and Relax NG, but I'm not familiar with those). Oddly, I don't think it's a required part of the spec, so different parsers would or would not display the default attributes, say when rendering to a web page. I think IE showed them, but Mozilla didn't which bummed me out.

It was very nice for hand-editing, saving a lot of typing, but sometimes awkward when I hadn't looked at the XML for a while, since it was hidden in the DTD, and not explicit in the document I was looking at. An editor that showed defaults in a ghost-like overlay would have been helpful there. But overally, the saving of typing vastly outweighed the lack of explicitness, since I was authoring a lot of XML by hand. Plus the resulting common-case files were much simpler and easy to read.

If it specifically about Ruby serialization, it may make more sense to have a ruby library that hacks yaml, like you do. I'd hate to have to manage a dtd just for defaults- why not add it to the code? Also, then it would possible provide a common mechanism for other persistence mechanisms- XML, etc. (though maybe that's over generalizing). I suppose the downside of this is that yaml is not self-documenting then, which is an important/nice aspect, especially if you want to use the YAML in downsteam applications (other languages, style-sheet renderings, etc.). On the other hand, the code should be self-documenting, ideally. But a dtd-type doc provides the documentation outside of the ruby context, such as for handediting. I guess that's the problem with multiple demands- it stresses the design :).

One thought...

require "yaml"
require "persistence/defaults" # hacks YAML to insert/remove defaults

class Document
    default_value :author => "Anonymous Coward" # explicit initial value doesn't easily equal a wanted default.
    ....
end

Nick

Hal Fulton wrote:

···

_why,

Just saw you post and was reminded to ask: How are we coming on a
"good" solution to my default values problem?

For those curious, what I want to do is establish defaults for
fields in an object, such that:

  1. When I dump an object, fields that still have the default
     values will be left unspecified.
  2. When I load an object, unspecified fields will be given their
     default values.

Right now I'm using an initialize_copy hack and some other stuff.

Something like this can greatly compress the YAML, I've found.
Very convenient when you read or hand-edit the file.

Cheers,
Hal

Hal Fulton wrote:

For those curious, what I want to do is establish defaults for
fields in an object, such that:

  1. When I dump an object, fields that still have the default
     values will be left unspecified.
  2. When I load an object, unspecified fields will be given their
     default values.

Hi, Hal. Good for you, bringing this up again. We didn't finish this discussion at all and the clock has struck loudly here: time to continue.

In Hobix, I am currently using a mixin for classes which need the above. This mixin relies upon the presence of a property_map method within those classes, which defines the full set of instance variables which I want to output in YAML, along with whether those fields are optional or not.

The mixin also requires default_#{ivar} methods for every instance variable you plan on having a default for. The mixin will use nil as the default, if no such method is found. You could include defaults in the property_map, but I sometimes base the default value of a field on the value of other fields (in the case of file paths), so I use a method for each instead.

So, for example, Hobix blog entries only require @title, @content and @author. Other fields are optional and aren't output to YAML if they are blank.

The Hobix::Entry#property_map basically looks like this:

   class Hobix::Entry
     include ToYamlExtras
     def property_map
         [
             ['@title', :req],
             ['@author', :req],
             ['@contributors', :opt],
             ['@created', :opt],
             ['@tagline', :opt],
             ['@summary', :opt],
             ['@content', :req]
         ]
     end
   end

I'm using an Array to ensure ordering of the elements upon output.

Here would be the ToYamlExtras mixin:

   module ToYamlExtras
     def to_yaml_properties
         property_map.find_all do |prop, req|
             case req
             when :opt
                 val = nil
                 if respond_to?( "default_#{ prop[1..-1] }" )
                     val = method( "default_#{ prop[1..-1] }" ).call
                 end
                 val != instance_variable_get( prop )
             when :req
                 true
             end
         end.
         collect { |prop, req| prop }
     end
     def initialize
         apply_defaults
         yield self if block_given?
     end
     def apply_defaults
         property_map.each do |prop, req|
             name = prop[1..-1]
             if instance_variable_get( prop ).nil? and
               respond_to?( "default_#{ name }" )
                 instance_variable_set( prop,
                   method( "default_#{ name }" ).call )
             end
         end
         self
     end
   end

As you can see, the mixin simply defines a to_yaml_properties method, which is a method understood by the YAML library. The to_yaml_properties method should return an Array of instance variable names, in the order they are output.

As you can see, the mixin also defines 'initialize', since you'll probably want to apply the defaults even if the object is created programmatically.

You'll also need to hook yourself a YAML type for each class. In the case of the Hobix::Entry class, I'm hooking !hobix.com,2004/entry.

   class Hobix::Entry
     def to_yaml_type
         "!hobix.com,2004/entry"
     end
   end

   YAML::add_domain_type( 'hobix.com,2004', 'entry' ) do |type, val|
     YAML::object_maker( Hobix::Entry, val ).apply_defaults
   end

A script including this mixin and a few tests of the above can be found at: http://whytheluckystiff.net/ruby/yaml-defaults-mixin.rb

_why

why the lucky stiff wrote:

Hal Fulton wrote:

For those curious, what I want to do is establish defaults for
fields in an object, such that:

  1. When I dump an object, fields that still have the default
     values will be left unspecified.
  2. When I load an object, unspecified fields will be given their
     default values.

Hi, Hal. Good for you, bringing this up again. We didn't finish this discussion at all and the clock has struck loudly here: time to continue.

Bonjour and thanks for the reply!

Are you near an irc client, btw, since we seem to be talking in near-
realtime?

In Hobix, I am currently using a mixin for classes which need the above. This mixin relies upon the presence of a property_map method within those classes, which defines the full set of instance variables which I want to output in YAML, along with whether those fields are optional or not.

This property_map isn't clear to me yet. Why not class-level data instead
of instance?

I hadn't thought about the issue of required vs. optional. I would have
just said: If it doesn't have a default, and it's not there, default it
to nil.

Ordering is also interesting; I hadn't thought of that either.

As you can see, the mixin simply defines a to_yaml_properties method, which is a method understood by the YAML library. The to_yaml_properties method should return an Array of instance variable names, in the order they are output.

Fine, clear enough.

As you can see, the mixin also defines 'initialize', since you'll probably want to apply the defaults even if the object is created programmatically.

This confuses me greatly. What if a class already has an #initialize (as
most do?).

You'll also need to hook yourself a YAML type for each class. In the case of the Hobix::Entry class, I'm hooking !hobix.com,2004/entry.

Argh. If I had to do this stuff every time, I'd almost rather keep using
the hack I'm using.

But I'll look at your examples. Maybe I just haven't seen the light yet.

Thanks much,
Hal

Hal Fulton wrote:

This property_map isn't clear to me yet. Why not class-level data instead
of instance?

You could certainly use a class method.

In Hobix, I like to encourage the user to override my classes and to create singletons to exhibit custom behavior. Instance methods are simpler in this regard.

I hadn't thought about the issue of required vs. optional. I would have
just said: If it doesn't have a default, and it's not there, default it
to nil.

Having required fields is nice when I am supplying the user with a blank object to fill in.

As you can see, the mixin also defines 'initialize', since you'll probably want to apply the defaults even if the object is created programmatically.

This confuses me greatly. What if a class already has an #initialize (as
most do?).

I'm providing this default #initialize as an example of using #apply_defaults in the constructor. It's okay if it's not used.

You'll also need to hook yourself a YAML type for each class. In the case of the Hobix::Entry class, I'm hooking !hobix.com,2004/entry.

Argh. If I had to do this stuff every time, I'd almost rather keep using
the hack I'm using.

If it's any reassurance, this will be simpler in Syck 0.50. You'll still have to set up the property_map and use the mixin to get this auto-defaults behavior. But assigning types will be as simple as:

   class Hobix::Entry
     tag_as "tag:hobix.com,2004:entry"
   end

For now, though, you'll need to do this for each class with a type:

   class Hobix::Entry
     def to_yaml_type
         "!hobix.com,2004/entry"
     end
   end

   YAML::add_domain_type( 'hobix.com,2004', 'entry' ) do |type, val|
     YAML::object_maker( Hobix::Entry, val ).apply_defaults
   end

_why

:)~ Drooling

Can't wait!
T.

P.S. the method name may be a bit too generic, though ?

···

On Friday 26 November 2004 11:36 am, why the lucky stiff wrote:

If it's any reassurance, this will be simpler in Syck 0.50. You'll
still have to set up the property_map and use the mixin to get this
auto-defaults behavior. But assigning types will be as simple as:

   class Hobix::Entry
     tag_as "tag:hobix.com,2004:entry"
   end