Ara.T.Howard
2006-Nov-04 01:41 UTC
[Rails] [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 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 42 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 42 42 42 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 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