At 02:27 PM 9/5/01, you wrote:
>class Module
>     def attr_default(symbol, default)
>         default = "'#{default}'" if default.type == String
>         module_eval "def #{symbol}() if @#{symbol}.nil?; #{default}; else
>@#{symbol}; end; end"
>     end
>
>     def attr_filter(symbol, *filters)
>         filtered_setter = "def #{symbol}=(value) "
>         filters.each { |f|
>             filtered_setter += " value=#{f}(value);"
>         }
>         filtered_setter += " @#{symbol}=value; end"
>         module_eval filtered_setter
>     end
>end

Thanks very much for posting this. I was really in the mood for
tinkering with code, and this was just the thing I needed. A crummy
day has ended as a fun one.

I've made a series of changes that I hope are useful. Warning: they
haven't been tested well.

First, I noticed a problem. The code doesn't work with arrays as the
default:

class Cat
   attr_default :lives, [1, 2, 3, 4, 5, 6, 7, 8, 9]
end

 > Cat.new.lives
123456789

The problem is #{default} here:

         module_eval "def #{symbol}() if @#{symbol}.nil?; #{default}; else 
@#{symbol}; end; end"

#{default.inspect} would work for Arrays (and Strings, as well,
eliminating the need for the String test in the line above).

But 'inspect' isn't quite the thing, since its contract is to produce
a human-readable form. What we want is a method that produces a string
which, when evaled, produces a copy of the original object. I'm going
to call that "to_e". For a lot of things, to_e can be the same as
inspect. For convenience, I'll stick it on Object:

class Object
   def to_e() inspect; end
end

But for my own classes, it can be defined differently than inspect:

class Cat
   def initialize(name) @name = name; end

   def inspect() "<The cat named '#{@name}'>"; end

   def to_e() "Cat.new(#{@name.to_e})"; end
end

Now I can define attr_default like this:

class Module
     def attr_default(symbol, default)
         module_eval "def #{symbol}() if @#{symbol}.nil?; #{default.to_e}; else
@#{symbol}; end; end"
     end
end

So now I can initialize Cat with lives:

class Cat
   attr_default :lives, [1, 2, 3, 4, 5, 6, 7, 8, 9]
end

 > Buffy = Cat.new("buffy")
<The cat named 'buffy'>
 > Buffy.lives
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Moreover, Cats can be the default values for attributes of other
classes:

class Dog
   attr_default :friend, Buffy
end

(Buffy is a constant so that it's in scope for the class definition.)

 > Dog.new.friend
<The cat named 'buffy'>

But this raises a question: how can I have all Dogs share the same
friend? Because right now they don't:

 > towser = Dog.new
#<Dog:0xa0ab860>
 > ralph = Dog.new
#<Dog:0xa09d790>
 > towser.friend
<The cat named 'buffy'>
 > ralph.friend
<The cat named 'buffy'>
 > towser.friend == ralph.friend
false

I decided to store the defaults in the class. Along the way, I also
decided to change something that bugged me a bit about the original:
that you couldn't set the created variables to nil:

class Var
   attr_default :val, true

   def val=(val)
     @val = val
   end
end

 > v = Var.new
#<Var:0xa092480>
 > v.val
true
 > v.val=nil
nil
 > v.val         # expect nil
true

I can fix this by initially setting the instance variable to the
default *unless* it already exists. If it already exists, it's already
been explicitly assigned to. Instance variables don't exist until the
first assignment. Here's the code. I also added a setter method.

class Module
   attr_accessor :attribute_defaults

   def attr_default(symbol, default)
     @attribute_defaults ||= Hash.new
     @attribute_defaults[symbol] = default

     getter = %Q{
       def #{symbol}()
         unless instance_variables.include? '@#{symbol}'
           @#{symbol} = self.class.attribute_defaults[:#{symbol}]
         end
         @#{symbol}
       end
     }
     puts getter
     module_eval getter

     setter = %Q{
       def #{symbol}=(val)
         @#{symbol}=val
       end
     }
     puts setter
     module_eval setter
   end
end

Both the Var and Dog examples work as I'd prefer. (Note: for some
reason I haven't tried to figure out, pasting the above into irb
causes parse errors, but loading it from a file does not.)

Finally, I thought that it would be better to use the existing name
attr_accessor, passing in the defaults as keyword arguments, like
this:

attr_accessor :ordinary, ordinary2, :defaulted=>5, :other=>9

I suspect there's a better way to do this, but the kids have come home
from skating and my hacking time is through for tonight:

class Module
   alias_method :old_attr_accessor, :attr_accessor

   attr_accessor :attribute_defaults

   def attr_accessor(*accessors)
     accessors.each { | e |
       if e.type == Hash
         e.each { | key, value |
           install_default_accessor(key, value)
         }
       else
         old_attr_accessor e
       end
     }
   end

   def install_default_accessor(symbol, default)
     @attribute_defaults ||= Hash.new
     @attribute_defaults[symbol] = default

     getter = %Q{
       def #{symbol}()
         unless instance_variables.include? '@#{symbol}'
           @#{symbol} = self.class.attribute_defaults[:#{symbol}]
         end
         @#{symbol}
       end
     }
     puts getter
     module_eval getter

     setter = %Q{
       def #{symbol}=(val)
         @#{symbol}=val
       end
     }
     puts setter
     module_eval setter
   end
end

class Multi
   attr_accessor :ordinary, :ordinary2, :defaulted=>5, :other=>9
end


 > m = Multi.new
#<Multi:0xa0ac2b0>
 > m.ordinary
nil
 > m.ordinary2
nil
 > m.defaulted
5
 > m.other
9


--
Brian Marick, marick / testing.com
www.testing.com - Software testing services and resources
www.testingcraft.com - Where software testers exchange techniques
www.visibleworkings.com - Adequate understanding of system internals