I wrote this a couple weeks ago ( http://eigenclass.org/hiki.rb?struct-alike+class+definition ) and was considering whether it deserves an actual release. The implementation is simple; taking just 40 lines of code, it allows you to do stuff like class MyClass < SuperClass.new(:a, :b) # doubly appropriate :) def sum @a + @b end end a = MyClass.new(1, 1) a # => #<MyClass:0xb7dfd254 @b=1, @a=1> a.sum # => 2 The generated class uses normal instance variables unlike Struct.new, and creates the accessors for you. You also get keyword arguments for free: b = MyClass.new :a => 1, :b => 41 b.sum # => 42 b # => #<MyClass:0xb7dfd024 @b=41, @a=1> b.b # => 41 Default values are handled as follows: class Foo < SuperClass.new(:text, :times) { @times ||= 2 } def greeting (["Hello, #{@text}"] * times).join("\n") end end Foo.new("SuperClass", 2).greeting # => "Hello, SuperClass\nHello, SuperClass" Foo.new(:text => "world").greeting # => "Hello, world\nHello, world" Unlike Struct.new, you can use SuperClass to generate classes in the middle of the inheritance chain: class X attr_reader :foo def initialize(foo = 1) @foo = foo end end class Y < SuperClass.new(:bar, :baz, X) {@baz ||= 10; initialize_super(@baz + 1) } def fubar; @foo + @baz end end Y.new(10, 1).foo # => 2 Y.new(:bar => 1).fubar # => 21 I have an extended implementation that also creates #eql?, #hash and #== methods with selectable semantics. Here's the basic implementation: # Copyright (C) 2006 Mauricio Fernandez <mfp@acm.org> http://eigenclass.org # Use and distribution under the same terms as Ruby. class SuperClass def self.new_class(accessor_type, *args, &block) parent = args.pop if Class === args.last parent ||= Object unless args.size > 1 raise ArgumentError, "No point in using SuperClass for a single argument!" end Class.new(parent) do @initialize_args = args.map{|x| "@#{x}".intern} class << self; attr_reader :initialize_args end case accessor_type when :ro : attr_reader(*args) when :rw : attr_accessor(*args) end define_method(:initialize) do |*a| args.each{|name| instance_variable_set("@#{name}", nil) } if a.size == 1 && Hash === a[0] args.each{|name| instance_variable_set("@#{name}", a[0][name.to_sym])} elsif a.size != args.size raise ArgumentError, "wrong number of arguments (#{a.size} for #{args.size})" else args.each_with_index{|name, i| instance_variable_set("@#{name}", a[i])} end instance_eval(&block) if block end if block super_meth = parent.instance_method(:initialize) define_method(:initialize_super){|*a| super_meth.bind(self).call(*a) } private :initialize_super end end end def self.new(*args, &block); new_class(:ro, *args, &block) end def self.new_rw(*args, &block); new_class(:rw, *args, &block) end end
on 01.06.2006 13:55
on 01.06.2006 14:34
Mauricio Fernandez wrote: > a = MyClass.new(1, 1) > b # => #<MyClass:0xb7dfd024 @b=41, @a=1> > Foo.new("SuperClass", 2).greeting # => "Hello, SuperClass\nHello, SuperClass" > @foo = foo > > parent = args.pop if Class === args.last > end > end > > def self.new(*args, &block); new_class(:ro, *args, &block) end > def self.new_rw(*args, &block); new_class(:rw, *args, &block) end > end > I'm sold. This will be great for easily defining event types in EventMachine. Why the complicated default argument values though? For simple cases how about just using hash syntax: MyClass = SuperClass.new(:a => 2, :b => 3)
on 01.06.2006 15:59
On Thu, Jun 01, 2006 at 09:33:29PM +0900, Jeff Rose wrote: > I'm sold. This will be great for easily defining event types in > EventMachine. Why the complicated default argument values though? For > simple cases how about just using hash syntax: > > MyClass = SuperClass.new(:a => 2, :b => 3) The problem with this is that it makes positional arguments impossible, since the Hash doesn't preserve the order; maybe MyClass = SuperClass.new(:a, 2, :b, 3) or MyClass = SuperClass.new(:a, :b, :a => 2, :b => 3) # (don't like the repetition though) ? Anyway, I realized I can remove a couple lines from SuperClass to turn the following (which I forgot to show in the previous msg) into the preferred style: # b is optional only when using the keyword arg style class Foo < SuperClass.new(:a, :b) def initialize(*args) super @b ||= "foo" end end which will look better than passing a block to SuperClass.new for most people. Maybe I should allow both styles: * mandatory keyword arguments, with default values specified in the SuperClass.new call (have to define how, two possibilities given above but there's probably a better way) * either full positional arguments or keyword arguments (at least one required to discriminate), default values set manually in #initialize as in the above example. The desired one could be specified either via an explicit call (SuperClass.new_kw_args vs. SuperClass.new_positional ?) or implicitly by the arguments given to SuperClass.new. It's all easy to implement but there are many options...
on 01.06.2006 17:29
On Thu, 1 Jun 2006, Mauricio Fernandez wrote: hi mauricio- thought i'd just chime in here and show how this would be done with traits since it has overlapping aims: > creates the accessors for you. > > You also get keyword arguments for free: > > b = MyClass.new :a => 1, :b => 41 > b.sum # => 42 > b # => #<MyClass:0xb7dfd024 @b=41, @a=1> > b.b # => 41 harp:~ > cat a.rb require 'traits' class MyClass include TraitInit trait_initialize 'a', 'b' def sum() a + b end end a = MyClass.new 40, 2 p a.a p a.b p a.sum a = MyClass.new 'b' => 2, 'a' => 40 p a.a p a.b p a.sum p MyClass.traits harp:~ > ruby a.rb 40 2 42 40 2 42 [["a", "a="], ["b", "b="]] > Default values are handled as follows: > > class Foo < SuperClass.new(:text, :times) { @times ||= 2 } > def greeting > (["Hello, #{@text}"] * times).join("\n") > end > end > > Foo.new("SuperClass", 2).greeting # => "Hello, SuperClass\nHello, SuperClass" > Foo.new(:text => "world").greeting # => "Hello, world\nHello, world" harp:~ > cat a.rb require 'traits' class MyClass include TraitInit trait_initialize 'a' => 40, 'b' => 2 def sum() a + b end end class MyClass2 < MyClass; end p MyClass.new.sum p MyClass2.new.sum harp:~ > ruby a.rb 42 42 note that it works with inheritence too. > class Y < SuperClass.new(:bar, :baz, X) {@baz ||= 10; initialize_super(@baz + 1) } > def fubar; @foo + @baz end > end > > Y.new(10, 1).foo # => 2 > Y.new(:bar => 1).fubar # => 21 harp:~ > cat a.rb require 'traits' class X include TraitInit trait_initialize trait :foo => 1 end class Y < X trait :bar, :baz => 10 trait(:foo){ baz + 1 } def fubar() foo + baz end end p Y.new(:bar => 10, :baz => 1).foo p Y.new(:bar => 1).fubar harp:~ > ruby a.rb 2 21 traits is here if anyone is interested http://rubyforge.org/projects/codeforpeople/ http://codeforpeople.com/lib/ruby/traits kind regards. -a
on 01.06.2006 19:22
On Fri, Jun 02, 2006 at 12:27:32AM +0900, ara.t.howard@noaa.gov wrote: > On Thu, 1 Jun 2006, Mauricio Fernandez wrote: > > thought i'd just chime in here and show how this would be done with traits > since it has overlapping aims: > [...] Please indulge some quick questioning on my part; all the answers are in the sources but my eyeballs hurt a bit atm. and I'd prefer to avoid reading traits-0.9.1.rb's ~700 lines of code :-) After a cursory look, it's clear traits and SuperClass are in different leagues. SuperClass goes for minimalism and meta-programming self-restraint (no methods added to core classes, no hooks used...); traits seems to do so much more and pays the cost (I can see a load of methods/instance variables being added at Object's level, Object.singleton_method_added, etc.). > harp:~ > cat a.rb > require 'traits' > class MyClass > include TraitInit > trait_initialize 'a', 'b' > def sum() a + b end > end Would def sum; @a + @b end also work? Plain instance variables are the main appeal of SuperStruct or SuperClass. > >Unlike Struct.new, you can use SuperClass to generate classes in the middle > >of the inheritance chain: > > [...] > trait :bar, :baz => 10 > trait(:foo){ baz + 1 } > def fubar() foo + baz end > end > > p Y.new(:bar => 10, :baz => 1).foo > p Y.new(:bar => 1).fubar Interesting. Can this be made to work if X is defined as class X attr_reader :foo def initialize(foo); @foo = foo end end and doesn't include TraitInit, or when it does something non-trivial in #initialize? In other words, is it possible to have a single class in the hierarchy use traits' trait_initialize without touching the rest? And is the block evaluated every time one calls #foo, or only once? One last question: does trait provide some mechanism to create #==, #hash and #eql?, and if so, are they static-ish (only considering instance variables/traits at the moment they were created) or dynamic (taking into account traits defined later)? I hesitated about which one would be better, and finally added both to SuperClass, but maybe there's something for/against one of them. Thanks,
on 01.06.2006 19:38
On Fri, 2 Jun 2006, Mauricio Fernandez wrote: > traits-0.9.1.rb's ~700 lines of code :-) After a cursory look, it's clear > traits and SuperClass are in different leagues. SuperClass goes for minimalism > and meta-programming self-restraint (no methods added to core classes, no > hooks used...); traits seems to do so much more and pays the cost (I can see a > load of methods/instance variables being added at Object's level, > Object.singleton_method_added, etc.). all true. traits does alot. in particular it gives 'pols' semantics to default values inherited via include or normal inheritence - something which is actually tricky to do in ruby. > Plain instance variables are the main appeal of SuperStruct or SuperClass. yes. >> trait :foo => 1 > > Interesting. Can this be made to work if X is defined as > class X > attr_reader :foo > def initialize(foo); @foo = foo end > end > and doesn't include TraitInit, or when it does something non-trivial in > #initialize? In other words, is it possible to have a single class in the > hierarchy use traits' trait_initialize without touching the rest? yes. trait_initialize is just a hook to do this def initialize *argv trait_init *argv end so any class may just use trait_init directly. > And is the block evaluated every time one calls #foo, or only once? only once. you can do trait 't' => 'simple_default_value' or trait('t'){ 'default_deferred_evaluated_in_context_of_self' } > One last question: does trait provide some mechanism to create #==, #hash > and #eql?, and if so, are they static-ish (only considering instance > variables/traits at the moment they were created) or dynamic (taking into > account traits defined later)? one neat thing about traits is that they are remembered dynamically and in the order defined. eg. class C trait 'a' trait 'b' class_trait 'c' end p C.traits #=> [['a', 'a='], ['b', 'b=']] p C.rtraits #=> ['a', 'b'] p C.class_traits #=> [['c', 'c=']] p C.class_rtraits #=> ['c'] so it's trival do do module TraitsEqual def eql other to_hash.eql other.to_hash end def to_hash self.class.rtraits.inject({}){|h,t| h.update t => send(t)} end end > I hesitated about which one would be better, and finally added both to > SuperClass, but maybe there's something for/against one of them. it's totally dynamic in traits - eg i go all the way up the chain and include any traits added later. it is a tough choice. for my use case i wanted this: module Properites 'width' => 42 'height' => 42 end class C include Properties trait('size'){ width * height } # this gets width, height, and size def to_hash self.class.rtraits.inject({}){|h,t| h.update t => send(t)} end end but i could see one going either way > Thanks, sure. it's all fun stuff eh? cheers. -a
on 02.06.2006 05:14
Mauricio Fernandez wrote:
> the Hash doesn't preserve the order; maybe
[snip]
That's another example of why I would like a data structure
that 1) had a convenient notation for literals and 2) was
"like" a Hash, but ordered.
Hal
on 02.06.2006 05:57
Hal Fulton wrote: > That's another example of why I would like a data structure > that 1) had a convenient notation for literals and 2) was > "like" a Hash, but ordered. Facet's has Association which gives one something quite like that. [ :a >> 1, :b >> 2 ] Of course it does take over the #>> operator, so it can't be used inside any class that defines it. But that's not a big deal. It just means you can't _extend_ the following classes with code that uses an Association. Bignum Fixnum Date IPAddr Process::Status T.
on 02.06.2006 06:40
On Fri, 2 Jun 2006 transfire@gmail.com wrote: > Of course it does take over the #>> operator, so it can't be used > T. that just gave me an idea: harp:~ > cat a.rb require 'arrayfields' class Map < Array class Pair < Array; end def initialize *pairs fields = [] pairs.each{|k, v| self << v and fields << k} self.fields = fields end def self.[](*a, &b) new(*a, &b) end end class Symbol def >(value) Map::Pair[self, value] end end def Map(*a, &b) Map.new(*a, &b) end def map(*a, &b) Map.new(*a, &b) end m = map :a > 4, :b > 2 p m p m[:a] p m[:b] p m.to_hash p m.keys harp:~ > ruby -r rubygems a.rb [4, 2] 4 2 {:b=>2, :a=>4} [:a, :b] what do you think? -a
on 02.06.2006 10:13
On Jun 1, 2006, at 11:13 PM, Hal Fulton wrote: > > [snip] > > That's another example of why I would like a data structure > that 1) had a convenient notation for literals and 2) was > "like" a Hash, but ordered. > > > Hal > > This is why I kind of like how in Perl => is just a synonym for comma (it is in ruby too, at least for Hash literals, the problem being that it forces a hash literal). then def f(*args) args end f(1 => 2) #=> [1,2] if you wanted a hash you could just do def g(*args) Hash[ *args ] end Of course this takes away your ability to write things like f(1, 2, :option => value) but you could still do f(1, 2, {:option => value }) I dunno. Maybe just use caller and some evil read the file in again (or SCRIPT_LINES__ to find out what order they called it in).
on 02.06.2006 12:21
Hal Fulton wrote: > That's another example of why I would like a data structure > that 1) had a convenient notation for literals and 2) was > "like" a Hash, but ordered. Wow, it would be great if we then could use #to_hash to determine whether an object is a hash, and then not require that the return value of that method be an instance of Hash. Then we could create an ordered hash from scratch, and use it all the places we currently use a hash. Wait... Daniel
on 03.06.2006 01:26
Daniel Schierbeck wrote: > hash from scratch, and use it all the places we currently use a hash. > > Wait... What is your point? Please elaborate... Hal
on 03.06.2006 08:15
ara.t.howard@noaa.gov wrote: > > [ :a >> 1, :b >> 2 ] > > Process::Status > fields = [] > > [4, 2] > 4 > 2 > {:b=>2, :a=>4} > [:a, :b] > > > what do you think? Cool. Pair is a lot like Association but you've added the Map class here. Of course you've limited yourself to symbol keys by using >. Using Associations does have some limitations as far as being a full fledged omap, since it's just an Array of Associations, but it would be easy enough to combine it with your map class. Put the two together and have: m = map :a>>4, :b>>2 Nice thing about Associations too is that have some other uses as well. The only thing that would be better is a literal form and perhaps a slighlty better operator. T.
on 05.06.2006 22:14
On Fri, Jun 02, 2006 at 02:35:26AM +0900, ara.t.howard@noaa.gov wrote: > >and doesn't include TraitInit, or when it does something non-trivial in > >#initialize? In other words, is it possible to have a single class in the > >hierarchy use traits' trait_initialize without touching the rest? > > yes. [...] > > >And is the block evaluated every time one calls #foo, or only once? > > only once. [...] Thanks for taking the time to answer my questions; it seems traits is very well-behaved indeed :)
on 06.06.2006 00:01
Logan Capaldo <logancapaldo@gmail.com> writes: > This is why I kind of like how in Perl => is just a synonym for comma > (it is in ruby too, at least for Hash literals, the problem being > that it forces a hash literal). then I stopped using Perl when I started using Ruby, but I seem to recall that => in Perl does more than comma. E.g. {a => 1, b => 2} is mostly equivalent to {'a', 1, 'b', 2}. Ruby doesn't do the magic quoting. Steve
on 06.06.2006 05:32
On Jun 5, 2006, at 5:59 PM, Steven Lumos wrote: > > Steve > > True, but in perl you have to "force" the evaluation of variables anyway. => auto-quoting only works for barewords (\w+), as soon as you do something like "Hello, world" => 1 it's exactly the same as "Hello, world", 1 Whether or not it turns into a hash or an array is of course dependant on the left-hand side. %hash = ( "a" => 1 ); @array = ( "a" => 1 ); Which I think may as well be equivalent to ruby's { "a" => 1 } [ "a" => 1 ] But (un?)fortunately they are not. [ "a" => 1 ] in ruby is [ { "a" => 1 } ] In fact in ruby => isn't equiv to comma, it's more like comma is equiv to => when surrounded by { }. Getting way off topic here.