An alternative to the class Foo < Struct.new(vars) idiom and

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 :slight_smile:
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 © 2006 Mauricio F. [email protected]

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

Mauricio F. 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 Thu, Jun 01, 2006 at 09:33:29PM +0900, Jeff R. 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 Thu, 1 Jun 2006, Mauricio F. 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 Fri, Jun 02, 2006 at 12:27:32AM +0900, removed_email_a[email protected] wrote:

On Thu, 1 Jun 2006, Mauricio F. 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 :slight_smile: 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,

Mauricio F. 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

Hal F. 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 Fri, 2 Jun 2006 [email protected] 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 Fri, 2 Jun 2006, Mauricio F. wrote:

traits-0.9.1.rb’s ~700 lines of code :slight_smile: 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 Jun 1, 2006, at 11:13 PM, Hal F. 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).

Daniel S. wrote:

hash from scratch, and use it all the places we currently use a hash.

Wait…

What is your point? Please elaborate…

Hal

Hal F. 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

[email protected] 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 Fri, Jun 02, 2006 at 02:35:26AM +0900, [email protected] 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 :slight_smile:

Logan C. [email protected] 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 Jun 5, 2006, at 5:59 PM, Steven L. 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.