Not quite a SuperStruct, maybe a SuperClass? automatic attributes and initialization
ruby-talk:192757 reminded me of SuperStruct. I was going to reply with a pointer to that library, but I realized that the usual
class MyClass < Struct.new(:a,:b) def initialize(*a) super # stuff end end
idiom and SuperStruct still left an important part of the solution space uncovered.
When you inherit from a Struct, the resulting class doesn't use plain instance variables, so you're forced to use the accessors. SuperStruct addresses that while emulating Struct quite closely, and also introduces OpenStruct-like features, so you get hash access with numeric, String and Symbol keys, #members, #to_a, pretty printing... But what if you don't need all that?
I'm guessing the OP would have wanted to do (at least that's what I wanted):
class Foo < InitializedClass.new(:a, :b, :c) def initialize(*a) super @c ||= 10 # this is how we manage default values end def bar a + b * c end end Foo.new(1,2,3).bar # => 7
And while we're at it, why not get named arguments for free too?
Foo.new(:a => 1, :b => 2).bar # => 21
This (and a few additional features) takes 56 lines in my first implementation, including #instance_exec, which I already wrote about.
Inheritance
One thing neither Struct nor SuperStruct can do is create a class derived from an existing one. Well, of course module inclusion is but another form of inheritance (and multiple too), but some times you might want the real thing, with matz's blessing.
Say you have
class Baz def initialize(foo, bar) @foo = a @bar = b end end
and want to create a subclass with a number of attributes, but you also need to be able to run Baz#initialize. If we were just subclassing manually,
super(whatever)
would do, but then there's no automatic accessors nor named arguments. Is it possible at all to get both at once?
This seemed tricky at first, but I managed to implement it so that you can do
InitializedClass.new(:my, :args, TheSuperClass){|sup| sup.call(whatever, you, want)}
where sup is a Proc which can be given the arguments to be passed to the #initialize method of the class you're deriving from.
class Bar < InitializedClass.new(:a, :b, :c, Baz){|sup| sup[@a * 2, @b + @c]} def compute @foo * @a + (@b - @c) * @bar end end b = Bar.new(1,2,3) b.compute # => -1 class Bar; attr_accessor :b end b.b = 10 b.compute # => 15
Not bad.
Implementation
I first wrote a few tests
require 'initializedclass' require 'test/unit' class Test_InitializedClass < Test::Unit::TestCase def test_basic_usage kl = InitializedClass.new(:a,:b,:c) foo = kl.new(1,2,3) assert_kind_of(kl, foo) assert_equal([1,2,3], [foo.a, foo.b, foo.c]) end def test_inheritance_is_respected kl1 = Class.new kl2 = InitializedClass.new(:a, :b, kl1) assert_equal(kl1, kl2.superclass) end def test_block_passed_to_initialize args = nil kl1 = Class.new{ define_method(:initialize){|*a| args = a } } kl2 = InitializedClass.new(:a, :b, kl1) {|sup| sup[@a, @b] } kl2.new(1,2) assert_equal([1,2], args) end def test_no_single_param_accepted assert_raise(ArgumentError){ InitializedClass.new(:a)} end def test_arity_is_checked kl1 = InitializedClass.new(:a, :b) assert_raise(ArgumentError){ kl1.new(1) } assert_raise(ArgumentError){ kl1.new(1,2,3) } end def test_allow_named_args kl1 = InitializedClass.new(:a, :b, :c) o = kl1.new :a => 1, :c => 2 expected = [1,nil,2] assert_equal(expected, [o.a, o.b, o.c]) assert_equal(expected, %w[@a @b @c].map{|x| o.instance_variable_get(x)}) end end
The implementation was rather easy
initializedclass.rb# Copyright (C) 2006 Mauricio Fernandez <mfp@acm.org> http://eigenclass.org # Use and distribution under the same terms as Ruby. # class Object module InstanceExecHelper; end include InstanceExecHelper def instance_exec(*args, &block) mname = "__instance_exec_#{Thread.current.object_id.abs}_#{object_id.abs}" InstanceExecHelper.module_eval{ define_method(mname, &block) } begin ret = send(mname, *args) ensure InstanceExecHelper.module_eval{ undef_method(mname) } rescue nil end ret end end class InitializedClass 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 InitializedClass for a single argument!" end kl = Class.new(parent) do 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 if block call_super = lambda{|*a| super(*a, &nil)} # &b only in 1.9 :-| instance_exec(call_super, &block) end 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
I'm quite satisfied with that; it feels much lighter than some of the metaprogramming I've done in the past. I might release it for real, maybe even under a boasting name:
class Foo < SuperClass.new(:a, :b, :c) end
It does make sense, doesn't it?
Better implementation - mfp (2006-05-15 (Mon) 12:17:14)
I was tired when I wrote the first one: instance_exec and a Proc gets created per object... that sucks.
I changed the super initialization thing so it now works like
klass = InitializedClass.new(:a, :b, Parent) { initialize_super(@a, @a + @b) }
The implementation is shorter too:
class InitializedClass
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 InitializedClass for a single argument!"
end
Class.new(parent) do
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
SuperClass = InitializedClass
Hash equality - Christian Neukirchen (2006-05-15 (Mon) 10:54:09)
Now, add #hash and #eql? and it will kick ass.
mfp 2006-05-15 (Mon) 11:35:38
Something like this I guess?
define_method(:hash){args.map{|name| instance_variable_get(name)}.hash }
define_method(:eql?){|other| other.class == self.class && args.all?{|name| self.instance_variable_get("@#{name}") == other.instance_variable_get("@#{name}") } }
or should it rather iterate over all of instance_variables, and not only the declared ones? And should that be class equality or just is_a? ? hmmm
- 133 http://search.live.com/results.aspx?q=superstruct&mrt=en-us&FORM=LIVSOP
- 33 http://anarchaia.org
- 22 http://www.artima.com/forums/flat.jsp?forum=123&thread=160303
- 14 http://www.ruby-forum.com/topic/67759
- 11 http://www.artima.com/buzz/community.jsp?forum=123
- 10 http://www.anarchaia.org
- 9 http://planetruby.0x42.net
- 3 http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/195443
- 2 http://www.google.com/url?sa=D&q=http://eigenclass.org/hiki.rb?struct-alike+class+definition
- 2 http://blogs.icerocket.com/search?p=2&q=superclass&dl=&dh=&
Keyword(s):[blog] [ruby] [frontpage] [class] [initialize] [meta] [struct] [superstruct] [superclass] [snippet]
References:[Ruby]