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

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Mauricio F. (Guest)
on 2006-06-01 15:55
(Received via mailing list)
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 F. <removed_email_address@domain.invalid>
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
Jeff R. (Guest)
on 2006-06-01 16:34
(Received via mailing list)
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)
Mauricio F. (Guest)
on 2006-06-01 17:59
(Received via mailing list)
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...
unknown (Guest)
on 2006-06-01 19:29
(Received via mailing list)
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
Mauricio F. (Guest)
on 2006-06-01 21:22
(Received via mailing list)
On Fri, Jun 02, 2006 at 12:27:32AM +0900, removed_email_address@domain.invalid 
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 :-) 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,
unknown (Guest)
on 2006-06-01 21:38
(Received via mailing list)
On Fri, 2 Jun 2006, Mauricio F. 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
Hal F. (Guest)
on 2006-06-02 07:14
(Received via mailing list)
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
unknown (Guest)
on 2006-06-02 07:57
(Received via mailing list)
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.
unknown (Guest)
on 2006-06-02 08:40
(Received via mailing list)
On Fri, 2 Jun 2006 removed_email_address@domain.invalid 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
Logan C. (Guest)
on 2006-06-02 12:13
(Received via mailing list)
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. (Guest)
on 2006-06-02 14:21
(Received via mailing list)
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
Hal F. (Guest)
on 2006-06-03 03:26
(Received via mailing list)
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
unknown (Guest)
on 2006-06-03 10:15
(Received via mailing list)
removed_email_address@domain.invalid 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.
Mauricio F. (Guest)
on 2006-06-06 00:14
(Received via mailing list)
On Fri, Jun 02, 2006 at 02:35:26AM +0900, removed_email_address@domain.invalid 
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 :)
Steven L. (Guest)
on 2006-06-06 02:01
(Received via mailing list)
Logan C. <removed_email_address@domain.invalid> 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
Logan C. (Guest)
on 2006-06-06 07:32
(Received via mailing list)
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.
This topic is locked and can not be replied to.