[ANN] traits-0.9.2 - better living through metaprogramming

URLS

   http://rubyforge.org/projects/codeforpeople/
   http://codeforpeople.com/lib/ruby/traits

ABOUT

   traits.rb is set of attr_* like methods on steroids, caffeine, and botox. it
   encourages better living through meta-programming and uniform access
   priciples. traits.rb supports smart inheritence of class attributes and a
   fistful of hooks for veryifying and munging attr values.

VERSION

   0.9.2

HISTORY

   0.9.2
     - fixed a bug where list traits, for example

         trait 'letters' => %w[ a b c ]

       were flattened in a way that exploded trait initialization

     - streamlined TraitInit module

     - added OpenTraits class and otraits method

         conf = otraits{
           port 42
           host 'forty-two'
         }
         p conf.port #=> 42
         p conf.host #=> 'forty-two'
         conf.username 'zaphod'
         p conf #=> {"username"=>"zaphod", "port"=>42, "host"=>"forty-two"}

AUTHOR

   ara [dot] t [dot] howard [at] noaa [dot] gov

SAMPLES

   <========< sample/a.rb >========>

   ~ > cat sample/a.rb

     require 'traits'

···

#
     # defining a trait is like attr_accessor in the simple case
     #
     class C
       trait :t
     end

     o = C::new
     o.t = 42
     p o.t

     #
     # and can be made even shorter
     #

     class B; has :x; end

     o = B::new
     o.x = 42
     p o.x

   ~ > ruby sample/a.rb

     42

   <========< sample/b.rb >========>

   ~ > cat sample/b.rb

     require 'traits'
     #
     # multiple traits can be defined at once using a list/array of string/sybmol
     # arguments
     #
     class C
       has :t0, :t1
       has %w( t2 t3 )
     end

     obj = C::new
     obj.t0 = 4
     obj.t3 = 2
     print obj.t0, obj.t3, "\n"

   ~ > ruby sample/b.rb

     42

   <========< sample/c.rb >========>

   ~ > cat sample/c.rb

     require 'traits'
     #
     # a hash argument can be used to specify default values
     #
     class C
       has 'a' => 4, :b => 2
     end

     o = C::new
     print o.a, o.b, "\n"

     #
     # and these traits are smartly inherited
     #
     class K < C; end

     o = K::new
     o.a = 40
     p( o.a + o.b ) # note that we pick up a default b from C class here since it
                    # has not been set

     o.a = 42
     o.b = nil
     p( o.b || o.a ) # but not here since we've explicitly set it to nil

     #
     # if a block is specifed as the default the initialization of the default value
     # is deferred until needed which makes for quite natural trait definitions. the
     # block is passed 'self' so references to the current object can be made. (if
     # this were not done 'self' in the block would be bound to the class!)
     #

     class C
       class << self
         has('classname'){ name.upcase }
       end

       has('classname'){ self.class.classname.downcase }
     end

     class B < C; end

     o = C::new
     p C::classname
     p o.classname

     o = B::new
     p B::classname
     p o.classname

   ~ > ruby sample/c.rb

     42
     "C"
     "c"
     "B"
     "b"

   <========< sample/d.rb >========>

   ~ > cat sample/d.rb

     require 'traits'
     #
     # all behaviours work within class scope (metal/singleton-class) to define
     # class methods
     #
     class C
       class << self
         traits 'a' => 4, 'b' => 2
       end
     end

     print C::a, C::b, "\n"

     #
     # singleton methods can even be defined on objects
     #

     class << (a = %w[dog cat ostrich])
       has 'category' => 'pets'
     end
     p a.category

     #
     # and modules
     #
     module Mmmm
       class << self; trait 'good' => 'bacon'; end
     end

     p Mmmm.good

   ~ > ruby sample/d.rb

     42
     "pets"
     "bacon"

   <========< sample/e.rb >========>

   ~ > cat sample/e.rb

     require 'traits'
     #
     # shorhands exit to enter 'class << self' in order to define class traits
     #
     class C
       class_trait 'a' => 4
       c_has :b => 2
     end

     print C::a, C::b, "\n"

   ~ > ruby sample/e.rb

     42

   <========< sample/f.rb >========>

   ~ > cat sample/f.rb

     require 'traits'
     #
     # as traits are defined they are remembered and can be accessed
     #
     class C
       class_trait :first_class_method
       trait :first_instance_method
     end

     class C
       class_trait :second_class_method
       trait :second_instance_method
     end

     #
     # readers and writers are remembered separatedly
     #
     p C::class_reader_traits
     p C::instance_writer_traits

     #
     # and can be gotten together at class or instance level
     #
     p C::class_traits
     p C::traits

   ~ > ruby sample/f.rb

     ["first_class_method", "second_class_method"]
     ["first_instance_method=", "second_instance_method="]
     [["first_class_method", "first_class_method="], ["second_class_method", "second_class_method="]]
     [["first_instance_method", "first_instance_method="], ["second_instance_method", "second_instance_method="]]

   <========< sample/g.rb >========>

   ~ > cat sample/g.rb

     require 'traits'
     #
     # another neat feature is that they are remembered per hierarchy
     #
     class C
       class_traits :base_class_method
       trait :base_instance_method
     end

     class K < C
       class_traits :derived_class_method
       trait :derived_instance_method
     end

     p C::class_traits
     p K::class_traits

   ~ > ruby sample/g.rb

     [["base_class_method", "base_class_method="]]
     [["derived_class_method", "derived_class_method="], ["base_class_method", "base_class_method="]]

   <========< sample/h.rb >========>

   ~ > cat sample/h.rb

     require 'traits'
     #
     # a depth first search path is used to find defaults
     #
     class C
       has 'a' => 42
     end
     class K < C; end

     k = K::new
     p k.a

     #
     # once assigned this is short-circuited
     #
     k.a = 'forty-two'
     p k.a

   ~ > ruby sample/h.rb

     42
     "forty-two"

   <========< sample/i.rb >========>

   ~ > cat sample/i.rb

     require 'traits'
     #
     # getters and setters can be defined separately
     #
     class C
       has_r :r
     end
     class D
       has_w :w
     end

     #
     # defining a reader trait still defines __public__ query and __private__ writer
     # methods
     #
     class C
       def using_private_writer_and_query
         p r?
         self.r = 42
         p r
       end
     end
     C::new.using_private_writer_and_query

     #
     # defining a writer trait still defines __private__ query and __private__ reader
     # methods
     #
     class D
       def using_private_reader
         p w?
         self.w = 'forty-two'
         p w
       end
     end
     D::new.using_private_reader

   ~ > ruby sample/i.rb

     false
     42
     false
     "forty-two"

   <========< sample/j.rb >========>

   ~ > cat sample/j.rb

     require 'traits'
     #
     # getters delegate to setters iff called with arguments
     #
     class AbstractWidget
       class_trait 'color' => 'pinky-green'
       class_trait 'size' => 42
       class_trait 'shape' => 'square'

       # we define instance traits which get their default from the class
       %w( color size shape ).each{|t| trait(t){self.class.send t}}

       def inspect
         "color <#{ color }> size <#{ size }> shape <#{ shape }>"
       end
     end

     class BlueWidget < AbstractWidget
       color 'blue'
       size 420
     end

     p BlueWidget::new

   ~ > ruby sample/j.rb

     color <blue> size <420> shape <square>

   <========< sample/k.rb >========>

   ~ > cat sample/k.rb

     require 'traits'
     #
     # the rememberance of traits can make generic intializers pretty slick
     #
     class C
       #
       # define class traits with defaults
       #
       class_traits(
         'a' => 40,
         'b' => 1,
         'c' => 0
       )

       #
       # define instance traits whose defaults come from readable class ones
       #
       class_rtraits.each{|ct| instance_trait ct => send(ct)}

       #
       # any option we respond_to? clobbers defaults
       #
       def initialize opts = {}
         opts.each{|k,v| send(k,v) if respond_to? k}
       end

       #
       # show anything we can read
       #
       def inspect
         self.class.rtraits.inject(0){|n,t| n += send(t)}
       end
     end

     c = C::new 'c' => 1
     p c

   ~ > ruby sample/k.rb

     42

   <========< sample/l.rb >========>

   ~ > cat sample/l.rb

     require 'traits'
     #
     # even defining single methods on object behaves
     #
     a = []

     class << a
       trait 'singleton_class' => class << self;self;end

       class << self
         class_trait 'x' => 42
       end
     end

     p a.singleton_class.x

   ~ > ruby sample/l.rb

     42

   <========< sample/m.rb >========>

   ~ > cat sample/m.rb

     require 'traits'
     #
     # pre and post hooks can be passed a proc or the name of a method, the arity is
     # detected and the proc/method sent either the value, or the name/value pair
     #

     class C
       HOOK_A = lambda{|value| puts "HOOK_A : #{ value }"}
       HOOK_B = lambda{|name, value| puts "HOOK_B : #{ name } = #{ value }"}

       def hook_a value
         puts "hook_a : #{ value }"
       end
       def hook_b name, value
         puts "hook_b : #{ name } = #{ value }"
       end

       trait 'x', 'pre' => HOOK_A, 'post' => 'hook_b'
       trait 'y', 'pre' => HOOK_B, 'post' => 'hook_a'
     end

     c = C::new
     c.x = 42
     c.y = 'forty-two'

   ~ > ruby sample/m.rb

     HOOK_A : 42
     hook_b : x = 42
     HOOK_B : y = forty-two
     hook_a : forty-two

   <========< sample/n.rb >========>

   ~ > cat sample/n.rb

     require 'traits'
     #
     # two kinds of in-place modifications are supported : casting and munging.
     # casting is a hook that requires either a proc or the name of a method that
     # will be used to convert the objects type. munging is similar execpt the
     # method is called on the object itself. like all hooks, lists may be provided
     # instead of a single argument
     #
     # you'll notice that the hooks and methods defined here are not strictly needed,
     # but are for illustration purposes only. note that all hooks operate in the
     # context of self - they have access to instance vars, etc., like instance_eval
     #

     class C
       INT = lambda{|i| int i}
       def int i
         Integer i
       end
       trait 'a', 'cast' => 'int'
       trait 'b', 'cast' => INT
       trait 'c', 'munge' => 'to_i'
       trait 'd', 'cast' => 'Integer'
       trait 'e', 'munge' => %w( to_i abs )
     end

     c = C::new

     c.a = '42'
     p c.a
     c.b = '42'
     p c.b
     c.c = '42'
     p c.c
     c.d = '42'
     p c.d
     c.e = '-42'
     p c.e

   ~ > ruby sample/n.rb

     42

   <========< sample/p.rb >========>

   ~ > cat sample/p.rb

     require 'traits'
     #
     # the TraitInit module provide a simple method for initializing an object's
     # traits from an options hash
     #

     class C
       include TraitInit

       LIST_OF_INTS = lambda{|a| Array === a and a.map{|i| Integer === i}.all?}
       LIST_OF_STRINGS = lambda{|a| Array === a and a.map{|s| String === s}.all?}

       trait :li, :validate => LIST_OF_INTS
       trait :ls, :validate => LIST_OF_STRINGS

       def initialize opts = {}
         trait_init opts
       end
     end

     c = C::new "li" => [4, 2], "ls" => %w[4 2]
     p c.li
     p c.ls

   ~ > ruby sample/p.rb

     [4, 2]
     ["4", "2"]

   <========< sample/q.rb >========>

   ~ > cat sample/q.rb

     require 'traits'
     #
     # the OpenTraits class is similar to an OpenStruct but, imho, easier to use.
     # the otraits shorthand can be used to contruct one
     #

     #
     # options passed as args dynamically create and init traits
     #
       config = otraits :port => 42
       p config.port

     #
     # any passed block does the same but, via a method missing hood and traits
     # getter/setters, the syntax is very clean
     #
       config = otraits{
         port 42
         host 'forty-two'
       }
       p config.port
       p config.host
       config.username 'zaphod'
       p config

   ~ > ruby sample/q.rb

     42
     "forty-two"
     {"username"=>"zaphod", "port"=>42, "host"=>"forty-two"}

CAVEATS

   this library is experimental and subject to change - though it has not for
   several versions and much of my code hinges is on it now so you can expect the
   interface to be stable in the near future - the only changes planned are those
   that fix bugs or add features.

LICENSE

   same as ruby's

-a
--
to foster inner awareness, introspection, and reasoning is more efficient than
meditation and prayer.
- h.h. the 14th dali lama

This looks beautiful, Ara. I'll definitely check it out.

···

On 8/21/06, Ara.T.Howard <ara.t.howard@noaa.gov> wrote:

URLS

   http://rubyforge.org/projects/codeforpeople/
   http://codeforpeople.com/lib/ruby/traits

ABOUT

   traits.rb is set of attr_* like methods on steroids, caffeine, and botox. it
   encourages better living through meta-programming and uniform access
   priciples. traits.rb supports smart inheritence of class attributes and a
   fistful of hooks for veryifying and munging attr values.