Forum: Ruby modularizing class methods

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.
47b1910084592eb77a032bc7d8d1a84e?d=identicon&s=25 Joel VanderWerf (Guest)
on 2006-05-28 03:42
(Received via mailing list)
Suppose you have a base class that defines some class methods, and that
these class methods are intended to be used in the derived classes to
(for example) add fields at incrementally increasing offsets in some
storage. Maybe you want to define named accessors for positions in an
array or string. Or maybe you are generating code to access fields in a
C struct. The point is that these class methods must be called in
sequence as the class is being defined, because some index is being
updated (as a class variable perhaps), or a because a C struct def is
being built up.

The problem is: how can you define such fields in a *module* so that you
can include the module in one of your derived classes, and have these
fields use the correct offset in the context of the class they are being
added to (which is not known at the time the module is defined)?

In other words you want to write code like this:

module NameFields
  field :first, :last, :middle
end

module AddressFields
  field :street, :city, :state, :zip
end

class Person < ArrayWithNamedFields
  include NameFields
  include AddressFields
end

per = Person.new
per.first = "Fred"
per.last = "Flintstone"
per.city = "Bedrock"

This doesn't work, not just because #field is undefined in NameFields
and AddressFields, but because if you did define it, it would use the
wrong array indexes when called in the module. The #field method should
only be called on the class that the field is being added to.

This statement of the problem is more wordy and confusing than the
solution. It's probably not original, but it's very simple, and a nice
example of meta-meta-programming that I happened to need right now. So
here it is. Maybe someone can think of a better name.

module ModuleMethodSaver
  def method_missing(meth, *args, &block)
    @saved ||= []
    @saved << [meth, args, block]
  end

  def included(m)
    if @saved
      @saved.each do |meth, args, block|
        m.send(meth, *args, &block)
      end
    end
  end
end

# First, a simple example:

module M
  extend ModuleMethodSaver

  foo 1,2,3
  bar "zap"
end

class Base
  def self.foo(*args)
    puts "#{self}.foo with #{args.inspect}"
  end

  def self.bar(*args)
    puts "#{self}.bar with #{args.inspect}"
  end
end

class C < Base
  include M
    # output:
    # ==> C.foo with [1, 2, 3]
    # ==> C.bar with ["zap"]
end

# The next example shows how ModuleMethodSaver can be used with a
# fixed-offset storage system, in this case based on Array. It could
# also be used with String (or BitStruct) or with CShadow (from
# cgenerator).

class ArrayWithNamedFields < Array
  def self.field(*names)
    names.each do |name|
      pos = @pos ||= 0
      define_method name do ||
        self[pos]
      end
      define_method "#{name}=" do |v|
        self[pos] = v
      end
      @pos += 1
    end
  end
end

module NameFields
  extend ModuleMethodSaver
  field :first, :last, :middle
end

module AddressFields
  extend ModuleMethodSaver
  field :street, :city, :state, :zip
end

class Person < ArrayWithNamedFields
  include NameFields
  include AddressFields
end

per = Person.new
per.first = "Fred"
per.last = "Flintstone"
per.city = "Bedrock"
p per  # ==> ["Fred", "Flintstone", nil, nil, "Bedrock"]
45196398e9685000d195ec626d477f0e?d=identicon&s=25 unknown (Guest)
on 2006-05-28 04:36
(Received via mailing list)
Interesting approach. It's a little bit of a misnomer mind you, becasue
you have taken over the include processes such that you are not
actually including module definitions, but rather are defining code in
the base class itself. In other words, while your class will report
ancestors of NameFields and AddressFields, there are actually no
methods defined in those modules.

I have seen another common way of doing this:

  module NameFields
    def included( base )
      base.class_eval %{
        field :first, :last, :middle
      }
    end
  end

This works too, of course, though I think your technique is more clever
and worth additional study. Nonetheless there is still the fact in
either case of being real module inclusion. Maybe it would be better
not to use the include mechinism. Ie. just create a different module
method to do the work. You could still go about it the same way, but
just use a different method other than #include.

Here is an example of what I came up with some time ago.


 class Module

  def package( name, &block )
    @__package__ ||= {}
    return @__package__ unless block_given?
    @__package__[name.to_sym] = block
  end

  def provide_features( base, *selection )
    selection.each do |s|
      base.class_eval( &@__package__[s.to_sym] )
    end
  end

  def use( package, *selection )
    if String === package or Symbol === package
      package = constant(package)
    end
    package.provide_features( self, *selection )
  end

 end


 #  _____         _
 # |_   _|__  ___| |_
 #   | |/ _ \/ __| __|
 #   | |  __/\__ \ |_
 #   |_|\___||___/\__|
 #

  require 'test/unit'

  class TCModule < Test::Unit::TestCase

    module MyPackages
      package :foo do
        def foo
          "yes"
        end
      end
    end

    class Y
      use MyPackages, :foo
    end

    def  test_package
      y = Y.new
      assert_equal( "yes", y.foo )
    end

  end


Though it may need some tweaking to work for your usecase, I suspect
something like this woud do the job nicely.

In any case I'm gogin to give your code some more thought. Please let
us know if you improve upon it.

T.
47b1910084592eb77a032bc7d8d1a84e?d=identicon&s=25 Joel VanderWerf (Guest)
on 2006-05-28 04:54
(Received via mailing list)
transfire@gmail.com wrote:
> Interesting approach. It's a little bit of a misnomer mind you, becasue
> you have taken over the include processes such that you are not
> actually including module definitions, but rather are defining code in
> the base class itself. In other words, while your class will report
> ancestors of NameFields and AddressFields, there are actually no
> methods defined in those modules.

True, but the parallel with the following is just so appealing, that I
thought #include was the best mechanism to use:

module NameFields
  attr_accessor :first, :last, :middle
end

module AddressFields
  attr_accessor :street, :city, :state, :zip
end

class Person
  include NameFields
  include AddressFields
end

(The above depends on the fact that attrs are stored by name, and not
incrementally allocated in some linear store.)

Anyway, you *might* want to put some definitions in the modules, and
have them propagate to the class via include, like this (in the context
of my example from the previous post):

module AddressFields
  extend ModuleMethodSaver
  field :street, :city, :state, :zip
  def postal_address
    [street, city, "#{state} #{zip}"].join("\n")
  end
end

Using include is the most natural way to do this.

> I have seen another common way of doing this:
>
>   module NameFields
>     def included( base )
>       base.class_eval %{
>         field :first, :last, :middle
>       }
>     end
>   end

Yep, same effect, but a little harder to write naturally. Also, you
cannot copy and paste from the class def to the module def, as you can
with ModuleMethodSaver:

  class Person < ArrayWithNamedFields
    field :first, :last, :middle
  end

is the same as

  module NameFields
    extend ModuleMethodSaver
    field :first, :last, :middle
  end

  class Person < ArrayWithNamedFields
    include NameFields
  end

So it makes refactoring easier. (Well, ok, you still have to type the
"extend" line. But that's easier to remember than the def included(...
stuff.)

Thanks for the comments!
This topic is locked and can not be replied to.